package cmd

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"log"
	"strings"
	"time"

	"github.com/agentic-research/mache/internal/graph"
	"github.com/agentic-research/mache/internal/leyline"
	"github.com/agentic-research/mache/internal/lsp"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

// lspEnrichHint is the user-facing guidance shown when LSP data is missing
// and ley-line daemon is not available. Single definition, used by both
// get_type_info and get_diagnostics handlers.
const lspEnrichHint = "Pre-enrich your database with ll-open, or install ley-line for live enrichment.\n" +
	"See: github.com/agentic-research/ley-line-open\n" +
	"Capability matrix: docs/ARCHITECTURE.md#interplay-with-ley-line-open"

func lspTableMissing(table, feature string) *mcp.CallToolResult {
	return mcp.NewToolResultError(fmt.Sprintf("no %s table in database. To get %s, either:\n"+
		"  1. Pre-enrich with: ll-open enrich-lsp (see github.com/agentic-research/ley-line-open)\n"+
		"  2. Pass a 'file' param to trigger live enrichment (requires ley-line daemon)\n"+
		"See docs/ARCHITECTURE.md#interplay-with-ley-line-open for which tools need which tables.", table, feature))
}

func lspEnrichFailed(feature string) *mcp.CallToolResult {
	return mcp.NewToolResultError(fmt.Sprintf("%s not available — LSP enrichment requires ley-line daemon.\n%s", feature, lspEnrichHint))
}

func makeGetTypeInfoHandler(g graph.Graph) server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		symbol := request.GetString("symbol", "")
		if symbol == "" {
			return mcp.NewToolResultError("symbol is required"), nil
		}

		// Fail fast on non-SQL backends with a clear message instead
		// of the generic post-assertion "LSP data not available" which
		// is the same string we use for "table missing." Today
		// lazyGraph always satisfies refsQuerier; this guards a future
		// backend that drops the method.
		qg, ok := g.(refsQuerier)
		if !ok {
			return mcp.NewToolResultError("get_type_info requires a SQL-capable graph backend (mache standalone or leyline-parsed .db)"), nil
		}

		// Check if _lsp_hover table exists
		rows, err := qg.QueryRefs(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='_lsp_hover'`)
		if err != nil {
			return mcp.NewToolResultError("LSP data not available for this data source"), nil
		}
		var refsTableExists int
		if rows.Next() {
			_ = rows.Scan(&refsTableExists)
		}
		_ = rows.Close()
		if refsTableExists == 0 {
			filePath := request.GetString("file", "")
			if filePath == "" {
				return lspTableMissing("_lsp_hover", "type info"), nil
			}
			result, err := enrichAndQueryTypeInfo(filePath, symbol)
			if err != nil {
				log.Printf("LSP auto-enrichment failed: %v", err)
				return lspEnrichFailed("type info"), nil
			}
			return result, nil
		}

		// LSP tables exist in mache's graph — query directly
		return queryTypeInfoFromGraph(qg, symbol)
	}
}

func makeGetDiagnosticsHandler(g graph.Graph) server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		symbol := request.GetString("symbol", "")
		limit := request.GetInt("limit", 50)

		// Mirror get_type_info: fail fast with a specific error if
		// the backend doesn't support SQL queries.
		qg, ok := g.(refsQuerier)
		if !ok {
			return mcp.NewToolResultError("get_diagnostics requires a SQL-capable graph backend (mache standalone or leyline-parsed .db)"), nil
		}

		// Check if _lsp table exists
		rows, err := qg.QueryRefs(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='_lsp'`)
		if err != nil {
			return mcp.NewToolResultError("LSP data not available for this data source"), nil
		}
		var refsTableExists int
		if rows.Next() {
			_ = rows.Scan(&refsTableExists)
		}
		_ = rows.Close()
		if refsTableExists == 0 {
			filePath := request.GetString("file", "")
			if filePath == "" {
				return lspTableMissing("_lsp", "diagnostics"), nil
			}
			result, err := enrichAndQueryDiagnostics(filePath, symbol, limit)
			if err != nil {
				log.Printf("LSP auto-enrichment failed: %v", err)
				return lspEnrichFailed("diagnostics"), nil
			}
			return result, nil
		}

		return queryDiagnosticsFromGraph(qg, symbol, limit)
	}
}

