/**
 * ContextProjectionBuilder — WP-11.
 *
 * Builds hot / warm / cold inheritance material packs under projections/<runId>/.
 * Packs are derivatives (rebuildable); they are not a new ledger of record.
 */

import { createHash } from "node:crypto";
import { promises as fs } from "node:fs";
import { SwitchboardPaths } from "../paths/SwitchboardPaths.js";
import type { ContextProjectionManifest } from "../contracts/ContextProjection.js";
import type { WorkFactBundle, WorkFactRef } from "./WorkFactReader.js";

export type InheritanceTier = "hot" | "warm" | "cold";

export interface ProjectionBuildInput {
  facts: WorkFactBundle;
  workerId: string;
  runId: string;
  tier: InheritanceTier;
  /**
   * Persist root that contains `switchboard/` (typically
   * `<project>/.codeflowmu/state`). Defaults to
   * `<projectRoot>/.codeflowmu/state`.
   */
  persistDir?: string;
  /** Extra notes written into CONTEXT.md */
  notes?: string[];
}

export interface ProjectionBuildResult {
  tier: InheritanceTier;
  runId: string;
  projectionDir: string;
  contextPath: string;
  manifestPath: string;
  manifest: ContextProjectionManifest;
  sufficient: boolean;
  sufficiencyGaps: string[];
}

function sha16(raw: string): string {
  return createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 16);
}

function pickRefs(facts: WorkFactBundle, tier: InheritanceTier): WorkFactRef[] {
  if (tier === "hot") {
    // Hot: only newest reports / deltas — keep last 2 reports + task pointer.
    return [...facts.reports].slice(-2);
  }
  if (tier === "warm") {
    return [
      ...facts.reports.slice(-5),
      ...facts.reviews.slice(-3),
      ...facts.issues.slice(-3),
    ];
  }
  // cold: full pack
  return [
    ...facts.reports,
    ...facts.reviews,
    ...facts.issues,
    ...facts.evals,
    ...facts.evidence,
  ];
}

function assessSufficiency(
  facts: WorkFactBundle,
  tier: InheritanceTier,
): { sufficient: boolean; gaps: string[] } {
  const gaps: string[] = [...facts.unknowns];
  if (facts.taskPath === "UNKNOWN") gaps.push("missing TASK file");
  if (facts.taskBodyExcerpt === "UNKNOWN") gaps.push("missing TASK body");
  if (tier === "cold") {
    if (facts.reports.length === 0) {
      gaps.push("cold pack requires at least one REPORT fact");
    }
    if (facts.threadKey === "UNKNOWN") {
      gaps.push("cold pack requires thread_key");
    }
  }
  // Deduplicate
  const unique = [...new Set(gaps)];
  const sufficient =
    facts.taskPath !== "UNKNOWN" &&
    facts.taskBodyExcerpt !== "UNKNOWN" &&
    (tier !== "cold" ||
      (facts.reports.length > 0 && facts.threadKey !== "UNKNOWN"));
  return { sufficient, gaps: unique };
}

function renderContextMd(
  input: ProjectionBuildInput,
  refs: WorkFactRef[],
  gaps: string[],
): string {
  const { facts, workerId, runId, tier } = input;
  const lines: string[] = [
    `# Context Projection — ${tier}`,
    "",
    `- schema: codeflowmu.context-projection/v1`,
    `- runId: ${runId}`,
    `- workerId: ${workerId}`,
    `- taskId: ${facts.taskId}`,
    `- thread_key: ${facts.threadKey}`,
    `- parent: ${facts.parent}`,
    `- references: ${facts.references.length ? facts.references.join(", ") : "UNKNOWN"}`,
    `- taskPath: ${facts.taskPath}`,
    `- tier: ${tier}`,
    `- readAt: ${facts.readAt}`,
    "",
    "## Task excerpt",
    "",
    facts.taskBodyExcerpt,
    "",
    "## Fact entries",
    "",
  ];

  if (refs.length === 0) {
    lines.push("- UNKNOWN (no related facts selected for this tier)");
  } else {
    for (const r of refs) {
      lines.push(
        `- [${r.kind}] ${r.path}${r.digest ? ` digest=${r.digest}` : ""}`,
      );
    }
  }

  lines.push("", "## Sufficiency gaps", "");
  if (gaps.length === 0) {
    lines.push("- none");
  } else {
    for (const g of gaps) lines.push(`- ${g}`);
  }

  if (input.notes?.length) {
    lines.push("", "## Notes", "");
    for (const n of input.notes) lines.push(`- ${n}`);
  }

  lines.push(
    "",
    "## Provenance rule",
    "",
    "Materials come only from real TASK / REPORT / Evidence files.",
    "Hidden thinking streams are never read.",
    "Missing facts are written as UNKNOWN — never fabricated.",
    "",
  );
  return lines.join("\n");
}

export class ContextProjectionBuilder {
  /**
   * Write CONTEXT.md + MANIFEST.json for the given tier.
   * Returns sufficiency assessment (cold may be insufficient → caller fails closed).
   */
  async build(input: ProjectionBuildInput): Promise<ProjectionBuildResult> {
    const persistDir =
      input.persistDir ??
      `${input.facts.projectRoot.replace(/[\\/]+$/, "")}/.codeflowmu/state`;
    const paths = new SwitchboardPaths({ persistDir });
    const refs = pickRefs(input.facts, input.tier);
    const { sufficient, gaps } = assessSufficiency(input.facts, input.tier);

    const contextBody = renderContextMd(input, refs, gaps);
    const contextPath = paths.projectionContextFile(input.runId);
    const manifestPath = paths.projectionManifestFile(input.runId);
    const projectionDir = paths.projectionDir(input.runId);

    await fs.mkdir(projectionDir, { recursive: true });
    await fs.writeFile(contextPath, contextBody, "utf8");

    const entries = [
      {
        kind: "context",
        path: "CONTEXT.md",
        digest: sha16(contextBody),
        note: `tier=${input.tier}`,
      },
      {
        kind: "task",
        path: input.facts.taskPath,
        note:
          input.facts.taskPath === "UNKNOWN" ? "UNKNOWN" : "source task file",
      },
      ...refs.map((r) => ({
        kind: r.kind,
        path: r.path,
        digest: r.digest,
        note: r.meta?.status,
      })),
    ];

    const manifest: ContextProjectionManifest = {
      schema: "codeflowmu.context-projection/v1",
      projectionId: `proj-${input.runId}`,
      workerId: input.workerId,
      runId: input.runId,
      createdAt: new Date().toISOString(),
      entries,
      notes: [
        `tier=${input.tier}`,
        `sufficient=${sufficient}`,
        ...(input.notes ?? []),
        ...gaps.map((g) => `gap:${g}`),
      ],
    };

    await fs.writeFile(
      manifestPath,
      `${JSON.stringify(manifest, null, 2)}\n`,
      "utf8",
    );

    return {
      tier: input.tier,
      runId: input.runId,
      projectionDir,
      contextPath,
      manifestPath,
      manifest,
      sufficient,
      sufficiencyGaps: gaps,
    };
  }

  /** Delete a projection pack so it can be rebuilt (derivative, not ledger). */
  async deletePack(persistDir: string, runId: string): Promise<void> {
    const paths = new SwitchboardPaths({ persistDir });
    await fs.rm(paths.projectionDir(runId), { recursive: true, force: true });
  }
}
