// Command i18n-validate scans Go source files for i18n key usage and validates
// them against the locale JSON files.
//
// Usage:
//
//	go run ./cmd/i18n-validate ./...
//	go run ./cmd/i18n-validate ./pkg/cli ./cmd/dev
//
// The validator checks:
//   - T("key") calls - validates key exists in locale files
//   - C("intent", ...) calls - validates intent exists in registered intents
//   - i18n.T("key") and i18n.C("intent", ...) qualified calls
//
// Exit codes:
//   - 0: All keys valid
//   - 1: Missing keys found
//   - 2: Error during validation
package main

import (
	"encoding/json"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

// KeyUsage records where a key is used in the source code.
type KeyUsage struct {
	Key      string
	File     string
	Line     int
	Function string // "T" or "C"
}

// ValidationResult holds the results of validation.
type ValidationResult struct {
	TotalKeys   int
	ValidKeys   int
	MissingKeys []KeyUsage
	IntentKeys  int
	MessageKeys int
}

func main() {
	if len(os.Args) < 2 {
		fmt.Fprintln(os.Stderr, "Usage: i18n-validate <packages...>")
		fmt.Fprintln(os.Stderr, "Example: i18n-validate ./...")
		os.Exit(2)
	}

	// Find the project root (where locales are)
	root, err := findProjectRoot()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error finding project root: %v\n", err)
		os.Exit(2)
	}

	// Load valid keys from locale files
	validKeys, err := loadValidKeys(filepath.Join(root, "pkg/i18n/locales"))
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error loading locale files: %v\n", err)
		os.Exit(2)
	}

	// Load valid intents
	validIntents := loadValidIntents()

	// Scan source files
	usages, err := scanPackages(os.Args[1:])
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error scanning packages: %v\n", err)
		os.Exit(2)
	}

	// Validate
	result := validate(usages, validKeys, validIntents)

	// Report
	printReport(result)

	if len(result.MissingKeys) > 0 {
		os.Exit(1)
	}
}

// findProjectRoot finds the project root by looking for go.mod.
func findProjectRoot() (string, error) {
	dir, err := os.Getwd()
	if err != nil {
		return "", err
	}

	for {
		if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
			return dir, nil
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			return "", fmt.Errorf("could not find go.mod in any parent directory")
		}
		dir = parent
	}
}

// loadValidKeys loads all valid keys from locale JSON files.
func loadValidKeys(localesDir string) (map[string]bool, error) {
	keys := make(map[string]bool)

	entries, err := os.ReadDir(localesDir)
	if err != nil {
		return nil, fmt.Errorf("reading locales dir: %w", err)
	}

	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
			continue
		}

		data, err := os.ReadFile(filepath.Join(localesDir, entry.Name()))
		if err != nil {
			return nil, fmt.Errorf("reading %s: %w", entry.Name(), err)
		}

		var raw map[string]any
		if err := json.Unmarshal(data, &raw); err != nil {
			return nil, fmt.Errorf("parsing %s: %w", entry.Name(), err)
		}

		extractKeys("", raw, keys)
	}

	return keys, nil
}

// extractKeys recursively extracts flattened keys from nested JSON.
func extractKeys(prefix string, data map[string]any, out map[string]bool) {
	for key, value := range data {
		fullKey := key
		if prefix != "" {
			fullKey = prefix + "." + key
		}

		switch v := value.(type) {
		case string:
			out[fullKey] = true
		case map[string]any:
			// Check if it's a plural/verb/noun object (has specific keys)
			if isPluralOrGrammarObject(v) {
				out[fullKey] = true
			} else {
				extractKeys(fullKey, v, out)
			}
		}
	}
}

// isPluralOrGrammarObject checks if a map is a leaf object (plural forms, verb forms, etc).
func isPluralOrGrammarObject(m map[string]any) bool {
	// CLDR plural keys
	_, hasOne := m["one"]
	_, hasOther := m["other"]
	_, hasZero := m["zero"]
	_, hasTwo := m["two"]
	_, hasFew := m["few"]
	_, hasMany := m["many"]

	// Grammar keys
	_, hasPast := m["past"]
	_, hasGerund := m["gerund"]
	_, hasGender := m["gender"]
	_, hasBase := m["base"]

	// Article keys
	_, hasDefault := m["default"]
	_, hasVowel := m["vowel"]

	if hasOne || hasOther || hasZero || hasTwo || hasFew || hasMany {
		return true
	}
	if hasPast || hasGerund || hasGender || hasBase {
		return true
	}
	if hasDefault || hasVowel {
		return true
	}

	return false
}

