/**
 * MCP Resources surface (introduced in v0.9.1).
 *
 * Resources let an MCP host pull a piece of Brain state by URI instead
 * of asking the agent to call `brain_query` first. The five URI shapes
 * we ship are:
 *
 *   osb://preferences/active     — Brain/active.md (auto-generated by dream)
 *   osb://digest/latest          — current `renderDigest({format: "markdown"})`
 *   osb://preference/{id}        — Brain/preferences/pref-{id}.md OR
 *                                  Brain/retired/ret-{id}.md when the
 *                                  preferences/ copy is gone
 *   osb://topic/{slug}           — synthesized markdown summary of every
 *                                  signal + the current pref / retired
 *                                  for the topic slug
 *   osb://log/{date}             — Brain/log/<date>.md as-is
 *
 * The first two are concrete resources (returned by `resources/list`).
 * The last three are templates (returned by `resources/templates/list`).
 * `resources/read` accepts both shapes uniformly.
 *
 * Implementation notes:
 *
 *   - All slug / id / date arguments are validated through the existing
 *     `validateSlug` / `validateIsoDate` helpers. A bad value is an
 *     `INVALID_PARAMS` error, NOT a 404 — we want client bugs to be
 *     visibly different from genuinely-missing files.
 *   - Read responses are text/markdown. We avoid serializing structured
 *     data here because the rest of the MCP surface already does that
 *     via tools; resources are the "give me the markdown body" channel.
 *   - Missing files produce `INTERNAL_ERROR` with a `not found` message
 *     — same shape as `brain_query`'s `BrainNotFoundError` so callers
 *     can treat both surfaces consistently.
 */

import { existsSync, readFileSync } from "node:fs";
import { listLogMarkdownFiles } from "../core/brain/log-jsonl.ts";

import { regenerateActive } from "../core/brain/active.ts";
import { buildBacklinkIndex } from "../core/brain/backlinks.ts";
import type { BacklinkRef } from "../core/brain/backlinks.ts";
import { renderDigest } from "../core/brain/digest.ts";
import { computeBrainStatus, type BrainStatusSnapshot } from "../core/brain/status.ts";
import {
  brainActivePath,
  preferencePath,
  retiredPath,
  validateIsoDate,
  validateSlug,
} from "../core/brain/paths.ts";
import { BrainNotFoundError, queryByPreference, queryByTopic } from "../core/brain/query.ts";
import { normaliseWikilinkTarget } from "../core/brain/wikilink.ts";
import type { BrainPreference, BrainRetired } from "../core/brain/types.ts";
import { INTERNAL_ERROR, INVALID_PARAMS, MCPError } from "./protocol.ts";

export interface ResourceContext {
  readonly vault: string;
}

export interface ResourceDescriptor {
  readonly uri: string;
  readonly name: string;
  readonly description: string;
  readonly mimeType: string;
}

export interface ResourceTemplateDescriptor {
  readonly uriTemplate: string;
  readonly name: string;
  readonly description: string;
  readonly mimeType: string;
}

export interface ResourceContent {
  readonly uri: string;
  readonly mimeType: string;
  readonly text: string;
}

const MIME_MARKDOWN = "text/markdown";

const CONCRETE: ReadonlyArray<ResourceDescriptor> = [
  {
    uri: "osb://preferences/active",
    name: "Active Brain preferences",
    description:
      "Auto-generated digest of confirmed and quarantined preferences plus the three most recently retired entries. Regenerated at the tail of every dream run.",
    mimeType: MIME_MARKDOWN,
  },
  {
    uri: "osb://digest/latest",
    name: "Latest Brain digest",
    description: "Same body as `brain_brief` view=digest (default window: last 24h). Markdown.",
    mimeType: MIME_MARKDOWN,
  },
  {
    uri: "osb://status",
    name: "Brain operational status",
    description:
      "Counts (inbox/preferences/retired/log/snapshots), last-activity timestamps, and a sanity flag for signals awaiting dream. Markdown.",
    mimeType: MIME_MARKDOWN,
  },
];

