import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { redactSecrets } from "./redact.ts";
import type { ContextItem } from "./context.ts";
import type { StateContradiction } from "./state.ts";

/**
 * Compiled Karpathy-style knowledge wiki (v0.5, issue #35).
 *
 * The wiki is a compiled, human-readable view — never the authoritative
 * database. Every generated claim links back to the context item / journal
 * entry / state record that produced it. Open contradictions remain visible;
 * compilation never resolves them. Generation is deterministic so diffs are
 * reviewable and tests are reproducible.
 */

export const WIKI_ROOT = ".kxm/knowledge/wiki";

export interface WikiSourcePool {
  project: string;
  /** State items for the project (all lifecycle states). */
  stateItems: ContextItem[];
  /** Journal- and store-derived knowledge/evidence/episode items. */
  contextItems: ContextItem[];
  /** Contradictions detected by the temporal state layer. */
  contradictions: StateContradiction[];
  /** Open contradiction journal item IDs (unresolved journal contradictions). */
  openContradictionItemIds: string[];
  /** Injected clock for reproducible builds. */
  compiledAt: string;
}

export interface CompiledWiki {
  /** Repo-relative path → file content, sorted by path for stable diffs. */
  pages: Map<string, string>;
  index: string;
  /** Metadata-only compile audit: what was rendered from where. */
  audit: {
    project: string;
    pages: string[];
    stateItems: number;
    contextItems: number;
    contradictions: number;
    compiledAt: string;
  };
}

const SECTION_BY_KIND: Record<string, string> = {
  knowledge: "architecture",
  decision: "decisions",
  episode: "incidents",
  skill: "patterns",
};

function wikiSectionFor(item: ContextItem): string {
  if (SECTION_BY_KIND[item.kind]) return SECTION_BY_KIND[item.kind]!;
  if (item.provenance.sourceRef?.startsWith("journal:")) {
    // Journal evidence items are incident-shaped learning.
    return "incidents";
  }
  return "patterns";
}

function claimLine(item: ContextItem): string {
  const refs = [`\`${item.id}\``];
  if (item.provenance.sourceRef) refs.push(`source: \`${item.provenance.sourceRef}\``);
  const lineage = item.provenance.derivedFrom ?? [];
  if (lineage.length > 0) refs.push(`derived from: ${lineage.map((id) => `\`${id}\``).join(", ")}`);
  const authority = item.authority === "policy" || item.authority === "instruction"
    ? ` (${item.authority})`
    : "";
  return `- ${redactSecrets(item.summary)}${authority} — ${refs.join(" · ")}`;
}

function page(title: string, heading: string, claims: ContextItem[], footer: string): string {
  const lines = [
    `# ${title}`,
    "",
    heading,
    "",
    ...(claims.length > 0 ? claims.map(claimLine) : ["_No reviewed records yet._"]),
    "",
    footer,
  ];
  return `${lines.join("\n")}\n`;
}

const GENERATED_FOOTER =
  "<!-- generated by kxm context wiki-compile; the wiki is a compiled view, not the authoritative database -->";

/** Compile the wiki deterministically. Same inputs always produce the same
 * bytes (only the index records `compiledAt`). */
