package skills

import (
	"bytes"
	"context"
	"fmt"
	"path/filepath"
	"sort"
	"strings"

	"github.com/Clyra-AI/wrkr/core/detect"
	"github.com/Clyra-AI/wrkr/core/detect/gaitpolicy"
	"github.com/Clyra-AI/wrkr/core/detect/promptchannel"
	"github.com/Clyra-AI/wrkr/core/model"
	"gopkg.in/yaml.v3"
)

const detectorID = "skills"

type Detector struct{}

func New() Detector { return Detector{} }

func (Detector) ID() string { return detectorID }

type frontmatter struct {
	AllowedTools []string `yaml:"allowed-tools"`
}

func (Detector) Detect(_ context.Context, scope detect.Scope, options detect.Options) ([]model.Finding, error) {
	if err := detect.ValidateScopeRoot(scope.Root); err != nil {
		return nil, err
	}
	if detect.IsLocalMachineScope(scope) {
		return nil, nil
	}

	files, walkErr := detect.WalkFilesWithParseErrors(detectorID, scope.Root, options)
	if walkErr != nil {
		return nil, walkErr
	}
	skills := make([]detect.WalkedFile, 0)
	for _, file := range files {
		rel := file.Rel
		if strings.HasSuffix(rel, "/SKILL.md") && (strings.HasPrefix(rel, ".claude/skills/") || strings.HasPrefix(rel, ".agents/skills/")) {
			skills = append(skills, file)
		}
	}
	sort.Slice(skills, func(i, j int) bool { return skills[i].Rel < skills[j].Rel })
	if len(skills) == 0 {
		return nil, nil
	}

	policyResult, blockedErr := gaitpolicy.LoadBlockedTools(scope.Root)
	if blockedErr != nil {
		return nil, blockedErr
	}
	blockedTools := policyResult.BlockedTools

	type counter struct {
		total int
		exec  int
		write int
		read  int
		none  int
	}
	stats := counter{total: len(skills)}
	ceiling := map[string]struct{}{}
	findings := make([]model.Finding, 0)

	for _, file := range skills {
		rel := file.Rel
		if file.ParseError != nil {
			findings = append(findings, model.Finding{
				FindingType: "parse_error",
				Severity:    model.SeverityMedium,
				ToolType:    "skill",
				Location:    rel,
				Repo:        scope.Repo,
				Org:         fallbackOrg(scope.Org),
				Detector:    detectorID,
				ParseError:  file.ParseError,
			})
			continue
		}
		payload, readErr := detect.ReadFileWithinRoot(detectorID, scope.Root, rel)
		if readErr != nil {
			findings = append(findings, model.Finding{
				FindingType: "parse_error",
				Severity:    model.SeverityMedium,
				ToolType:    "skill",
				Location:    rel,
				Repo:        scope.Repo,
				Org:         fallbackOrg(scope.Org),
				Detector:    detectorID,
				ParseError:  readErr,
			})
			continue
		}
		tools, parseErr := parseAllowedTools(scope.Root, rel)
		if parseErr != nil {
			parseErr.Detector = detectorID
			findings = append(findings, model.Finding{
				FindingType: "parse_error",
				Severity:    model.SeverityMedium,
				ToolType:    "skill",
				Location:    rel,
				Repo:        scope.Repo,
				Org:         fallbackOrg(scope.Org),
				Detector:    detectorID,
				ParseError:  parseErr,
			})
			continue
		}
		permissions := normalizeTools(tools)
		hasExec := false
		hasWrite := false
		hasRead := false
		for _, permission := range permissions {
			switch permission {
			case "proc.exec":
				hasExec = true
			case "filesystem.write":
				hasWrite = true
			case "filesystem.read":
				hasRead = true
			}
		}
		switch {
		case hasExec:
			stats.exec++
		case hasWrite:
			stats.write++
		case hasRead:
			stats.read++
		default:
			stats.none++
		}

		findings = append(findings, model.Finding{
			FindingType: "skill",
			Severity:    severityForSkill(hasExec, hasWrite),
			ToolType:    "skill",
			Location:    rel,
			Repo:        scope.Repo,
			Org:         fallbackOrg(scope.Org),
			Detector:    detectorID,
			Permissions: permissions,
			Evidence: []model.Evidence{
				{Key: "allowed_tools_count", Value: fmt.Sprintf("%d", len(permissions))},
			},
		})
		if hint := promptchannel.ExtractSemanticHints(rel, []string{string(payload)}); len(hint.Permissions) > 0 || len(hint.Evidence) > 0 {
			evidence := []model.Evidence{}
			keys := make([]string, 0, len(hint.Evidence))
			for key := range hint.Evidence {
				keys = append(keys, key)
			}
			sort.Strings(keys)
			for _, key := range keys {
				for _, value := range hint.Evidence[key] {
					evidence = append(evidence, model.Evidence{Key: key, Value: value})
				}
			}
			findings = append(findings, model.Finding{
				FindingType: "skill_semantic",
				Severity:    severityForSkill(containsPermission(hint.Permissions, "proc.exec"), containsPermission(hint.Permissions, "deploy.write") || containsPermission(hint.Permissions, "release.write")),
				ToolType:    "skill",
				Location:    rel,
				Repo:        scope.Repo,
				Org:         fallbackOrg(scope.Org),
				Detector:    detectorID,
				Permissions: append([]string(nil), hint.Permissions...),
				Evidence:    evidence,
				Remediation: "Narrow skill prose so deploy, release, destructive, approval-bypass, and proof-required actions remain explicit and reviewable.",
			})
		}

		for _, permission := range permissions {
			if _, exists := ceiling[permission]; exists {
				continue
			}
			ceiling[permission] = struct{}{}
			if permission == "proc.exec" {
				findings = append(findings, model.Finding{
					FindingType: "skill_contribution",
					Severity:    model.SeverityHigh,
					ToolType:    "skill",
					Location:    rel,
					Repo:        scope.Repo,
					Org:         fallbackOrg(scope.Org),
					Detector:    detectorID,
					Permissions: []string{permission},
					Evidence: []model.Evidence{
						{Key: "adds_high_privilege", Value: permission},
					},
				})
			}
		}

		for _, permission := range permissions {
			if ruleSource, blocked := blockedTools[permission]; blocked {
				findings = append(findings, model.Finding{
					FindingType: "skill_policy_conflict",
					Severity:    model.SeverityHigh,
					ToolType:    "skill",
					Location:    rel,
					Repo:        scope.Repo,
					Org:         fallbackOrg(scope.Org),
					Detector:    detectorID,
					Permissions: []string{permission},
					Evidence: []model.Evidence{
						{Key: "skill", Value: rel},
						{Key: "grant", Value: permission},
						{Key: "conflicting_policy_rule", Value: ruleSource},
					},
				})
			}
		}
	}

	ceilingList := make([]string, 0, len(ceiling))
	for permission := range ceiling {
		ceilingList = append(ceilingList, permission)
	}
	sort.Strings(ceilingList)
	execRatio := ratio(stats.exec, stats.total)
	writeRatio := ratio(stats.write, stats.total)
	execWriteRatio := ratio(stats.exec+stats.write, stats.total)

	findings = append(findings, model.Finding{
		FindingType: "skill_metrics",
		Severity:    model.SeverityMedium,
		ToolType:    "skill",
		Location:    filepath.ToSlash(".agents/skills"),
		Repo:        scope.Repo,
		Org:         fallbackOrg(scope.Org),
		Detector:    detectorID,
		Permissions: ceilingList,
		Evidence: []model.Evidence{
			{Key: "skill_privilege_ceiling", Value: strings.Join(ceilingList, ",")},
			{Key: "skill_privilege_concentration.exec_ratio", Value: execRatio},
			{Key: "skill_privilege_concentration.write_ratio", Value: writeRatio},
			{Key: "skill_privilege_concentration.exec_write_ratio", Value: execWriteRatio},
			{Key: "skill_sprawl.total", Value: fmt.Sprintf("%d", stats.total)},
			{Key: "skill_sprawl.exec", Value: fmt.Sprintf("%d", stats.exec)},
			{Key: "skill_sprawl.write", Value: fmt.Sprintf("%d", stats.write)},
			{Key: "skill_sprawl.read", Value: fmt.Sprintf("%d", stats.read)},
			{Key: "skill_sprawl.none", Value: fmt.Sprintf("%d", stats.none)},
		},
	})

	model.SortFindings(findings)
	return findings, nil
}

