package compliance

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

// Finding represents a single scan result.
type Finding struct {
	ID       string `json:"id"`
	Severity string `json:"severity"` // critical, high, medium, low
	Module   string `json:"module"`   // license, deps, secrets, exposure, vocab
	File     string `json:"file"`
	Line     int    `json:"line"`
	Summary  string `json:"summary"`
}

// ScanResult holds all findings for a module.
type ScanResult struct {
	Module   string    `json:"module"`
	Findings []Finding `json:"findings"`
}

// scanRule defines a pattern to search for.
type scanRule struct {
	ID       string
	Severity string
	Pattern  *regexp.Regexp
	Summary  string
	Globs    []string // file globs to include, empty = all text files
}

// excludedDirs are directories skipped during scanning.
var excludedDirs = map[string]bool{
	"vendor": true, "node_modules": true, ".git": true, ".claude": true,
	"skills": true, // skill documentation, not project source
}

// excludedPaths are relative paths skipped (scanner's own source to avoid self-scan).
var excludedPaths = map[string]bool{
	"shortcuts/compliance": true,
}

// excludedExts are file extensions skipped during scanning.
var excludedExts = map[string]bool{
	".exe": true, ".dll": true, ".so": true, ".dylib": true,
	".bin": true, ".jpg": true, ".jpeg": true, ".png": true,
	".gif": true, ".ico": true, ".svg": true, ".pdf": true,
	".zip": true, ".gz": true, ".tgz": true,
}

// excludeFiles are specific files skipped during scanning.
var excludeFiles = map[string]bool{
	"go.sum": true, "package-lock.json": true,
}

// textExts are extensions treated as text files.
var textExts = map[string]bool{
	".go": true, ".js": true, ".ts": true, ".tsx": true, ".jsx": true,
	".py": true, ".rb": true, ".java": true, ".c": true, ".h": true,
	".cpp": true, ".hpp": true, ".rs": true, ".swift": true, ".kt": true,
	".yaml": true, ".yml": true, ".json": true, ".xml": true, ".toml": true,
	".md": true, ".txt": true, ".sh": true, ".bash": true, ".ps1": true,
	".css": true, ".html": true, ".htm": true, ".sql": true, ".proto": true,
	".cfg": true, ".conf": true, ".ini": true, ".env": true, ".lock": true,
	".mod": true,
}

func isTextFile(path string) bool {
	ext := strings.ToLower(filepath.Ext(path))
	return textExts[ext]
}

func shouldSkip(path string, info os.FileInfo) bool {
	name := info.Name()
	if info.IsDir() {
		if excludedDirs[name] {
			return true
		}
		return false
	}
	if excludedExts[strings.ToLower(filepath.Ext(name))] {
		return true
	}
	if excludeFiles[name] {
		return true
	}
	return false
}

// walkFiles walks the repo and yields text file paths (relative to root).
func walkFiles(root string) ([]string, error) {
	var files []string
	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil
		}
		if shouldSkip(path, info) {
			if info.IsDir() {
				return filepath.SkipDir
			}
			return nil
		}
		if !info.IsDir() && isTextFile(path) {
			rel, _ := filepath.Rel(root, path)
			rel = filepath.ToSlash(rel)
			// skip excluded paths (scanner's own source)
			for prefix := range excludedPaths {
				if strings.HasPrefix(rel, prefix) {
					return nil
				}
			}
			files = append(files, rel)
		}
		return nil
	})
	return files, err
}

// scanFiles scans files against rules and returns deduplicated findings.
// When a line matches multiple rules, only the highest-severity rule is reported.
func scanFiles(root string, rules []scanRule) []Finding {
	files, err := walkFiles(root)
	if err != nil {
		return []Finding{{ID: "ERR", Severity: "critical", Module: "scanner", Summary: fmt.Sprintf("walk error: %v", err)}}
	}

	// dedup by file+line, keeping the highest severity
	sevRank := map[string]int{"critical": 4, "high": 3, "medium": 2, "low": 1}
	seen := make(map[string]Finding) // key: "file:line"

	for _, f := range files {
		for _, rule := range rules {
			if !ruleMatchesFile(f, rule.Globs) {
				continue
			}
			for _, m := range scanFile(filepath.Join(root, f), rule) {
				key := fmt.Sprintf("%s:%d", m.File, m.Line)
				if prev, ok := seen[key]; !ok || sevRank[m.Severity] > sevRank[prev.Severity] {
					seen[key] = m
				}
			}
		}
	}

	var findings []Finding
	for _, f := range seen {
		findings = append(findings, f)
	}
	return findings
}

func ruleMatchesFile(file string, globs []string) bool {
	if len(globs) == 0 {
		return true
	}
	for _, g := range globs {
		matched, _ := filepath.Match(g, filepath.Base(file))
		if matched {
			return true
		}
	}
	return false
}

func scanFile(path string, rule scanRule) []Finding {
	f, err := os.Open(path)
	if err != nil {
		return nil
	}
	defer f.Close()

	var findings []Finding
	scanner := bufio.NewScanner(f)
	lineNum := 0
	for scanner.Scan() {
		lineNum++
		if rule.Pattern.MatchString(scanner.Text()) {
			findings = append(findings, Finding{
				ID:       rule.ID,
				Severity: rule.Severity,
				Module:   ruleMod(rule.ID),
				File:     path,
				Line:     lineNum,
				Summary:  rule.Summary,
			})
		}
	}
	return findings
}

func ruleMod(id string) string {
	switch {
	case strings.HasPrefix(id, "L-"):
		return "license"
	case strings.HasPrefix(id, "D-"):
		return "deps"
	case strings.HasPrefix(id, "S-"):
		return "secrets"
	case strings.HasPrefix(id, "P-"), strings.HasPrefix(id, "E-"):
		return "exposure"
	case strings.HasPrefix(id, "C-"):
		return "vocab"
	}
	return "unknown"
}
