import { createHash } from "node:crypto";
import { readFile, writeFile, copyFile, mkdir } from "node:fs/promises";
import { join } from "node:path";
import { computeBlockHash } from "./journal-sentinel.js";
import { buildJournalManifestEntries, mergeJournalIntoManifest, renderJournalSectionMd, insertJournalSectionIntoIndex, readManifestIfExists, } from "./journal-generator-helpers.js";
// Typed errors (CQ8)
export class NoPhasesError extends Error {
    constructor() { super("no `### Week` headings found in source; NoPhasesError"); this.name = "NoPhasesError"; }
}
export class SlugCollisionError extends Error {
    constructor(slug) {
        super(`slug collision: two weeks resolve to "${slug}". Disambiguate titles or date ranges. SlugCollisionError`);
        this.name = "SlugCollisionError";
    }
}
export class SourceDriftError extends Error {
    constructor(newSha, recordedSha) {
        super(`source file changed since dry-run (sha ${newSha} ≠ ${recordedSha}), aborting`);
        this.name = "SourceDriftError";
    }
}
// Constants + path helpers (CQ14 single-site path construction)
const SCHEMA_VERSION = "1";
const GITIGNORE_ENTRY_REL = ".codesift/wiki/journal/.migrate-state.json";
const NO_PLAN_MSG = "No migration plan found. Run 'codesift journal migrate --dry-run' first to generate a migration plan, then run without --dry-run to execute.";
const journalDir = (outputDir) => join(outputDir, "journal");
const phasesDir = (outputDir) => join(journalDir(outputDir), "phases");
const phasePath = (outputDir, slug) => join(phasesDir(outputDir), `${slug}.md`);
const overviewPath = (outputDir) => join(journalDir(outputDir), "overview.md");
const statePath = (outputDir) => join(journalDir(outputDir), ".migrate-state.json");
const gitignorePath = (repoRoot) => join(repoRoot, ".gitignore");
const RE_H2 = /^## (.+)$/;
const RE_TIMELINE = /^## Timeline\s*$/;
const RE_WEEK = /^### Week (\d+) — (.+)$/;
const RE_ENTRY_SINGLE = /^#### (\d{4}-\d{2}-\d{2}) — /;
const RE_ENTRY_RANGE = /^#### (\d{4}-\d{2}-\d{2}) [–-] (\d{4}-\d{2}-\d{2}) — /;
function parse(source) {
    const lines = source.replace(/\r\n/g, "\n").split("\n");
    const weeks = [];
    const overviewSections = [];
    let mode = { kind: "none" };
    for (const line of lines) {
        if (RE_TIMELINE.test(line)) {
            mode = { kind: "none" };
            continue;
        }
        const weekMatch = RE_WEEK.exec(line);
        if (weekMatch) {
            const week = { headingLine: line, title: weekMatch[2], entries: [], preamble: [] };
            weeks.push(week);
            mode = { kind: "week", week, currentEntry: null };
            continue;
        }
        if (RE_H2.test(line)) { // Any ## heading ends week/overview mode and starts a new overview section.
            const sec = { headingLine: line, bodyLines: [] };
            overviewSections.push(sec);
            mode = { kind: "overview", sec };
            continue;
        }
        if (mode.kind === "week") {
            const rangeMatch = RE_ENTRY_RANGE.exec(line);
            const singleMatch = rangeMatch ? null : RE_ENTRY_SINGLE.exec(line);
            if (rangeMatch || singleMatch) {
                const startDate = (rangeMatch ? rangeMatch[1] : singleMatch[1]);
                const entry = { startDate, titleLine: line, bodyLines: [] };
                mode.week.entries.push(entry);
                mode.currentEntry = entry;
                continue;
            }
            if (mode.currentEntry)
                mode.currentEntry.bodyLines.push(line);
            else
                mode.week.preamble.push(line);
        }
        else if (mode.kind === "overview") {
            mode.sec.bodyLines.push(line);
        }
    }
    return { weeks, overviewSections };
}
const slugifyTitle = (title) => title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
const deriveSlug = (week) => {
    const month = week.entries[0]?.startDate.slice(0, 7) ?? "0000-00";
    return `${month}-${slugifyTitle(week.title)}`;
};
function renderPhaseFile(week) {
    const lines = [
        `## ${week.title}`, "",
        "<!-- auto:begin phase-summary -->",
        "<!-- TODO: generate phase summary via `codesift journal init` -->",
        "<!-- auto:end phase-summary -->",
        "", "## My notes", week.headingLine, ...week.preamble,
    ];
    for (const entry of week.entries)
        lines.push(entry.titleLine, ...entry.bodyLines);
    return lines.join("\n").replace(/\n+$/, "\n");
}
function renderOverviewFile(sections) {
    const lines = ["# Overview", "", "<!-- manual:begin migrated-overview -->"];
    for (const sec of sections)
        lines.push(sec.headingLine, ...sec.bodyLines);
    lines.push("<!-- manual:end migrated-overview -->", "");
    return lines.join("\n");
}
const sha256 = (s) => createHash("sha256").update(s).digest("hex");
async function ensureGitignoreEntry(repoRoot, entry) {
    const path = gitignorePath(repoRoot);
    try {
        const existing = await readFile(path, "utf-8").catch(() => "");
        if (existing.split("\n").includes(entry))
            return;
        const suffix = existing && !existing.endsWith("\n") ? "\n" : "";
        await writeFile(path, existing + suffix + entry + "\n", "utf-8");
    }
    catch (err) {
        console.warn(`WARN journal: could not update .gitignore (${err.message}); add ${entry} manually.`);
    }
}
export async function runMigrate(opts) {
    const { source, repoRoot, outputDir, dryRun } = opts;
    const sourceContent = await readFile(source, "utf-8");
    const parsed = parse(sourceContent);
    if (parsed.weeks.length === 0)
        throw new NoPhasesError();
    const slugs = parsed.weeks.map(deriveSlug);
    const seen = new Set();
    for (const s of slugs) {
        if (seen.has(s))
            throw new SlugCollisionError(s);
        seen.add(s);
    }
    const sourceSha = sha256(sourceContent);
    await mkdir(journalDir(outputDir), { recursive: true });
    if (dryRun) {
        const state = { source_sha256: sourceSha, planned_phase_slugs: slugs, schema_version: SCHEMA_VERSION };
        await writeFile(statePath(outputDir), JSON.stringify(state, null, 2), "utf-8");
        await ensureGitignoreEntry(repoRoot, GITIGNORE_ENTRY_REL);
        return { status: "planned", phaseCount: parsed.weeks.length };
    }
    // Live run: require plan, validate SHA.
    let stateRaw;
    try {
        stateRaw = await readFile(statePath(outputDir), "utf-8");
    }
    catch {
        throw new Error(NO_PLAN_MSG);
    }
    const state = JSON.parse(stateRaw);
    if (state.source_sha256 !== sourceSha)
        throw new SourceDriftError(sourceSha, state.source_sha256);
    // .bak MUST precede any phase writes (test i).
    await copyFile(source, source + ".bak");
    await mkdir(phasesDir(outputDir), { recursive: true });
    const wx = { encoding: "utf-8", flag: "wx" }; // no-clobber; hand-edits require .bak rollback
    const phaseWrites = [];
    const scaffoldHash = computeBlockHash("\n<!-- TODO: generate phase summary via `codesift journal init` -->\n");
    for (let i = 0; i < parsed.weeks.length; i++) {
        const slug = slugs[i];
        await writeFile(phasePath(outputDir, slug), renderPhaseFile(parsed.weeks[i]), wx);
        phaseWrites.push({ slug, title: parsed.weeks[i].title, file: `journal/phases/${slug}.md`, hash: scaffoldHash });
    }
    await writeFile(overviewPath(outputDir), renderOverviewFile(parsed.overviewSections), wx);
    // Register journal pages in wiki-manifest.json + index.md (best-effort, CQ8).
    const manifestPath = join(outputDir, "wiki-manifest.json");
    const indexPath = join(outputDir, "index.md");
    const entries = buildJournalManifestEntries(phaseWrites, true); // migrator always writes overview.md
    try {
        const merged = mergeJournalIntoManifest(await readManifestIfExists(manifestPath), entries);
        await writeFile(manifestPath, JSON.stringify(merged, null, 2), "utf-8");
        try {
            const existing = await readFile(indexPath, "utf-8").catch(() => "");
            await writeFile(indexPath, insertJournalSectionIntoIndex(existing, renderJournalSectionMd(entries)), "utf-8");
        }
        catch (err) {
            console.warn(`WARN journal: could not update index.md (${err.message})`);
        }
    }
    catch (err) {
        console.warn(`WARN journal: could not update wiki-manifest.json (${err.message})`);
    }
    return { status: "ok", phaseCount: parsed.weeks.length };
}
//# sourceMappingURL=journal-migrator.js.map