const TEMPLATES: ReadonlyArray<ResourceTemplateDescriptor> = [
  {
    uriTemplate: "osb://preference/{id}",
    name: "Brain preference by id",
    description:
      "Body of the matching `pref-{id}.md` (or `ret-{id}.md` when the active copy is gone).",
    mimeType: MIME_MARKDOWN,
  },
  {
    uriTemplate: "osb://topic/{slug}",
    name: "Brain topic summary",
    description:
      "Markdown synthesis of every signal (active + processed), the current preference (or retired), and the most recent log mentions for the topic slug.",
    mimeType: MIME_MARKDOWN,
  },
  {
    uriTemplate: "osb://log/{date}",
    name: "Brain daily log",
    description: "Body of `Brain/log/<date>.md` (YYYY-MM-DD).",
    mimeType: MIME_MARKDOWN,
  },
  {
    uriTemplate: "osb://backlinks/{id}",
    name: "Brain backlinks for id",
    description:
      "Inbound references to the given Brain artifact id (preference, retired, or signal). Rendered as a count plus a grouped list of sources.",
    mimeType: MIME_MARKDOWN,
  },
];

export function listResources(): ReadonlyArray<ResourceDescriptor> {
  return CONCRETE;
}

export function listResourceTemplates(): ReadonlyArray<ResourceTemplateDescriptor> {
  return TEMPLATES;
}

export function readResource(ctx: ResourceContext, uri: string): ResourceContent {
  const parsed = parseUri(uri);
  switch (parsed.kind) {
    case "active":
      return readActive(ctx, uri);
    case "digestLatest":
      return readDigestLatest(ctx, uri);
    case "status":
      return readStatus(ctx, uri);
    case "preference":
      return readPreference(ctx, uri, parsed.id);
    case "topic":
      return readTopic(ctx, uri, parsed.slug);
    case "log":
      return readLog(ctx, uri, parsed.date);
    case "backlinks":
      return readBacklinks(ctx, uri, parsed.id);
  }
}

// ----- URI parsing ---------------------------------------------------------

type Parsed =
  | { readonly kind: "active" }
  | { readonly kind: "digestLatest" }
  | { readonly kind: "status" }
  | { readonly kind: "preference"; readonly id: string }
  | { readonly kind: "topic"; readonly slug: string }
  | { readonly kind: "log"; readonly date: string }
  | { readonly kind: "backlinks"; readonly id: string };

function parseUri(uri: string): Parsed {
  if (typeof uri !== "string" || !uri.startsWith("osb://")) {
    throw new MCPError(INVALID_PARAMS, `unsupported resource uri: ${uri}`);
  }
  const remainder = uri.slice("osb://".length);
  // Resources have at most one slash for the templated variants
  // (`preference/foo`). We don't accept deeper paths.
  const parts = remainder.split("/");
  const head = parts[0] ?? "";
  if (head === "preferences" && parts[1] === "active" && parts.length === 2) {
    return { kind: "active" };
  }
  if (head === "digest" && parts[1] === "latest" && parts.length === 2) {
    return { kind: "digestLatest" };
  }
  if (head === "status" && parts.length === 1) {
    return { kind: "status" };
  }
  if (head === "preference" && parts.length === 2) {
    return { kind: "preference", id: parts[1]! };
  }
  if (head === "topic" && parts.length === 2) {
    return { kind: "topic", slug: parts[1]! };
  }
  if (head === "log" && parts.length === 2) {
    return { kind: "log", date: parts[1]! };
  }
  if (head === "backlinks" && parts.length === 2) {
    return { kind: "backlinks", id: parts[1]! };
  }
  throw new MCPError(INVALID_PARAMS, `unsupported resource uri: ${uri}`);
}

// ----- Readers -------------------------------------------------------------

function readActive(ctx: ResourceContext, uri: string): ResourceContent {
  const path = brainActivePath(ctx.vault);
  // First read attempt: the file usually exists because dream
  // regenerates it. On a fresh vault that has never been dreamed, the
  // file may not exist yet — generate it on demand so the resource is
  // always meaningful. We do NOT auto-generate on every read (that
  // would shift expensive work onto a hot path); only on the first
  // read of a missing file.
  if (!existsSync(path)) {
    try {
      regenerateActive(ctx.vault);
    } catch (err) {
      throw new MCPError(
        INTERNAL_ERROR,
        `active.md not present and on-demand regeneration failed: ${(err as Error).message}`,
      );
    }
  }
  return readMarkdown(uri, path);
}

function readDigestLatest(ctx: ResourceContext, uri: string): ResourceContent {
  const rendered = renderDigest(ctx.vault, { format: "markdown" });
  return {
    uri,
    mimeType: MIME_MARKDOWN,
    text: rendered.content,
  };
}

function readStatus(ctx: ResourceContext, uri: string): ResourceContent {
  const snapshot = computeBrainStatus(ctx.vault);
  return {
    uri,
    mimeType: MIME_MARKDOWN,
    text: renderStatusMarkdown(snapshot),
  };
}

