package plugins

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

	"github.com/go-git/go-git/v6"
	"github.com/spf13/afero"
)

const registryRepo = "https://github.com/AvengeMedia/dms-plugin-registry.git"

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"`
}

type GitClient interface {
	PlainClone(path string, url string) error
	Pull(path string) error
	HasUpdates(path string) (bool, 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) HasUpdates(path string) (bool, 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
			}
		}
	}

	// If we couldn't find a remote HEAD, assume no updates
	if remoteHead == "" {
		return false, nil
	}

	// Compare local HEAD with remote HEAD
	return head.Hash().String() != remoteHead, nil
}

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

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

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

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

func (r *Registry) Update() error {
	exists, err := afero.DirExists(r.fs, r.cacheDir)
	if err != nil {
		return fmt.Errorf("failed to check cache directory: %w", err)
	}

	if !exists {
		if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
			return fmt.Errorf("failed to create cache directory: %w", err)
		}

		if err := r.git.PlainClone(r.cacheDir, registryRepo); err != nil {
			return fmt.Errorf("failed to clone registry: %w", err)
		}
	} else {
		// Try to pull, if it fails (e.g., shallow clone corruption), delete and re-clone
		if err := r.git.Pull(r.cacheDir); err != nil {
			// Repository is likely corrupted or has issues, delete and re-clone
			if err := r.fs.RemoveAll(r.cacheDir); err != nil {
				return fmt.Errorf("failed to remove corrupted registry: %w", err)
			}

			if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
				return fmt.Errorf("failed to create cache directory: %w", err)
			}

			if err := r.git.PlainClone(r.cacheDir, registryRepo); err != nil {
				return fmt.Errorf("failed to re-clone registry: %w", err)
			}
		}
	}

	return r.loadPlugins()
}

func (r *Registry) loadPlugins() error {
	pluginsDir := filepath.Join(r.cacheDir, "plugins")

	entries, err := afero.ReadDir(r.fs, pluginsDir)
	if err != nil {
		return fmt.Errorf("failed to read plugins directory: %w", err)
	}

	r.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")
		}

		r.plugins = append(r.plugins, plugin)
	}

	return nil
}

func (r *Registry) List() ([]Plugin, error) {
	if len(r.plugins) == 0 {
		if err := r.Update(); err != nil {
			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)
}
