package plugins

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
)

type Registry interface {
	List(ctx context.Context, capability string) ([]Manifest, error)
	ListAll(ctx context.Context, capability string) ([]Manifest, error)
	Get(ctx context.Context, id string) (Manifest, error)
	Resolve(ctx context.Context, id string, capability string) (Manifest, error)
	Refresh(ctx context.Context) error
}

type DirectoryRegistry struct {
	roots []string
	mu    sync.RWMutex
	items map[string]Manifest
}

func NewDirectoryRegistry(roots []string) *DirectoryRegistry {
	cleaned := make([]string, 0, len(roots))
	seen := map[string]struct{}{}
	for _, root := range roots {
		root = strings.TrimSpace(root)
		if root == "" {
			continue
		}
		abs, err := filepath.Abs(root)
		if err == nil {
			root = abs
		}
		if _, ok := seen[root]; ok {
			continue
		}
		seen[root] = struct{}{}
		cleaned = append(cleaned, root)
	}
	return &DirectoryRegistry{roots: cleaned, items: map[string]Manifest{}}
}

func DefaultRoots() []string {
	return []string{
		filepath.Join("data", "plugins"),
		"plugins",
	}
}

func (r *DirectoryRegistry) Refresh(ctx context.Context) error {
	items := map[string]Manifest{}
	for _, root := range r.roots {
		if err := ctx.Err(); err != nil {
			return err
		}
		manifests, err := scanRoot(root)
		if err != nil {
			if errors.Is(err, os.ErrNotExist) {
				continue
			}
			return err
		}
		for _, manifest := range manifests {
			items[manifest.ID] = manifest
		}
	}
	r.mu.Lock()
	r.items = items
	r.mu.Unlock()
	return nil
}

func (r *DirectoryRegistry) List(ctx context.Context, capability string) ([]Manifest, error) {
	return r.list(ctx, capability, false)
}

func (r *DirectoryRegistry) ListAll(ctx context.Context, capability string) ([]Manifest, error) {
	return r.list(ctx, capability, true)
}

func (r *DirectoryRegistry) list(ctx context.Context, capability string, includeDisabled bool) ([]Manifest, error) {
	if err := r.ensureLoaded(ctx); err != nil {
		return nil, err
	}
	r.mu.RLock()
	defer r.mu.RUnlock()
	out := make([]Manifest, 0, len(r.items))
	for _, manifest := range r.items {
		if !includeDisabled && !manifest.Enabled {
			continue
		}
		if manifest.HasCapability(capability) {
			out = append(out, manifest)
		}
	}
	sort.Slice(out, func(i, j int) bool {
		if out[i].ID == out[j].ID {
			return out[i].Version < out[j].Version
		}
		return out[i].ID < out[j].ID
	})
	return out, nil
}

func (r *DirectoryRegistry) Get(ctx context.Context, id string) (Manifest, error) {
	if err := r.ensureLoaded(ctx); err != nil {
		return Manifest{}, err
	}
	r.mu.RLock()
	defer r.mu.RUnlock()
	manifest, ok := r.items[strings.TrimSpace(id)]
	if !ok {
		return Manifest{}, ErrNotFound
	}
	return manifest, nil
}

func (r *DirectoryRegistry) Resolve(ctx context.Context, id string, capability string) (Manifest, error) {
	manifest, err := r.Get(ctx, id)
	if err != nil {
		return Manifest{}, err
	}
	if !manifest.HasCapability(capability) {
		return Manifest{}, fmt.Errorf("%w: %s does not provide %s", ErrCapabilityMissing, id, capability)
	}
	if !manifest.Enabled {
		return Manifest{}, fmt.Errorf("%w: %s", ErrDisabled, id)
	}
	return manifest, nil
}

func (r *DirectoryRegistry) ensureLoaded(ctx context.Context) error {
	r.mu.RLock()
	loaded := r.items != nil && len(r.items) > 0
	r.mu.RUnlock()
	if loaded {
		return nil
	}
	return r.Refresh(ctx)
}

func scanRoot(root string) ([]Manifest, error) {
	stat, err := os.Stat(root)
	if err != nil {
		return nil, err
	}
	if !stat.IsDir() {
		return nil, fmt.Errorf("plugin root is not a directory: %s", root)
	}
	paths := []string{}
	if exists(filepath.Join(root, "plugin.json")) {
		paths = append(paths, filepath.Join(root, "plugin.json"))
	}
	entries, err := os.ReadDir(root)
	if err != nil {
		return nil, err
	}
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}
		first := filepath.Join(root, entry.Name(), "plugin.json")
		if exists(first) {
			paths = append(paths, first)
			continue
		}
		versionEntries, err := os.ReadDir(filepath.Join(root, entry.Name()))
		if err != nil {
			continue
		}
		for _, versionEntry := range versionEntries {
			if !versionEntry.IsDir() {
				continue
			}
			candidate := filepath.Join(root, entry.Name(), versionEntry.Name(), "plugin.json")
			if exists(candidate) {
				paths = append(paths, candidate)
			}
		}
	}
	out := make([]Manifest, 0, len(paths))
	for _, path := range paths {
		manifest, err := readManifest(path)
		if err != nil {
			return nil, err
		}
		out = append(out, manifest)
	}
	return out, nil
}

func readManifest(path string) (Manifest, error) {
	raw, err := os.ReadFile(path)
	if err != nil {
		return Manifest{}, err
	}
	var manifest Manifest
	if err := json.Unmarshal(raw, &manifest); err != nil {
		return Manifest{}, fmt.Errorf("%w: %s: %v", ErrInvalidManifest, path, err)
	}
	if manifest.SchemaVersion == 0 {
		manifest.SchemaVersion = 1
	}
	if manifest.Runtime == "" {
		manifest.Runtime = RuntimeJSONRPC
	}
	manifest.Enabled = true
	manifest.Status = "enabled"
	if manifest.Name == "" {
		manifest.Name = manifest.ID
	}
	if manifest.Version == "" {
		manifest.Version = "0.0.0"
	}
	manifest.Dir = filepath.Dir(path)
	applyState(&manifest)
	if err := manifest.Validate(); err != nil {
		return Manifest{}, fmt.Errorf("%w: %s", err, path)
	}
	return manifest, nil
}

type stateFile struct {
	Enabled bool   `json:"enabled"`
	Status  string `json:"status,omitempty"`
}

func applyState(manifest *Manifest) {
	raw, err := os.ReadFile(filepath.Join(manifest.Dir, "plugin.state.json"))
	if err != nil {
		return
	}
	var state stateFile
	if err := json.Unmarshal(raw, &state); err != nil {
		return
	}
	manifest.Enabled = state.Enabled
	if state.Status != "" {
		manifest.Status = state.Status
	} else if state.Enabled {
		manifest.Status = "enabled"
	} else {
		manifest.Status = "disabled"
	}
}

func exists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}