function readPreference(ctx: ResourceContext, uri: string, rawId: string): ResourceContent {
  if (!rawId.trim()) {
    throw new MCPError(INVALID_PARAMS, `resource id must not be empty: ${uri}`);
  }
  // Accept `pref-foo`, `ret-foo`, or the bare slug. Strip the prefix
  // (if any) for `validateSlug` — that helper rejects `..` traversal
  // and other shape problems with a clear INVALID_PARAMS-shaped
  // message, which is a better diagnosis than a downstream
  // "not found" error. The prefixed form is then handed to
  // queryByPreference, which handles the active → retired fallback.
  const slug = rawId.startsWith("pref-")
    ? rawId.slice("pref-".length)
    : rawId.startsWith("ret-")
      ? rawId.slice("ret-".length)
      : rawId;
  try {
    validateSlug(slug);
  } catch (err) {
    throw new MCPError(INVALID_PARAMS, (err as Error).message);
  }
  const normalized =
    rawId.startsWith("pref-") || rawId.startsWith("ret-") ? rawId : `pref-${rawId}`;
  let result;
  try {
    result = queryByPreference(ctx.vault, normalized);
  } catch (err) {
    if (err instanceof BrainNotFoundError) {
      throw new MCPError(INTERNAL_ERROR, err.message);
    }
    throw err;
  }
  // queryByPreference returns the parsed object but not its path; we
  // re-derive the path from the resolved id so we can hand back the
  // raw markdown body (the resource contract is "give me the file",
  // not "give me a structured projection").
  const resolvedId = result.preference.id;
  const resolvedSlug = resolvedId.startsWith("pref-")
    ? resolvedId.slice("pref-".length)
    : resolvedId.slice("ret-".length);
  const filePath = resolvedId.startsWith("pref-")
    ? preferencePath(ctx.vault, resolvedSlug)
    : retiredPath(ctx.vault, resolvedSlug);
  return readMarkdown(uri, filePath);
}

function readTopic(ctx: ResourceContext, uri: string, rawSlug: string): ResourceContent {
  if (!rawSlug.trim()) {
    throw new MCPError(INVALID_PARAMS, `topic slug must not be empty: ${uri}`);
  }
  validateSlug(rawSlug);
  let result;
  try {
    result = queryByTopic(ctx.vault, rawSlug);
  } catch (err) {
    if (err instanceof BrainNotFoundError) {
      throw new MCPError(INTERNAL_ERROR, err.message);
    }
    throw err;
  }
  return {
    uri,
    mimeType: MIME_MARKDOWN,
    text: renderTopicMarkdown(rawSlug, result),
  };
}

function readLog(ctx: ResourceContext, uri: string, rawDate: string): ResourceContent {
  validateIsoDate(rawDate);
  // Per-device shards (Memory Integrity Suite): a day's human view can
  // span several markdown files. The legacy single file is returned
  // verbatim (byte-identical to pre-shard behaviour); multiple shards
  // are concatenated in shard order.
  const files = listLogMarkdownFiles(ctx.vault).filter((f) => f.date === rawDate);
  if (files.length === 0) {
    throw new MCPError(INTERNAL_ERROR, `no log file for date '${rawDate}'`);
  }
  if (files.length === 1) return readMarkdown(uri, files[0]!.path);
  const text = files.map((f) => readFileSync(f.path, "utf8").trimEnd()).join("\n\n");
  return { uri, mimeType: MIME_MARKDOWN, text };
}

function readBacklinks(ctx: ResourceContext, uri: string, rawId: string): ResourceContent {
  if (!rawId.trim()) {
    throw new MCPError(INVALID_PARAMS, `backlinks target must not be empty: ${uri}`);
  }
  // Normalise to the same form the index keys use so `[[pref-foo]]`,
  // `pref-foo`, and `Brain/preferences/pref-foo.md` all resolve.
  const target = normaliseWikilinkTarget(rawId);
  const index = buildBacklinkIndex(ctx.vault);
  const refs = index.get(target) ?? [];
  return {
    uri,
    mimeType: MIME_MARKDOWN,
    text: renderBacklinksMarkdown(target, refs),
  };
}

// ----- Helpers -------------------------------------------------------------

function readMarkdown(uri: string, path: string): ResourceContent {
  const text = readFileSync(path, "utf8");
  return { uri, mimeType: MIME_MARKDOWN, text };
}

