import { clearParseCache } from "../files.js";
import { isClosedStatus } from "../status-guards.js";
import { isNonEmptyString } from "../validation.js";
import { transaction, getMilestone, getSlice, insertTask, upsertSlicePlanning, upsertTaskPlanning, insertGateRow, } from "../nerf-db.js";
import { invalidateStateCache } from "../state.js";
import { renderPlanFromDb } from "../markdown-renderer.js";
import { renderAllProjections } from "../workflow-projections.js";
import { writeManifest } from "../workflow-manifest.js";
import { appendEvent } from "../workflow-events.js";
function validateTasks(value) {
    if (!Array.isArray(value) || value.length === 0) {
        throw new Error("tasks must be a non-empty array");
    }
    const seen = new Set();
    return value.map((entry, index) => {
        if (!entry || typeof entry !== "object") {
            throw new Error(`tasks[${index}] must be an object`);
        }
        const obj = entry;
        const taskId = obj.taskId;
        const title = obj.title;
        const description = obj.description;
        const estimate = obj.estimate;
        const files = obj.files;
        const verify = obj.verify;
        const inputs = obj.inputs;
        const expectedOutput = obj.expectedOutput;
        const observabilityImpact = obj.observabilityImpact;
        if (!isNonEmptyString(taskId))
            throw new Error(`tasks[${index}].taskId must be a non-empty string`);
        if (seen.has(taskId))
            throw new Error(`tasks[${index}].taskId must be unique`);
        seen.add(taskId);
        if (!isNonEmptyString(title))
            throw new Error(`tasks[${index}].title must be a non-empty string`);
        if (!isNonEmptyString(description))
            throw new Error(`tasks[${index}].description must be a non-empty string`);
        if (!isNonEmptyString(estimate))
            throw new Error(`tasks[${index}].estimate must be a non-empty string`);
        if (!Array.isArray(files) || files.some((item) => !isNonEmptyString(item))) {
            throw new Error(`tasks[${index}].files must be an array of non-empty strings`);
        }
        if (!isNonEmptyString(verify))
            throw new Error(`tasks[${index}].verify must be a non-empty string`);
        if (!Array.isArray(inputs) || inputs.some((item) => !isNonEmptyString(item))) {
            throw new Error(`tasks[${index}].inputs must be an array of non-empty strings`);
        }
        if (!Array.isArray(expectedOutput) || expectedOutput.some((item) => !isNonEmptyString(item))) {
            throw new Error(`tasks[${index}].expectedOutput must be an array of non-empty strings`);
        }
        if (observabilityImpact !== undefined && !isNonEmptyString(observabilityImpact)) {
            throw new Error(`tasks[${index}].observabilityImpact must be a non-empty string when provided`);
        }
        return {
            taskId,
            title,
            description,
            estimate,
            files,
            verify,
            inputs,
            expectedOutput,
            observabilityImpact: typeof observabilityImpact === "string" ? observabilityImpact : "",
        };
    });
}
function validateParams(params) {
    if (!isNonEmptyString(params?.milestoneId))
        throw new Error("milestoneId is required");
    if (!isNonEmptyString(params?.sliceId))
        throw new Error("sliceId is required");
    if (!isNonEmptyString(params?.goal))
        throw new Error("goal is required");
    if (!isNonEmptyString(params?.successCriteria))
        throw new Error("successCriteria is required");
    if (!isNonEmptyString(params?.proofLevel))
        throw new Error("proofLevel is required");
    if (!isNonEmptyString(params?.integrationClosure))
        throw new Error("integrationClosure is required");
    if (!isNonEmptyString(params?.observabilityImpact))
        throw new Error("observabilityImpact is required");
    return {
        ...params,
        tasks: validateTasks(params.tasks),
    };
}
export async function handlePlanSlice(rawParams, basePath) {
    let params;
    try {
        params = validateParams(rawParams);
    }
    catch (err) {
        return { error: `validation failed: ${err.message}` };
    }
    // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
    // Guards must be inside the transaction so the state they check cannot
    // change between the read and the write (#2723).
    let guardError = null;
    try {
        transaction(() => {
            const parentMilestone = getMilestone(params.milestoneId);
            if (!parentMilestone) {
                guardError = `milestone not found: ${params.milestoneId}`;
                return;
            }
            if (isClosedStatus(parentMilestone.status)) {
                guardError = `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})`;
                return;
            }
            const parentSlice = getSlice(params.milestoneId, params.sliceId);
            if (!parentSlice) {
                guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
                return;
            }
            if (isClosedStatus(parentSlice.status)) {
                guardError = `cannot re-plan slice ${params.sliceId}: it is already complete — use nerf_slice_reopen first`;
                return;
            }
            upsertSlicePlanning(params.milestoneId, params.sliceId, {
                goal: params.goal,
                successCriteria: params.successCriteria,
                proofLevel: params.proofLevel,
                integrationClosure: params.integrationClosure,
                observabilityImpact: params.observabilityImpact,
            });
            for (const task of params.tasks) {
                insertTask({
                    id: task.taskId,
                    sliceId: params.sliceId,
                    milestoneId: params.milestoneId,
                    title: task.title,
                    status: "pending",
                });
                upsertTaskPlanning(params.milestoneId, params.sliceId, task.taskId, {
                    title: task.title,
                    description: task.description,
                    estimate: task.estimate,
                    files: task.files,
                    verify: task.verify,
                    inputs: task.inputs,
                    expectedOutput: task.expectedOutput,
                    observabilityImpact: task.observabilityImpact ?? "",
                    fullPlanMd: task.fullPlanMd,
                });
            }
            // Seed quality gate rows inside the transaction — all-or-nothing with
            // the plan data so a crash can't leave orphaned gates without tasks.
            const sliceGates = ["Q3", "Q4"];
            for (const gid of sliceGates) {
                insertGateRow({ milestoneId: params.milestoneId, sliceId: params.sliceId, gateId: gid, scope: "slice" });
            }
            const taskGates = ["Q5", "Q6", "Q7"];
            for (const task of params.tasks) {
                for (const gid of taskGates) {
                    insertGateRow({ milestoneId: params.milestoneId, sliceId: params.sliceId, gateId: gid, scope: "task", taskId: task.taskId });
                }
            }
            insertGateRow({ milestoneId: params.milestoneId, sliceId: params.sliceId, gateId: "Q8", scope: "slice" });
        });
    }
    catch (err) {
        return { error: `db write failed: ${err.message}` };
    }
    if (guardError) {
        return { error: guardError };
    }
    try {
        const renderResult = await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);
        invalidateStateCache();
        clearParseCache();
        // ── Post-mutation hook: projections, manifest, event log ─────────────
        try {
            await renderAllProjections(basePath, params.milestoneId);
            writeManifest(basePath);
            appendEvent(basePath, {
                cmd: "plan-slice",
                params: { milestoneId: params.milestoneId, sliceId: params.sliceId },
                ts: new Date().toISOString(),
                actor: "agent",
                actor_name: params.actorName,
                trigger_reason: params.triggerReason,
            });
        }
        catch (hookErr) {
            process.stderr.write(`nerf: plan-slice post-mutation hook warning: ${hookErr.message}\n`);
        }
        return {
            milestoneId: params.milestoneId,
            sliceId: params.sliceId,
            planPath: renderResult.planPath,
            taskPlanPaths: renderResult.taskPlanPaths,
        };
    }
    catch (renderErr) {
        process.stderr.write(`nerf-db: plan_slice — render failed (DB rows preserved for debugging): ${renderErr.message}\n`);
        invalidateStateCache();
        return { error: `render failed: ${renderErr.message}` };
    }
}
