package core

import (
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"strings"
	"sync"
)

// Skill represents an agent skill discovered from a SKILL.md file.
type Skill struct {
	Name        string // skill name (= subdirectory name)
	DisplayName string // optional display name from frontmatter
	Description string // from frontmatter or first line of content
	Prompt      string // the instruction content (body after frontmatter)
	Source      string // directory path where this skill was found
}

// SkillRegistry discovers and caches agent skills from skill directories.
// Skills are project-level: each Engine has its own SkillRegistry.
type SkillRegistry struct {
	mu   sync.RWMutex
	dirs []string
	// cached results; nil means not yet scanned
	cache []*Skill
}

func NewSkillRegistry() *SkillRegistry {
	return &SkillRegistry{}
}

// SetDirs configures which directories to scan for skills.
func (r *SkillRegistry) SetDirs(dirs []string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.dirs = dirs
	r.cache = nil
}

// Resolve looks up a skill by name. Returns nil if not found.
// Hyphens and underscores are treated as equivalent so that Telegram-sanitized
// names (e.g. "calendar_scheduler") match original skill names ("calendar-scheduler").
func (r *SkillRegistry) Resolve(name string) *Skill {
	norm := normalizeCommandName(name)
	for _, s := range r.ListAll() {
		if normalizeCommandName(s.Name) == norm {
			return s
		}
	}
	return nil
}

// normalizeCommandName folds case and treats hyphens/underscores as equivalent.
func normalizeCommandName(s string) string {
	return strings.ToLower(strings.ReplaceAll(s, "-", "_"))
}

// ListAll returns all discovered skills. Results are cached after first scan.
func (r *SkillRegistry) ListAll() []*Skill {
	r.mu.RLock()
	if r.cache != nil {
		defer r.mu.RUnlock()
		return r.cache
	}
	r.mu.RUnlock()

	r.mu.Lock()
	defer r.mu.Unlock()

	// double-check after acquiring write lock
	if r.cache != nil {
		return r.cache
	}

	var result []*Skill
	seen := make(map[string]bool)

	for _, dir := range r.dirs {
		result = append(result, discoverSkillsInDir(dir, seen)...)
	}

	r.cache = result
	return result
}

// discoverSkillsInDir scans a single skill root directory for immediate
// subdirectories that contain a SKILL.md file. Per the Claude Code CLI
// convention (issue #1304), only depth-1 layout is recognised:
//
//	<root>/<skill-name>/SKILL.md        — registered
//	<root>/<skill-name>/references/...  — asset, NOT registered
//
// Nested SKILL.md files inside a skill (e.g. example templates shipped
// alongside the real one) are treated as skill assets and ignored, so they
// cannot leak into platform command menus as phantom slash commands.
func discoverSkillsInDir(scanRoot string, seen map[string]bool) []*Skill {
	entries, err := os.ReadDir(scanRoot)
	if err != nil {
		return nil
	}

	var result []*Skill
	for _, entry := range entries {
		if !isDiscoverableSubdir(scanRoot, entry) {
			continue
		}

		skillName := entry.Name()
		if seen[strings.ToLower(skillName)] {
			continue
		}

		skillDir := filepath.Join(scanRoot, skillName)
		skillFile := filepath.Join(skillDir, "SKILL.md")
		data, err := os.ReadFile(skillFile)
		if err != nil {
			// No SKILL.md at the top of this subdir — and we deliberately
			// do NOT recurse, matching the Claude Code CLI rule.
			continue
		}

		skill := parseSkillMD(skillName, string(data), skillDir)
		if skill == nil {
			continue
		}

		seen[strings.ToLower(skillName)] = true
		result = append(result, skill)
		slog.Debug("skill: discovered", "name", skillName, "dir", skillDir)
	}

	return result
}

// isDiscoverableSubdir reports whether entry should be considered as a
// potential skill subdirectory of parentDir. Accepts real directories and
// symlinks that resolve to directories.
func isDiscoverableSubdir(parentDir string, entry os.DirEntry) bool {
	if entry.IsDir() {
		return true
	}
	if entry.Type()&os.ModeSymlink == 0 {
		return false
	}
	info, err := os.Stat(filepath.Join(parentDir, entry.Name()))
	return err == nil && info.IsDir()
}