// enrichAndQueryTypeInfo triggers LSP enrichment and queries hover data
// directly from the ley-line daemon's arena via UDS, bypassing mache's
// in-memory graph (which doesn't have _lsp* tables).
// Uses a single connection for both enrichment and query.
func enrichAndQueryTypeInfo(filePath, symbol string) (*mcp.CallToolResult, error) {
	sockPath, err := leyline.DiscoverOrStart()
	if err != nil {
		return nil, fmt.Errorf("discover/start leyline: %w", err)
	}
	client, err := leyline.DialSocket(sockPath)
	if err != nil {
		return nil, fmt.Errorf("dial: %w", err)
	}
	defer func() { _ = client.Close() }()

	// Enrichment phase — 30s timeout for LSP
	if err := client.SetDeadline(time.Now().Add(30 * time.Second)); err != nil {
		return nil, fmt.Errorf("set deadline: %w", err)
	}
	resp, err := client.Tool("lsp", map[string]any{"file": filePath})
	if err != nil {
		return nil, err
	}
	log.Printf("LSP enrichment via ley-line daemon: %v", resp)

	// Query phase — reuse same connection, reset deadline
	if err := client.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
		return nil, fmt.Errorf("set query deadline: %w", err)
	}

	escaped := escapeLikePattern(symbol)
	rows, err := client.Query(fmt.Sprintf(
		`SELECT node_id, hover_text FROM _lsp_hover WHERE node_id LIKE '%%%s' ESCAPE '\'`, escaped))
	if err != nil {
		return nil, fmt.Errorf("query _lsp_hover via daemon: %w", err)
	}

	type hoverResult struct {
		NodeID    string `json:"node_id"`
		HoverText string `json:"hover_text"`
	}

	var results []hoverResult
	for _, row := range rows {
		if len(row) >= 2 {
			results = append(results, hoverResult{
				NodeID:    fmt.Sprint(row[0]),
				HoverText: fmt.Sprint(row[1]),
			})
		}
	}

	// Fallback: broader match
	if len(results) == 0 {
		rows, err = client.Query(fmt.Sprintf(
			`SELECT node_id, hover_text FROM _lsp_hover WHERE node_id LIKE '%%%s%%' ESCAPE '\'`, escaped))
		if err == nil {
			for _, row := range rows {
				if len(row) >= 2 {
					results = append(results, hoverResult{
						NodeID:    fmt.Sprint(row[0]),
						HoverText: fmt.Sprint(row[1]),
					})
				}
			}
		}
	}

	if len(results) == 0 {
		return mcp.NewToolResultText(fmt.Sprintf("LSP enrichment completed but no hover info found for %q", symbol)), nil
	}

	data, _ := json.MarshalIndent(results, "", "  ")
	return mcp.NewToolResultText(string(data)), nil
}

// queryTypeInfoFromGraph queries _lsp_hover from mache's in-memory graph
// (used when LSP tables already exist in the graph, e.g. from leyline lsp CLI).
func queryTypeInfoFromGraph(qg refsQuerier, symbol string) (*mcp.CallToolResult, error) {
	type hoverResult struct {
		NodeID    string `json:"node_id"`
		HoverText string `json:"hover_text"`
	}

	rows, err := qg.QueryRefs(
		`SELECT node_id, hover_text FROM _lsp_hover WHERE node_id LIKE ? ESCAPE '\'`,
		"%/"+escapeLikeMeta(symbol),
	)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("query _lsp_hover: %v", err)), nil
	}

	var results []hoverResult
	for rows.Next() {
		var r hoverResult
		if err := rows.Scan(&r.NodeID, &r.HoverText); err != nil {
			continue
		}
		results = append(results, r)
	}
	_ = rows.Close()

	if len(results) == 0 {
		rows, err = qg.QueryRefs(
			`SELECT node_id, hover_text FROM _lsp_hover WHERE node_id LIKE ? ESCAPE '\'`,
			"%"+escapeLikeMeta(symbol)+"%",
		)
		if err == nil {
			for rows.Next() {
				var r hoverResult
				if err := rows.Scan(&r.NodeID, &r.HoverText); err != nil {
					continue
				}
				results = append(results, r)
			}
			_ = rows.Close()
		}
	}

	if len(results) == 0 {
		return mcp.NewToolResultText(fmt.Sprintf("no type info found for %q", symbol)), nil
	}

	data, _ := json.MarshalIndent(results, "", "  ")
	return mcp.NewToolResultText(string(data)), nil
}