func parseAllowedTools(root, rel string) ([]string, *model.ParseError) {
	payload, parseErr := detect.ReadFileWithinRoot(detectorID, root, rel)
	if parseErr != nil {
		return nil, parseErr
	}
	content := string(payload)
	tools := make([]string, 0)

	if strings.HasPrefix(content, "---\n") {
		idx := strings.Index(content[4:], "\n---\n")
		if idx > 0 {
			var front frontmatter
			decoder := yaml.NewDecoder(bytes.NewBufferString(content[4 : 4+idx]))
			if decodeErr := decoder.Decode(&front); decodeErr == nil {
				tools = append(tools, front.AllowedTools...)
			}
		}
	}

	lines := strings.Split(content, "\n")
	inAllowedSection := false
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		lower := strings.ToLower(trimmed)
		if strings.HasPrefix(lower, "allowed-tools:") {
			inAllowedSection = true
			inline := strings.TrimSpace(strings.TrimPrefix(trimmed, "allowed-tools:"))
			if strings.HasPrefix(inline, "[") && strings.HasSuffix(inline, "]") {
				inline = strings.TrimSuffix(strings.TrimPrefix(inline, "["), "]")
				for _, part := range strings.Split(inline, ",") {
					tools = append(tools, strings.Trim(strings.TrimSpace(part), "\""))
				}
			}
			continue
		}
		if strings.HasPrefix(lower, "##") || strings.HasPrefix(lower, "###") {
			inAllowedSection = false
		}
		if inAllowedSection && (strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(trimmed, "* ")) {
			tools = append(tools, strings.TrimSpace(trimmed[2:]))
		}
	}
	return tools, nil
}

