package scanner

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

type IgnoreMatcher struct {
	patterns []ignorePattern
}

type ignorePattern struct {
	pattern  string
	negate   bool
	dirOnly  bool
	anchored bool
	basename bool
	source   string
}

type defaultIgnorePattern struct {
	pattern string
	reason  string
}

var defaultIgnorePatterns = []defaultIgnorePattern{
	// Dependency directories.
	{pattern: "node_modules/", reason: "dependency directory"},
	{pattern: ".git/", reason: "git metadata"},
	{pattern: ".ua/", reason: "Understand Anything analyzer-owned directory"},
	{pattern: ".agentteam-insight/", reason: "AgentTeam Insight analyzer-owned directory"},
	{pattern: "vendor/", reason: "dependency directory"},
	{pattern: "venv/", reason: "dependency directory"},
	{pattern: ".venv/", reason: "dependency directory"},
	{pattern: "__pycache__/", reason: "dependency directory"},

	// Build output.
	{pattern: "dist/", reason: "build output"},
	{pattern: "build/", reason: "build output"},
	{pattern: "out/", reason: "build output"},
	{pattern: "coverage/", reason: "build output"},
	{pattern: ".next/", reason: "build output"},
	{pattern: ".cache/", reason: "build output"},
	{pattern: ".turbo/", reason: "build output"},
	{pattern: "target/", reason: "build output"},
	{pattern: "obj/", reason: "build output"},

	// Lock files.
	{pattern: "*.lock", reason: "lock file"},
	{pattern: "package-lock.json", reason: "lock file"},
	{pattern: "yarn.lock", reason: "lock file"},
	{pattern: "pnpm-lock.yaml", reason: "lock file"},

	// Binary/asset files.
	{pattern: "*.png", reason: "binary/asset file"},
	{pattern: "*.jpg", reason: "binary/asset file"},
	{pattern: "*.jpeg", reason: "binary/asset file"},
	{pattern: "*.gif", reason: "binary/asset file"},
	{pattern: "*.svg", reason: "binary/asset file"},
	{pattern: "*.ico", reason: "binary/asset file"},
	{pattern: "*.woff", reason: "binary/asset file"},
	{pattern: "*.woff2", reason: "binary/asset file"},
	{pattern: "*.ttf", reason: "binary/asset file"},
	{pattern: "*.eot", reason: "binary/asset file"},
	{pattern: "*.mp3", reason: "binary/asset file"},
	{pattern: "*.mp4", reason: "binary/asset file"},
	{pattern: "*.pdf", reason: "binary/asset file"},
	{pattern: "*.zip", reason: "binary/asset file"},
	{pattern: "*.tar", reason: "binary/asset file"},
	{pattern: "*.gz", reason: "binary/asset file"},

	// Generated files.
	{pattern: "*.min.js", reason: "generated file"},
	{pattern: "*.min.css", reason: "generated file"},
	{pattern: "*.map", reason: "generated file"},
	{pattern: "*.generated.*", reason: "generated file"},

	// IDE/editor.
	{pattern: ".idea/", reason: "IDE/editor directory"},
	{pattern: ".vscode/", reason: "IDE/editor directory"},

	// Misc.
	{pattern: "LICENSE", reason: "default ignore pattern LICENSE"},
	{pattern: ".gitignore", reason: "default ignore pattern .gitignore"},
	{pattern: ".agentteam-insightignore", reason: "graph builder ignore file"},
	{pattern: ".editorconfig", reason: "default ignore pattern .editorconfig"},
	{pattern: ".prettierrc", reason: "default ignore pattern .prettierrc"},
	{pattern: ".eslintrc*", reason: "default ignore pattern .eslintrc*"},
	{pattern: "*.log", reason: "log file"},
}

func LoadIgnoreMatcher(root string) (IgnoreMatcher, error) {
	return LoadIgnoreMatcherWithProjectRoot(root, root)
}

func DefaultIgnoreMatcher() IgnoreMatcher {
	matcher := IgnoreMatcher{}
	for _, item := range defaultIgnorePatterns {
		pattern, ok := parseIgnoreLine(item.pattern, "default")
		if !ok {
			continue
		}
		pattern.source = item.reason
		matcher.patterns = append(matcher.patterns, pattern)
	}
	return matcher
}

func LoadIgnoreMatcherWithProjectRoot(root string, projectRoot string) (IgnoreMatcher, error) {
	matcher := DefaultIgnoreMatcher()
	sources := []struct {
		base string
		name string
	}{
		{base: projectRoot, name: ".agentteam-insightignore"},
		{base: root, name: ".agentteam-insightignore"},
		{base: root, name: ".gitignore"},
	}
	for _, source := range sources {
		patterns, err := readIgnoreFile(filepath.Join(source.base, source.name), source.name)
		if err != nil {
			return matcher, err
		}
		if source.name == ".gitignore" {
			patterns = dropDefaultCoveredGitignorePatterns(patterns)
		}
		matcher.patterns = append(matcher.patterns, patterns...)
	}
	return matcher, nil
}

func readIgnoreFile(path string, source string) ([]ignorePattern, error) {
	file, err := os.Open(path)
	if os.IsNotExist(err) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	defer file.Close()
	patterns := []ignorePattern{}
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		pattern, ok := parseIgnoreLine(scanner.Text(), source)
		if !ok {
			continue
		}
		patterns = append(patterns, pattern)
	}
	return patterns, scanner.Err()
}

