package local

import (
	"crypto/sha256"
	"fmt"
	"io/fs"
	"log"
	"path/filepath"
	"strings"

	"github.com/initializ/forge/forge-skills/contract"
	"github.com/initializ/forge/forge-skills/parser"
)

// Scan walks top-level directories in fsys looking for SKILL.md files.
// It parses frontmatter to extract SkillDescriptor fields.
// Hidden directories (starting with ".") and "_template/" are skipped.
func Scan(fsys fs.FS) ([]contract.SkillDescriptor, error) {
	return ScanWithRoot(fsys, "")
}

// ScanWithRoot works like Scan but, when rootPath is non-empty, resolves
// symlinks and verifies each entry stays within rootPath. Entries that
// resolve outside the root are skipped with a log warning.
func ScanWithRoot(fsys fs.FS, rootPath string) ([]contract.SkillDescriptor, error) {
	// Pre-resolve the real root path for symlink checks.
	var realRoot string
	if rootPath != "" {
		var err error
		realRoot, err = filepath.EvalSymlinks(rootPath)
		if err != nil {
			return nil, fmt.Errorf("resolving root path: %w", err)
		}
	}

	entries, err := fs.ReadDir(fsys, ".")
	if err != nil {
		return nil, err
	}

	var skills []contract.SkillDescriptor
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}
		name := entry.Name()
		if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
			continue
		}

		// Symlink escape check when rootPath is provided.
		if realRoot != "" {
			resolved, symErr := filepath.EvalSymlinks(filepath.Join(realRoot, name))
			if symErr != nil {
				log.Printf("[trust] warning: cannot resolve symlink for skill %q: %v", name, symErr)
				continue
			}
			if !strings.HasPrefix(resolved, realRoot+string(filepath.Separator)) && resolved != realRoot {
				log.Printf("[trust] warning: skill %q symlink resolves outside root, skipping", name)
				continue
			}
		}

		// Check for SKILL.md
		skillPath := name + "/SKILL.md"
		f, err := fsys.Open(skillPath)
		if err != nil {
			continue // no SKILL.md, skip
		}

		// Read raw content for checksum computation
		rawContent, readErr := fs.ReadFile(fsys, skillPath)
		if readErr != nil {
			_ = f.(interface{ Close() error }).Close()
			continue
		}

		// Parse frontmatter
		_, meta, parseErr := parser.ParseWithMetadata(f)
		_ = f.(interface{ Close() error }).Close()
		if parseErr != nil {
			continue
		}

		// Compute checksum
		h := sha256.Sum256(rawContent)
		checksum := fmt.Sprintf("sha256:%x", h)

		sd := contract.SkillDescriptor{
			Name: name,
			Provenance: &contract.Provenance{
				Source:   "local",
				Trust:    contract.TrustLocal,
				Checksum: checksum,
			},
		}

		if meta != nil {
			if meta.Name != "" {
				sd.Name = meta.Name
			}
			if meta.Description != "" {
				sd.Description = meta.Description
			}

			sd.Category = meta.Category
			sd.Tags = meta.Tags
			sd.Icon = meta.Icon

			// Extract forge-specific fields
			if meta.Metadata != nil {
				if forgeMap, ok := meta.Metadata["forge"]; ok {
					sd.RequiredBins, sd.RequiredEnv, sd.OneOfEnv, sd.OptionalEnv, sd.EgressDomains, sd.DeniedTools, sd.TimeoutHint = extractFromForgeMap(forgeMap)
				}
			}

			// Extract version from frontmatter if present
			if sd.Provenance != nil && meta.Metadata != nil {
				if forgeMap, ok := meta.Metadata["forge"]; ok {
					if v, ok := forgeMap["version"]; ok {
						if vs, ok := v.(string); ok {
							sd.Provenance.Version = vs
						}
					}
				}
			}
		}

		// Warn about unsigned local skills (skip for embedded/builtin which
		// go through Scan with empty rootPath before their trust is upgraded).
		if realRoot != "" && sd.Provenance != nil && sd.Provenance.SignedBy == "" {
			log.Printf("[trust] warning: skill %q loaded without signature", sd.Name)
		}

		// Derive display name from skill name if not set
		if sd.DisplayName == "" {
			sd.DisplayName = deriveDisplayName(sd.Name)
		}

		skills = append(skills, sd)
	}

	return skills, nil
}

// extractFromForgeMap extracts typed fields from the forge metadata map.
func extractFromForgeMap(forgeMap map[string]any) (bins, reqEnv, oneOfEnv, optEnv, egress, deniedTools []string, timeoutHint int) {
	// Extract egress_domains
	if raw, ok := forgeMap["egress_domains"]; ok {
		if arr, ok := raw.([]any); ok {
			for _, v := range arr {
				if s, ok := v.(string); ok {
					egress = append(egress, s)
				}
			}
		}
	}

	// Extract denied_tools
	if raw, ok := forgeMap["denied_tools"]; ok {
		if arr, ok := raw.([]any); ok {
			for _, v := range arr {
				if s, ok := v.(string); ok {
					deniedTools = append(deniedTools, s)
				}
			}
		}
	}

	// Extract timeout_hint
	if raw, ok := forgeMap["timeout_hint"]; ok {
		switch v := raw.(type) {
		case int:
			timeoutHint = v
		case float64:
			timeoutHint = int(v)
		}
	}

	// Extract requires
	reqRaw, ok := forgeMap["requires"]
	if !ok {
		return
	}
	reqMap, ok := reqRaw.(map[string]any)
	if !ok {
		return
	}

	// bins — handle both string items and map items (rich BinRequirement)
	if binsRaw, ok := reqMap["bins"]; ok {
		if arr, ok := binsRaw.([]any); ok {
			for _, v := range arr {
				switch item := v.(type) {
				case string:
					bins = append(bins, item)
				case map[string]any:
					if name, ok := item["name"].(string); ok && name != "" {
						bins = append(bins, name)
					}
				}
			}
		}
	}

	// env
	envRaw, ok := reqMap["env"]
	if !ok {
		return
	}
	envMap, ok := envRaw.(map[string]any)
	if !ok {
		return
	}

	reqEnv = extractStringSlice(envMap, "required")
	oneOfEnv = extractStringSlice(envMap, "one_of")
	optEnv = extractStringSlice(envMap, "optional")
	return
}

func extractStringSlice(m map[string]any, key string) []string {
	raw, ok := m[key]
	if !ok {
		return nil
	}
	arr, ok := raw.([]any)
	if !ok {
		return nil
	}
	var result []string
	for _, v := range arr {
		if s, ok := v.(string); ok {
			result = append(result, s)
		}
	}
	return result
}

// deriveDisplayName converts a kebab-case skill name to a title-case display name.
func deriveDisplayName(name string) string {
	parts := strings.Split(name, "-")
	for i, p := range parts {
		if len(p) > 0 {
			parts[i] = strings.ToUpper(p[:1]) + p[1:]
		}
	}
	return strings.Join(parts, " ")
}
