package registry

import (
	"encoding/json"
	"fmt"
	"strings"
	"time"
)

func marshalStrings(ss []string) string {
	if ss == nil {
		ss = []string{}
	}
	b, _ := json.Marshal(ss)
	return string(b)
}

func unmarshalStrings(s string) []string {
	var out []string
	json.Unmarshal([]byte(s), &out)
	if out == nil {
		out = []string{}
	}
	return out
}

func marshalJSON(v map[string]any) string {
	if v == nil {
		v = map[string]any{}
	}
	b, _ := json.Marshal(v)
	return string(b)
}

func unmarshalJSON(s string) map[string]any {
	var out map[string]any
	json.Unmarshal([]byte(s), &out)
	if out == nil {
		out = map[string]any{}
	}
	return out
}

func marshalProps(ps []PropDef) string {
	if ps == nil {
		ps = []PropDef{}
	}
	b, _ := json.Marshal(ps)
	return string(b)
}

func unmarshalProps(s string) []PropDef {
	var out []PropDef
	json.Unmarshal([]byte(s), &out)
	return out
}

// InsertFunction inserts or replaces a function entry.
func (db *DB) InsertFunction(f *Function) error {
	now := time.Now().UTC()
	if f.CreatedAt.IsZero() {
		f.CreatedAt = now
	}
	if f.UpdatedAt.IsZero() {
		f.UpdatedAt = now
	}

	if f.ID == "" {
		f.ID = GenerateID(f.Name, f.Lang, f.Domain)
	}

	var hasState *int
	if f.HasState != nil {
		v := 0
		if *f.HasState {
			v = 1
		}
		hasState = &v
	}

	_, err := db.conn.Exec(`
		INSERT OR REPLACE INTO functions (
			id, name, kind, lang, domain, version, purity, signature,
			description, tags, uses_functions, uses_types, returns,
			returns_optional, error_type, imports, example, tested,
			tests, test_file_path, file_path, content_hash, created_at, updated_at,
			props, emits, has_state, framework, variant,
			notes, documentation, code,
			source_repo, source_license, source_file,
			params_schema
		) VALUES (
			?, ?, ?, ?, ?, ?, ?, ?,
			?, ?, ?, ?, ?,
			?, ?, ?, ?, ?,
			?, ?, ?, ?, ?, ?,
			?, ?, ?, ?, ?,
			?, ?, ?,
			?, ?, ?,
			?
		)`,
		f.ID, f.Name, string(f.Kind), f.Lang, f.Domain, f.Version, string(f.Purity), f.Signature,
		f.Description, marshalStrings(f.Tags), marshalStrings(f.UsesFunctions), marshalStrings(f.UsesTypes), marshalStrings(f.Returns),
		f.ReturnsOptional, f.ErrorType, marshalStrings(f.Imports), f.Example, f.Tested,
		marshalStrings(f.Tests), f.TestFilePath, f.FilePath, f.ContentHash, f.CreatedAt.Format(time.RFC3339), f.UpdatedAt.Format(time.RFC3339),
		marshalProps(f.Props), marshalStrings(f.Emits), hasState, f.Framework, marshalStrings(f.Variant),
		f.Notes, f.Documentation, f.Code,
		f.SourceRepo, f.SourceLicense, f.SourceFile,
		f.ParamsSchema,
	)
	return err
}