// enrichAndQueryDiagnostics triggers LSP enrichment and queries diagnostics
// directly from the ley-line daemon's arena via UDS.
// Uses a single connection for both enrichment and query.
func enrichAndQueryDiagnostics(filePath, symbol string, limit int) (*mcp.CallToolResult, error) {
	sockPath, err := leyline.DiscoverOrStart()
	if err != nil {
		return nil, fmt.Errorf("discover/start leyline: %w", err)
	}
	client, err := leyline.DialSocket(sockPath)
	if err != nil {
		return nil, fmt.Errorf("dial: %w", err)
	}
	defer func() { _ = client.Close() }()

	if err := client.SetDeadline(time.Now().Add(30 * time.Second)); err != nil {
		return nil, fmt.Errorf("set deadline: %w", err)
	}
	resp, err := client.Tool("lsp", map[string]any{"file": filePath})
	if err != nil {
		return nil, err
	}
	log.Printf("LSP enrichment via ley-line daemon: %v", resp)

	if err := client.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
		return nil, fmt.Errorf("set query deadline: %w", err)
	}

	var query string
	if symbol != "" {
		escaped := escapeLikePattern(symbol)
		query = fmt.Sprintf(
			`SELECT node_id, symbol_kind, diagnostics FROM _lsp WHERE diagnostics IS NOT NULL AND diagnostics != '' AND node_id LIKE '%%%s%%' ESCAPE '\' LIMIT %d`,
			escaped, limit)
	} else {
		query = fmt.Sprintf(
			`SELECT node_id, symbol_kind, diagnostics FROM _lsp WHERE diagnostics IS NOT NULL AND diagnostics != '' LIMIT %d`,
			limit)
	}

	rows, err := client.Query(query)
	if err != nil {
		return nil, fmt.Errorf("query _lsp via daemon: %w", err)
	}

	type diagResult struct {
		NodeID      string `json:"node_id"`
		SymbolKind  string `json:"symbol_kind"`
		Diagnostics string `json:"diagnostics"`
	}

	var results []diagResult
	for _, row := range rows {
		if len(row) >= 3 {
			results = append(results, diagResult{
				NodeID:      fmt.Sprint(row[0]),
				SymbolKind:  fmt.Sprint(row[1]),
				Diagnostics: fmt.Sprint(row[2]),
			})
		}
	}

	if len(results) == 0 {
		if symbol != "" {
			return mcp.NewToolResultText(fmt.Sprintf("no diagnostics found for %q", symbol)), nil
		}
		return mcp.NewToolResultText("no diagnostics found"), nil
	}

	data, _ := json.MarshalIndent(results, "", "  ")
	return mcp.NewToolResultText(string(data)), nil
}