// Dirs returns the configured skill directories.
func (r *SkillRegistry) Dirs() []string {
	r.mu.RLock()
	defer r.mu.RUnlock()
	return append([]string(nil), r.dirs...)
}

// Invalidate clears the cache so skills will be re-scanned on next access.
func (r *SkillRegistry) Invalidate() {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.cache = nil
}

// parseSkillMD parses a SKILL.md file with optional YAML frontmatter.
//
// Format:
//
//	---
//	description: Short description
//	name: Display Name
//	---
//	Prompt/instruction content here...
func parseSkillMD(skillName, raw, sourceDir string) *Skill {
	content := strings.TrimSpace(raw)
	if content == "" {
		return nil
	}

	var frontmatter map[string]string
	body := content

	if strings.HasPrefix(content, "---") {
		rest := content[3:]
		endIdx := strings.Index(rest, "\n---")
		if endIdx >= 0 {
			fmBlock := rest[:endIdx]
			body = strings.TrimSpace(rest[endIdx+4:])
			frontmatter = parseFrontmatter(fmBlock)
		}
	}

	if body == "" {
		return nil
	}

	description := ""
	displayName := ""
	if frontmatter != nil {
		description = frontmatter["description"]
		displayName = frontmatter["name"]
	}

	if description == "" {
		first, _, _ := strings.Cut(body, "\n")
		first = strings.TrimSpace(first)
		if len([]rune(first)) > 80 {
			first = string([]rune(first)[:80]) + "..."
		}
		description = first
	}

	return &Skill{
		Name:        skillName,
		DisplayName: displayName,
		Description: description,
		Prompt:      body,
		Source:      sourceDir,
	}
}

// parseFrontmatter extracts simple key: value pairs from a YAML-like block.
// Handles quoted values, and YAML block scalar indicators (>-, |-, >, |)
// by reading the following indented lines as the value.
func parseFrontmatter(block string) map[string]string {
	m := make(map[string]string)
	lines := strings.Split(block, "\n")
	for i := 0; i < len(lines); i++ {
		line := strings.TrimSpace(lines[i])
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		key, val, ok := strings.Cut(line, ":")
		if !ok {
			continue
		}
		key = strings.TrimSpace(key)
		val = strings.TrimSpace(val)

		// Handle YAML block scalar indicators: >-, |-, >, |
		if val == ">-" || val == "|-" || val == ">" || val == "|" {
			var blockLines []string
			for i+1 < len(lines) {
				next := lines[i+1]
				// Block continues while lines are indented (start with space/tab)
				if len(next) == 0 || (next[0] != ' ' && next[0] != '\t') {
					break
				}
				i++
				blockLines = append(blockLines, strings.TrimSpace(next))
			}
			val = strings.Join(blockLines, " ")
		}

		val = strings.Trim(val, `"'`)
		if key != "" {
			m[strings.ToLower(key)] = val
		}
	}
	return m
}

// BuildSkillInvocationPrompt constructs the message sent to the agent when
// a user invokes a skill. Instead of raw prompt expansion, we instruct the
// agent to execute the skill.
func BuildSkillInvocationPrompt(skill *Skill, args []string) string {
	var sb strings.Builder

	sb.WriteString("The user is asking you to execute the following skill.\n\n")

	name := skill.DisplayName
	if name == "" {
		name = skill.Name
	}
	fmt.Fprintf(&sb, "## Skill: %s\n", name)

	if skill.Description != "" {
		fmt.Fprintf(&sb, "## Description: %s\n", skill.Description)
	}

	sb.WriteString("\n## Skill Instructions:\n")
	sb.WriteString(skill.Prompt)

	if len(args) > 0 {
		sb.WriteString("\n\n## User Arguments:\n")
		sb.WriteString(strings.Join(args, " "))
	}

	sb.WriteString("\n\nPlease follow the skill instructions above to complete the task.")
	return sb.String()
}
