package services

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

	"sproutclaw-web/internal/models"
	"sproutclaw-web/internal/utils"
)

var npmSpecRe = regexp.MustCompile(`^(@?[^@]+(?:/[^@]+)?)(?:@(.+))?$`)

// ListExtensions returns local extensions (enabled and disabled) plus npm packages
// declared in settings.json. Local ones live in extensions/ and extensions-disabled/.
func ListExtensions(agentDir string) []models.ExtensionInfo {
	var exts []models.ExtensionInfo
	exts = append(exts, scanExtensionsDir(filepath.Join(agentDir, "extensions"), true)...)
	exts = append(exts, scanExtensionsDir(filepath.Join(agentDir, "extensions-disabled"), false)...)
	exts = append(exts, listNpmExtensions(agentDir)...)
	return exts
}

func scanExtensionsDir(dir string, enabled bool) []models.ExtensionInfo {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil
	}
	var exts []models.ExtensionInfo
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		ext := buildExtensionInfo(filepath.Join(dir, e.Name()), e.Name())
		// Anything physically under extensions/ or extensions-disabled/ is a
		// local extension and can be toggled, regardless of its package.json name.
		ext.Category = "local"
		ext.Toggleable = true
		ext.Enabled = enabled
		exts = append(exts, ext)
	}
	return exts
}

func buildExtensionInfo(path, id string) models.ExtensionInfo {
	info := models.ExtensionInfo{
		Name: id,
		Path: path,
	}

	// try package.json for name/version
	pkgPath := filepath.Join(path, "package.json")
	if data, err := os.ReadFile(pkgPath); err == nil {
		var pkg struct {
			Name    string `json:"name"`
			Version string `json:"version"`
		}
		if json.Unmarshal(data, &pkg) == nil {
			if pkg.Name != "" {
				info.Name = pkg.Name
			}
			info.Version = pkg.Version
		}
	}
	return info
}

func listNpmExtensions(agentDir string) []models.ExtensionInfo {
	sources := readNpmPackageSources(filepath.Join(agentDir, "settings.json"))
	if len(sources) == 0 {
		return nil
	}

	seen := make(map[string]bool, len(sources))
	var exts []models.ExtensionInfo
	for _, source := range sources {
		name, ok := npmPackageName(source)
		if !ok || seen[name] {
			continue
		}
		seen[name] = true
		exts = append(exts, buildNpmExtensionInfo(agentDir, name, source))
	}
	return exts
}

func readNpmPackageSources(settingsPath string) []string {
	data, err := os.ReadFile(settingsPath)
	if err != nil {
		return nil
	}

	var settings struct {
		Packages   []json.RawMessage `json:"packages"`
		Extensions []string          `json:"extensions"`
	}
	if err := json.Unmarshal(data, &settings); err != nil {
		return nil
	}

	var sources []string
	for _, raw := range settings.Packages {
		if source := parsePackageSource(raw); source != "" {
			sources = append(sources, source)
		}
	}
	for _, source := range settings.Extensions {
		if strings.HasPrefix(source, "npm:") {
			sources = append(sources, source)
		}
	}
	return sources
}

func parsePackageSource(raw json.RawMessage) string {
	var source string
	if err := json.Unmarshal(raw, &source); err == nil {
		return strings.TrimSpace(source)
	}

	var obj struct {
		Source string `json:"source"`
	}
	if err := json.Unmarshal(raw, &obj); err == nil {
		return strings.TrimSpace(obj.Source)
	}
	return ""
}

func npmPackageName(source string) (string, bool) {
	if !strings.HasPrefix(source, "npm:") {
		return "", false
	}
	spec := strings.TrimSpace(strings.TrimPrefix(source, "npm:"))
	if spec == "" {
		return "", false
	}
	name, _ := parseNpmSpec(spec)
	if name == "" {
		return "", false
	}
	return name, true
}

func parseNpmSpec(spec string) (name, version string) {
	match := npmSpecRe.FindStringSubmatch(spec)
	if match == nil {
		return spec, ""
	}
	name = match[1]
	version = match[2]
	return name, version
}

func buildNpmExtensionInfo(agentDir, packageName, source string) models.ExtensionInfo {
	modPath := filepath.Join(agentDir, "npm", "node_modules", packageName)
	info := models.ExtensionInfo{
		Name:       packageName,
		Path:       modPath,
		Source:     source,
		Category:   "npm",
		Enabled:    true,
		Toggleable: false,
	}

	pkgPath := filepath.Join(modPath, "package.json")
	if data, err := os.ReadFile(pkgPath); err == nil {
		var pkg struct {
			Name    string `json:"name"`
			Version string `json:"version"`
		}
		if json.Unmarshal(data, &pkg) == nil {
			if pkg.Name != "" {
				info.Name = pkg.Name
			}
			info.Version = pkg.Version
		}
	}
	return info
}

// ToggleExtension moves a local extension between extensions/ and
// extensions-disabled/ directories. The agent's loader only scans extensions/,
// so moving a directory out of it disables that extension. npm extensions live
// elsewhere and are not managed here.
func ToggleExtension(agentDir, extPath string, enable bool) error {
	extDir := filepath.Join(agentDir, "extensions")
	disabledDir := filepath.Join(agentDir, "extensions-disabled")

	if !isUnderDir(extPath, extDir) && !isUnderDir(extPath, disabledDir) {
		return fmt.Errorf("extension path not in managed directories")
	}

	name := filepath.Base(extPath)
	var src, dst string
	if enable {
		src = filepath.Join(disabledDir, name)
		dst = filepath.Join(extDir, name)
	} else {
		src = filepath.Join(extDir, name)
		dst = filepath.Join(disabledDir, name)
	}

	if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
		return err
	}
	return utils.Move(src, dst)
}