func parseIgnoreLine(line string, source string) (ignorePattern, bool) {
	line = strings.TrimSpace(strings.TrimSuffix(line, "\r"))
	if line == "" || strings.HasPrefix(line, "#") {
		return ignorePattern{}, false
	}
	pattern := ignorePattern{source: source}
	if strings.HasPrefix(line, "!") {
		pattern.negate = true
		line = strings.TrimPrefix(line, "!")
	}
	if strings.HasSuffix(line, "/") {
		pattern.dirOnly = true
		line = strings.TrimRight(line, "/")
	}
	if strings.HasPrefix(line, "/") {
		pattern.anchored = true
		line = strings.TrimLeft(line, "/")
	}
	line = filepath.ToSlash(strings.TrimSpace(line))
	if line == "" {
		return ignorePattern{}, false
	}
	pattern.basename = !strings.Contains(line, "/")
	pattern.pattern = line
	return pattern, true
}

func (matcher IgnoreMatcher) Match(rel string, isDir bool) (bool, string) {
	rel = cleanIgnoreRel(rel)
	if rel == "" {
		return false, ""
	}
	ignored := false
	reason := ""
	for _, pattern := range matcher.patterns {
		if pattern.matches(rel, isDir) {
			ignored = !pattern.negate
			if ignored {
				reason = pattern.reason()
			} else {
				reason = ""
			}
		}
	}
	return ignored, reason
}

func (pattern ignorePattern) reason() string {
	if pattern.source != "" && pattern.source != "default" {
		if strings.HasPrefix(pattern.source, ".") || strings.Contains(pattern.source, string(filepath.Separator)) || strings.Contains(pattern.source, "/") {
			return "ignore pattern " + pattern.pattern + " from " + filepath.ToSlash(pattern.source)
		}
		return pattern.source
	}
	return "ignore pattern " + pattern.pattern
}

func cleanIgnoreRel(rel string) string {
	rel = filepath.ToSlash(strings.TrimSpace(strings.TrimPrefix(rel, "./")))
	if rel == "." {
		return ""
	}
	return strings.TrimPrefix(rel, "/")
}

func (pattern ignorePattern) matches(rel string, isDir bool) bool {
	rel = cleanIgnoreRel(rel)
	if rel == "" {
		return false
	}
	if pattern.dirOnly {
		for _, candidate := range directoryCandidates(rel, isDir) {
			if pattern.matchesPath(candidate) {
				return true
			}
		}
		return false
	}
	return pattern.matchesPath(rel)
}

func directoryCandidates(rel string, isDir bool) []string {
	parts := strings.Split(rel, "/")
	limit := len(parts) - 1
	if isDir {
		limit = len(parts)
	}
	if limit < 1 {
		return nil
	}
	candidates := make([]string, 0, limit)
	for i := 1; i <= limit; i++ {
		candidates = append(candidates, strings.Join(parts[:i], "/"))
	}
	return candidates
}

func (pattern ignorePattern) matchesPath(rel string) bool {
	pat := pattern.pattern
	if strings.HasSuffix(pat, "/**") {
		prefix := strings.TrimSuffix(pat, "/**")
		if pattern.matchLiteralOrGlob(prefix, rel) {
			return true
		}
	}
	if pattern.basename {
		for _, part := range strings.Split(rel, "/") {
			if globMatch(pat, part, true) {
				return true
			}
		}
		return false
	}
	return pattern.matchLiteralOrGlob(pat, rel)
}

func (pattern ignorePattern) matchLiteralOrGlob(pat string, rel string) bool {
	if globMatch(pat, rel, pattern.anchored) {
		return true
	}
	if !pattern.anchored && strings.Contains(pat, "/") {
		parts := strings.Split(rel, "/")
		for i := 1; i < len(parts); i++ {
			if globMatch(pat, strings.Join(parts[i:], "/"), true) {
				return true
			}
		}
	}
	return false
}

func globMatch(pattern string, value string, anchored bool) bool {
	if pattern == value {
		return true
	}
	re, err := regexp.Compile(globToRegexp(pattern, anchored))
	if err != nil {
		return false
	}
	return re.MatchString(value)
}

func globToRegexp(pattern string, anchored bool) string {
	var builder strings.Builder
	builder.WriteString("^")
	if !anchored && strings.Contains(pattern, "/") {
		builder.WriteString("(?:.*/)?")
	}
	for i := 0; i < len(pattern); i++ {
		ch := pattern[i]
		switch ch {
		case '*':
			if i+1 < len(pattern) && pattern[i+1] == '*' {
				i++
				if i+1 < len(pattern) && pattern[i+1] == '/' {
					i++
					builder.WriteString("(?:.*/)?")
				} else {
					builder.WriteString(".*")
				}
			} else {
				builder.WriteString("[^/]*")
			}
		case '?':
			builder.WriteString("[^/]")
		case '[':
			end := strings.IndexByte(pattern[i+1:], ']')
			if end >= 0 {
				class := pattern[i : i+end+2]
				builder.WriteString(class)
				i += end + 1
			} else {
				builder.WriteString("\\[")
			}
		case '.', '+', '(', ')', '|', '^', '$', '{', '}', '\\':
			builder.WriteByte('\\')
			builder.WriteByte(ch)
		default:
			builder.WriteByte(ch)
		}
	}
	builder.WriteString("$")
	return builder.String()
}

func dropDefaultCoveredGitignorePatterns(patterns []ignorePattern) []ignorePattern {
	out := patterns[:0]
	for _, pattern := range patterns {
		if !pattern.negate && isCoveredByDefaults(pattern.pattern) {
			continue
		}
		out = append(out, pattern)
	}
	return out
}