func containsPermission(values []string, want string) bool {
	want = strings.TrimSpace(want)
	for _, value := range values {
		if strings.TrimSpace(value) == want {
			return true
		}
	}
	return false
}

func normalizeTools(in []string) []string {
	set := map[string]struct{}{}
	for _, item := range in {
		normalized := normalizeTool(item)
		if normalized == "" {
			continue
		}
		set[normalized] = struct{}{}
	}
	out := make([]string, 0, len(set))
	for item := range set {
		out = append(out, item)
	}
	sort.Strings(out)
	return out
}

func normalizeTool(in string) string {
	trimmed := strings.ToLower(strings.TrimSpace(in))
	switch {
	case trimmed == "":
		return ""
	case strings.Contains(trimmed, "bash"), strings.Contains(trimmed, "exec"), strings.Contains(trimmed, "proc.exec"):
		return "proc.exec"
	case strings.Contains(trimmed, "write"), strings.Contains(trimmed, "file_edit"):
		return "filesystem.write"
	case strings.Contains(trimmed, "read"), strings.Contains(trimmed, "search"), strings.Contains(trimmed, "grep"):
		return "filesystem.read"
	case strings.Contains(trimmed, "mcp"):
		return "mcp.access"
	default:
		return strings.ReplaceAll(trimmed, " ", "_")
	}
}

func ratio(part, total int) string {
	if total == 0 {
		return "0.00"
	}
	return fmt.Sprintf("%.2f", float64(part)/float64(total))
}

func severityForSkill(hasExec, hasWrite bool) string {
	switch {
	case hasExec:
		return model.SeverityHigh
	case hasWrite:
		return model.SeverityMedium
	default:
		return model.SeverityLow
	}
}

func fallbackOrg(org string) string {
	if strings.TrimSpace(org) == "" {
		return "local"
	}
	return org
}
