/**
 * Pure transport-agnostic resource fetchers — every MCP resource the
 * codemap server exposes (`codemap://recipes`, `codemap://recipes/{id}`,
 * `codemap://schema`, `codemap://skill`, `codemap://rule`,
 * `codemap://mcp-instructions`, `codemap://files/{path}`,
 * `codemap://symbols/{name}`) is also reachable
 * over HTTP via `GET /resources/{encoded-uri}`. Lazy-cached:
 * `schema` / `skill` / `rule` / `mcp-instructions` (constant for the process lifetime).
 * Live read-per-call: `recipes` / `recipes/{id}` (recipes carry inline
 * recency fields that must reflect mutations during the server lifetime),
 * `files/{path}` / `symbols/{name}` (the index can change between calls
 * under `--watch`).
 */

import { closeDb, openDb } from "../db";
import type { CodemapDatabase } from "../db";
import { assembleAgentContent, assembleMcpInstructions } from "./agent-content";
import {
  getQueryRecipeCatalogEntry,
  listQueryRecipeCatalog,
} from "./query-recipes";
import { enrichWithRecency } from "./recipe-recency";
import { buildShowResult, findSymbolsByName } from "./show-engine";

export interface ResourcePayload {
  mimeType: string;
  text: string;
}

// Recipes / one-recipe deliberately NOT cached — inline recency fields
// (last_run_at / run_count) need to reflect mutations during the
// `codemap mcp` / `codemap serve` lifetime; a cached snapshot would
// freeze them at first-read. Schema / skill / rule stay cached — none
// change mid-session.
let schemaCache: ResourcePayload | undefined;
let schemaCatalogCache: SchemaCatalogRow[] | undefined;
let skillCache: ResourcePayload | undefined;
let ruleCache: ResourcePayload | undefined;
let mcpInstructionsCache: ResourcePayload | undefined;

/**
 * Test-only escape hatch — drops every cached payload so a temp-DB test
 * can re-read with fresh state. Production code never calls this.
 */
export function _resetResourceCachesForTests(): void {
  schemaCache = undefined;
  schemaCatalogCache = undefined;
  skillCache = undefined;
  ruleCache = undefined;
  mcpInstructionsCache = undefined;
}

/**
 * Read a `codemap://...` resource. Returns the payload or `undefined`
 * for unknown URIs (caller decides whether to 404 / throw).
 *
 * Data-shaped URIs (`files/{path}`, `symbols/{name}`) read live from
 * `.codemap/index.db` every call — no caching, since the index can
 * change between requests under `--watch`. Schema / skill / rule /
 * mcp-instructions cache lazily per process; recipes / recipes/{id} /
 * files / symbols read live every call.
 */
export function readResource(uri: string): ResourcePayload | undefined {
  if (uri === "codemap://recipes") return readRecipesCatalog();
  if (uri.startsWith("codemap://recipes/")) {
    const id = uri.slice("codemap://recipes/".length);
    return readOneRecipe(id);
  }
  if (uri === "codemap://schema") return readSchema();
  if (uri === "codemap://skill") return readSkill();
  if (uri === "codemap://rule") return readRule();
  if (uri === "codemap://mcp-instructions") return readMcpInstructions();
  if (uri.startsWith("codemap://files/")) {
    const path = decodeURIComponent(uri.slice("codemap://files/".length));
    return readFileResource(path);
  }
  if (uri.startsWith("codemap://symbols/")) {
    return readSymbolsResource(uri);
  }
  return undefined;
}

/**
 * List every available resource (URIs only — caller fetches payloads
 * separately). Used by the MCP `resources/list` request and HTTP
 * `GET /resources`. Mirrors what `ResourceTemplate.list` returns
 * for the recipes URI plus schema / skill / rule / mcp-instructions / files / symbols URIs.
 */
export function listResources(): { uri: string; description: string }[] {
  const out: { uri: string; description: string }[] = [
    {
      uri: "codemap://recipes",
      description:
        "Recipe catalog (bundled + project-local): id, description, sql, optional per-row actions.",
    },
    {
      uri: "codemap://schema",
      description:
        "DDL of every table in the codemap index (default `.codemap/index.db`; cached after first read from sqlite_schema).",
    },
    {
      uri: "codemap://skill",
      description:
        "Full text of the assembled codemap skill (`templates/agent-content/skill/`).",
    },
    {
      uri: "codemap://rule",
      description:
        "Full text of the assembled codemap rule (`templates/agent-content/rule/`).",
    },
    {
      uri: "codemap://mcp-instructions",
      description: "MCP initialize tool-selection playbook.",
    },
    {
      uri: "codemap://files/{path}",
      description:
        "Per-file roll-up: symbols, imports, exports, coverage. Encode `{path}` URI-style. Reads live (no caching).",
    },
    {
      uri: "codemap://symbols/{name}",
      description:
        "Symbol lookup by exact name. Returns {matches, disambiguation?} envelope. Optional `?in=<path-prefix>` filter (mirrors `show --in`). Reads live (no caching).",
    },
  ];
  for (const entry of listQueryRecipeCatalog()) {
    out.push({
      uri: `codemap://recipes/${entry.id}`,
      description: entry.description,
    });
  }
  return out;
}