// InsertType inserts or replaces a type entry.
func (db *DB) InsertType(t *Type) error {
	now := time.Now().UTC()
	if t.CreatedAt.IsZero() {
		t.CreatedAt = now
	}
	if t.UpdatedAt.IsZero() {
		t.UpdatedAt = now
	}

	if t.ID == "" {
		t.ID = GenerateID(t.Name, t.Lang, t.Domain)
	}

	_, err := db.conn.Exec(`
		INSERT OR REPLACE INTO types (
			id, name, lang, domain, version, algebraic,
			definition, description, tags, uses_types,
			file_path, content_hash, created_at, updated_at,
			examples, notes, documentation, code,
			source_repo, source_license, source_file
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		t.ID, t.Name, t.Lang, t.Domain, t.Version, string(t.Algebraic),
		t.Definition, t.Description, marshalStrings(t.Tags), marshalStrings(t.UsesTypes),
		t.FilePath, t.ContentHash, t.CreatedAt.Format(time.RFC3339), t.UpdatedAt.Format(time.RFC3339),
		t.Examples, t.Notes, t.Documentation, t.Code,
		t.SourceRepo, t.SourceLicense, t.SourceFile,
	)
	return err
}

// SearchFunctions performs FTS search on functions with optional filters.
// tags filter: each entry must be present in functions.tags JSON array (AND across tags).
func (db *DB) SearchFunctions(query string, kind Kind, purity Purity, lang, domain string, tags ...string) ([]Function, error) {
	where := []string{}
	args := []any{}

	if query != "" {
		where = append(where, "f.id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH ?)")
		args = append(args, query)
	}
	if kind != "" {
		where = append(where, "f.kind = ?")
		args = append(args, string(kind))
	}
	if purity != "" {
		where = append(where, "f.purity = ?")
		args = append(args, string(purity))
	}
	if lang != "" {
		where = append(where, "f.lang = ?")
		args = append(args, lang)
	}
	if domain != "" {
		where = append(where, "f.domain = ?")
		args = append(args, domain)
	}
	for _, t := range tags {
		if t == "" {
			continue
		}
		where = append(where, "EXISTS (SELECT 1 FROM json_each(f.tags) WHERE value = ?)")
		args = append(args, t)
	}

	sql := "SELECT * FROM functions f"
	if len(where) > 0 {
		sql += " WHERE " + strings.Join(where, " AND ")
	}
	sql += " ORDER BY f.name"

	rows, err := db.conn.Query(sql, args...)
	if err != nil {
		return nil, fmt.Errorf("search functions: %w", err)
	}
	defer rows.Close()

	return scanFunctions(rows)
}

// SearchTypes performs FTS search on types with optional filters.
// tags filter: each entry must be present in types.tags JSON array (AND across tags).
func (db *DB) SearchTypes(query string, lang, domain string, tags ...string) ([]Type, error) {
	where := []string{}
	args := []any{}

	if query != "" {
		where = append(where, "t.id IN (SELECT id FROM types_fts WHERE types_fts MATCH ?)")
		args = append(args, query)
	}
	if lang != "" {
		where = append(where, "t.lang = ?")
		args = append(args, lang)
	}
	if domain != "" {
		where = append(where, "t.domain = ?")
		args = append(args, domain)
	}
	for _, t := range tags {
		if t == "" {
			continue
		}
		where = append(where, "EXISTS (SELECT 1 FROM json_each(t.tags) WHERE value = ?)")
		args = append(args, t)
	}

	sql := "SELECT * FROM types t"
	if len(where) > 0 {
		sql += " WHERE " + strings.Join(where, " AND ")
	}
	sql += " ORDER BY t.name"

	rows, err := db.conn.Query(sql, args...)
	if err != nil {
		return nil, fmt.Errorf("search types: %w", err)
	}
	defer rows.Close()

	return scanTypes(rows)
}

// GetFunction returns a single function by ID.
func (db *DB) GetFunction(id string) (*Function, error) {
	rows, err := db.conn.Query("SELECT * FROM functions WHERE id = ?", id)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	fns, err := scanFunctions(rows)
	if err != nil {
		return nil, err
	}
	if len(fns) == 0 {
		return nil, fmt.Errorf("function %q not found", id)
	}
	return &fns[0], nil
}

// GetType returns a single type by ID.
func (db *DB) GetType(id string) (*Type, error) {
	rows, err := db.conn.Query("SELECT * FROM types WHERE id = ?", id)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	ts, err := scanTypes(rows)
	if err != nil {
		return nil, err
	}
	if len(ts) == 0 {
		return nil, fmt.Errorf("type %q not found", id)
	}
	return &ts[0], nil
}

// GetFunctionsByName returns all functions matching a given name (across langs/domains).
func (db *DB) GetFunctionsByName(name string) ([]Function, error) {
	rows, err := db.conn.Query("SELECT * FROM functions WHERE name = ? ORDER BY lang, domain", name)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanFunctions(rows)
}

// DeleteFunction removes a function by ID.
func (db *DB) DeleteFunction(id string) error {
	_, err := db.conn.Exec("DELETE FROM functions WHERE id = ?", id)
	return err
}

// DeleteType removes a type by ID.
func (db *DB) DeleteType(id string) error {
	_, err := db.conn.Exec("DELETE FROM types WHERE id = ?", id)
	return err
}

// InsertApp inserts or replaces an app entry.
func (db *DB) InsertApp(a *App) error {
	now := time.Now().UTC()
	if a.CreatedAt.IsZero() {
		a.CreatedAt = now
	}
	if a.UpdatedAt.IsZero() {
		a.UpdatedAt = now
	}

	if a.ID == "" {
		a.ID = GenerateID(a.Name, a.Lang, a.Domain)
	}

	var (
		svcPort        int
		svcHealth      string
		svcHealthTO    int
		svcUnit        string
		svcScope       string
		svcRestart     string
		svcRuntime     string
		svcLocalOnly   int
	)
	if a.Service != nil {
		svcPort = a.Service.Port
		svcHealth = a.Service.HealthEndpoint
		svcHealthTO = a.Service.HealthTimeoutS
		svcUnit = a.Service.SystemdUnit
		svcScope = a.Service.SystemdScope
		svcRestart = a.Service.RestartPolicy
		svcRuntime = a.Service.Runtime
		if a.Service.IsLocalOnly {
			svcLocalOnly = 1
		}
	}

	if a.Version == "" {
		a.Version = "0.1.0"
	}

	_, err := db.conn.Exec(`
		INSERT OR REPLACE INTO apps (
			id, name, lang, domain, description, tags,
			uses_functions, uses_types, framework, entry_point,
			documentation, notes, dir_path, content_hash, created_at, updated_at, repo_url, project_id, uses_modules,
			service_port, service_health_endpoint, service_health_timeout_s,
			service_systemd_unit, service_systemd_scope, service_restart_policy,
			service_runtime, service_is_local_only, version
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		a.ID, a.Name, a.Lang, a.Domain, a.Description, marshalStrings(a.Tags),
		marshalStrings(a.UsesFunctions), marshalStrings(a.UsesTypes), a.Framework, a.EntryPoint,
		a.Documentation, a.Notes, a.DirPath, a.ContentHash, a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339),
		a.RepoURL, a.ProjectID, marshalStrings(a.UsesModules),
		svcPort, svcHealth, svcHealthTO, svcUnit, svcScope, svcRestart, svcRuntime, svcLocalOnly, a.Version,
	)
	if err != nil {
		return err
	}

	// Replace service_targets for this app (idempotent).
	if _, err := db.conn.Exec("DELETE FROM service_targets WHERE app_id = ?", a.ID); err != nil {
		return fmt.Errorf("clearing service_targets for %s: %w", a.ID, err)
	}
	if a.Service != nil {
		for _, pc := range a.Service.PCTargets {
			if pc == "" {
				continue
			}
			if _, err := db.conn.Exec(
				"INSERT OR REPLACE INTO service_targets (app_id, pc_id, role) VALUES (?, ?, 'primary')",
				a.ID, pc,
			); err != nil {
				return fmt.Errorf("inserting service_target %s/%s: %w", a.ID, pc, err)
			}
		}
	}
	return nil
}

