package infra

import (
	"database/sql"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	_ "github.com/mattn/go-sqlite3"
)

// CppAppAudit holds the standard-conformance report for a single C++ app.
type CppAppAudit struct {
	AppID   string   `json:"app_id"`
	DirPath string   `json:"dir_path"`
	OK      bool     `json:"ok"`
	Issues  []string `json:"issues"`
}

// AuditCppApps walks every app registered with lang='cpp' in registry.db and
// checks conformance with cpp/PATTERNS.md and .claude/rules/cpp_apps.md:
//   - main.cpp exists at the root of the app dir
//   - main.cpp uses fn::run_app(...) (not glfwInit directly)
//   - cfg.about is declared (or .about = passed in run_app literal)
//   - cfg.log is declared
//   - main.cpp does NOT call fn_ui::app_menubar(nullptr,...) manually
//   - main.cpp does NOT call ImGui::DockSpaceOverViewport(...) unless
//     cfg.auto_dockspace = false is also set
//   - app.md frontmatter declares framework: imgui and entry_point
//
// Apps whose dir does not exist on disk are skipped with a placeholder issue.
func AuditCppApps(registryRoot string) ([]CppAppAudit, error) {
	dbPath := filepath.Join(registryRoot, "registry.db")
	dsn := fmt.Sprintf("file:%s?mode=ro&_foreign_keys=on", dbPath)
	db, err := sql.Open("sqlite3", dsn)
	if err != nil {
		return nil, fmt.Errorf("audit_cpp_apps: open db: %w", err)
	}
	defer db.Close()
	if err := db.Ping(); err != nil {
		return nil, fmt.Errorf("audit_cpp_apps: ping db: %w", err)
	}

	rows, err := db.Query(`SELECT id, dir_path, COALESCE(framework, '') FROM apps WHERE lang = 'cpp' ORDER BY id`)
	if err != nil {
		return nil, fmt.Errorf("audit_cpp_apps: query apps: %w", err)
	}
	defer rows.Close()

	var results []CppAppAudit
	for rows.Next() {
		var id, dir, framework string
		if err := rows.Scan(&id, &dir, &framework); err != nil {
			continue
		}
		// Solo auditar apps imgui — apps CLI/test no necesitan cfg.about/log/menubar.
		if framework != "imgui" {
			continue
		}
		audit := CppAppAudit{AppID: id, DirPath: dir}

		absDir := dir
		if !filepath.IsAbs(absDir) {
			absDir = filepath.Join(registryRoot, dir)
		}
		if _, err := os.Stat(absDir); os.IsNotExist(err) {
			audit.Issues = append(audit.Issues, "directory_missing")
			results = append(results, audit)
			continue
		}

		// main.cpp checks
		mainPath := filepath.Join(absDir, "main.cpp")
		mainBytes, err := os.ReadFile(mainPath)
		if err != nil {
			audit.Issues = append(audit.Issues, "main.cpp_missing_or_unreadable")
		} else {
			src := string(mainBytes)
			if !strings.Contains(src, "fn::run_app") {
				audit.Issues = append(audit.Issues, "no_fn_run_app_call")
			}
			if strings.Contains(src, "glfwInit(") {
				audit.Issues = append(audit.Issues, "manual_glfwInit_call")
			}
			if !cppHasAbout(src) {
				audit.Issues = append(audit.Issues, "missing_cfg_about")
			}
			if !cppHasLog(src) {
				audit.Issues = append(audit.Issues, "missing_cfg_log")
			}
			if cppHasManualMenubar(src) {
				audit.Issues = append(audit.Issues, "manual_app_menubar_call")
			}
			if cppHasManualDockSpace(src) && !cppHasAutoDockspaceFalse(src) {
				audit.Issues = append(audit.Issues, "manual_DockSpaceOverViewport_without_auto_dockspace_false")
			}
		}

		// app.md checks
		appMdPath := filepath.Join(absDir, "app.md")
		mdBytes, err := os.ReadFile(appMdPath)
		if err != nil {
			audit.Issues = append(audit.Issues, "app.md_missing")
		} else {
			md := string(mdBytes)
			if !strings.Contains(md, "framework:") || !strings.Contains(md, "imgui") {
				audit.Issues = append(audit.Issues, "app.md_missing_framework_imgui")
			}
			if !strings.Contains(md, "entry_point:") {
				audit.Issues = append(audit.Issues, "app.md_missing_entry_point")
			}
		}

		audit.OK = len(audit.Issues) == 0
		results = append(results, audit)
	}
	return results, nil
}

// cppHasAbout returns true if main.cpp sets cfg.about or passes .about = in a
// run_app literal initializer.
func cppHasAbout(src string) bool {
	return strings.Contains(src, "cfg.about") ||
		strings.Contains(src, ".about ") ||
		strings.Contains(src, ".about=") ||
		strings.Contains(src, "about_window_set_info")
}

// cppHasLog returns true if main.cpp sets cfg.log or .log = in a run_app
// literal initializer.
func cppHasLog(src string) bool {
	return strings.Contains(src, "cfg.log") ||
		strings.Contains(src, ".log ") ||
		strings.Contains(src, ".log=")
}