function readRecipesCatalog(): ResourcePayload {
  return {
    mimeType: "application/json",
    text: JSON.stringify(enrichWithRecency(listQueryRecipeCatalog())),
  };
}

function readOneRecipe(id: string): ResourcePayload | undefined {
  const entry = getQueryRecipeCatalogEntry(id);
  if (entry === undefined) return undefined;
  const [enriched] = enrichWithRecency([entry]);
  return {
    mimeType: "application/json",
    text: JSON.stringify(enriched),
  };
}

export interface SchemaCatalogRow {
  name: string;
  ddl: string;
}

function loadSchemaCatalogRows(db: CodemapDatabase): SchemaCatalogRow[] {
  const rows = db
    .query(
      "SELECT name, sql FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name",
    )
    .all() as { name: string; sql: string | null }[];
  return rows
    .filter((r) => r.sql !== null)
    .map((r) => ({ name: r.name, ddl: r.sql! }));
}

function readSchema(): ResourcePayload {
  if (schemaCache !== undefined) return schemaCache;
  const rows = buildSchemaCatalog();
  schemaCache = {
    mimeType: "application/json",
    text: JSON.stringify(rows),
  };
  return schemaCache;
}

function readSkill(): ResourcePayload {
  if (skillCache !== undefined) return skillCache;
  skillCache = {
    mimeType: "text/markdown",
    text: assembleAgentContent("skill"),
  };
  return skillCache;
}

function readRule(): ResourcePayload {
  if (ruleCache !== undefined) return ruleCache;
  ruleCache = {
    mimeType: "text/markdown",
    text: assembleAgentContent("rule"),
  };
  return ruleCache;
}

function readMcpInstructions(): ResourcePayload {
  if (mcpInstructionsCache !== undefined) return mcpInstructionsCache;
  mcpInstructionsCache = {
    mimeType: "text/markdown",
    text: assembleMcpInstructions(),
  };
  return mcpInstructionsCache;
}

/**
 * Per-file roll-up JSON (symbols, imports, exports, coverage). Returns
 * `undefined` when the path is not indexed. Shared by MCP/HTTP resources
 * and `codemap file`.
 */
export function fileResourceUri(path: string): string {
  return `codemap://files/${encodeURIComponent(path)}`;
}

/** Error text aligned with MCP `readTemplateResource` for missing file URIs. */
export function unknownFileResourceError(path: string): string {
  return `codemap: unknown file resource "${fileResourceUri(path)}".`;
}

export function symbolResourceUri(name: string, inPath?: string): string {
  const base = `codemap://symbols/${encodeURIComponent(name)}`;
  if (inPath !== undefined && inPath.length > 0) {
    return `${base}?in=${encodeURIComponent(inPath)}`;
  }
  return base;
}

/** Error text aligned with MCP `readTemplateResource` for invalid symbol URIs. */
export function unknownSymbolResourceError(
  name: string,
  inPath?: string,
): string {
  return `codemap: unknown symbol resource "${symbolResourceUri(name, inPath)}".`;
}

export function buildFileRollup(
  path: string,
): Record<string, unknown> | undefined {
  const db = openDb();
  try {
    return buildFileRollupPayload(db, path);
  } finally {
    closeDb(db, { readonly: true });
  }
}

/**
 * DDL catalog for every user table in the index DB. Shared by MCP/HTTP
 * resources and `codemap schema`.
 */
export function buildSchemaCatalog(): SchemaCatalogRow[] {
  if (schemaCatalogCache !== undefined) return schemaCatalogCache;
  const db = openDb();
  try {
    schemaCatalogCache = loadSchemaCatalogRows(db);
    return schemaCatalogCache;
  } finally {
    closeDb(db, { readonly: true });
  }
}

