import fs from "node:fs";
import path from "node:path";
import { admitTaskStateEventStream } from "../task-state-events/event-admission.js";
import { validateTaskStateEvent } from "../task-state-events/validate-event.js";
import { TASK_STATE_MANIFEST_RELATIVE, TASK_STATE_ROOT_DIR } from "./constants.js";
import { digestTaskStateCanonicalJson } from "./integrity.js";
import { resolveSnapshotMetaRelativePath } from "./layout.js";
import { projectionFromSnapshotContent } from "./snapshot-projection.js";
import { computeManifestDigest, validateTaskStateGitManifest } from "./validate-manifest.js";
import { validateTaskStateGitSnapshotMeta } from "./validate-snapshot-meta.js";
function readUtf8(filePath) {
    if (!fs.existsSync(filePath)) {
        return null;
    }
    return fs.readFileSync(filePath, "utf8");
}
function parseJsonLines(text) {
    const values = [];
    for (const line of text.split("\n")) {
        const trimmed = line.trim();
        if (!trimmed || trimmed.startsWith("#")) {
            continue;
        }
        try {
            values.push(JSON.parse(trimmed));
        }
        catch (error) {
            return {
                ok: false,
                message: error instanceof Error ? error.message : String(error)
            };
        }
    }
    return { ok: true, values };
}
function admissionCodeToFindingCode(code) {
    switch (code) {
        case "unsupported-schema-version":
            return "event-unsupported-schema-version";
        case "unknown-event-kind":
            return "event-unknown-event-kind";
        case "schema-validation-failed":
            return "event-schema-validation-failed";
        default:
            return "event-admission-rejected";
    }
}
function verifyRawEventSchemas(rawEvents, findings) {
    for (const event of rawEvents) {
        if (event !== null &&
            typeof event === "object" &&
            !Array.isArray(event) &&
            event.schemaVersion !== 1) {
            findings.push({
                code: "event-unsupported-schema-version",
                message: `schemaVersion ${String(event.schemaVersion)} is not supported (expected 1)`
            });
            continue;
        }
        const validated = validateTaskStateEvent(event);
        if (!validated.ok) {
            findings.push({
                code: "event-schema-validation-failed",
                message: "event failed JSON schema validation",
                path: validated.errors.join("; ")
            });
        }
    }
}
function verifyEventChain(events, findings, baseline) {
    if (events.length === 0) {
        return;
    }
    const bySequence = [...events].sort((a, b) => a.sequence - b.sequence || a.eventId.localeCompare(b.eventId));
    const maxSequence = bySequence.at(-1)?.sequence ?? 0;
    const firstExpectedSequence = baseline ? baseline.throughSequence + 1 : 0;
    for (let expected = firstExpectedSequence; expected <= maxSequence; expected++) {
        if (!bySequence.some((event) => event.sequence === expected)) {
            findings.push({
                code: "event-sequence-gap",
                message: `Missing event for sequence ${expected} (max sequence ${maxSequence})`
            });
        }
    }
    const eventsBySequence = new Map();
    for (const event of bySequence) {
        eventsBySequence.set(event.sequence, event);
    }
    for (const event of bySequence) {
        if (baseline && event.sequence <= baseline.throughSequence) {
            continue;
        }
        if (baseline && event.sequence === firstExpectedSequence) {
            const allowedParentIds = new Set([baseline.throughEventId, null]);
            if (!allowedParentIds.has(event.parentEventId)) {
                findings.push({
                    code: "event-parent-mismatch",
                    message: `Event ${event.eventId} (sequence ${event.sequence}) parentEventId ${String(event.parentEventId)} expected ${String(baseline.throughEventId)}`
                });
            }
            continue;
        }
        if (!baseline && event.sequence === 0) {
            if (event.parentEventId !== null) {
                findings.push({
                    code: "event-parent-mismatch",
                    message: `Sequence 0 event ${event.eventId} must have parentEventId null, got ${String(event.parentEventId)}`
                });
            }
            continue;
        }
        const parent = eventsBySequence.get(event.sequence - 1);
        const expectedParentId = parent?.eventId ?? null;
        if (event.parentEventId !== expectedParentId) {
            findings.push({
                code: "event-parent-mismatch",
                message: `Event ${event.eventId} (sequence ${event.sequence}) parentEventId ${String(event.parentEventId)} expected ${String(expectedParentId)}`
            });
        }
    }
}
function verifySnapshot(layoutRoot, snapshotId, findings) {
    const metaRel = resolveSnapshotMetaRelativePath(snapshotId);
    const metaAbs = path.join(layoutRoot, metaRel);
    const metaText = readUtf8(metaAbs);
    if (!metaText) {
        findings.push({
            code: "snapshot-meta-missing",
            message: `Missing snapshot metadata at ${metaRel}`,
            path: metaRel
        });
        return null;
    }
    let metaParsed;
    try {
        metaParsed = JSON.parse(metaText);
    }
    catch {
        findings.push({
            code: "snapshot-meta-invalid",
            message: `Snapshot metadata is not valid JSON at ${metaRel}`,
            path: metaRel
        });
        return null;
    }
    const metaValidated = validateTaskStateGitSnapshotMeta(metaParsed);
    if (!metaValidated.ok) {
        findings.push({
            code: "snapshot-meta-invalid",
            message: metaValidated.errors.join("; "),
            path: metaRel
        });
        return null;
    }
    const meta = metaValidated.data;
    const contentRel = meta.contentPath.startsWith(`${TASK_STATE_ROOT_DIR}/`)
        ? meta.contentPath
        : path.posix.join(TASK_STATE_ROOT_DIR, meta.contentPath);
    const contentAbs = path.join(layoutRoot, contentRel);
    const contentText = readUtf8(contentAbs);
    if (!contentText) {
        findings.push({
            code: "snapshot-content-missing",
            message: `Missing snapshot content at ${contentRel}`,
            path: contentRel
        });
        return null;
    }
    let contentParsed;
    try {
        contentParsed = JSON.parse(contentText);
    }
    catch {
        findings.push({
            code: "snapshot-content-digest-mismatch",
            message: `Snapshot content is not valid JSON at ${contentRel}`,
            path: contentRel
        });
        return null;
    }
    const digest = digestTaskStateCanonicalJson(contentParsed);
    if (digest !== meta.contentDigest) {
        findings.push({
            code: "snapshot-content-digest-mismatch",
            message: `Snapshot ${snapshotId} contentDigest mismatch: expected ${meta.contentDigest}, computed ${digest}`,
            path: contentRel
        });
    }
    return { meta, content: contentParsed };
}
/** Verify on-disk layout under `<workspace>/task-state/` (or explicit layout root). */
export function verifyTaskStateLayoutOnDisk(layoutRoot) {
    const findings = [];
    const manifestAbs = path.join(layoutRoot, TASK_STATE_MANIFEST_RELATIVE);
    const manifestText = readUtf8(manifestAbs);
    if (!manifestText) {
        findings.push({
            code: "manifest-missing",
            message: `Missing ${TASK_STATE_MANIFEST_RELATIVE}`,
            path: TASK_STATE_MANIFEST_RELATIVE
        });
        return finalize(findings);
    }
    let manifestParsed;
    try {
        manifestParsed = JSON.parse(manifestText);
    }
    catch {
        findings.push({
            code: "manifest-invalid",
            message: "Manifest is not valid JSON",
            path: TASK_STATE_MANIFEST_RELATIVE
        });
        return finalize(findings);
    }
    const manifestResult = validateTaskStateGitManifest(manifestParsed);
    if (!manifestResult.ok) {
        findings.push({
            code: "manifest-invalid",
            message: manifestResult.errors.join("; "),
            path: TASK_STATE_MANIFEST_RELATIVE
        });
        return finalize(findings);
    }
    const manifest = manifestResult.data;
    if (manifest.manifestDigest) {
        const expected = computeManifestDigest(manifest);
        if (manifest.manifestDigest !== expected) {
            findings.push({
                code: "manifest-digest-mismatch",
                message: `manifestDigest mismatch: expected ${expected}, got ${manifest.manifestDigest}`,
                path: TASK_STATE_MANIFEST_RELATIVE
            });
        }
    }
    const eventsDir = path.join(layoutRoot, TASK_STATE_ROOT_DIR, "events");
    const eventPaths = [];
    if (fs.existsSync(eventsDir)) {
        for (const name of fs.readdirSync(eventsDir).sort()) {
            if (name.endsWith(".jsonl")) {
                eventPaths.push(path.join(eventsDir, name));
            }
        }
    }
    const rawEvents = [];
    for (const segmentAbs of eventPaths) {
        const segmentRel = path.relative(layoutRoot, segmentAbs).split(path.sep).join("/");
        const text = readUtf8(segmentAbs);
        if (text === null) {
            findings.push({
                code: "event-segment-missing",
                message: `Missing segment ${segmentRel}`,
                path: segmentRel
            });
            continue;
        }
        const lines = parseJsonLines(text);
        if (!lines.ok) {
            findings.push({
                code: "event-parse-failed",
                message: `Failed to parse JSONL in ${segmentRel}: ${lines.message}`,
                path: segmentRel
            });
            continue;
        }
        rawEvents.push(...lines.values);
    }
    const snapshotId = manifest.head.latestSnapshotId;
    const snapshot = snapshotId ? verifySnapshot(layoutRoot, snapshotId, findings) : null;
    verifyRawEventSchemas(rawEvents, findings);
    const admissionEvents = snapshot
        ? rawEvents.filter((event) => {
            return (event !== null &&
                typeof event === "object" &&
                !Array.isArray(event) &&
                typeof event.sequence === "number" &&
                event.sequence > snapshot.meta.throughSequence);
        })
        : rawEvents;
    let parsedEvents = [];
    const admitted = admitTaskStateEventStream(admissionEvents, snapshot ? { initialProjection: projectionFromSnapshotContent(snapshot.content) } : undefined);
    if (!admitted.ok) {
        findings.push({
            code: admissionCodeToFindingCode(admitted.error.code),
            message: admitted.error.message,
            path: admitted.error.details?.join("; ")
        });
    }
    else {
        parsedEvents = admitted.events;
        verifyEventChain(parsedEvents, findings, snapshot
            ? {
                throughSequence: snapshot.meta.throughSequence,
                throughEventId: snapshot.meta.throughEventId
            }
            : undefined);
    }
    const rawEventSequences = rawEvents
        .map((event) => event !== null &&
        typeof event === "object" &&
        !Array.isArray(event) &&
        typeof event.sequence === "number"
        ? event.sequence
        : null)
        .filter((sequence) => sequence !== null);
    const maxSequence = Math.max(snapshot?.meta.throughSequence ?? 0, ...rawEventSequences, 0);
    if (manifest.head.latestSequence !== maxSequence) {
        findings.push({
            code: "event-head-sequence-mismatch",
            message: `Manifest head.latestSequence ${manifest.head.latestSequence} does not match event tail ${maxSequence}`
        });
    }
    return finalize(findings);
}
/** Verify `<workspacePath>/task-state/**` layout. */
export function verifyTaskStateLayoutInWorkspace(workspacePath) {
    return verifyTaskStateLayoutOnDisk(workspacePath);
}
function finalize(findings) {
    return {
        passed: findings.length === 0,
        findingCount: findings.length,
        findings
    };
}
