package infra

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

// AppDriftEntry describes an app and its module drift state relative to the registry.
type AppDriftEntry struct {
	AppName         string            `json:"app_name"`
	AppID           string            `json:"app_id"`
	DirPath         string            `json:"dir_path"`
	BuildKind       string            `json:"build_kind"`       // "windows-deployed" | "windows-build" | "linux-build" | ""
	BinaryPath      string            `json:"binary_path"`      // path inspected (or modules_generated.cpp)
	LinkedModules   map[string]string `json:"linked_modules"`   // module name -> embedded version
	RegistryModules map[string]string `json:"registry_modules"` // module name -> registry version
	Stale           []string          `json:"stale"`            // modules with drift (linked != registry)
	Status          string            `json:"status"`           // "ok" | "drift" | "no-build"
}

// AuditAppDrift scans all C++ apps in the registry, reads the module versions
// embedded in their compiled artifacts, and compares them against the versions
// declared in modules/*/module.md.
//
// Binary inspection priority (first found wins):
//  1. windows-deployed: <windowsDeployRoot>/<name>/<name>.exe — PE binary search
//  2. windows-build:    cpp/build/windows/apps/<name>/<name>_modules_generated.cpp
//  3. linux-build:      cpp/build/linux/apps/<name>/<name>_modules_generated.cpp
//
// If windowsDeployRoot is empty, only cpp/build paths are checked.
// registryRoot is the absolute path to the fn_registry root.
func AuditAppDrift(registryRoot string, windowsDeployRoot string) ([]AppDriftEntry, error) {
	// 1. Read registry module versions from modules/*/module.md
	registryMods, err := readRegistryModules(registryRoot)
	if err != nil {
		return nil, fmt.Errorf("audit_app_drift: read registry modules: %w", err)
	}

	// 2. Find all C++ app.md files.
	appDirs, err := findAppDirs(registryRoot)
	if err != nil {
		return nil, fmt.Errorf("audit_app_drift: find app dirs: %w", err)
	}

	var result []AppDriftEntry

	for _, dir := range appDirs {
		appMDPath := filepath.Join(dir, "app.md")
		meta, err := readCppAppMeta(appMDPath)
		if err != nil || meta.Lang != "cpp" {
			continue
		}

		entry := AppDriftEntry{
			AppName:         meta.Name,
			AppID:           meta.ID,
			DirPath:         meta.DirPath,
			LinkedModules:   map[string]string{},
			RegistryModules: registryMods,
		}

		// 3. Find artifact and extract linked module versions.
		kind, artifactPath, linked := resolveLinkedVersions(
			meta.Name, registryRoot, windowsDeployRoot,
		)
		entry.BuildKind = kind
		entry.BinaryPath = artifactPath
		entry.LinkedModules = linked

		if kind == "" || linked == nil {
			entry.Status = "no-build"
			result = append(result, entry)
			continue
		}

		// 4. Compute drift: only for modules that are both linked AND in registry.
		var stale []string
		for modName, linkedVer := range linked {
			regVer, inRegistry := registryMods[modName]
			if !inRegistry {
				continue
			}
			if linkedVer != regVer {
				stale = append(stale, modName)
			}
		}
		sort.Strings(stale)
		entry.Stale = stale

		if len(stale) > 0 {
			entry.Status = "drift"
		} else {
			entry.Status = "ok"
		}

		result = append(result, entry)
	}

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

// readRegistryModules reads modules/*/module.md and returns name -> version map.
// Uses stdlib only: reads files line by line, extracts name/version with regex.
func readRegistryModules(root string) (map[string]string, error) {
	modulesDir := filepath.Join(root, "modules")
	entries, err := os.ReadDir(modulesDir)
	if err != nil {
		if os.IsNotExist(err) {
			return map[string]string{}, nil
		}
		return nil, err
	}

	out := map[string]string{}
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		mdPath := filepath.Join(modulesDir, e.Name(), "module.md")
		name, version, err := parseModuleMD(mdPath)
		if err != nil || name == "" || version == "" {
			continue
		}
		out[name] = version
	}
	return out, nil
}

var (
	moduleNameRE    = regexp.MustCompile(`(?m)^name:\s*(.+?)\s*$`)
	moduleVersionRE = regexp.MustCompile(`(?m)^version:\s*(.+?)\s*$`)
)

// parseModuleMD extracts name and version from a module.md frontmatter using
// simple line-by-line regex (no external YAML deps).
func parseModuleMD(path string) (name, version string, err error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return "", "", err
	}
	// Restrict to frontmatter only (between --- markers).
	fm := extractFrontmatter(string(data))

	if m := moduleNameRE.FindStringSubmatch(fm); len(m) > 1 {
		name = strings.TrimSpace(strings.Trim(m[1], `"`))
	}
	if m := moduleVersionRE.FindStringSubmatch(fm); len(m) > 1 {
		version = strings.TrimSpace(strings.Trim(m[1], `"`))
	}
	return name, version, nil
}

// extractFrontmatter returns the YAML block between the first --- pair, or the
// full content if no markers found.
func extractFrontmatter(content string) string {
	if !strings.HasPrefix(content, "---") {
		return content
	}
	rest := content[3:]
	// Skip optional newline after opening ---
	if strings.HasPrefix(rest, "\n") {
		rest = rest[1:]
	}
	end := strings.Index(rest, "\n---")
	if end < 0 {
		return rest
	}
	return rest[:end]
}