// queryDiagnosticsFromGraph queries _lsp from mache's in-memory graph.
func queryDiagnosticsFromGraph(qg refsQuerier, symbol string, limit int) (*mcp.CallToolResult, error) {
	type diagResult struct {
		NodeID      string `json:"node_id"`
		SymbolKind  string `json:"symbol_kind"`
		Diagnostics string `json:"diagnostics"`
	}

	var query string
	var args []any
	if symbol != "" {
		query = `SELECT node_id, symbol_kind, diagnostics FROM _lsp WHERE diagnostics IS NOT NULL AND diagnostics != '' AND node_id LIKE ? ESCAPE '\' LIMIT ?`
		args = []any{"%" + escapeLikeMeta(symbol) + "%", limit}
	} else {
		query = `SELECT node_id, symbol_kind, diagnostics FROM _lsp WHERE diagnostics IS NOT NULL AND diagnostics != '' LIMIT ?`
		args = []any{limit}
	}

	rows, err := qg.QueryRefs(query, args...)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("query _lsp: %v", err)), nil
	}

	var results []diagResult
	for rows.Next() {
		var r diagResult
		if err := rows.Scan(&r.NodeID, &r.SymbolKind, &r.Diagnostics); err != nil {
			continue
		}
		results = append(results, r)
	}
	_ = rows.Close()

	if len(results) == 0 {
		if symbol != "" {
			return mcp.NewToolResultText(fmt.Sprintf("no diagnostics found for %q", symbol)), nil
		}
		return mcp.NewToolResultText("no diagnostics found"), nil
	}

	data, _ := json.MarshalIndent(results, "", "  ")
	return mcp.NewToolResultText(string(data)), nil
}

// ---------------------------------------------------------------------------
// LSP definition/reference query helpers
// ---------------------------------------------------------------------------

// lspDefLocation represents a definition location from the _lsp_defs table.
type lspDefLocation struct {
	NodeID    string `json:"node_id"`
	URI       string `json:"uri"`
	StartLine int    `json:"start_line"`
	StartCol  int    `json:"start_col"`
	EndLine   int    `json:"end_line"`
	EndCol    int    `json:"end_col"`
}

// lspRefLocation represents a reference location from the _lsp_refs table.
type lspRefLocation struct {
	NodeID    string `json:"node_id"`
	URI       string `json:"uri"`
	StartLine int    `json:"start_line"`
	StartCol  int    `json:"start_col"`
	EndLine   int    `json:"end_line"`
	EndCol    int    `json:"end_col"`
}

// queryLSPDefs queries the _lsp_defs table for definition locations
// of a symbol. Returns (nil, nil) if the table does not exist.
//
// Per ADR-0013 Step 5 (mache-346d2b), this prefers the direct
// def_token match when ley-line-open's post-Step-1 column is
// present. The legacy suffix-then-broader-LIKE fallback survives
// for pre-Step-1 .dbs that still have only the (node_id, def_uri,
// ...) columns.
func queryLSPDefs(qg refsQuerier, symbol string) ([]lspDefLocation, error) {
	hasDefToken, err := tableHasColumn(qg, "_lsp_defs", "def_token")
	if err != nil {
		return nil, fmt.Errorf("probe _lsp_defs schema: %w", err)
	}
	if !hasDefToken {
		// Pre-Step-1 schema (or table missing entirely). Fall back to
		// node_id LIKE matching against the trailing component, with
		// a broader substring fallback if that returns nothing.
		return queryLSPDefsLegacy(qg, symbol)
	}

	rows, err := qg.QueryRefs(
		`SELECT node_id, def_uri, def_start_line, def_start_col, def_end_line, def_end_col
		 FROM _lsp_defs
		 WHERE def_token = ?`,
		symbol,
	)
	if err != nil {
		return nil, fmt.Errorf("query _lsp_defs: %w", err)
	}
	defer func() { _ = rows.Close() }()

	var results []lspDefLocation
	for rows.Next() {
		var r lspDefLocation
		if err := rows.Scan(&r.NodeID, &r.URI, &r.StartLine, &r.StartCol, &r.EndLine, &r.EndCol); err != nil {
			continue
		}
		results = append(results, r)
	}
	return results, rows.Err()
}

