package plugins

import (
	"archive/zip"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
)

const (
	defaultMaxPluginPackageSize = 100 << 20
	defaultMaxPluginFileSize    = 80 << 20
)

type InstallOptions struct {
	Filename string
	Replace  bool
	Enable   bool
}

type InstallResult struct {
	Manifest Manifest `json:"manifest"`
	Path     string   `json:"path"`
	Replaced bool     `json:"replaced"`
}

func (s *Service) Install(ctx context.Context, reader io.Reader, opts InstallOptions) (InstallResult, error) {
	if s == nil {
		return InstallResult{}, ErrNotFound
	}
	installer, err := newInstallerFromService(s)
	if err != nil {
		return InstallResult{}, err
	}
	result, err := installer.Install(ctx, reader, opts)
	if err != nil {
		return InstallResult{}, err
	}
	_ = s.Refresh(ctx)
	return result, nil
}

func (s *Service) Preview(ctx context.Context, reader io.Reader, filename string) (Manifest, error) {
	if s == nil {
		return Manifest{}, ErrNotFound
	}
	installer, err := newInstallerFromService(s)
	if err != nil {
		return Manifest{}, err
	}
	return installer.Preview(ctx, reader, filename)
}

func (s *Service) InstallPath(id string, version string) (string, error) {
	if s == nil {
		return "", ErrNotFound
	}
	installer, err := newInstallerFromService(s)
	if err != nil {
		return "", err
	}
	return installer.targetDir(id, version)
}

func (s *Service) SetEnabled(ctx context.Context, id string, version string, enabled bool) (Manifest, error) {
	installer, err := newInstallerFromService(s)
	if err != nil {
		return Manifest{}, err
	}
	manifest, err := installer.SetEnabled(ctx, id, version, enabled)
	if err != nil {
		return Manifest{}, err
	}
	_ = s.Refresh(ctx)
	return manifest, nil
}

func (s *Service) Uninstall(ctx context.Context, id string, version string) (map[string]any, error) {
	installer, err := newInstallerFromService(s)
	if err != nil {
		return nil, err
	}
	result, err := installer.Uninstall(ctx, id, version)
	if err != nil {
		return nil, err
	}
	_ = s.Refresh(ctx)
	return result, nil
}

type Installer struct {
	root string
}

func NewInstaller(root string) (*Installer, error) {
	root = strings.TrimSpace(root)
	if root == "" {
		root = filepath.Join("data", "plugins")
	}
	abs, err := filepath.Abs(root)
	if err != nil {
		return nil, err
	}
	return &Installer{root: abs}, nil
}

func newInstallerFromService(s *Service) (*Installer, error) {
	if registry, ok := s.registry.(*DirectoryRegistry); ok && len(registry.roots) > 0 {
		return NewInstaller(registry.roots[0])
	}
	return NewInstaller(filepath.Join("data", "plugins"))
}

func (i *Installer) Install(ctx context.Context, reader io.Reader, opts InstallOptions) (InstallResult, error) {
	if err := ctx.Err(); err != nil {
		return InstallResult{}, err
	}
	if !strings.HasSuffix(strings.ToLower(strings.TrimSpace(opts.Filename)), ".qplug") && !strings.HasSuffix(strings.ToLower(strings.TrimSpace(opts.Filename)), ".zip") {
		return InstallResult{}, fmt.Errorf("%w: package must be .qplug or .zip", ErrInvalidManifest)
	}
	if err := os.MkdirAll(i.root, 0o755); err != nil {
		return InstallResult{}, err
	}
	tempFile, err := os.CreateTemp("", "qycw-plugin-*.qplug")
	if err != nil {
		return InstallResult{}, err
	}
	tempPath := tempFile.Name()
	defer func() { _ = os.Remove(tempPath) }()
	written, err := io.Copy(tempFile, io.LimitReader(reader, defaultMaxPluginPackageSize+1))
	closeErr := tempFile.Close()
	if err != nil {
		return InstallResult{}, err
	}
	if closeErr != nil {
		return InstallResult{}, closeErr
	}
	if written > defaultMaxPluginPackageSize {
		return InstallResult{}, fmt.Errorf("%w: package too large", ErrInvalidManifest)
	}

	tempDir, err := os.MkdirTemp("", "qycw-plugin-install-*")
	if err != nil {
		return InstallResult{}, err
	}
	defer func() { _ = os.RemoveAll(tempDir) }()
	if err := unzip(tempPath, tempDir); err != nil {
		return InstallResult{}, err
	}
	manifestPath, err := findPackageManifest(tempDir)
	if err != nil {
		return InstallResult{}, err
	}
	manifest, err := readManifest(manifestPath)
	if err != nil {
		return InstallResult{}, err
	}
	if _, err := manifest.EntryForCurrentPlatform(); err != nil {
		return InstallResult{}, err
	}

	target, err := i.targetDir(manifest.ID, manifest.Version)
	if err != nil {
		return InstallResult{}, err
	}
	replaced := false
	if exists(target) {
		if !opts.Replace {
			return InstallResult{}, fmt.Errorf("%w: plugin %s %s already installed", ErrInvalidManifest, manifest.ID, manifest.Version)
		}
		if err := ensureWithin(i.root, target); err != nil {
			return InstallResult{}, err
		}
		if err := os.RemoveAll(target); err != nil {
			return InstallResult{}, err
		}
		replaced = true
	}
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return InstallResult{}, err
	}
	packageRoot := filepath.Dir(manifestPath)
	if err := copyDir(packageRoot, target); err != nil {
		return InstallResult{}, err
	}
	if err := writeState(target, opts.Enable); err != nil {
		return InstallResult{}, err
	}
	installed, err := readManifest(filepath.Join(target, "plugin.json"))
	if err != nil {
		return InstallResult{}, err
	}
	return InstallResult{Manifest: installed, Path: target, Replaced: replaced}, nil
}