export function compileKnowledgeWiki(pool: WikiSourcePool): CompiledWiki {
  const pages = new Map<string, string>();

  const live = pool.contextItems.filter(
    (item) => item.status !== "superseded" && item.status !== "rejected",
  );
  const superseded = pool.contextItems.filter((item) => item.status === "superseded");

  // Group live items by wiki section, then by stable summary-derived slug.
  const sections = new Map<string, ContextItem[]>();
  for (const item of live) {
    const section = wikiSectionFor(item);
    const bucket = sections.get(section) ?? [];
    bucket.push(item);
    sections.set(section, bucket);
  }

  const indexLines: string[] = [
    `# ${pool.project} knowledge wiki`,
    "",
    "Compiled synthesis of durable journal evidence, temporal state, and governed records. Every claim links back to its evidence; open contradictions are listed, never silently resolved.",
    "",
  ];

  for (const section of [...sections.keys()].sort()) {
    const items = sections.get(section)!.sort((left, right) => left.id.localeCompare(right.id));
    const path = `${WIKI_ROOT}/${section}/${pool.project}.md`;
    pages.set(
      path,
      page(
        `${pool.project} — ${section}`,
        `Claims below are compiled from reviewed records. Each line links the record ID and source.`,
        items,
        GENERATED_FOOTER,
      ),
    );
    indexLines.push(`- [${section}](${section}/${pool.project}.md) — ${items.length} claim(s)`);
  }

  // Temporal state: rendered from the authoritative state layer, preserving
  // current/superseded distinctions. Superseded state is explicitly labeled.
  const currentState = pool.stateItems.filter((item) => item.status === "current");
  const supersededState = pool.stateItems.filter((item) => item.status === "superseded");
  const statePath = `${WIKI_ROOT}/architecture/${pool.project}-state.md`;
  const stateLines = [
    `# ${pool.project} — temporal state`,
    "",
    "Rendered from the authoritative state layer. Current values are live; superseded values are kept visible with their validity window and successor link.",
    "",
    "## Current",
    "",
    ...(currentState.length > 0
      ? currentState
        .sort((left, right) => (left.stateKey ?? "").localeCompare(right.stateKey ?? ""))
        .map((item) => claimLine(item))
      : ["_No current state records._"]),
    "",
    "## Superseded",
    "",
    ...(supersededState.length > 0
      ? supersededState
        .sort((left, right) => (left.stateKey ?? "").localeCompare(right.stateKey ?? ""))
        .map((item) => claimLine(item))
      : ["_No superseded state records._"]),
    "",
    GENERATED_FOOTER,
  ];
  pages.set(statePath, `${stateLines.join("\n")}\n`);
  indexLines.push(`- [temporal state](${pool.project}-state.md) — ${currentState.length} current, ${supersededState.length} superseded`);

  // Decisions page mirrors decision-shaped knowledge records explicitly.
  const decisions = live.filter((item) => item.kind === "knowledge" && item.provenance.sourceRef?.startsWith("journal:") === false);
  if (decisions.length > 0) {
    pages.set(
      `${WIKI_ROOT}/decisions/${pool.project}.md`,
      page(
        `${pool.project} — decisions`,
        "Decision records with their evidence links.",
        decisions.sort((left, right) => left.id.localeCompare(right.id)),
        GENERATED_FOOTER,
      ),
    );
    indexLines.push(`- [decisions (tracked records)](decisions/${pool.project}.md) — ${decisions.length}`);
  }

  // Contradictions: compiled visibility, zero silent resolution.
  const contradictionLines: string[] = [
    `# ${pool.project} — open contradictions`,
    "",
    "These remain unresolved by compilation. Resolution happens through evidence-backed state promotion or journal decisions, never through re-generating this page.",
    "",
  ];
  let contradictionCount = 0;
  for (const contradiction of [...pool.contradictions].sort((left, right) => left.stateKey.localeCompare(right.stateKey))) {
    contradictionCount += 1;
    contradictionLines.push(
      `## \`${contradiction.stateKey}\``,
      "",
      `- Competing current records: ${contradiction.competingCurrentIds.map((id) => `\`${id}\``).join(", ") || "none"}`,
      `- Competing proposals: ${contradiction.competingProposalIds.map((id) => `\`${id}\``).join(", ") || "none"}`,
      "",
    );
  }
  for (const id of [...pool.openContradictionItemIds].sort()) {
    contradictionCount += 1;
    contradictionLines.push(`- Unresolved journal contradiction: \`${id}\``);
  }
  if (contradictionCount === 0) contradictionLines.push("_No open contradictions._");
  contradictionLines.push("", GENERATED_FOOTER);
  pages.set(`${WIKI_ROOT}/contradictions/${pool.project}.md`, `${contradictionLines.join("\n")}\n`);
  indexLines.push(`- [contradictions](contradictions/${pool.project}.md) — ${contradictionCount} open`);

  // Superseded knowledge is retained in history so current/superseded
  // distinctions survive rebuilds.
  if (superseded.length > 0) {
    pages.set(
      `${WIKI_ROOT}/incidents/${pool.project}-history.md`,
      page(
        `${pool.project} — superseded knowledge history`,
        "Superseded records retained for learning. Never presented as current truth.",
        superseded.sort((left, right) => left.id.localeCompare(right.id)),
        GENERATED_FOOTER,
      ),
    );
    indexLines.push(`- [superseded history](incidents/${pool.project}-history.md) — ${superseded.length}`);
  }

  indexLines.push("", GENERATED_FOOTER);
  const index = `${indexLines.join("\n")}\n`;
  pages.set(`${WIKI_ROOT}/index.md`, index);

  return {
    pages,
    index,
    audit: {
      project: pool.project,
      pages: [...pages.keys()].sort(),
      stateItems: pool.stateItems.length,
      contextItems: pool.contextItems.length,
      contradictions: contradictionCount,
      compiledAt: pool.compiledAt,
    },
  };
}

