package plugins

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/AvengeMedia/DankMaterialShell/core/internal/registries"
	"github.com/go-git/go-git/v6"
	"github.com/go-git/go-git/v6/plumbing"
	"github.com/spf13/afero"
)

type Plugin struct {
	ID           string   `json:"id"`
	Name         string   `json:"name"`
	Capabilities []string `json:"capabilities"`
	Category     string   `json:"category"`
	Repo         string   `json:"repo"`
	Path         string   `json:"path,omitempty"`
	Author       string   `json:"author"`
	Description  string   `json:"description"`
	Dependencies []string `json:"dependencies,omitempty"`
	Compositors  []string `json:"compositors"`
	Distro       []string `json:"distro"`
	Screenshot   string   `json:"screenshot,omitempty"`
	RequiresDMS  string   `json:"requires_dms,omitempty"`
	Featured     bool     `json:"featured,omitempty"`
}

type GitClient interface {
	PlainClone(path string, url string) error
	Pull(path string) error
	OriginURL(path string) (string, error)
	HasUpdates(path string) (hasUpdates bool, localHash string, remoteHash string, err error)
	CurrentRevision(path string) (string, error)
	CheckoutRevision(path string, revision string) error
}

type realGitClient struct{}

func (g *realGitClient) PlainClone(path string, url string) error {
	_, err := git.PlainClone(path, &git.CloneOptions{
		URL:      url,
		Progress: os.Stdout,
	})
	return err
}

func (g *realGitClient) Pull(path string) error {
	repo, err := git.PlainOpen(path)
	if err != nil {
		return err
	}

	worktree, err := repo.Worktree()
	if err != nil {
		return err
	}

	err = worktree.Pull(&git.PullOptions{})
	if err != nil && err.Error() != "already up-to-date" {
		return err
	}

	return nil
}

func (g *realGitClient) OriginURL(path string) (string, error) {
	repo, err := git.PlainOpen(path)
	if err != nil {
		return "", err
	}
	remote, err := repo.Remote("origin")
	if err != nil {
		return "", err
	}
	urls := remote.Config().URLs
	if len(urls) == 0 {
		return "", errors.New("origin remote has no URL")
	}
	return urls[0], nil
}

func (g *realGitClient) HasUpdates(path string) (bool, string, string, error) {
	repo, err := git.PlainOpen(path)
	if err != nil {
		return false, "", "", err
	}

	// Fetch remote changes
	err = repo.Fetch(&git.FetchOptions{})
	if err != nil && err.Error() != "already up-to-date" {
		// If fetch fails, we can't determine if there are updates
		// Return false and the error
		return false, "", "", err
	}

	// Get the HEAD reference
	head, err := repo.Head()
	if err != nil {
		return false, "", "", err
	}

	// Get the remote HEAD reference (typically origin/HEAD or origin/main or origin/master)
	remote, err := repo.Remote("origin")
	if err != nil {
		return false, "", "", err
	}

	refs, err := remote.List(&git.ListOptions{})
	if err != nil {
		return false, "", "", err
	}

	// Find the default branch remote ref
	var remoteHead string
	for _, ref := range refs {
		if ref.Name().IsBranch() {
			// Try common branch names
			if ref.Name().Short() == "main" || ref.Name().Short() == "master" {
				remoteHead = ref.Hash().String()
				break
			}
		}
	}

	localHash := head.Hash().String()
	// If we couldn't find a remote HEAD, assume no updates
	if remoteHead == "" {
		return false, localHash, "", nil
	}

	// Compare local HEAD with remote HEAD
	return localHash != remoteHead, localHash, remoteHead, nil
}

func (g *realGitClient) CurrentRevision(path string) (string, error) {
	repo, err := git.PlainOpen(path)
	if err != nil {
		return "", err
	}
	head, err := repo.Head()
	if err != nil {
		return "", err
	}
	return head.Hash().String(), nil
}

func (g *realGitClient) CheckoutRevision(path string, revision string) error {
	if !gitCommitPattern.MatchString(revision) {
		return fmt.Errorf("invalid git revision %q", revision)
	}

	repo, err := git.PlainOpen(path)
	if err != nil {
		return err
	}
	hash := plumbing.NewHash(revision)
	if _, objectErr := repo.CommitObject(hash); objectErr != nil {
		if _, remoteErr := repo.Remote("origin"); remoteErr != nil {
			return fmt.Errorf("revision %s is not available in repository: %w", revision, objectErr)
		}
		fetchErr := repo.Fetch(&git.FetchOptions{})
		if fetchErr != nil && fetchErr != git.NoErrAlreadyUpToDate {
			return fmt.Errorf("failed to fetch locked revision: %w", fetchErr)
		}
	}
	if _, err := repo.CommitObject(hash); err != nil {
		return fmt.Errorf("revision %s is not available in repository: %w", revision, err)
	}
	worktree, err := repo.Worktree()
	if err != nil {
		return err
	}
	return worktree.Reset(&git.ResetOptions{Commit: hash, Mode: git.HardReset})
}