func (i *Installer) Preview(ctx context.Context, reader io.Reader, filename string) (Manifest, error) {
	if err := ctx.Err(); err != nil {
		return Manifest{}, err
	}
	if !strings.HasSuffix(strings.ToLower(strings.TrimSpace(filename)), ".qplug") && !strings.HasSuffix(strings.ToLower(strings.TrimSpace(filename)), ".zip") {
		return Manifest{}, fmt.Errorf("%w: package must be .qplug or .zip", ErrInvalidManifest)
	}
	tempFile, err := os.CreateTemp("", "qycw-plugin-preview-*.qplug")
	if err != nil {
		return Manifest{}, err
	}
	tempPath := tempFile.Name()
	defer func() { _ = os.Remove(tempPath) }()
	written, err := io.Copy(tempFile, io.LimitReader(reader, defaultMaxPluginPackageSize+1))
	closeErr := tempFile.Close()
	if err != nil {
		return Manifest{}, err
	}
	if closeErr != nil {
		return Manifest{}, closeErr
	}
	if written > defaultMaxPluginPackageSize {
		return Manifest{}, fmt.Errorf("%w: package too large", ErrInvalidManifest)
	}
	tempDir, err := os.MkdirTemp("", "qycw-plugin-preview-*")
	if err != nil {
		return Manifest{}, err
	}
	defer func() { _ = os.RemoveAll(tempDir) }()
	if err := unzip(tempPath, tempDir); err != nil {
		return Manifest{}, err
	}
	manifestPath, err := findPackageManifest(tempDir)
	if err != nil {
		return Manifest{}, err
	}
	manifest, err := readManifest(manifestPath)
	if err != nil {
		return Manifest{}, err
	}
	_, err = manifest.EntryForCurrentPlatform()
	return manifest, err
}

func (i *Installer) SetEnabled(ctx context.Context, id string, version string, enabled bool) (Manifest, error) {
	if err := ctx.Err(); err != nil {
		return Manifest{}, err
	}
	if !SafeID(id) {
		return Manifest{}, fmt.Errorf("%w: id", ErrInvalidManifest)
	}
	target, err := i.resolveInstalledDir(id, version)
	if err != nil {
		return Manifest{}, err
	}
	if err := writeState(target, enabled); err != nil {
		return Manifest{}, err
	}
	return readManifest(filepath.Join(target, "plugin.json"))
}

func (i *Installer) Uninstall(ctx context.Context, id string, version string) (map[string]any, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	if !SafeID(id) {
		return nil, fmt.Errorf("%w: id", ErrInvalidManifest)
	}
	target, err := i.resolveInstalledDir(id, version)
	if err != nil {
		return nil, err
	}
	manifest, _ := readManifest(filepath.Join(target, "plugin.json"))
	if err := ensureWithin(i.root, target); err != nil {
		return nil, err
	}
	if err := os.RemoveAll(target); err != nil {
		return nil, err
	}
	base := filepath.Join(i.root, id)
	if entries, err := os.ReadDir(base); err == nil && len(entries) == 0 {
		_ = os.Remove(base)
	}
	return map[string]any{"deleted": true, "id": id, "version": manifest.Version, "path": target}, nil
}