// GetApp returns a single app by ID.
func (db *DB) GetApp(id string) (*App, error) {
	rows, err := db.conn.Query("SELECT * FROM apps WHERE id = ?", id)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	apps, err := scanApps(rows)
	if err != nil {
		return nil, err
	}
	if len(apps) == 0 {
		return nil, fmt.Errorf("app %q not found", id)
	}
	return &apps[0], nil
}

// SearchApps performs FTS search on apps with optional filters.
func (db *DB) SearchApps(query string, lang, domain string) ([]App, error) {
	where := []string{}
	args := []any{}

	if query != "" {
		where = append(where, "a.id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH ?)")
		args = append(args, query)
	}
	if lang != "" {
		where = append(where, "a.lang = ?")
		args = append(args, lang)
	}
	if domain != "" {
		where = append(where, "a.domain = ?")
		args = append(args, domain)
	}

	sql := "SELECT * FROM apps a"
	if len(where) > 0 {
		sql += " WHERE " + strings.Join(where, " AND ")
	}
	sql += " ORDER BY a.name"

	rows, err := db.conn.Query(sql, args...)
	if err != nil {
		return nil, fmt.Errorf("search apps: %w", err)
	}
	defer rows.Close()

	return scanApps(rows)
}

func scanApps(rows interface{ Next() bool; Scan(...any) error }) ([]App, error) {
	var result []App
	for rows.Next() {
		var a App
		var tagsJSON, usesFnJSON, usesTypJSON, usesModJSON string
		var createdAt, updatedAt string
		var svcPort, svcHealthTO, svcLocalOnly int
		var svcHealth, svcUnit, svcScope, svcRestart, svcRuntime string

		err := rows.Scan(
			&a.ID, &a.Name, &a.Lang, &a.Domain, &a.Description, &tagsJSON,
			&usesFnJSON, &usesTypJSON, &a.Framework, &a.EntryPoint,
			&a.Documentation, &a.Notes, &a.DirPath, &createdAt, &updatedAt, &a.ContentHash,
			&a.RepoURL, &a.ProjectID, &usesModJSON,
			&svcPort, &svcHealth, &svcHealthTO, &svcUnit, &svcScope, &svcRestart, &svcRuntime, &svcLocalOnly,
			&a.Version,
		)
		if err != nil {
			return nil, fmt.Errorf("scanning app: %w", err)
		}

		a.Tags = unmarshalStrings(tagsJSON)
		a.UsesFunctions = unmarshalStrings(usesFnJSON)
		a.UsesTypes = unmarshalStrings(usesTypJSON)
		a.UsesModules = unmarshalStrings(usesModJSON)
		a.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
		a.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)

		if svcPort != 0 || svcHealth != "" || svcUnit != "" || svcScope != "" || svcRestart != "" || svcRuntime != "" || svcLocalOnly != 0 {
			a.Service = &ServiceSpec{
				Port:           svcPort,
				HealthEndpoint: svcHealth,
				HealthTimeoutS: svcHealthTO,
				SystemdUnit:    svcUnit,
				SystemdScope:   svcScope,
				RestartPolicy:  svcRestart,
				Runtime:        svcRuntime,
				IsLocalOnly:    svcLocalOnly != 0,
			}
		}

		result = append(result, a)
	}
	return result, nil
}

