package infra

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

// DataTableUsageEntry reporta el estado de la integracion data_table en una app.
type DataTableUsageEntry struct {
	AppName     string    `json:"app_name"`
	AppID       string    `json:"app_id"`
	DirPath     string    `json:"dir_path"`
	DeclaresUse bool      `json:"declares_use"`  // uses_modules incluye data_table_cpp
	Findings    []Finding `json:"findings"`
	Status      string    `json:"status"` // "ok" | "warn" | "n/a"
}

// Finding describe un anti-patron detectado en la integracion data_table de una app.
type Finding struct {
	Kind     string `json:"kind"`    // inline_begintable | state_not_persistent | no_child_host | no_event_sink | cmake_missing_link
	Severity string `json:"severity"` // "error" | "warn" | "info"
	File     string `json:"file"`
	Line     int    `json:"line"`    // 0 si no aplica
	Snippet  string `json:"snippet"` // contexto opcional (hasta 120 chars)
}

// regexes pre-compilados para los detectores
var (
	// data_table::State declarada en una funcion (sin static ni thread_local).
	// Captura lineas con "data_table::State <ident>;" que no tengan static/thread_local antes.
	reStateDecl = regexp.MustCompile(`\bdata_table::State\s+\w+\s*;`)

	// static o thread_local precediendo data_table::State en la misma linea.
	reStaticState = regexp.MustCompile(`(?:static|thread_local)\s+data_table::State`)

	// Deteccion heuristica de inicio de funcion (cobertura parcial).
	reFuncOpen = regexp.MustCompile(`(?:static\s+)?(?:void|bool|int|ImVec2|auto|std::\w+)\s+\w+\s*\(`)

	// ImGui::BeginTable inline (fuera de data_table::render).
	reBeginTable = regexp.MustCompile(`ImGui::BeginTable\s*\(`)

	// data_table::render call.
	reRender = regexp.MustCompile(`data_table::render\s*\(`)

	// BeginChild o Begin inmediatamente antes del render (heuristica 30 lineas).
	reBeginChildOrBegin = regexp.MustCompile(`ImGui::(?:BeginChild|Begin)\s*\(`)

	// Presencia de event sink en el archivo.
	reEventSink = regexp.MustCompile(`(?:events_out|TableEvent|TableEventKind)`)
)

// AuditDataTableUsage escanea todas las apps cpp del registry, detecta anti-patrones
// en su uso de data_table::render, y devuelve entries por app.
//
// Detecta:
//   - inline_begintable: llamada directa a ImGui::BeginTable sin pasar por data_table::render.
//   - state_not_persistent: data_table::State sin static/thread_local (stack local).
//   - no_child_host: data_table::render sin BeginChild/Begin en las 30 lineas previas.
//   - no_event_sink: usa data_table pero no captura TableEvent/events_out (severity info).
//   - cmake_missing_link: uses_modules incluye data_table_cpp pero CMakeLists no enlaza fn_module_data_table.
func AuditDataTableUsage(registryRoot string) ([]DataTableUsageEntry, error) {
	candidates, err := findAppDirs(registryRoot)
	if err != nil {
		return nil, fmt.Errorf("audit_data_table_usage: find app dirs: %w", err)
	}

	var results []DataTableUsageEntry

	for _, dir := range candidates {
		appMDPath := filepath.Join(dir, "app.md")
		raw, err := os.ReadFile(appMDPath)
		if err != nil {
			continue
		}
		fm, err := parseFrontmatterRaw(string(raw))
		if err != nil {
			continue
		}
		if fm.Lang != "cpp" {
			continue
		}

		declaresUse := dtSliceContains(fm.UsesModules, "data_table_cpp")
		relDir, _ := filepath.Rel(registryRoot, dir)

		entry := DataTableUsageEntry{
			AppName:     fm.Name,
			AppID:       buildID(fm),
			DirPath:     relDir,
			DeclaresUse: declaresUse,
			Status:      "n/a",
		}

		if !declaresUse {
			results = append(results, entry)
			continue
		}

		absDir := dir
		if !filepath.IsAbs(absDir) {
			absDir = filepath.Join(registryRoot, dir)
		}
		if _, err := os.Stat(absDir); os.IsNotExist(err) {
			entry.Status = "warn"
			entry.Findings = append(entry.Findings, Finding{
				Kind:     "directory_missing",
				Severity: "warn",
				File:     relDir,
			})
			results = append(results, entry)
			continue
		}

		// 1. cmake_missing_link
		cmakePath := filepath.Join(absDir, "CMakeLists.txt")
		if cmakeFindings := auditCMakeLink(cmakePath, relDir); len(cmakeFindings) > 0 {
			entry.Findings = append(entry.Findings, cmakeFindings...)
		}

		// Collect all .cpp and .h files (1 level + subdirs), excluding vendor/ build/ .git/
		sources, err := collectSourceFiles(absDir)
		if err != nil {
			entry.Status = "warn"
			results = append(results, entry)
			continue
		}

		// 2–5. Per-file analysis
		for _, srcPath := range sources {
			data, err := os.ReadFile(srcPath)
			if err != nil {
				continue
			}
			src := string(data)
			relSrc, _ := filepath.Rel(registryRoot, srcPath)

			entry.Findings = append(entry.Findings, auditInlineBeginTable(src, relSrc)...)
			entry.Findings = append(entry.Findings, auditStateNotPersistent(src, relSrc)...)
			entry.Findings = append(entry.Findings, auditNoChildHost(src, relSrc)...)
		}

		// 4. no_event_sink: check across ALL source files for event sink presence
		if noEventSinkFinding := auditNoEventSink(sources, relDir); noEventSinkFinding != nil {
			entry.Findings = append(entry.Findings, *noEventSinkFinding)
		}

		// Derive status
		hasError := false
		hasWarn := false
		for _, f := range entry.Findings {
			switch f.Severity {
			case "error":
				hasError = true
			case "warn":
				hasWarn = true
			}
		}
		switch {
		case hasError || hasWarn:
			entry.Status = "warn"
		default:
			entry.Status = "ok"
		}

		results = append(results, entry)
	}

	sort.Slice(results, func(i, j int) bool { return results[i].AppID < results[j].AppID })
	return results, nil
}