// cppHasManualMenubar returns true if main.cpp invokes
// fn_ui::app_menubar(...) directly. The framework already calls it once per
// frame, so any manual call is a duplicate.
func cppHasManualMenubar(src string) bool {
	return strings.Contains(src, "fn_ui::app_menubar(") ||
		strings.Contains(src, "app_menubar(nullptr")
}

// cppHasManualDockSpace returns true if main.cpp invokes
// ImGui::DockSpaceOverViewport(...) inside its render(). Coexists with the
// framework's auto dockspace only if cfg.auto_dockspace = false is set.
func cppHasManualDockSpace(src string) bool {
	return strings.Contains(src, "DockSpaceOverViewport(")
}

// cppHasAutoDockspaceFalse returns true if main.cpp opts out of the
// framework auto-dockspace via cfg.auto_dockspace = false or
// .auto_dockspace = false.
func cppHasAutoDockspaceFalse(src string) bool {
	return strings.Contains(src, "auto_dockspace = false") ||
		strings.Contains(src, "auto_dockspace=false")
}

// CppTableMigrationAudit holds the BeginTable migration status for a single C++ app.
type CppTableMigrationAudit struct {
	AppID              string `json:"app_id"`
	DirPath            string `json:"dir_path"`
	BeginTableCount    int    `json:"begin_table_count"`
	UsesDataTableViz   bool   `json:"uses_data_table_viz"`
	Status             string `json:"status"` // "clean", "candidate", "mixed"
	Message            string `json:"message"`
}

// AuditCppTableMigration scans C++ apps (imgui framework) for inline
// ImGui::BeginTable calls and checks whether the app already declares
// data_table_cpp_viz in its uses_functions.
//
// Status values:
//   - "clean"     — no ImGui::BeginTable found (fully migrated or never had tables)
//   - "mixed"     — has ImGui::BeginTable AND declares data_table_cpp_viz
//                   (partial migration OK, wave N in progress)
//   - "candidate" — has ImGui::BeginTable but does NOT declare data_table_cpp_viz
//                   (migration not started; consider data_table_cpp_viz, issue 0081)
func AuditCppTableMigration(registryRoot string) ([]CppTableMigrationAudit, error) {
	dbPath := filepath.Join(registryRoot, "registry.db")
	dsn := fmt.Sprintf("file:%s?mode=ro&_foreign_keys=on", dbPath)
	db, err := sql.Open("sqlite3", dsn)
	if err != nil {
		return nil, fmt.Errorf("audit_cpp_table_migration: open db: %w", err)
	}
	defer db.Close()
	if err := db.Ping(); err != nil {
		return nil, fmt.Errorf("audit_cpp_table_migration: ping db: %w", err)
	}

	rows, err := db.Query(`SELECT id, dir_path, COALESCE(framework,''), COALESCE(uses_functions,'') FROM apps WHERE lang = 'cpp' ORDER BY id`)
	if err != nil {
		return nil, fmt.Errorf("audit_cpp_table_migration: query apps: %w", err)
	}
	defer rows.Close()

	var results []CppTableMigrationAudit
	for rows.Next() {
		var id, dir, framework, usesFunctions string
		if err := rows.Scan(&id, &dir, &framework, &usesFunctions); err != nil {
			continue
		}
		if framework != "imgui" {
			continue
		}

		absDir := dir
		if !filepath.IsAbs(absDir) {
			absDir = filepath.Join(registryRoot, dir)
		}
		if _, err := os.Stat(absDir); os.IsNotExist(err) {
			continue // directory_missing already reported by AuditCppApps
		}

		// Count ImGui::BeginTable occurrences across all .cpp files (excluding tests/).
		count := 0
		err := filepath.WalkDir(absDir, func(path string, d os.DirEntry, walkErr error) error {
			if walkErr != nil {
				return nil
			}
			// Skip test directories.
			if d.IsDir() && (d.Name() == "tests" || d.Name() == "test") {
				return filepath.SkipDir
			}
			if d.IsDir() {
				return nil
			}
			if !strings.HasSuffix(d.Name(), ".cpp") {
				return nil
			}
			data, err := os.ReadFile(path)
			if err != nil {
				return nil
			}
			src := string(data)
			for start := 0; ; {
				idx := strings.Index(src[start:], "ImGui::BeginTable(")
				if idx < 0 {
					break
				}
				count++
				start += idx + len("ImGui::BeginTable(")
			}
			return nil
		})
		if err != nil {
			continue
		}

		usesViz := strings.Contains(usesFunctions, "data_table_cpp_viz")

		audit := CppTableMigrationAudit{
			AppID:            id,
			DirPath:          dir,
			BeginTableCount:  count,
			UsesDataTableViz: usesViz,
		}

		switch {
		case count == 0:
			audit.Status = "clean"
			audit.Message = ""
		case usesViz:
			audit.Status = "mixed"
			audit.Message = fmt.Sprintf("%d ImGui::BeginTable inline (partial migration OK — data_table_cpp_viz already declared)", count)
		default:
			audit.Status = "candidate"
			audit.Message = fmt.Sprintf("candidate to migrate: %d ImGui::BeginTable inline detected, consider data_table_cpp_viz (issue 0081)", count)
		}

		results = append(results, audit)
	}
	return results, nil
}