function buildFileRollupPayload(
  db: CodemapDatabase,
  path: string,
): Record<string, unknown> | undefined {
  // Confirm the file exists in the index — fail-closed if not, so callers
  // can distinguish "unknown URI" from "valid URI, empty roll-up".
  const file = db
    .query("SELECT path, language, line_count FROM files WHERE path = ?")
    .get(path) as
    | { path: string; language: string; line_count: number }
    | null
    | undefined;
  if (file === undefined || file === null) return undefined;

  const symbols = db
    .query(
      `SELECT name, kind, line_start, line_end, signature, is_exported,
                is_default_export, parent_name, visibility, doc_comment
         FROM symbols
         WHERE file_path = ?
         ORDER BY line_start ASC`,
    )
    .all(path);

  const imports = db
    .query(
      `SELECT source, resolved_path, specifiers, is_type_only, line_number
         FROM imports
         WHERE file_path = ?
         ORDER BY line_number ASC`,
    )
    .all(path) as {
    source: string;
    resolved_path: string | null;
    specifiers: string;
    is_type_only: number;
    line_number: number;
  }[];

  const exports = db
    .query(
      `SELECT name, kind, is_default, re_export_source
         FROM exports
         WHERE file_path = ?
         ORDER BY name ASC`,
    )
    .all(path);

  const coverage = db
    .query(
      `SELECT name, line_start, coverage_pct, hit_statements, total_statements
         FROM coverage
         WHERE file_path = ?
         ORDER BY line_start ASC`,
    )
    .all(path) as {
    name: string;
    line_start: number;
    coverage_pct: number | null;
    hit_statements: number;
    total_statements: number;
  }[];

  // Parse imports.specifiers JSON inline so consumers don't have to.
  const importsParsed = imports.map((i) => ({
    source: i.source,
    resolved_path: i.resolved_path,
    specifiers: safeParseSpecifiers(i.specifiers),
    is_type_only: i.is_type_only === 1,
    line_number: i.line_number,
  }));

  // Coverage roll-up summary (avg + count) plus per-symbol detail.
  const measured = coverage.length;
  const avg =
    measured === 0
      ? null
      : Math.round(
          (coverage.reduce((a, c) => a + (c.coverage_pct ?? 0), 0) / measured) *
            10,
        ) / 10;

  const payload = {
    path: file.path,
    language: file.language,
    line_count: file.line_count,
    symbols,
    imports: importsParsed,
    exports,
    coverage:
      measured === 0
        ? null
        : {
            measured_symbols: measured,
            avg_coverage_pct: avg,
            per_symbol: coverage,
          },
  };

  return payload;
}

function readFileResource(path: string): ResourcePayload | undefined {
  const db = openDb();
  try {
    const payload = buildFileRollupPayload(db, path);
    if (payload === undefined) return undefined;
    return {
      mimeType: "application/json",
      text: JSON.stringify(payload),
    };
  } finally {
    closeDb(db, { readonly: true });
  }
}

/**
 * Exact-name symbol lookup envelope (`{matches, disambiguation?}`). Returns
 * `undefined` when `name` is empty. Shared by MCP/HTTP resources and
 * `codemap symbols`.
 */
export function buildSymbolLookup(
  name: string,
  inPath?: string,
): Record<string, unknown> | undefined {
  if (name.length === 0) return undefined;
  const db = openDb();
  try {
    const matches = findSymbolsByName(db, { name, inPath });
    return buildShowResult(matches) as unknown as Record<string, unknown>;
  } finally {
    closeDb(db, { readonly: true });
  }
}

/**
 * Symbol lookup by exact name. Returns the same `{matches,
 * disambiguation?}` envelope as the `show` verb (per PR #39). Supports
 * `?in=<path-prefix>` query parameter mirroring `show --in <path>`.
 * Reads live every call (no caching).
 */
function readSymbolsResource(uri: string): ResourcePayload | undefined {
  // Use the URL parser to split path + search. Codemap URIs work because
  // the protocol is `codemap:` and host is `symbols`.
  let parsed: URL;
  try {
    parsed = new URL(uri);
  } catch {
    return undefined;
  }
  // pathname is `/<name>` — strip the leading slash. URI-decode the name
  // since callers may have encoded reserved characters.
  const name = decodeURIComponent(parsed.pathname.replace(/^\//, ""));
  if (name.length === 0) return undefined;

  const inPath = parsed.searchParams.get("in") ?? undefined;
  const payload = buildSymbolLookup(name, inPath);
  if (payload === undefined) return undefined;

  return {
    mimeType: "application/json",
    text: JSON.stringify(payload),
  };
}

/**
 * Defensive JSON.parse for `imports.specifiers` — falls back to an
 * empty array if the column ever holds malformed JSON. Production data
 * should always be valid (the indexer writes via JSON.stringify), but a
 * resource handler that throws on a bad row is worse than one that
 * returns an empty list and lets the caller see what's there.
 */
function safeParseSpecifiers(raw: string): string[] {
  try {
    const parsed = JSON.parse(raw);
    return Array.isArray(parsed) ? parsed : [];
  } catch {
    return [];
  }
}