// auditCMakeLink verifica que CMakeLists.txt enlaza fn_module_data_table.
func auditCMakeLink(cmakePath, relDir string) []Finding {
	data, err := os.ReadFile(cmakePath)
	if err != nil {
		// No CMakeLists at all — report as error since app declares data_table_cpp
		return []Finding{{
			Kind:     "cmake_missing_link",
			Severity: "error",
			File:     relDir,
			Snippet:  "CMakeLists.txt not found",
		}}
	}
	if !strings.Contains(string(data), "fn_module_data_table") {
		return []Finding{{
			Kind:     "cmake_missing_link",
			Severity: "error",
			File:     strings.TrimSuffix(relDir, "/") + "/CMakeLists.txt",
			Snippet:  "target_link_libraries missing fn_module_data_table",
		}}
	}
	return nil
}

// auditInlineBeginTable detecta llamadas ImGui::BeginTable que no son parte de
// data_table::render. Heuristica: si el archivo llama ImGui::BeginTable en una
// linea que no es parte de la implementacion del modulo data_table.
func auditInlineBeginTable(src, relFile string) []Finding {
	lines := strings.Split(src, "\n")
	var findings []Finding
	for i, line := range lines {
		if !reBeginTable.MatchString(line) {
			continue
		}
		// False-positive suppression: si el archivo ES parte del modulo data_table, ignorar
		if strings.Contains(relFile, "data_table") && strings.Contains(relFile, "modules") {
			continue
		}
		findings = append(findings, Finding{
			Kind:     "inline_begintable",
			Severity: "warn",
			File:     relFile,
			Line:     i + 1,
			Snippet:  truncateSnippet(strings.TrimSpace(line), 120),
		})
	}
	return findings
}

// auditStateNotPersistent detecta data_table::State declarada en el body de una
// funcion sin static ni thread_local. Heuristica: busca la linea con la declaracion
// y retrocede hasta encontrar si hay un reFuncOpen antes que un closing brace a
// nivel 0. Si la declaracion no tiene static/thread_local en la misma linea,
// y esta dentro de un bloque de funcion (heuristica de depth de llaves), reporta.
func auditStateNotPersistent(src, relFile string) []Finding {
	lines := strings.Split(src, "\n")
	var findings []Finding

	for i, line := range lines {
		if !reStateDecl.MatchString(line) {
			continue
		}
		// Si la misma linea tiene static o thread_local -> OK
		if reStaticState.MatchString(line) {
			continue
		}
		// Check context: si pertenece a una struct/class member (no dentro de funcion)
		// Heuristica: mirar hacia atras si antes de depth>0 hay un { de funcion
		if isInsideFunctionBody(lines, i) {
			findings = append(findings, Finding{
				Kind:     "state_not_persistent",
				Severity: "warn",
				File:     relFile,
				Line:     i + 1,
				Snippet:  truncateSnippet(strings.TrimSpace(line), 120),
			})
		}
	}
	return findings
}

// isInsideFunctionBody retorna true si la linea en `lineIdx` esta dentro del
// cuerpo de una funcion (heuristica de balance de llaves retrocediendo).
func isInsideFunctionBody(lines []string, lineIdx int) bool {
	depth := 0
	for i := lineIdx - 1; i >= 0; i-- {
		line := lines[i]
		for _, ch := range line {
			switch ch {
			case '}':
				depth++
			case '{':
				depth--
			}
		}
		if depth < 0 {
			// Encontramos la apertura del bloque que nos contiene
			// Si esa linea parece un function header, es funcion body
			return reFuncOpen.MatchString(line)
		}
	}
	return false
}