// GetServicePCTargets returns the pc_ids declared in service_targets for an app.
// Empty slice when the app has no declared targets. Issue 0105.
func (db *DB) GetServicePCTargets(appID string) ([]string, error) {
	rows, err := db.conn.Query(
		"SELECT pc_id FROM service_targets WHERE app_id = ? ORDER BY pc_id",
		appID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []string
	for rows.Next() {
		var pc string
		if err := rows.Scan(&pc); err != nil {
			return nil, err
		}
		out = append(out, pc)
	}
	return out, nil
}

// Purge deletes all data from functions, types, apps, analysis, projects, vaults and modules. Used before re-indexing.
func (db *DB) Purge() error {
	if _, err := db.conn.Exec("DELETE FROM functions"); err != nil {
		return err
	}
	if _, err := db.conn.Exec("DELETE FROM types"); err != nil {
		return err
	}
	if _, err := db.conn.Exec("DELETE FROM apps"); err != nil {
		return err
	}
	if _, err := db.conn.Exec("DELETE FROM service_targets"); err != nil {
		return err
	}
	if _, err := db.conn.Exec("DELETE FROM analysis"); err != nil {
		return err
	}
	if _, err := db.conn.Exec("DELETE FROM projects"); err != nil {
		return err
	}
	if _, err := db.conn.Exec("DELETE FROM vaults"); err != nil {
		return err
	}
	_, err := db.conn.Exec("DELETE FROM modules")
	return err
}

// PurgeLocalOnly deletes functions, types, and only locally-present apps/analysis/projects/vaults.
// Remote-only records (repo_url set, not in local ID maps) are preserved.
func (db *DB) PurgeLocalOnly(localAppIDs, localAnalysisIDs, localProjectIDs map[string]bool) error {
	if _, err := db.conn.Exec("DELETE FROM functions"); err != nil {
		return err
	}
	if _, err := db.conn.Exec("DELETE FROM types"); err != nil {
		return err
	}
	// Delete local apps (those scanned from disk)
	for id := range localAppIDs {
		if _, err := db.conn.Exec("DELETE FROM apps WHERE id = ?", id); err != nil {
			return err
		}
		if _, err := db.conn.Exec("DELETE FROM service_targets WHERE app_id = ?", id); err != nil {
			return err
		}
	}
	// Delete apps without repo_url (legacy local-only apps not yet pushed)
	if _, err := db.conn.Exec("DELETE FROM apps WHERE repo_url = '' OR repo_url IS NULL"); err != nil {
		return err
	}
	// Orphan service_targets cleanup
	if _, err := db.conn.Exec("DELETE FROM service_targets WHERE app_id NOT IN (SELECT id FROM apps)"); err != nil {
		return err
	}
	// Same for analysis
	for id := range localAnalysisIDs {
		if _, err := db.conn.Exec("DELETE FROM analysis WHERE id = ?", id); err != nil {
			return err
		}
	}
	if _, err := db.conn.Exec("DELETE FROM analysis WHERE repo_url = '' OR repo_url IS NULL"); err != nil {
		return err
	}
	// Projects: delete locally-scanned, preserve remote-only
	for id := range localProjectIDs {
		if _, err := db.conn.Exec("DELETE FROM projects WHERE id = ?", id); err != nil {
			return err
		}
	}
	if _, err := db.conn.Exec("DELETE FROM projects WHERE repo_url = '' OR repo_url IS NULL"); err != nil {
		return err
	}
	// Vaults: always purge and re-insert from vault.yaml
	if _, err := db.conn.Exec("DELETE FROM vaults"); err != nil {
		return err
	}
	// Modules: always purge and re-insert from modules/*/module.md
	if _, err := db.conn.Exec("DELETE FROM modules"); err != nil {
		return err
	}
	return nil
}

// --- Analysis CRUD ---

// InsertAnalysis inserts or replaces an analysis entry.
func (db *DB) InsertAnalysis(a *Analysis) error {
	now := time.Now().UTC()
	if a.CreatedAt.IsZero() {
		a.CreatedAt = now
	}
	if a.UpdatedAt.IsZero() {
		a.UpdatedAt = now
	}

	if a.ID == "" {
		a.ID = GenerateID(a.Name, a.Lang, a.Domain)
	}

	_, err := db.conn.Exec(`
		INSERT OR REPLACE INTO analysis (
			id, name, lang, domain, description, tags,
			uses_functions, uses_types, framework, entry_point,
			documentation, notes, repo_url, dir_path, content_hash, created_at, updated_at, project_id, uses_modules
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		a.ID, a.Name, a.Lang, a.Domain, a.Description, marshalStrings(a.Tags),
		marshalStrings(a.UsesFunctions), marshalStrings(a.UsesTypes), a.Framework, a.EntryPoint,
		a.Documentation, a.Notes, a.RepoURL, a.DirPath, a.ContentHash,
		a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339), a.ProjectID, marshalStrings(a.UsesModules),
	)
	return err
}

// GetAnalysis returns a single analysis by ID.
func (db *DB) GetAnalysis(id string) (*Analysis, error) {
	rows, err := db.conn.Query("SELECT * FROM analysis WHERE id = ?", id)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items, err := scanAnalysis(rows)
	if err != nil {
		return nil, err
	}
	if len(items) == 0 {
		return nil, fmt.Errorf("analysis %q not found", id)
	}
	return &items[0], nil
}

// SearchAnalysis performs FTS search on analysis with optional filters.
func (db *DB) SearchAnalysis(query string, lang, domain string) ([]Analysis, error) {
	where := []string{}
	args := []any{}

	if query != "" {
		where = append(where, "a.id IN (SELECT id FROM analysis_fts WHERE analysis_fts MATCH ?)")
		args = append(args, query)
	}
	if lang != "" {
		where = append(where, "a.lang = ?")
		args = append(args, lang)
	}
	if domain != "" {
		where = append(where, "a.domain = ?")
		args = append(args, domain)
	}

	sql := "SELECT * FROM analysis a"
	if len(where) > 0 {
		sql += " WHERE " + strings.Join(where, " AND ")
	}
	sql += " ORDER BY a.name"

	rows, err := db.conn.Query(sql, args...)
	if err != nil {
		return nil, fmt.Errorf("search analysis: %w", err)
	}
	defer rows.Close()

	return scanAnalysis(rows)
}

// ListAllAnalysis returns all analysis entries.
func (db *DB) ListAllAnalysis() ([]Analysis, error) {
	return db.SearchAnalysis("", "", "")
}

// ListAllApps returns all app entries.
func (db *DB) ListAllApps() ([]App, error) {
	return db.SearchApps("", "", "")
}

func scanAnalysis(rows interface{ Next() bool; Scan(...any) error }) ([]Analysis, error) {
	var result []Analysis
	for rows.Next() {
		var a Analysis
		var tagsJSON, usesFnJSON, usesTypJSON, usesModJSON string
		var createdAt, updatedAt string

		err := rows.Scan(
			&a.ID, &a.Name, &a.Lang, &a.Domain, &a.Description, &tagsJSON,
			&usesFnJSON, &usesTypJSON, &a.Framework, &a.EntryPoint,
			&a.Documentation, &a.Notes, &a.RepoURL, &a.DirPath, &a.ContentHash,
			&createdAt, &updatedAt, &a.ProjectID, &usesModJSON,
		)
		if err != nil {
			return nil, fmt.Errorf("scanning analysis: %w", err)
		}

		a.Tags = unmarshalStrings(tagsJSON)
		a.UsesFunctions = unmarshalStrings(usesFnJSON)
		a.UsesTypes = unmarshalStrings(usesTypJSON)
		a.UsesModules = unmarshalStrings(usesModJSON)
		a.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
		a.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)

		result = append(result, a)
	}
	return result, nil
}

// --- Project CRUD ---

// InsertProject inserts or replaces a project entry.
func (db *DB) InsertProject(p *Project) error {
	now := time.Now().UTC()
	if p.CreatedAt.IsZero() {
		p.CreatedAt = now
	}
	if p.UpdatedAt.IsZero() {
		p.UpdatedAt = now
	}

	_, err := db.conn.Exec(`
		INSERT OR REPLACE INTO projects (
			id, name, description, tags, repo_url, dir_path,
			documentation, notes, content_hash, created_at, updated_at
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		p.ID, p.Name, p.Description, marshalStrings(p.Tags), p.RepoURL, p.DirPath,
		p.Documentation, p.Notes, p.ContentHash,
		p.CreatedAt.Format(time.RFC3339), p.UpdatedAt.Format(time.RFC3339),
	)
	return err
}

// GetProject returns a single project by ID.
func (db *DB) GetProject(id string) (*Project, error) {
	rows, err := db.conn.Query("SELECT * FROM projects WHERE id = ?", id)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	ps, err := scanProjects(rows)
	if err != nil {
		return nil, err
	}
	if len(ps) == 0 {
		return nil, fmt.Errorf("project %q not found", id)
	}
	return &ps[0], nil
}

// SearchProjects performs FTS search on projects.
func (db *DB) SearchProjects(query string) ([]Project, error) {
	where := []string{}
	args := []any{}

	if query != "" {
		where = append(where, "p.id IN (SELECT id FROM projects_fts WHERE projects_fts MATCH ?)")
		args = append(args, query)
	}

	sql := "SELECT * FROM projects p"
	if len(where) > 0 {
		sql += " WHERE " + strings.Join(where, " AND ")
	}
	sql += " ORDER BY p.name"

	rows, err := db.conn.Query(sql, args...)
	if err != nil {
		return nil, fmt.Errorf("search projects: %w", err)
	}
	defer rows.Close()

	return scanProjects(rows)
}

// ListAllProjects returns all project entries.
func (db *DB) ListAllProjects() ([]Project, error) {
	return db.SearchProjects("")
}

// GetProjectApps returns all apps belonging to a project.
func (db *DB) GetProjectApps(projectID string) ([]App, error) {
	rows, err := db.conn.Query("SELECT * FROM apps WHERE project_id = ? ORDER BY name", projectID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanApps(rows)
}

// GetProjectAnalysis returns all analysis entries belonging to a project.
func (db *DB) GetProjectAnalysis(projectID string) ([]Analysis, error) {
	rows, err := db.conn.Query("SELECT * FROM analysis WHERE project_id = ? ORDER BY name", projectID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanAnalysis(rows)
}

func scanProjects(rows interface{ Next() bool; Scan(...any) error }) ([]Project, error) {
	var result []Project
	for rows.Next() {
		var p Project
		var tagsJSON string
		var createdAt, updatedAt string

		err := rows.Scan(
			&p.ID, &p.Name, &p.Description, &tagsJSON, &p.RepoURL, &p.DirPath,
			&p.Documentation, &p.Notes, &p.ContentHash, &createdAt, &updatedAt,
		)
		if err != nil {
			return nil, fmt.Errorf("scanning project: %w", err)
		}

		p.Tags = unmarshalStrings(tagsJSON)
		p.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
		p.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)

		result = append(result, p)
	}
	return result, nil
}

// --- Vault CRUD ---

// InsertVault inserts or replaces a vault entry.
func (db *DB) InsertVault(v *Vault) error {
	now := time.Now().UTC()
	if v.CreatedAt.IsZero() {
		v.CreatedAt = now
	}
	if v.UpdatedAt.IsZero() {
		v.UpdatedAt = now
	}

	sym := 0
	if v.Symlink {
		sym = 1
	}

	_, err := db.conn.Exec(`
		INSERT OR REPLACE INTO vaults (
			id, name, project_id, description, path, symlink, tags,
			content_hash, created_at, updated_at
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		v.ID, v.Name, v.ProjectID, v.Description, v.Path, sym, marshalStrings(v.Tags),
		v.ContentHash, v.CreatedAt.Format(time.RFC3339), v.UpdatedAt.Format(time.RFC3339),
	)
	return err
}

// GetVault returns a single vault by ID.
func (db *DB) GetVault(id string) (*Vault, error) {
	rows, err := db.conn.Query("SELECT * FROM vaults WHERE id = ?", id)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	vs, err := scanVaults(rows)
	if err != nil {
		return nil, err
	}
	if len(vs) == 0 {
		return nil, fmt.Errorf("vault %q not found", id)
	}
	return &vs[0], nil
}

// SearchVaults performs search on vaults with optional project filter.
func (db *DB) SearchVaults(query, projectID string) ([]Vault, error) {
	where := []string{}
	args := []any{}

	if query != "" {
		where = append(where, "name LIKE ? OR description LIKE ?")
		q := "%" + query + "%"
		args = append(args, q, q)
	}
	if projectID != "" {
		where = append(where, "project_id = ?")
		args = append(args, projectID)
	}

	sql := "SELECT * FROM vaults"
	if len(where) > 0 {
		sql += " WHERE " + strings.Join(where, " AND ")
	}
	sql += " ORDER BY name"

	rows, err := db.conn.Query(sql, args...)
	if err != nil {
		return nil, fmt.Errorf("search vaults: %w", err)
	}
	defer rows.Close()

	return scanVaults(rows)
}

// GetProjectVaults returns all vaults belonging to a project.
func (db *DB) GetProjectVaults(projectID string) ([]Vault, error) {
	return db.SearchVaults("", projectID)
}

func scanVaults(rows interface{ Next() bool; Scan(...any) error }) ([]Vault, error) {
	var result []Vault
	for rows.Next() {
		var v Vault
		var tagsJSON string
		var createdAt, updatedAt string
		var sym int

		err := rows.Scan(
			&v.ID, &v.Name, &v.ProjectID, &v.Description, &v.Path, &sym, &tagsJSON,
			&v.ContentHash, &createdAt, &updatedAt,
		)
		if err != nil {
			return nil, fmt.Errorf("scanning vault: %w", err)
		}

		v.Symlink = sym == 1
		v.Tags = unmarshalStrings(tagsJSON)
		v.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
		v.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)

		result = append(result, v)
	}
	return result, nil
}

func scanFunctions(rows interface{ Next() bool; Scan(...any) error }) ([]Function, error) {
	var result []Function
	for rows.Next() {
		var f Function
		var tagsJSON, usesFnJSON, usesTypJSON, returnsJSON, importsJSON, testsJSON string
		var propsJSON, emitsJSON, variantJSON string
		var hasState *int
		var createdAt, updatedAt string

		err := rows.Scan(
			&f.ID, &f.Name, &f.Kind, &f.Lang, &f.Domain, &f.Version, &f.Purity, &f.Signature,
			&f.Description, &tagsJSON, &usesFnJSON, &usesTypJSON, &returnsJSON,
			&f.ReturnsOptional, &f.ErrorType, &importsJSON, &f.Example, &f.Tested,
			&testsJSON, &f.TestFilePath, &f.FilePath, &createdAt, &updatedAt,
			&propsJSON, &emitsJSON, &hasState, &f.Framework, &variantJSON,
			&f.Notes, &f.Documentation, &f.Code, &f.ContentHash,
			&f.SourceRepo, &f.SourceLicense, &f.SourceFile,
			&f.ParamsSchema,
		)
		if err != nil {
			return nil, fmt.Errorf("scanning function: %w", err)
		}

		f.Tags = unmarshalStrings(tagsJSON)
		f.UsesFunctions = unmarshalStrings(usesFnJSON)
		f.UsesTypes = unmarshalStrings(usesTypJSON)
		f.Returns = unmarshalStrings(returnsJSON)
		f.Imports = unmarshalStrings(importsJSON)
		f.Tests = unmarshalStrings(testsJSON)
		f.Props = unmarshalProps(propsJSON)
		f.Emits = unmarshalStrings(emitsJSON)
		f.Variant = unmarshalStrings(variantJSON)
		f.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
		f.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)

		if hasState != nil {
			v := *hasState == 1
			f.HasState = &v
		}

		result = append(result, f)
	}
	return result, nil
}

func scanTypes(rows interface{ Next() bool; Scan(...any) error }) ([]Type, error) {
	var result []Type
	for rows.Next() {
		var t Type
		var tagsJSON, usesTypJSON string
		var createdAt, updatedAt string

		err := rows.Scan(
			&t.ID, &t.Name, &t.Lang, &t.Domain, &t.Version, &t.Algebraic,
			&t.Definition, &t.Description, &tagsJSON, &usesTypJSON,
			&t.FilePath, &createdAt, &updatedAt,
			&t.Examples, &t.Notes, &t.Documentation, &t.Code, &t.ContentHash,
			&t.SourceRepo, &t.SourceLicense, &t.SourceFile,
		)
		if err != nil {
			return nil, fmt.Errorf("scanning type: %w", err)
		}

		t.Tags = unmarshalStrings(tagsJSON)
		t.UsesTypes = unmarshalStrings(usesTypJSON)
		t.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
		t.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)

		result = append(result, t)
	}
	return result, nil
}

// --- Unit Tests CRUD ---

// InsertUnitTest inserts or replaces a unit test entry.
func (db *DB) InsertUnitTest(ut *UnitTest) error {
	now := time.Now().UTC()
	if ut.CreatedAt.IsZero() {
		ut.CreatedAt = now
	}
	if ut.UpdatedAt.IsZero() {
		ut.UpdatedAt = now
	}

	_, err := db.conn.Exec(`
		INSERT OR REPLACE INTO unit_tests (
			id, function_id, name, code, file_path, lang, created_at, updated_at
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
		ut.ID, ut.FunctionID, ut.Name, ut.Code, ut.FilePath, ut.Lang,
		ut.CreatedAt.Format(time.RFC3339), ut.UpdatedAt.Format(time.RFC3339),
	)
	return err
}

// GetUnitTestsByFunction returns all unit tests for a given function ID.
func (db *DB) GetUnitTestsByFunction(functionID string) ([]UnitTest, error) {
	rows, err := db.conn.Query(
		"SELECT id, function_id, name, code, file_path, lang, created_at, updated_at FROM unit_tests WHERE function_id = ? ORDER BY name",
		functionID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanUnitTests(rows)
}

// SearchUnitTests performs FTS search on unit tests.
func (db *DB) SearchUnitTests(query string, lang string) ([]UnitTest, error) {
	where := []string{}
	args := []any{}

	if query != "" {
		where = append(where, "ut.id IN (SELECT id FROM unit_tests_fts WHERE unit_tests_fts MATCH ?)")
		args = append(args, query)
	}
	if lang != "" {
		where = append(where, "ut.lang = ?")
		args = append(args, lang)
	}

	sql := "SELECT id, function_id, name, code, file_path, lang, created_at, updated_at FROM unit_tests ut"
	if len(where) > 0 {
		sql += " WHERE " + strings.Join(where, " AND ")
	}
	sql += " ORDER BY ut.function_id, ut.name"

	rows, err := db.conn.Query(sql, args...)
	if err != nil {
		return nil, fmt.Errorf("search unit tests: %w", err)
	}
	defer rows.Close()
	return scanUnitTests(rows)
}

func scanUnitTests(rows interface{ Next() bool; Scan(...any) error }) ([]UnitTest, error) {
	var result []UnitTest
	for rows.Next() {
		var ut UnitTest
		var createdAt, updatedAt string
		err := rows.Scan(&ut.ID, &ut.FunctionID, &ut.Name, &ut.Code, &ut.FilePath, &ut.Lang, &createdAt, &updatedAt)
		if err != nil {
			return nil, fmt.Errorf("scanning unit test: %w", err)
		}
		ut.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
		ut.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
		result = append(result, ut)
	}
	return result, nil
}

// PurgeUnitTests deletes all unit test entries. Used before re-indexing.
func (db *DB) PurgeUnitTests() error {
	_, err := db.conn.Exec("DELETE FROM unit_tests")
	return err
}

// --- Proposal CRUD ---

// InsertProposal inserts or replaces a proposal.
func (db *DB) InsertProposal(p *Proposal) error {
	now := time.Now().UTC()
	if p.CreatedAt.IsZero() {
		p.CreatedAt = now
	}
	p.UpdatedAt = now

	if p.Status == "" {
		p.Status = ProposalPending
	}

	_, err := db.conn.Exec(`
		INSERT OR REPLACE INTO proposals (
			id, kind, target_id, title, description, evidence,
			status, created_by, reviewed_by, created_at, updated_at
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		p.ID, string(p.Kind), p.TargetID, p.Title, p.Description,
		marshalJSON(p.Evidence), string(p.Status), p.CreatedBy, p.ReviewedBy,
		p.CreatedAt.Format(time.RFC3339), p.UpdatedAt.Format(time.RFC3339),
	)
	return err
}

// GetProposal returns a proposal by ID.
func (db *DB) GetProposal(id string) (*Proposal, error) {
	rows, err := db.conn.Query(`
		SELECT id, kind, target_id, title, description, evidence,
		       status, created_by, reviewed_by, created_at, updated_at
		FROM proposals WHERE id = ?`, id)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	ps, err := scanProposals(rows)
	if err != nil {
		return nil, err
	}
	if len(ps) == 0 {
		return nil, fmt.Errorf("proposal %q not found", id)
	}
	return &ps[0], nil
}

// UpdateProposal updates an existing proposal.
func (db *DB) UpdateProposal(p *Proposal) error {
	p.UpdatedAt = time.Now().UTC()
	_, err := db.conn.Exec(`
		UPDATE proposals SET kind=?, target_id=?, title=?, description=?, evidence=?,
		       status=?, created_by=?, reviewed_by=?, updated_at=?
		WHERE id=?`,
		string(p.Kind), p.TargetID, p.Title, p.Description,
		marshalJSON(p.Evidence), string(p.Status), p.CreatedBy, p.ReviewedBy,
		p.UpdatedAt.Format(time.RFC3339), p.ID,
	)
	return err
}

// DeleteProposal removes a proposal by ID.
func (db *DB) DeleteProposal(id string) error {
	_, err := db.conn.Exec("DELETE FROM proposals WHERE id = ?", id)
	return err
}

// ListProposals returns proposals filtered by kind and/or status.
func (db *DB) ListProposals(kind ProposalKind, status ProposalStatus) ([]Proposal, error) {
	where := []string{}
	args := []any{}
	if kind != "" {
		where = append(where, "kind = ?")
		args = append(args, string(kind))
	}
	if status != "" {
		where = append(where, "status = ?")
		args = append(args, string(status))
	}

	q := `SELECT id, kind, target_id, title, description, evidence,
	             status, created_by, reviewed_by, created_at, updated_at
	      FROM proposals`
	if len(where) > 0 {
		q += " WHERE " + strings.Join(where, " AND ")
	}
	q += " ORDER BY created_at DESC"

	rows, err := db.conn.Query(q, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanProposals(rows)
}

// SearchProposals performs FTS search on proposals with optional filters.
func (db *DB) SearchProposals(query string, kind ProposalKind, status ProposalStatus) ([]Proposal, error) {
	where := []string{}
	args := []any{}

	if query != "" {
		where = append(where, "p.id IN (SELECT id FROM proposals_fts WHERE proposals_fts MATCH ?)")
		args = append(args, query)
	}
	if kind != "" {
		where = append(where, "p.kind = ?")
		args = append(args, string(kind))
	}
	if status != "" {
		where = append(where, "p.status = ?")
		args = append(args, string(status))
	}

	q := `SELECT p.id, p.kind, p.target_id, p.title, p.description, p.evidence,
	             p.status, p.created_by, p.reviewed_by, p.created_at, p.updated_at
	      FROM proposals p`
	if len(where) > 0 {
		q += " WHERE " + strings.Join(where, " AND ")
	}
	q += " ORDER BY p.created_at DESC"

	rows, err := db.conn.Query(q, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanProposals(rows)
}

func scanProposals(rows interface{ Next() bool; Scan(...any) error }) ([]Proposal, error) {
	var result []Proposal
	for rows.Next() {
		var p Proposal
		var evidenceJSON, createdAt, updatedAt string
		err := rows.Scan(
			&p.ID, &p.Kind, &p.TargetID, &p.Title, &p.Description, &evidenceJSON,
			&p.Status, &p.CreatedBy, &p.ReviewedBy, &createdAt, &updatedAt,
		)
		if err != nil {
			return nil, fmt.Errorf("scanning proposal: %w", err)
		}
		p.Evidence = unmarshalJSON(evidenceJSON)
		p.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
		p.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
		result = append(result, p)
	}
	return result, nil
}

// --- PcLocation CRUD ---

// InsertPcLocation inserts or replaces a pc_location entry.
func (db *DB) InsertPcLocation(loc *PcLocation) error {
	now := time.Now().UTC()
	if loc.CreatedAt.IsZero() {
		loc.CreatedAt = now
	}
	if loc.UpdatedAt.IsZero() {
		loc.UpdatedAt = now
	}
	if loc.ID == "" {
		loc.ID = loc.EntityType + "_" + loc.EntityID + "_" + loc.PcID
	}
	if loc.Status == "" {
		loc.Status = "active"
	}

	_, err := db.conn.Exec(`
		INSERT OR REPLACE INTO pc_locations (
			id, entity_type, entity_id, pc_id, dir_path, status, notes, created_at, updated_at
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		loc.ID, loc.EntityType, loc.EntityID, loc.PcID, loc.DirPath, loc.Status, loc.Notes,
		loc.CreatedAt.Format(time.RFC3339), loc.UpdatedAt.Format(time.RFC3339),
	)
	return err
}

// GetPcLocationsByPC returns all locations for a given PC.
func (db *DB) GetPcLocationsByPC(pcID string) ([]PcLocation, error) {
	rows, err := db.conn.Query(
		"SELECT id, entity_type, entity_id, pc_id, dir_path, status, notes, created_at, updated_at FROM pc_locations WHERE pc_id = ? ORDER BY entity_type, entity_id",
		pcID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanPcLocations(rows)
}

// GetPcLocationsByEntity returns all PC locations for a given entity.
func (db *DB) GetPcLocationsByEntity(entityType, entityID string) ([]PcLocation, error) {
	rows, err := db.conn.Query(
		"SELECT id, entity_type, entity_id, pc_id, dir_path, status, notes, created_at, updated_at FROM pc_locations WHERE entity_type = ? AND entity_id = ? ORDER BY pc_id",
		entityType, entityID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanPcLocations(rows)
}

// ListAllPcLocations returns all pc_location entries.
func (db *DB) ListAllPcLocations() ([]PcLocation, error) {
	rows, err := db.conn.Query(
		"SELECT id, entity_type, entity_id, pc_id, dir_path, status, notes, created_at, updated_at FROM pc_locations ORDER BY pc_id, entity_type, entity_id",
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanPcLocations(rows)
}

// DeletePcLocationsByPC removes all locations for a given PC.
func (db *DB) DeletePcLocationsByPC(pcID string) error {
	_, err := db.conn.Exec("DELETE FROM pc_locations WHERE pc_id = ?", pcID)
	return err
}

func scanPcLocations(rows interface{ Next() bool; Scan(...any) error }) ([]PcLocation, error) {
	var result []PcLocation
	for rows.Next() {
		var loc PcLocation
		var createdAt, updatedAt string
		err := rows.Scan(
			&loc.ID, &loc.EntityType, &loc.EntityID, &loc.PcID,
			&loc.DirPath, &loc.Status, &loc.Notes, &createdAt, &updatedAt,
		)
		if err != nil {
			return nil, fmt.Errorf("scanning pc_location: %w", err)
		}
		loc.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
		loc.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
		result = append(result, loc)
	}
	return result, nil
}

// --- Sync helpers ---

// AllApps returns all apps (for sync export).
func (db *DB) AllApps() ([]App, error) {
	return db.SearchApps("", "", "")
}

// AllAnalysis returns all analysis entries (for sync export).
func (db *DB) AllAnalysis() ([]Analysis, error) {
	return db.SearchAnalysis("", "", "")
}

// AllProposals returns all proposals (for sync export).
func (db *DB) AllProposals() ([]Proposal, error) {
	return db.ListProposals("", "")
}

// AllVaults returns all vaults (for sync export).
func (db *DB) AllVaults() ([]Vault, error) {
	return db.SearchVaults("", "")
}

// --- Module CRUD ---

// InsertModule inserts or replaces a module entry.
func (db *DB) InsertModule(m *Module) error {
	now := time.Now().UTC()
	if m.CreatedAt.IsZero() {
		m.CreatedAt = now
	}
	if m.UpdatedAt.IsZero() {
		m.UpdatedAt = now
	}
	if m.ID == "" {
		m.ID = GenerateModuleID(m.Name, m.Lang)
	}

	_, err := db.conn.Exec(`
		INSERT OR REPLACE INTO modules (
			id, name, version, lang, description, members, tags,
			dir_path, repo_url, documentation, notes, content_hash, created_at, updated_at
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		m.ID, m.Name, m.Version, m.Lang, m.Description,
		marshalStrings(m.Members), marshalStrings(m.Tags),
		m.DirPath, m.RepoURL, m.Documentation, m.Notes, m.ContentHash,
		m.CreatedAt.Format(time.RFC3339), m.UpdatedAt.Format(time.RFC3339),
	)
	return err
}

// GetModule returns a single module by ID.
func (db *DB) GetModule(id string) (*Module, error) {
	rows, err := db.conn.Query("SELECT * FROM modules WHERE id = ?", id)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items, err := scanModules(rows)
	if err != nil {
		return nil, err
	}
	if len(items) == 0 {
		return nil, fmt.Errorf("module %q not found", id)
	}
	return &items[0], nil
}

// SearchModules performs FTS search on modules with optional filters.
func (db *DB) SearchModules(query, lang string) ([]Module, error) {
	where := []string{}
	args := []any{}

	if query != "" {
		where = append(where, "m.id IN (SELECT id FROM modules_fts WHERE modules_fts MATCH ?)")
		args = append(args, query)
	}
	if lang != "" {
		where = append(where, "m.lang = ?")
		args = append(args, lang)
	}

	sql := "SELECT * FROM modules m"
	if len(where) > 0 {
		sql += " WHERE " + strings.Join(where, " AND ")
	}
	sql += " ORDER BY m.name"

	rows, err := db.conn.Query(sql, args...)
	if err != nil {
		return nil, fmt.Errorf("search modules: %w", err)
	}
	defer rows.Close()

	return scanModules(rows)
}

// ListAllModules returns all module entries.
func (db *DB) ListAllModules() ([]Module, error) {
	return db.SearchModules("", "")
}

// AllModules returns all modules (for sync export).
func (db *DB) AllModules() ([]Module, error) {
	return db.SearchModules("", "")
}

func scanModules(rows interface{ Next() bool; Scan(...any) error }) ([]Module, error) {
	var result []Module
	for rows.Next() {
		var m Module
		var membersJSON, tagsJSON string
		var createdAt, updatedAt string

		err := rows.Scan(
			&m.ID, &m.Name, &m.Version, &m.Lang, &m.Description,
			&membersJSON, &tagsJSON,
			&m.DirPath, &m.RepoURL, &m.Documentation, &m.Notes, &m.ContentHash,
			&createdAt, &updatedAt,
		)
		if err != nil {
			return nil, fmt.Errorf("scanning module: %w", err)
		}

		m.Members = unmarshalStrings(membersJSON)
		m.Tags = unmarshalStrings(tagsJSON)
		m.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
		m.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)

		result = append(result, m)
	}
	return result, nil
}