// queryLSPRefs returns reference locations for a symbol.
//
// Per mache-6bd4d8 (T8.8 mirror): the primary source is the sibling
// `.bindings.capnp` event log, read via internal/lsp.ReadBindingLog.
// `_lsp_refs` SQL columns were retired as the consumer-side contract
// — capnp records carry the full (refToken, refUri, refRange) tuple
// without a SQL JOIN, structurally precluding be6136-class column-
// name-as-protocol disagreements.
//
// Falls back to the legacy SQL path (queryLSPRefsLegacy) only when
// the sibling capnp log is absent AND the .db has the pre-Step-1
// `_lsp_refs` schema (no ref_token column). That window is narrow:
// post-LLO-T8.2 builds always emit the capnp log.
//
// Returns (nil, nil) when neither source has data for the symbol.
func queryLSPRefs(qg refsQuerier, symbol string) ([]lspRefLocation, error) {
	// Capnp source: requires the querier to know its dbPath
	// (dbPathProvider opt-in, mache-190508 step 3). When available,
	// it's the canonical source.
	if dp, ok := qg.(dbPathProvider); ok {
		if results, err := readLSPRefsFromCapnp(dp.DBPath(), symbol); err != nil {
			return nil, err
		} else if results != nil {
			return results, nil
		}
		// Capnp present but no match for this symbol → empty slice
		// vs nil distinction handled by readLSPRefsFromCapnp:
		// nil means "no log to consult", []lspRefLocation{} means
		// "log present, zero matches". Fall through only on nil.
	}

	// Legacy path: only fires when capnp is absent. queryLSPRefsLegacy
	// handles both the post-Step-1 schema (with ref_token column) and
	// the pre-Step-1 LIKE-suffix shape.
	return queryLSPRefsLegacy(qg, symbol)
}

// readLSPRefsFromCapnp reads the sibling .bindings.capnp event log and
// filters records by RefToken == symbol. Returns:
//
//	(nil, nil)      — no sibling log exists; caller should fall back
//	([]..., nil)    — log exists; slice contains all matches (possibly empty)
//	(nil, err)      — log exists but couldn't be read
//
// dbPath empty string is treated as "no source" (nil, nil).
func readLSPRefsFromCapnp(dbPath, symbol string) ([]lspRefLocation, error) {
	if dbPath == "" {
		return nil, nil
	}
	logPath := lsp.SiblingBindingLogPath(dbPath)
	records, err := lsp.ReadBindingLog(logPath)
	if errors.Is(err, fs.ErrNotExist) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("read binding log %s: %w", logPath, err)
	}

	// Intentional empty (not nil) slice: "log present, zero matches"
	// is meaningfully different from "no log to consult" — see the
	// caller's nil-check.
	results := []lspRefLocation{}
	for _, r := range records {
		if r.RefToken != symbol {
			continue
		}
		results = append(results, lspRefLocation{
			NodeID:    r.TargetNodeID,
			URI:       r.RefURI,
			StartLine: int(r.RefRange.StartLine),
			StartCol:  int(r.RefRange.StartColumn),
			EndLine:   int(r.RefRange.EndLine),
			EndCol:    int(r.RefRange.EndColumn),
		})
	}
	return results, nil
}

// queryLSPDefsLegacy is the pre-Step-1 query path: node_id suffix
// match, then broader substring LIKE if that returns nothing. Kept
// for .dbs that still have the legacy _lsp_defs schema (no def_token
// column). New consumers should rely on queryLSPDefs which prefers
// the direct token match when available.
func queryLSPDefsLegacy(qg refsQuerier, symbol string) ([]lspDefLocation, error) {
	// First confirm the table exists at all — tableHasColumn returns
	// false for both "no table" and "table without column", so we
	// have to disambiguate here for the legacy code path.
	exists, err := refsTableExists(qg, "_lsp_defs")
	if err != nil {
		return nil, err
	}
	if !exists {
		return nil, nil
	}

	escaped := escapeLikeMeta(symbol)
	rows, err := qg.QueryRefs(
		`SELECT node_id, def_uri, def_start_line, def_start_col, def_end_line, def_end_col
		 FROM _lsp_defs WHERE node_id LIKE ? ESCAPE '\'`,
		"%/"+escaped,
	)
	if err != nil {
		return nil, fmt.Errorf("query _lsp_defs (legacy suffix): %w", err)
	}

	var results []lspDefLocation
	for rows.Next() {
		var r lspDefLocation
		if err := rows.Scan(&r.NodeID, &r.URI, &r.StartLine, &r.StartCol, &r.EndLine, &r.EndCol); err != nil {
			continue
		}
		results = append(results, r)
	}
	_ = rows.Close()

	if len(results) == 0 {
		rows, err = qg.QueryRefs(
			`SELECT node_id, def_uri, def_start_line, def_start_col, def_end_line, def_end_col
			 FROM _lsp_defs WHERE node_id LIKE ? ESCAPE '\'`,
			"%"+escaped+"%",
		)
		if err != nil {
			return nil, fmt.Errorf("query _lsp_defs (legacy broad): %w", err)
		}
		for rows.Next() {
			var r lspDefLocation
			if err := rows.Scan(&r.NodeID, &r.URI, &r.StartLine, &r.StartCol, &r.EndLine, &r.EndCol); err != nil {
				continue
			}
			results = append(results, r)
		}
		_ = rows.Close()
	}
	return results, nil
}

