package registry

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// IndexResult holds stats from an indexing run.
type IndexResult struct {
	Functions        int
	Types            int
	Apps             int
	Analysis         int
	UnitTests        int
	ValidationErrors []string
	Warnings         []string
	Errors           []string
}

// Index walks the registry root, parses all .md files, validates integrity,
// and populates the database. It uses two passes:
// 1. Parse all entries and collect known IDs
// 2. Validate references against known IDs, then insert valid entries
//
// Scans functions/ and types/ at the root level, plus any language-specific
// directories (e.g. python/functions/, python/types/).
func Index(db *DB, root string) (*IndexResult, error) {
	// Load existing timestamps before purging so we can preserve created_at
	oldFuncs, oldTypes, oldApps, oldAnalysis, err := db.LoadTimestamps()
	if err != nil {
		return nil, fmt.Errorf("loading timestamps: %w", err)
	}

	result := &IndexResult{}

	// Pass 1: parse everything from all source directories
	var functions []*Function
	var types []*Type

	// Directories to scan for functions and types.
	funcDirs := []string{filepath.Join(root, "functions")}
	typeDirs := []string{filepath.Join(root, "types")}

	// Discover language-specific directories (e.g. python/functions/, python/types/)
	entries, _ := os.ReadDir(root)
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		langFuncs := filepath.Join(root, e.Name(), "functions")
		if fi, err := os.Stat(langFuncs); err == nil && fi.IsDir() {
			funcDirs = append(funcDirs, langFuncs)
		}
		langTypes := filepath.Join(root, e.Name(), "types")
		if fi, err := os.Stat(langTypes); err == nil && fi.IsDir() {
			typeDirs = append(typeDirs, langTypes)
		}
	}

	for _, dir := range funcDirs {
		walkMD(dir, func(path string) {
			f, err := ParseFunctionMD(path, root)
			if err != nil {
				result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", path, err))
				return
			}
			functions = append(functions, f)
		})
	}

	for _, dir := range typeDirs {
		walkMD(dir, func(path string) {
			t, err := ParseTypeMD(path, root)
			if err != nil {
				result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", path, err))
				return
			}
			types = append(types, t)
		})
	}

	// Parse apps from apps/*/app.md
	var apps []*App
	localAppIDs := make(map[string]bool)
	appsDir := filepath.Join(root, "apps")
	if fi, err := os.Stat(appsDir); err == nil && fi.IsDir() {
		entries, _ := os.ReadDir(appsDir)
		for _, e := range entries {
			if !e.IsDir() {
				continue
			}
			appMD := filepath.Join(appsDir, e.Name(), "app.md")
			if _, err := os.Stat(appMD); err != nil {
				continue
			}
			a, err := ParseAppMD(appMD, root)
			if err != nil {
				result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", appMD, err))
				continue
			}
			apps = append(apps, a)
			localAppIDs[a.ID] = true
		}
	}

	// Parse analysis from analysis/*/analysis.md
	var analyses []*Analysis
	localAnalysisIDs := make(map[string]bool)
	analysisDir := filepath.Join(root, "analysis")
	if fi, err := os.Stat(analysisDir); err == nil && fi.IsDir() {
		entries, _ := os.ReadDir(analysisDir)
		for _, e := range entries {
			if !e.IsDir() {
				continue
			}
			analysisMD := filepath.Join(analysisDir, e.Name(), "analysis.md")
			if _, err := os.Stat(analysisMD); err != nil {
				continue
			}
			an, err := ParseAnalysisMD(analysisMD, root)
			if err != nil {
				result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", analysisMD, err))
				continue
			}
			analyses = append(analyses, an)
			localAnalysisIDs[an.ID] = true
		}
	}

	// Selective purge: preserve remote-only apps/analysis (have repo_url, not cloned locally)
	if err := db.PurgeLocalOnly(localAppIDs, localAnalysisIDs); err != nil {
		return nil, fmt.Errorf("purging database: %w", err)
	}

	// Build known ID sets
	knownFunctions := make(map[string]bool, len(functions))
	for _, f := range functions {
		knownFunctions[f.ID] = true
	}
	knownTypes := make(map[string]bool, len(types))
	for _, t := range types {
		knownTypes[t.ID] = true
	}

	now := time.Now().UTC()

	// Pass 2: validate, assign timestamps via hash comparison, and insert
	for _, t := range types {
		if verr := ValidateType(t, knownTypes); verr != nil {
			result.ValidationErrors = append(result.ValidationErrors, verr.Error())
			continue
		}
		t.ContentHash = ComputeTypeHash(t)
		applyTimestamps(&t.CreatedAt, &t.UpdatedAt, t.ContentHash, oldTypes[t.ID], now)
		if err := db.InsertType(t); err != nil {
			result.Errors = append(result.Errors, fmt.Sprintf("insert %s: %v", t.ID, err))
			continue
		}
		result.Types++
	}

	for _, f := range functions {
		if verr := ValidateFunction(f, knownFunctions, knownTypes); verr != nil {
			result.ValidationErrors = append(result.ValidationErrors, verr.Error())
			continue
		}
		f.ContentHash = ComputeFunctionHash(f)
		applyTimestamps(&f.CreatedAt, &f.UpdatedAt, f.ContentHash, oldFuncs[f.ID], now)
		if err := db.InsertFunction(f); err != nil {
			result.Errors = append(result.Errors, fmt.Sprintf("insert %s: %v", f.ID, err))
			continue
		}
		result.Functions++
	}

	for _, a := range apps {
		if verr := ValidateApp(a, knownFunctions, knownTypes); verr != nil {
			result.ValidationErrors = append(result.ValidationErrors, verr.Error())
			continue
		}
		a.ContentHash = ComputeAppHash(a)
		applyTimestamps(&a.CreatedAt, &a.UpdatedAt, a.ContentHash, oldApps[a.ID], now)
		if err := db.InsertApp(a); err != nil {
			result.Errors = append(result.Errors, fmt.Sprintf("insert %s: %v", a.ID, err))
			continue
		}
		result.Apps++
	}

	for _, an := range analyses {
		if verr := ValidateAnalysis(an, knownFunctions, knownTypes); verr != nil {
			result.ValidationErrors = append(result.ValidationErrors, verr.Error())
			continue
		}
		an.ContentHash = ComputeAnalysisHash(an)
		applyTimestamps(&an.CreatedAt, &an.UpdatedAt, an.ContentHash, oldAnalysis[an.ID], now)
		if err := db.InsertAnalysis(an); err != nil {
			result.Errors = append(result.Errors, fmt.Sprintf("insert %s: %v", an.ID, err))
			continue
		}
		result.Analysis++
	}

	// Extract unit tests from test files of tested functions
	if err := db.PurgeUnitTests(); err != nil {
		result.Warnings = append(result.Warnings, fmt.Sprintf("purging unit_tests: %v", err))
	}
	for _, f := range functions {
		if !f.Tested || f.TestFilePath == "" {
			continue
		}
		absTestPath := filepath.Join(root, f.TestFilePath)
		cases, err := parseTestFile(absTestPath, f.Lang)
		if err != nil {
			result.Warnings = append(result.Warnings, fmt.Sprintf("%s: parsing tests: %v", f.ID, err))
			continue
		}
		for i, tc := range cases {
			ut := &UnitTest{
				ID:         fmt.Sprintf("%s_t%d", f.ID, i),
				FunctionID: f.ID,
				Name:       tc.Name,
				Code:       tc.Code,
				FilePath:   f.TestFilePath,
				Lang:       f.Lang,
				CreatedAt:  now,
				UpdatedAt:  now,
			}
			if err := db.InsertUnitTest(ut); err != nil {
				result.Warnings = append(result.Warnings, fmt.Sprintf("insert unit_test %s: %v", ut.ID, err))
				continue
			}
			result.UnitTests++
		}
	}

	// Post-insert: warn about file_path entries that don't exist on disk
	// and functions missing params_schema
	missingParams := 0
	for _, f := range functions {
		if f.FilePath != "" {
			abs := filepath.Join(root, f.FilePath)
			if _, err := os.Stat(abs); err != nil {
				result.Warnings = append(result.Warnings, fmt.Sprintf("%s: file_path %q not found", f.ID, f.FilePath))
			}
		}
		if f.TestFilePath != "" {
			abs := filepath.Join(root, f.TestFilePath)
			if _, err := os.Stat(abs); err != nil {
				result.Warnings = append(result.Warnings, fmt.Sprintf("%s: test_file_path %q not found", f.ID, f.TestFilePath))
			}
		}
		if f.ParamsSchema == "" {
			missingParams++
		}
	}
	if missingParams > 0 {
		result.Warnings = append(result.Warnings, fmt.Sprintf("%d functions missing params_schema (run 'fn check params' to list)", missingParams))
	}
	for _, t := range types {
		if t.FilePath != "" {
			abs := filepath.Join(root, t.FilePath)
			if _, err := os.Stat(abs); err != nil {
				result.Warnings = append(result.Warnings, fmt.Sprintf("%s: file_path %q not found", t.ID, t.FilePath))
			}
		}
	}

	return result, nil
}

// applyTimestamps sets created_at and updated_at based on whether the entry
// existed before and whether its content changed.
//   - New entry (no old record): both set to now
//   - Unchanged (hash matches): both preserved from old record
//   - Changed (hash differs): created_at preserved, updated_at set to now
func applyTimestamps(createdAt, updatedAt *time.Time, newHash string, old timestampRecord, now time.Time) {
	if old.CreatedAt.IsZero() {
		// New entry
		*createdAt = now
		*updatedAt = now
		return
	}
	// Existing entry — always preserve created_at
	*createdAt = old.CreatedAt
	if old.ContentHash == newHash {
		// No changes — preserve updated_at too
		*updatedAt = old.UpdatedAt
	} else {
		*updatedAt = now
	}
}

// walkMD walks a directory recursively and calls fn for each .md file found.
func walkMD(dir string, fn func(path string)) {
	if _, err := os.Stat(dir); err != nil {
		return
	}
	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if err != nil || info.IsDir() || !strings.HasSuffix(path, ".md") {
			return nil
		}
		fn(path)
		return nil
	})
}