func (i *Installer) targetDir(id string, version string) (string, error) {
	if !SafeID(id) || strings.TrimSpace(version) == "" || strings.Contains(version, "..") || strings.ContainsAny(version, `/\`) {
		return "", fmt.Errorf("%w: plugin id/version", ErrInvalidManifest)
	}
	target := filepath.Join(i.root, id, version)
	if err := ensureWithin(i.root, target); err != nil {
		return "", err
	}
	return target, nil
}

func (i *Installer) resolveInstalledDir(id string, version string) (string, error) {
	if strings.TrimSpace(version) != "" {
		target, err := i.targetDir(id, version)
		if err != nil {
			return "", err
		}
		if !exists(filepath.Join(target, "plugin.json")) {
			return "", ErrNotFound
		}
		return target, nil
	}
	base := filepath.Join(i.root, id)
	entries, err := os.ReadDir(base)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return "", ErrNotFound
		}
		return "", err
	}
	candidates := []string{}
	for _, entry := range entries {
		if entry.IsDir() && exists(filepath.Join(base, entry.Name(), "plugin.json")) {
			candidates = append(candidates, entry.Name())
		}
	}
	if len(candidates) == 0 {
		return "", ErrNotFound
	}
	// DirectoryRegistry keeps the latest loaded version by ID; use lexical latest here too.
	sortStrings(candidates)
	return i.targetDir(id, candidates[len(candidates)-1])
}

func unzip(zipPath string, target string) error {
	reader, err := zip.OpenReader(zipPath)
	if err != nil {
		return err
	}
	defer reader.Close()
	for _, file := range reader.File {
		cleanName := filepath.Clean(file.Name)
		if cleanName == "." || strings.HasPrefix(cleanName, "..") || filepath.IsAbs(cleanName) {
			return fmt.Errorf("%w: unsafe path %s", ErrInvalidManifest, file.Name)
		}
		path := filepath.Join(target, cleanName)
		if err := ensureWithin(target, path); err != nil {
			return err
		}
		if file.FileInfo().IsDir() {
			if err := os.MkdirAll(path, 0o755); err != nil {
				return err
			}
			continue
		}
		if file.UncompressedSize64 > defaultMaxPluginFileSize {
			return fmt.Errorf("%w: file too large %s", ErrInvalidManifest, file.Name)
		}
		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
			return err
		}
		src, err := file.Open()
		if err != nil {
			return err
		}
		dst, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
		if err != nil {
			_ = src.Close()
			return err
		}
		_, copyErr := io.Copy(dst, io.LimitReader(src, defaultMaxPluginFileSize+1))
		closeSrcErr := src.Close()
		closeDstErr := dst.Close()
		if copyErr != nil {
			return copyErr
		}
		if closeSrcErr != nil {
			return closeSrcErr
		}
		if closeDstErr != nil {
			return closeDstErr
		}
	}
	return nil
}

func findPackageManifest(root string) (string, error) {
	rootManifest := filepath.Join(root, "plugin.json")
	if exists(rootManifest) {
		return rootManifest, nil
	}
	var found string
	err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			if path != root && filepath.Dir(path) != root {
				return filepath.SkipDir
			}
			return nil
		}
		if filepath.Base(path) == "plugin.json" {
			if found != "" {
				return fmt.Errorf("%w: package contains multiple plugin.json files", ErrInvalidManifest)
			}
			found = path
		}
		return nil
	})
	if err != nil {
		return "", err
	}
	if found == "" {
		return "", fmt.Errorf("%w: missing plugin.json", ErrInvalidManifest)
	}
	return found, nil
}

func copyDir(src string, dst string) error {
	return filepath.WalkDir(src, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return err
		}
		rel, err := filepath.Rel(src, path)
		if err != nil {
			return err
		}
		if rel == "." {
			return os.MkdirAll(dst, 0o755)
		}
		target := filepath.Join(dst, rel)
		if err := ensureWithin(dst, target); err != nil {
			return err
		}
		info, err := d.Info()
		if err != nil {
			return err
		}
		if d.IsDir() {
			return os.MkdirAll(target, info.Mode())
		}
		srcFile, err := os.Open(path)
		if err != nil {
			return err
		}
		defer srcFile.Close()
		dstFile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode())
		if err != nil {
			return err
		}
		_, copyErr := io.Copy(dstFile, srcFile)
		closeErr := dstFile.Close()
		if copyErr != nil {
			return copyErr
		}
		return closeErr
	})
}

func writeState(dir string, enabled bool) error {
	state := stateFile{Enabled: enabled}
	if enabled {
		state.Status = "enabled"
	} else {
		state.Status = "disabled"
	}
	raw, err := json.MarshalIndent(state, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(filepath.Join(dir, "plugin.state.json"), raw, 0o644)
}

func ensureWithin(root string, path string) error {
	absRoot, err := filepath.Abs(root)
	if err != nil {
		return err
	}
	absPath, err := filepath.Abs(path)
	if err != nil {
		return err
	}
	rel, err := filepath.Rel(absRoot, absPath)
	if err != nil {
		return err
	}
	if rel == "." || (!strings.HasPrefix(rel, "..") && !filepath.IsAbs(rel)) {
		return nil
	}
	return fmt.Errorf("%w: unsafe path %s", ErrInvalidManifest, path)
}

func sortStrings(values []string) {
	for i := 0; i < len(values); i++ {
		for j := i + 1; j < len(values); j++ {
			if values[j] < values[i] {
				values[i], values[j] = values[j], values[i]
			}
		}
	}
}