// loadValidIntents returns the set of valid intent keys.
func loadValidIntents() map[string]bool {
	// Core intents - these match what's defined in intents.go
	return map[string]bool{
		// Destructive
		"core.delete":    true,
		"core.remove":    true,
		"core.discard":   true,
		"core.reset":     true,
		"core.overwrite": true,
		// Creation
		"core.create": true,
		"core.add":    true,
		"core.clone":  true,
		"core.copy":   true,
		// Modification
		"core.save":   true,
		"core.update": true,
		"core.rename": true,
		"core.move":   true,
		// Git
		"core.commit": true,
		"core.push":   true,
		"core.pull":   true,
		"core.merge":  true,
		"core.rebase": true,
		// Network
		"core.install":  true,
		"core.download": true,
		"core.upload":   true,
		"core.publish":  true,
		"core.deploy":   true,
		// Process
		"core.start":   true,
		"core.stop":    true,
		"core.restart": true,
		"core.run":     true,
		"core.build":   true,
		"core.test":    true,
		// Information
		"core.continue": true,
		"core.proceed":  true,
		"core.confirm":  true,
		// Additional
		"core.sync":     true,
		"core.boot":     true,
		"core.format":   true,
		"core.analyse":  true,
		"core.link":     true,
		"core.unlink":   true,
		"core.fetch":    true,
		"core.generate": true,
		"core.validate": true,
		"core.check":    true,
		"core.scan":     true,
	}
}

// scanPackages scans Go packages for i18n key usage.
func scanPackages(patterns []string) ([]KeyUsage, error) {
	var usages []KeyUsage

	for _, pattern := range patterns {
		// Expand pattern
		matches, err := expandPattern(pattern)
		if err != nil {
			return nil, fmt.Errorf("expanding pattern %q: %w", pattern, err)
		}

		for _, dir := range matches {
			dirUsages, err := scanDirectory(dir)
			if err != nil {
				return nil, fmt.Errorf("scanning %s: %w", dir, err)
			}
			usages = append(usages, dirUsages...)
		}
	}

	return usages, nil
}

// expandPattern expands a Go package pattern to directories.
func expandPattern(pattern string) ([]string, error) {
	// Handle ./... or ... pattern
	if strings.HasSuffix(pattern, "...") {
		base := strings.TrimSuffix(pattern, "...")
		base = strings.TrimSuffix(base, "/")
		if base == "" || base == "." {
			base = "."
		}
		return findAllGoDirs(base)
	}

	// Single directory
	return []string{pattern}, nil
}

// findAllGoDirs finds all directories containing .go files.
func findAllGoDirs(root string) ([]string, error) {
	var dirs []string
	seen := make(map[string]bool)

	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil // Continue walking even on error
		}

		if info == nil {
			return nil
		}

		// Skip vendor, testdata, and hidden directories (but not . itself)
		if info.IsDir() {
			name := info.Name()
			if name == "vendor" || name == "testdata" || (strings.HasPrefix(name, ".") && name != ".") {
				return filepath.SkipDir
			}
			return nil
		}

		// Check for .go files
		if strings.HasSuffix(path, ".go") {
			dir := filepath.Dir(path)
			if !seen[dir] {
				seen[dir] = true
				dirs = append(dirs, dir)
			}
		}

		return nil
	})

	return dirs, err
}

// scanDirectory scans a directory for i18n key usage.
func scanDirectory(dir string) ([]KeyUsage, error) {
	var usages []KeyUsage

	fset := token.NewFileSet()
	// Parse all .go files except those ending exactly in _test.go
	pkgs, err := parser.ParseDir(fset, dir, func(fi os.FileInfo) bool {
		name := fi.Name()
		// Only exclude files that are actual test files (ending in _test.go)
		// Files like "go_test_cmd.go" should be included
		return strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go")
	}, 0)
	if err != nil {
		return nil, err
	}

	for _, pkg := range pkgs {
		for filename, file := range pkg.Files {
			fileUsages := scanFile(fset, filename, file)
			usages = append(usages, fileUsages...)
		}
	}

	return usages, nil
}