/** Write compiled pages to disk. Deterministic content keeps diffs bounded
 * and reviewable; only `index.md` embeds the compile timestamp. */
export function writeCompiledWiki(root: string, wiki: CompiledWiki): string[] {
  const written: string[] = [];
  for (const [relativePath, content] of [...wiki.pages].sort(([left], [right]) => left.localeCompare(right))) {
    const absolute = join(root, relativePath);
    mkdirSync(absolute.slice(0, absolute.lastIndexOf("/")), { recursive: true });
    writeFileSync(absolute, content);
    written.push(relativePath);
  }
  return written;
}

export interface WikiLintIssue {
  severity: "error" | "warning";
  rule: "broken_ref" | "orphan_page" | "stale_state_link" | "unresolved_contradiction";
  path: string;
  message: string;
}

/** Lint a compiled wiki: broken evidence refs, orphan pages, stale state
 * links, and unresolved contradictions. Wiki content stays reviewable; lint
 * failures block release gates, not runtime work. */
export function lintKnowledgeWiki(
  pages: Map<string, string>,
  pool: Pick<WikiSourcePool, "project" | "stateItems" | "contextItems" | "contradictions" | "openContradictionItemIds">,
): WikiLintIssue[] {
  const issues: WikiLintIssue[] = [];
  const knownIds = new Set<string>([
    ...pool.stateItems.map((item) => item.id),
    ...pool.contextItems.map((item) => item.id),
  ]);

  const index = pages.get(`${WIKI_ROOT}/index.md`);
  for (const [path, content] of pages) {
    if (path.endsWith("index.md")) continue;
    // Broken refs: backticked record IDs that the pool does not know.
    for (const match of content.matchAll(/`((?:ctx|journal)_[A-Za-z0-9_]+)`/g)) {
      const id = match[1]!;
      if (!knownIds.has(id)) {
        issues.push({ severity: "error", rule: "broken_ref", path, message: `references unknown record ${id}` });
      }
    }
    // Orphan pages: exist but are not linked from the index.
    if (index && !index.includes(path.split("/").pop()!)) {
      issues.push({ severity: "warning", rule: "orphan_page", path, message: "not linked from index.md" });
    }
    // Stale state links: pages claiming "Current" for a superseded state key.
    for (const item of pool.stateItems) {
      if (item.status !== "superseded" || !item.stateKey) continue;
      const claimPattern = new RegExp(`Current[\\s\\S]{0,400}\`${item.id}\``, "u");
      if (claimPattern.test(content)) {
        issues.push({
          severity: "error",
          rule: "stale_state_link",
          path,
          message: `renders superseded state item ${item.id} (key ${item.stateKey}) as current`,
        });
      }
    }
  }

  // Unresolved contradictions must stay visible: an index without the
  // contradictions entry while contradictions exist is a lint error.
  const hasContradictions = pool.contradictions.length > 0 || pool.openContradictionItemIds.length > 0;
  if (hasContradictions && index && !index.includes("contradictions")) {
    issues.push({
      severity: "error",
      rule: "unresolved_contradiction",
      path: `${WIKI_ROOT}/index.md`,
      message: "open contradictions exist but the index does not surface them",
    });
  }

  return issues.sort((left, right) => left.path.localeCompare(right.path) || left.message.localeCompare(right.message));
}