// cppAppMeta holds the fields we need from an app.md for C++ apps.
type cppAppMeta struct {
	Name    string
	ID      string
	Lang    string
	Domain  string
	DirPath string
}

var (
	appLangRE    = regexp.MustCompile(`(?m)^lang:\s*(.+?)\s*$`)
	appNameRE    = regexp.MustCompile(`(?m)^name:\s*(.+?)\s*$`)
	appDomainRE  = regexp.MustCompile(`(?m)^domain:\s*(.+?)\s*$`)
	appDirPathRE = regexp.MustCompile(`(?m)^dir_path:\s*"?(.+?)"?\s*$`)
)

// readCppAppMeta extracts name, lang, domain and dir_path from an app.md.
func readCppAppMeta(path string) (cppAppMeta, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return cppAppMeta{}, err
	}
	fm := extractFrontmatter(string(data))

	var meta cppAppMeta
	if m := appNameRE.FindStringSubmatch(fm); len(m) > 1 {
		meta.Name = strings.TrimSpace(strings.Trim(m[1], `"`))
	}
	if m := appLangRE.FindStringSubmatch(fm); len(m) > 1 {
		meta.Lang = strings.TrimSpace(strings.Trim(m[1], `"`))
	}
	if m := appDomainRE.FindStringSubmatch(fm); len(m) > 1 {
		meta.Domain = strings.TrimSpace(strings.Trim(m[1], `"`))
	}
	if m := appDirPathRE.FindStringSubmatch(fm); len(m) > 1 {
		meta.DirPath = strings.TrimSpace(strings.Trim(m[1], `"`))
	}

	// Derive ID: name_lang_domain
	meta.ID = meta.Name
	if meta.Lang != "" {
		meta.ID += "_" + meta.Lang
	}
	if meta.Domain != "" {
		meta.ID += "_" + meta.Domain
	}

	return meta, nil
}

// resolveLinkedVersions finds the best artifact for a given app and extracts
// module name->version from it.
//
// Priority: windows-deployed > windows-build > linux-build.
// Returns ("", "", nil) when no artifact is found.
func resolveLinkedVersions(
	appName string,
	registryRoot string,
	windowsDeployRoot string,
) (kind string, artifactPath string, linked map[string]string) {

	// 1. Windows deployed exe.
	if windowsDeployRoot != "" {
		exePath := filepath.Join(windowsDeployRoot, appName, appName+".exe")
		if _, err := os.Stat(exePath); err == nil {
			if mods := extractModulesFromBinary(exePath); len(mods) > 0 {
				return "windows-deployed", exePath, mods
			}
		}
	}

	// 2. Windows build modules_generated.cpp.
	winGenCPP := filepath.Join(
		registryRoot, "cpp", "build", "windows", "apps", appName,
		appName+"_modules_generated.cpp",
	)
	if _, err := os.Stat(winGenCPP); err == nil {
		if mods := extractModulesFromGeneratedCPP(winGenCPP); len(mods) > 0 {
			return "windows-build", winGenCPP, mods
		}
	}

	// 3. Linux build modules_generated.cpp.
	linuxGenCPP := filepath.Join(
		registryRoot, "cpp", "build", "linux", "apps", appName,
		appName+"_modules_generated.cpp",
	)
	if _, err := os.Stat(linuxGenCPP); err == nil {
		if mods := extractModulesFromGeneratedCPP(linuxGenCPP); len(mods) > 0 {
			return "linux-build", linuxGenCPP, mods
		}
	}

	return "", "", nil
}

// binaryModuleRE matches the pattern <module_name>\x00<semver>\x00 as it
// appears in PE (Windows) binaries compiled from the generated ModuleInfo struct.
// Group 1 = module name, group 2 = version string.
var binaryModuleRE = regexp.MustCompile(`([a-z][a-z0-9_]{1,63})\x00(\d+\.\d+\.\d+)\x00`)

// extractModulesFromBinary scans a PE binary for the contiguous pattern
// <module_name>\x00<semver>\x00 that the MSVC/MinGW compiler emits for the
// ModuleInfo struct string literals. Returns nil when no matches are found.
//
// Note: ELF (Linux) binaries may not have this contiguous layout — use
// extractModulesFromGeneratedCPP instead for Linux builds.
func extractModulesFromBinary(path string) map[string]string {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil
	}

	out := map[string]string{}
	for _, m := range binaryModuleRE.FindAllSubmatch(data, -1) {
		if len(m) < 3 {
			continue
		}
		name := string(m[1])
		ver := string(m[2])
		if _, seen := out[name]; !seen {
			out[name] = ver
		}
	}

	if len(out) == 0 {
		return nil
	}
	return out
}

// generatedModuleRE matches lines like: { "data_table", "1.5.0", "..." }
var generatedModuleRE = regexp.MustCompile(`\{\s*"([a-z][a-z0-9_]*)"\s*,\s*"(\d+\.\d+\.\d+)"`)

// extractModulesFromGeneratedCPP parses a *_modules_generated.cpp file and
// returns module name -> version. This is the authoritative source for Linux
// builds and a reliable fallback for Windows builds.
func extractModulesFromGeneratedCPP(path string) map[string]string {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil
	}

	out := map[string]string{}
	for _, m := range generatedModuleRE.FindAllSubmatch(data, -1) {
		if len(m) < 3 {
			continue
		}
		name := string(m[1])
		ver := string(m[2])
		if _, seen := out[name]; !seen {
			out[name] = ver
		}
	}

	if len(out) == 0 {
		return nil
	}
	return out
}