// queryLSPRefsLegacy mirrors queryLSPDefsLegacy for the refs table.
func queryLSPRefsLegacy(qg refsQuerier, symbol string) ([]lspRefLocation, error) {
	exists, err := refsTableExists(qg, "_lsp_refs")
	if err != nil {
		return nil, err
	}
	if !exists {
		return nil, nil
	}

	escaped := escapeLikeMeta(symbol)
	rows, err := qg.QueryRefs(
		`SELECT node_id, ref_uri, ref_start_line, ref_start_col, ref_end_line, ref_end_col
		 FROM _lsp_refs WHERE node_id LIKE ? ESCAPE '\'`,
		"%/"+escaped,
	)
	if err != nil {
		return nil, fmt.Errorf("query _lsp_refs (legacy suffix): %w", err)
	}

	var results []lspRefLocation
	for rows.Next() {
		var r lspRefLocation
		if err := rows.Scan(&r.NodeID, &r.URI, &r.StartLine, &r.StartCol, &r.EndLine, &r.EndCol); err != nil {
			continue
		}
		results = append(results, r)
	}
	_ = rows.Close()

	if len(results) == 0 {
		rows, err = qg.QueryRefs(
			`SELECT node_id, ref_uri, ref_start_line, ref_start_col, ref_end_line, ref_end_col
			 FROM _lsp_refs WHERE node_id LIKE ? ESCAPE '\'`,
			"%"+escaped+"%",
		)
		if err != nil {
			return nil, fmt.Errorf("query _lsp_refs (legacy broad): %w", err)
		}
		for rows.Next() {
			var r lspRefLocation
			if err := rows.Scan(&r.NodeID, &r.URI, &r.StartLine, &r.StartCol, &r.EndLine, &r.EndCol); err != nil {
				continue
			}
			results = append(results, r)
		}
		_ = rows.Close()
	}
	return results, nil
}

// refsTableExists returns true iff the named SQLite table is present in
// the active database. Used by queryLSP*Legacy to short-circuit when
// the table doesn't exist (consistent with the original "(nil, nil)
// = no table" contract).
func refsTableExists(qg refsQuerier, name string) (bool, error) {
	if !isSimpleIdent(name) {
		return false, fmt.Errorf("invalid table name: %q", name)
	}
	rows, err := qg.QueryRefs(
		`SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?`,
		name,
	)
	if err != nil {
		return false, err
	}
	defer func() { _ = rows.Close() }()
	return rows.Next(), rows.Err()
}

// escapeLikeMeta escapes SQL LIKE metacharacters (%, _, \) only.
// Safe with parameterized queries — does not modify quotes.
func escapeLikeMeta(s string) string {
	r := strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`)
	return r.Replace(s)
}

// escapeLikePattern escapes LIKE metacharacters AND doubles single quotes.
// For non-parameterized queries (daemon socket path) only.
func escapeLikePattern(s string) string {
	return strings.ReplaceAll(escapeLikeMeta(s), `'`, `''`)
}