/**
 * Render the topic-query result as a compact markdown document. Kept
 * here (not in `query.ts`) because the markdown shape is a
 * resource-surface concern; callers that want the structured form can
 * still invoke `queryByTopic` directly.
 */
function renderStatusMarkdown(s: BrainStatusSnapshot): string {
  const lines: string[] = [];
  lines.push("# Brain status");
  lines.push("");
  if (!s.present) {
    lines.push("_Brain layer not present in this vault. Run `o2b brain init` first._");
    return lines.join("\n") + "\n";
  }
  lines.push("## Counts");
  lines.push("");
  lines.push(`- inbox: ${s.counts.inbox} (processed: ${s.counts.inbox_processed})`);
  lines.push(`- preferences: ${s.counts.preferences}`);
  for (const [status, n] of Object.entries(s.counts.preferences_by_status).toSorted()) {
    lines.push(`  - ${status}: ${n}`);
  }
  lines.push(`- retired: ${s.counts.retired}`);
  lines.push(`- log days: ${s.counts.log_days}`);
  lines.push(`- snapshots: ${s.counts.snapshots}`);
  lines.push("");
  lines.push("## Activity");
  lines.push("");
  lines.push(`- last dream: ${s.last_dream_at ?? "_(never)_"}`);
  lines.push(`- last apply-evidence: ${s.last_apply_evidence_at ?? "_(never)_"}`);
  lines.push("");
  if (s.sanity.signals_awaiting_dream > 0) {
    lines.push("## Sanity");
    lines.push("");
    lines.push(
      `- **${s.sanity.signals_awaiting_dream}** signal(s) in inbox/ predate the unconfirmed_window — run \`o2b brain dream\`.`,
    );
    lines.push("");
  }
  while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
  return lines.join("\n") + "\n";
}

function renderBacklinksMarkdown(target: string, refs: ReadonlyArray<BacklinkRef>): string {
  const lines: string[] = [];
  lines.push(`# Backlinks to \`${target}\``);
  lines.push("");
  lines.push(`Total inbound references: ${refs.length}`);
  lines.push("");
  if (refs.length === 0) {
    lines.push("_No inbound references found._");
    return lines.join("\n") + "\n";
  }
  // Group by source-kind so the reader sees structure rather than a
  // flat list — the same source kind (e.g. multiple log apply-evidence
  // events) clusters together.
  const byKind = new Map<string, BacklinkRef[]>();
  for (const r of refs) {
    const arr = byKind.get(r.sourceKind) ?? [];
    arr.push(r);
    byKind.set(r.sourceKind, arr);
  }
  for (const [kind, group] of byKind) {
    lines.push(`## ${kind} (${group.length})`);
    lines.push("");
    for (const r of group) {
      const suffix = r.timestamp ? ` — ${r.timestamp}` : "";
      lines.push(`- \`${r.source}\` (field: \`${r.field}\`)${suffix}`);
    }
    lines.push("");
  }
  while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
  return lines.join("\n") + "\n";
}

function renderTopicMarkdown(slug: string, result: ReturnType<typeof queryByTopic>): string {
  const lines: string[] = [];
  lines.push(`# Brain topic: ${slug}`);
  lines.push("");
  const pref = result.preference;
  if (pref) {
    if (pref.id.startsWith("ret-")) {
      const r = pref as BrainRetired;
      const date = r.retired_at.slice(0, 10);
      lines.push("## Retired preference (no current active)");
      lines.push("");
      lines.push(`- \`${r.id}\` — retired_reason: \`${r.retired_reason}\` on ${date}`);
      lines.push(`  - ${r.principle}`);
    } else {
      const p = pref as BrainPreference;
      lines.push("## Current preference");
      lines.push("");
      lines.push(`- \`${p.id}\` — status: \`${p.status}\`, confidence: \`${p.confidence}\``);
      lines.push(`  - ${p.principle}`);
    }
    lines.push("");
  }
  const sigs = result.signals;
  if (sigs.length > 0) {
    lines.push(`## Signals (${sigs.length})`);
    lines.push("");
    for (const s of sigs) {
      lines.push(`- \`${s.id}\` — ${s.signal} signal by ${s.agent} at ${s.created_at}`);
    }
    lines.push("");
  }
  const log = result.all_log_events;
  if (log.length > 0) {
    lines.push(`## Recent log entries (${log.length})`);
    lines.push("");
    for (const e of log) {
      lines.push(`- ${e.timestamp} — \`${e.eventType}\``);
    }
    lines.push("");
  }
  while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
  return lines.join("\n") + "\n";
}