type Registry struct {
	fs         afero.Fs
	cacheDir   string
	registries []registries.Source
	plugins    []Plugin
	git        GitClient
}

func NewRegistry() (*Registry, error) {
	return NewRegistryWithFs(afero.NewOsFs())
}

func NewRegistryWithFs(fs afero.Fs) (*Registry, error) {
	return &Registry{
		fs:         fs,
		cacheDir:   getCacheDir(),
		registries: registries.Load(fs),
		git:        &realGitClient{},
	}, nil
}

func (r *Registry) cacheDirFor(src registries.Source) string {
	return filepath.Join(r.cacheDir, src.Name)
}

func getCacheDir() string {
	return filepath.Join(os.TempDir(), "dankdots-plugin-registry")
}

// A cached clone is reused only when its origin still matches the configured
// URL; renamed or re-pointed registries re-clone instead of pulling from the
// stale remote.
func (r *Registry) updateOne(src registries.Source) error {
	dir := r.cacheDirFor(src)
	exists, err := afero.DirExists(r.fs, dir)
	if err != nil {
		return fmt.Errorf("failed to check cache directory: %w", err)
	}

	if exists {
		origin, originErr := r.git.OriginURL(dir)
		if originErr == nil && origin == src.URL && r.git.Pull(dir) == nil {
			return nil
		}
		if err := r.fs.RemoveAll(dir); err != nil {
			return fmt.Errorf("failed to remove stale registry cache: %w", err)
		}
	}

	if err := r.fs.MkdirAll(filepath.Dir(dir), 0o755); err != nil {
		return fmt.Errorf("failed to create cache directory: %w", err)
	}
	if err := r.git.PlainClone(dir, src.URL); err != nil {
		return fmt.Errorf("failed to clone: %w", err)
	}
	return nil
}

// A registry without a plugins/ directory is a valid themes-only registry.
func (r *Registry) loadPluginsFrom(dir string) ([]Plugin, error) {
	pluginsDir := filepath.Join(dir, "plugins")
	entries, err := afero.ReadDir(r.fs, pluginsDir)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, fmt.Errorf("failed to read plugins directory: %w", err)
	}

	var plugins []Plugin
	for _, entry := range entries {
		if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
			continue
		}

		data, err := afero.ReadFile(r.fs, filepath.Join(pluginsDir, entry.Name()))
		if err != nil {
			continue
		}

		var plugin Plugin
		if err := json.Unmarshal(data, &plugin); err != nil {
			continue
		}

		if plugin.ID == "" {
			plugin.ID = strings.TrimSuffix(entry.Name(), ".json")
		}

		plugins = append(plugins, plugin)
	}
	return plugins, nil
}

// Pre-multi-registry caches were a single clone at the base dir; the per-name
// layout nests under it, so a leftover clone is deleted wholesale first.
func (r *Registry) resetLegacyCache() {
	if exists, _ := afero.DirExists(r.fs, filepath.Join(r.cacheDir, ".git")); exists {
		_ = r.fs.RemoveAll(r.cacheDir)
	}
}

// Update refreshes every configured registry, aggregating plugins in
// declaration order (first occurrence of an ID wins). A failing registry is
// reported in the joined error but does not block the others.
func (r *Registry) Update() error {
	r.resetLegacyCache()
	r.plugins = []Plugin{}
	seen := make(map[string]struct{})
	var errs []error
	for _, src := range r.registries {
		if err := r.updateOne(src); err != nil {
			errs = append(errs, fmt.Errorf("registry %s: %w", src.Name, err))
			continue
		}
		plugins, err := r.loadPluginsFrom(r.cacheDirFor(src))
		if err != nil {
			errs = append(errs, fmt.Errorf("registry %s: %w", src.Name, err))
			continue
		}
		for _, p := range plugins {
			if _, dup := seen[p.ID]; dup {
				continue
			}
			seen[p.ID] = struct{}{}
			r.plugins = append(r.plugins, p)
		}
	}
	return errors.Join(errs...)
}

func (r *Registry) List() ([]Plugin, error) {
	if len(r.plugins) == 0 {
		if err := r.Update(); err != nil && len(r.plugins) == 0 {
			return nil, err
		}
	}

	return SortByFirstParty(r.plugins), nil
}

func (r *Registry) Search(query string) ([]Plugin, error) {
	allPlugins, err := r.List()
	if err != nil {
		return nil, err
	}

	if query == "" {
		return allPlugins, nil
	}

	return SortByFirstParty(FuzzySearch(query, allPlugins)), nil
}

func (r *Registry) Get(idOrName string) (*Plugin, error) {
	plugins, err := r.List()
	if err != nil {
		return nil, err
	}

	// First, try to find by ID (preferred method)
	for _, p := range plugins {
		if p.ID == idOrName {
			return &p, nil
		}
	}

	// Fallback to name for backward compatibility
	for _, p := range plugins {
		if p.Name == idOrName {
			return &p, nil
		}
	}

	return nil, fmt.Errorf("plugin not found: %s", idOrName)
}