// scanFile scans a single file for i18n key usage.
func scanFile(fset *token.FileSet, filename string, file *ast.File) []KeyUsage {
	var usages []KeyUsage

	ast.Inspect(file, func(n ast.Node) bool {
		call, ok := n.(*ast.CallExpr)
		if !ok {
			return true
		}

		funcName := getFuncName(call)
		if funcName == "" {
			return true
		}

		// Check for T(), C(), i18n.T(), i18n.C()
		switch funcName {
		case "T", "i18n.T", "_", "i18n._":
			if key := extractStringArg(call, 0); key != "" {
				pos := fset.Position(call.Pos())
				usages = append(usages, KeyUsage{
					Key:      key,
					File:     filename,
					Line:     pos.Line,
					Function: "T",
				})
			}
		case "C", "i18n.C":
			if key := extractStringArg(call, 0); key != "" {
				pos := fset.Position(call.Pos())
				usages = append(usages, KeyUsage{
					Key:      key,
					File:     filename,
					Line:     pos.Line,
					Function: "C",
				})
			}
		case "I", "i18n.I":
			if key := extractStringArg(call, 0); key != "" {
				pos := fset.Position(call.Pos())
				usages = append(usages, KeyUsage{
					Key:      key,
					File:     filename,
					Line:     pos.Line,
					Function: "C", // I() is an intent builder
				})
			}
		}

		return true
	})

	return usages
}

// getFuncName extracts the function name from a call expression.
func getFuncName(call *ast.CallExpr) string {
	switch fn := call.Fun.(type) {
	case *ast.Ident:
		return fn.Name
	case *ast.SelectorExpr:
		if ident, ok := fn.X.(*ast.Ident); ok {
			return ident.Name + "." + fn.Sel.Name
		}
	}
	return ""
}

// extractStringArg extracts a string literal from a call argument.
func extractStringArg(call *ast.CallExpr, index int) string {
	if index >= len(call.Args) {
		return ""
	}

	arg := call.Args[index]

	// Direct string literal
	if lit, ok := arg.(*ast.BasicLit); ok && lit.Kind == token.STRING {
		// Remove quotes
		s := lit.Value
		if len(s) >= 2 {
			return s[1 : len(s)-1]
		}
	}

	// Identifier (constant reference) - we skip these as they're type-safe
	if _, ok := arg.(*ast.Ident); ok {
		return "" // Skip constants like IntentCoreDelete
	}

	// Selector (like i18n.IntentCoreDelete) - skip these too
	if _, ok := arg.(*ast.SelectorExpr); ok {
		return ""
	}

	return ""
}

// validate validates key usages against valid keys and intents.
func validate(usages []KeyUsage, validKeys, validIntents map[string]bool) ValidationResult {
	result := ValidationResult{
		TotalKeys: len(usages),
	}

	for _, usage := range usages {
		if usage.Function == "C" {
			result.IntentKeys++
			// Check intent keys
			if validIntents[usage.Key] {
				result.ValidKeys++
			} else {
				// Also allow custom intents (non-core.* prefix)
				if !strings.HasPrefix(usage.Key, "core.") {
					result.ValidKeys++ // Assume custom intents are valid
				} else {
					result.MissingKeys = append(result.MissingKeys, usage)
				}
			}
		} else {
			result.MessageKeys++
			// Check message keys
			if validKeys[usage.Key] {
				result.ValidKeys++
			} else if strings.HasPrefix(usage.Key, "core.") {
				// core.* keys used with T() are intent keys
				if validIntents[usage.Key] {
					result.ValidKeys++
				} else {
					result.MissingKeys = append(result.MissingKeys, usage)
				}
			} else {
				result.MissingKeys = append(result.MissingKeys, usage)
			}
		}
	}

	return result
}

// printReport prints the validation report.
func printReport(result ValidationResult) {
	fmt.Printf("i18n Validation Report\n")
	fmt.Printf("======================\n\n")
	fmt.Printf("Total keys scanned:  %d\n", result.TotalKeys)
	fmt.Printf("  Message keys (T):  %d\n", result.MessageKeys)
	fmt.Printf("  Intent keys (C):   %d\n", result.IntentKeys)
	fmt.Printf("Valid keys:          %d\n", result.ValidKeys)
	fmt.Printf("Missing keys:        %d\n", len(result.MissingKeys))

	if len(result.MissingKeys) > 0 {
		fmt.Printf("\nMissing Keys:\n")
		fmt.Printf("-------------\n")

		// Sort by file then line
		sort.Slice(result.MissingKeys, func(i, j int) bool {
			if result.MissingKeys[i].File != result.MissingKeys[j].File {
				return result.MissingKeys[i].File < result.MissingKeys[j].File
			}
			return result.MissingKeys[i].Line < result.MissingKeys[j].Line
		})

		for _, usage := range result.MissingKeys {
			fmt.Printf("  %s:%d: %s(%q)\n", usage.File, usage.Line, usage.Function, usage.Key)
		}

		fmt.Printf("\nAdd these keys to pkg/i18n/locales/en_GB.json or use constants from pkg/i18n/keys.go\n")
	} else {
		fmt.Printf("\nAll keys are valid!\n")
	}
}
