package infra

import (
	"crypto/sha1"
	"database/sql"
	"encoding/json"
	"fmt"
	"path/filepath"
	"strings"
	"time"

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

// ProposalDraft is a candidate proposal computed from call_monitor telemetry
// + registry static data. Persistence step writes it into registry.db.proposals
// with deterministic id (so re-runs are idempotent).
type ProposalDraft struct {
	ID          string         `json:"id"`
	Kind        string         `json:"kind"`         // new_function | improve_function | deprecate_function | new_pipeline | ...
	TargetID    string         `json:"target_id"`    // existing function id (empty for new_function)
	Title       string         `json:"title"`
	Description string         `json:"description"`
	RuleID      string         `json:"rule_id"`      // copy_detected | orphan | bug | wrapper_skipped | ...
	Evidence    map[string]any `json:"evidence"`
}

// GenerateProposalsFromTelemetry inspects call_monitor.operations.db and
// registry.db, applies the rules documented in issue 0085, and returns the
// proposed drafts. It does NOT write to any DB. Call PersistProposalDrafts
// to upsert into registry.db.proposals.
//
// Implemented rules (MVP):
//   - copy_detected → improve_function (target = matched registry_id)
//   - orphan        → deprecate_function (calls_90d=0 AND writes_count=0
//                     AND no upstream consumer in registry.db.functions.uses_functions)
//   - bug           → improve_function (error_rate > 0.1 AND calls_total > 3)
//   - wrapper_skip  → improve_function (violations.function_id grouped, count > 3)
//
// Skipped rules (no data yet): perf_regression, test_flaky, e2e_blast_radius,
// pattern_inline (requires patterns table populated).
func GenerateProposalsFromTelemetry(registryRoot string) ([]ProposalDraft, error) {
	regPath := filepath.Join(registryRoot, "registry.db")
	monPath := filepath.Join(registryRoot, "projects", "fn_monitoring", "apps", "call_monitor", "operations.db")

	regDB, err := sql.Open("sqlite3", regPath+"?_journal_mode=WAL")
	if err != nil {
		return nil, fmt.Errorf("open registry: %w", err)
	}
	defer regDB.Close()

	monDB, err := sql.Open("sqlite3", monPath+"?_journal_mode=WAL")
	if err != nil {
		return nil, fmt.Errorf("open call_monitor: %w", err)
	}
	defer monDB.Close()

	// Build set of "function_ids consumed by anything upstream" from registry
	// (functions/apps/analysis.uses_functions JSON array). Used to filter orphan
	// candidates.
	consumers, err := loadConsumerSet(regDB)
	if err != nil {
		return nil, err
	}

	var drafts []ProposalDraft

	// --- Rule 1: copy_detected ---
	rows, err := monDB.Query(`SELECT app_file, app_function, registry_id, kind, similarity
		FROM copied_code ORDER BY detected_at DESC`)
	if err == nil {
		for rows.Next() {
			var appFile, appFunc, regID, kind string
			var sim float64
			if err := rows.Scan(&appFile, &appFunc, &regID, &kind, &sim); err != nil {
				continue
			}
			drafts = append(drafts, ProposalDraft{
				ID:       deterministicID("copy_detected", regID, appFile+":"+appFunc),
				Kind:     "improve_function",
				TargetID: regID,
				Title:    fmt.Sprintf("Replace inline copy with import: %s in %s::%s", regID, shortPath(appFile), appFunc),
				Description: fmt.Sprintf("App `%s` declares `%s` with a body matching `%s` (%s, similarity %.2f). "+
					"Replace the inline body with a registry import to centralize maintenance.",
					appFile, appFunc, regID, kind, sim),
				RuleID: "copy_detected",
				Evidence: map[string]any{
					"rule_id":      "copy_detected",
					"app_file":     appFile,
					"app_function": appFunc,
					"registry_id":  regID,
					"kind":         kind,
					"similarity":   sim,
				},
			})
		}
		rows.Close()
	}

	// --- Rule 2: orphan (deprecate_function) ---
	// function_stats rows with no calls in 90d AND no writes.
	// Cross-check against consumers set: if anybody imports it, NOT orphan.
	rows, err = monDB.Query(`
		SELECT function_id, calls_90d, writes_count
		FROM function_stats
		WHERE calls_90d = 0 AND writes_count = 0`)
	if err == nil {
		for rows.Next() {
			var fnID string
			var calls90d, writes int
			if err := rows.Scan(&fnID, &calls90d, &writes); err != nil {
				continue
			}
			if _, isConsumed := consumers[fnID]; isConsumed {
				continue
			}
			drafts = append(drafts, ProposalDraft{
				ID:       deterministicID("orphan", fnID, ""),
				Kind:     "deprecate_function",
				TargetID: fnID,
				Title:    "Deprecate orphan function: " + fnID,
				Description: fmt.Sprintf("`%s` has no calls in 90d, no recent writes, and no upstream consumer "+
					"declared in registry. Candidate for deprecation. Verify with `fn doctor unused` and manual review.", fnID),
				RuleID: "orphan",
				Evidence: map[string]any{
					"rule_id":      "orphan",
					"calls_90d":    calls90d,
					"writes_count": writes,
				},
			})
		}
		rows.Close()
	}

	// --- Rule 3: bug (improve_function on error_rate) ---
	rows, err = monDB.Query(`
		SELECT function_id, calls_total, errors_total, error_rate
		FROM function_stats
		WHERE error_rate > 0.1 AND calls_total > 3`)
	if err == nil {
		for rows.Next() {
			var fnID string
			var calls, errs int
			var rate float64
			if err := rows.Scan(&fnID, &calls, &errs, &rate); err != nil {
				continue
			}
			drafts = append(drafts, ProposalDraft{
				ID:       deterministicID("bug", fnID, ""),
				Kind:     "improve_function",
				TargetID: fnID,
				Title:    fmt.Sprintf("Investigate high error rate: %s (%.0f%% over %d calls)", fnID, rate*100, calls),
				Description: fmt.Sprintf("`%s` fails %.1f%% of the time across %d calls (%d errors). "+
					"Check call_monitor.operations.db for recent error_class samples.",
					fnID, rate*100, calls, errs),
				RuleID: "bug",
				Evidence: map[string]any{
					"rule_id":      "bug",
					"calls_total":  calls,
					"errors_total": errs,
					"error_rate":   rate,
				},
			})
		}
		rows.Close()
	}

	// --- Rule 4: wrapper_skip (violations grouped) ---
	rows, err = monDB.Query(`
		SELECT function_id, COUNT(*) AS hits, MAX(severity) AS sev
		FROM violations
		WHERE function_id != ''
		GROUP BY function_id
		HAVING hits > 3`)
	if err == nil {
		for rows.Next() {
			var fnID, sev string
			var hits int
			if err := rows.Scan(&fnID, &hits, &sev); err != nil {
				continue
			}
			drafts = append(drafts, ProposalDraft{
				ID:       deterministicID("wrapper_skip", fnID, ""),
				Kind:     "improve_function",
				TargetID: fnID,
				Title:    fmt.Sprintf("Wrapper skipped %d times: %s", hits, fnID),
				Description: fmt.Sprintf("Agent bypassed the registry wrapper for `%s` %d times. "+
					"API surface may be incomplete — check what callers needed and extend.", fnID, hits),
				RuleID: "wrapper_skip",
				Evidence: map[string]any{
					"rule_id":  "wrapper_skip",
					"hits":     hits,
					"severity": sev,
				},
			})
		}
		rows.Close()
	}

	return drafts, nil
}

// PersistProposalDrafts upserts each draft into registry.db.proposals via
// INSERT OR IGNORE so previously-reviewed proposals (approved/rejected) are
// preserved untouched. Returns (newly_inserted, total_seen, error).
func PersistProposalDrafts(registryRoot string, drafts []ProposalDraft) (int, int, error) {
	if len(drafts) == 0 {
		return 0, 0, nil
	}
	regPath := filepath.Join(registryRoot, "registry.db")
	conn, err := sql.Open("sqlite3", regPath+"?_journal_mode=WAL")
	if err != nil {
		return 0, 0, fmt.Errorf("open registry: %w", err)
	}
	defer conn.Close()

	tx, err := conn.Begin()
	if err != nil {
		return 0, 0, err
	}
	stmt, err := tx.Prepare(`INSERT OR IGNORE INTO proposals
		(id, kind, target_id, title, description, evidence, status, created_by, reviewed_by, created_at, updated_at)
		VALUES (?, ?, ?, ?, ?, ?, 'pending', 'reactive_loop', '', ?, ?)`)
	if err != nil {
		_ = tx.Rollback()
		return 0, 0, err
	}
	defer stmt.Close()

	now := time.Now().UTC().Format(time.RFC3339)
	inserted := 0
	for _, d := range drafts {
		evidence, _ := json.Marshal(d.Evidence)
		res, err := stmt.Exec(d.ID, d.Kind, d.TargetID, d.Title, d.Description, string(evidence), now, now)
		if err != nil {
			_ = tx.Rollback()
			return 0, 0, err
		}
		if n, _ := res.RowsAffected(); n > 0 {
			inserted++
		}
	}
	if err := tx.Commit(); err != nil {
		return 0, 0, err
	}
	return inserted, len(drafts), nil
}

// ---- Helpers ----

// loadConsumerSet returns every function id that appears in any uses_functions
// JSON array across functions, apps, and analysis tables. Used to filter orphan
// candidates.
func loadConsumerSet(db *sql.DB) (map[string]struct{}, error) {
	consumers := make(map[string]struct{})

	queries := []string{
		"SELECT uses_functions FROM functions WHERE uses_functions != '[]'",
		"SELECT uses_functions FROM apps WHERE uses_functions != '[]'",
		"SELECT uses_functions FROM analysis WHERE uses_functions != '[]'",
	}
	for _, q := range queries {
		rows, err := db.Query(q)
		if err != nil {
			// Table may not exist (e.g. analysis); ignore.
			continue
		}
		for rows.Next() {
			var raw string
			if err := rows.Scan(&raw); err != nil {
				continue
			}
			var ids []string
			if err := json.Unmarshal([]byte(raw), &ids); err != nil {
				continue
			}
			for _, id := range ids {
				consumers[id] = struct{}{}
			}
		}
		rows.Close()
	}
	return consumers, nil
}

// deterministicID returns a stable proposal id derived from (rule, target, extra).
// Format: "auto_<rule>_<sha1-10>" so re-running the generator never inserts
// duplicates of the same logical proposal.
func deterministicID(rule, target, extra string) string {
	key := rule + "|" + target + "|" + extra
	h := sha1.Sum([]byte(key))
	short := fmt.Sprintf("%x", h)[:10]
	return "auto_" + rule + "_" + short
}

func shortPath(p string) string {
	idx := strings.LastIndex(p, "/")
	if idx == -1 || idx == len(p)-1 {
		return p
	}
	parent := p[:idx]
	pidx := strings.LastIndex(parent, "/")
	if pidx == -1 {
		return p
	}
	return p[pidx+1:]
}