// auditNoChildHost detecta llamadas a data_table::render sin BeginChild/Begin
// en las 30 lineas previas.
func auditNoChildHost(src, relFile string) []Finding {
	lines := strings.Split(src, "\n")
	var findings []Finding
	for i, line := range lines {
		if !reRender.MatchString(line) {
			continue
		}
		// Skip comment lines (// ...) — they contain "data_table::render" as text, not calls.
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "//") {
			continue
		}
		// Look back up to 30 lines for BeginChild or Begin
		start := i - 30
		if start < 0 {
			start = 0
		}
		found := false
		for j := start; j < i; j++ {
			if reBeginChildOrBegin.MatchString(lines[j]) {
				found = true
				break
			}
		}
		if !found {
			findings = append(findings, Finding{
				Kind:     "no_child_host",
				Severity: "warn",
				File:     relFile,
				Line:     i + 1,
				Snippet:  truncateSnippet(strings.TrimSpace(line), 120),
			})
		}
	}
	return findings
}

// auditNoEventSink verifica que al menos un archivo fuente use TableEvent/events_out.
// Si ninguno lo hace, retorna un finding de severidad info.
func auditNoEventSink(sources []string, relDir string) *Finding {
	for _, srcPath := range sources {
		data, err := os.ReadFile(srcPath)
		if err != nil {
			continue
		}
		if reEventSink.Match(data) {
			return nil
		}
	}
	return &Finding{
		Kind:     "no_event_sink",
		Severity: "info",
		File:     relDir,
		Snippet:  "no TableEvent / events_out found in any source file",
	}
}

// collectSourceFiles retorna todos los .cpp y .h dentro de absDir,
// excluyendo vendor/, build/, .git/, tests/, test/.
func collectSourceFiles(absDir string) ([]string, error) {
	skipDirs := map[string]bool{
		"vendor": true,
		"build":  true,
		".git":   true,
		"tests":  true,
		"test":   true,
	}
	var files []string
	err := filepath.WalkDir(absDir, func(path string, d os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return nil
		}
		if d.IsDir() {
			if skipDirs[d.Name()] {
				return filepath.SkipDir
			}
			return nil
		}
		name := d.Name()
		if strings.HasSuffix(name, ".cpp") || strings.HasSuffix(name, ".h") {
			files = append(files, path)
		}
		return nil
	})
	return files, err
}

// parseFrontmatterRaw extrae los campos minimos de un frontmatter YAML sin
// dependencias externas (regex linea a linea).
type rawFrontmatter struct {
	Name        string
	Lang        string
	Domain      string
	UsesModules []string
}

func parseFrontmatterRaw(content string) (rawFrontmatter, error) {
	if !strings.HasPrefix(content, "---") {
		return rawFrontmatter{}, fmt.Errorf("no frontmatter")
	}
	rest := content[4:]
	end := strings.Index(rest, "\n---")
	if end < 0 {
		return rawFrontmatter{}, fmt.Errorf("unclosed frontmatter")
	}
	body := rest[:end]

	var fm rawFrontmatter
	inModules := false
	for _, line := range strings.Split(body, "\n") {
		// uses_modules: [data_table_cpp, framework_cpp] (inline array)
		if strings.HasPrefix(line, "uses_modules:") {
			inModules = true
			val := strings.TrimSpace(strings.TrimPrefix(line, "uses_modules:"))
			if strings.HasPrefix(val, "[") {
				val = strings.Trim(val, "[]")
				for _, item := range strings.Split(val, ",") {
					item = strings.TrimSpace(item)
					if item != "" {
						fm.UsesModules = append(fm.UsesModules, item)
					}
				}
				inModules = false
			}
			continue
		}
		// Multi-line array entry: "  - data_table_cpp"
		if inModules {
			trimmed := strings.TrimSpace(line)
			if strings.HasPrefix(trimmed, "-") {
				item := strings.TrimSpace(strings.TrimPrefix(trimmed, "-"))
				if item != "" {
					fm.UsesModules = append(fm.UsesModules, item)
				}
				continue
			}
			// End of array
			if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") {
				inModules = false
			}
		}
		if strings.HasPrefix(line, "name:") {
			fm.Name = strings.TrimSpace(strings.TrimPrefix(line, "name:"))
		} else if strings.HasPrefix(line, "lang:") {
			fm.Lang = strings.TrimSpace(strings.TrimPrefix(line, "lang:"))
			fm.Lang = strings.Trim(fm.Lang, `"'`)
		} else if strings.HasPrefix(line, "domain:") {
			fm.Domain = strings.TrimSpace(strings.TrimPrefix(line, "domain:"))
			fm.Domain = strings.Trim(fm.Domain, `"'`)
		}
	}
	return fm, nil
}

// buildID construye el ID del registry a partir del frontmatter.
func buildID(fm rawFrontmatter) string {
	id := fm.Name
	if fm.Lang != "" {
		id += "_" + fm.Lang
	}
	if fm.Domain != "" {
		id += "_" + fm.Domain
	}
	return id
}

// dtSliceContains retorna true si s esta en slice.
func dtSliceContains(slice []string, s string) bool {
	for _, v := range slice {
		if v == s {
			return true
		}
	}
	return false
}

// truncateSnippet recorta s a maxLen caracteres para uso en snippets de Finding.
func truncateSnippet(s string, maxLen int) string {
	if len(s) <= maxLen {
		return s
	}
	return s[:maxLen]
}
