import { getTransitionAction, isTransitionAllowed } from "../transitions.js";
import { isPlanningStateEvent, isTaskStateEvent } from "./canonical-state-events.js";
import { CANONICAL_STATE_EVENT_LOG_SUPPORTED_KINDS, TASK_STATE_EVENT_LOG_SUPPORTED_SCHEMA_VERSION } from "./event-admission-policy.js";
import { applyTaskStateEvent, createEmptyTaskStateProjection } from "./event-applier.js";
import { applyPlanningStateEvent, createEmptyPlanningStateProjection } from "./planning-event-applier.js";
import { validateCanonicalStateEvent } from "./validate-canonical-event.js";
function isRecord(value) {
    return value !== null && typeof value === "object" && !Array.isArray(value);
}
function payloadTaskId(event) {
    if (!isTaskStateEvent(event)) {
        return undefined;
    }
    const payload = event.payload;
    if (payload && typeof payload === "object" && "taskId" in payload) {
        const taskId = payload.taskId;
        return typeof taskId === "string" ? taskId : undefined;
    }
    return undefined;
}
function checkIdempotency(event, priorEvents) {
    const key = event.clientMutationId?.trim();
    if (!key) {
        return null;
    }
    for (const prior of priorEvents) {
        if (prior.clientMutationId?.trim() !== key) {
            continue;
        }
        if (prior.eventId === event.eventId) {
            continue;
        }
        return {
            code: "duplicate-idempotency-key",
            message: `clientMutationId '${key}' already used by event ${prior.eventId}`
        };
    }
    return null;
}
function cloneTaskProjection(projection) {
    return {
        ...projection,
        tasksById: { ...projection.tasksById },
        transitionLog: [...projection.transitionLog],
        mutationLog: [...projection.mutationLog],
        taskVersions: [...projection.taskVersions]
    };
}
function clonePlanningProjection(projection) {
    return {
        ...projection,
        phaseCatalogByKey: { ...projection.phaseCatalogByKey },
        phaseNotesById: Object.fromEntries(Object.entries(projection.phaseNotesById).map(([id, note]) => [id, { ...note, refs: note.refs.map((r) => ({ ...r })) }])),
        phaseNoteSuggestionsById: { ...projection.phaseNoteSuggestionsById },
        ideasById: { ...projection.ideasById },
        moduleStateById: Object.fromEntries(Object.entries(projection.moduleStateById).map(([id, row]) => [id, { ...row, state: { ...row.state } }])),
        workspaceStatus: projection.workspaceStatus ? { ...projection.workspaceStatus } : null,
        workspaceStatusAudits: [...projection.workspaceStatusAudits],
        appliedWorkspaceMutationIds: new Set(projection.appliedWorkspaceMutationIds),
        appliedNoteIdempotencyKeys: new Set(projection.appliedNoteIdempotencyKeys),
        appliedSuggestionMutationIds: new Set(projection.appliedSuggestionMutationIds),
        appliedIdeaMutationIds: new Set(projection.appliedIdeaMutationIds),
        appliedModuleStateMutationIds: new Set(projection.appliedModuleStateMutationIds)
    };
}
function buildProjections(priorEvents, initialTaskProjection, initialPlanningProjection) {
    let taskProjection = initialTaskProjection
        ? cloneTaskProjection(initialTaskProjection)
        : createEmptyTaskStateProjection();
    let planningProjection = initialPlanningProjection
        ? clonePlanningProjection(initialPlanningProjection)
        : createEmptyPlanningStateProjection();
    const ordered = [...priorEvents].sort((a, b) => {
        if (a.sequence !== b.sequence) {
            return a.sequence - b.sequence;
        }
        return a.eventId.localeCompare(b.eventId);
    });
    for (const event of ordered) {
        if (isTaskStateEvent(event)) {
            const applied = applyTaskStateEvent(taskProjection, event);
            if (!applied.ok) {
                return {
                    ok: false,
                    error: {
                        code: "replay-conflict",
                        message: applied.error.message,
                        details: [applied.error.code]
                    }
                };
            }
            taskProjection = applied.projection;
        }
        else if (isPlanningStateEvent(event)) {
            const applied = applyPlanningStateEvent(planningProjection, event);
            if (!applied.ok) {
                return {
                    ok: false,
                    error: {
                        code: applied.error.code === "workspace-revision-mismatch"
                            ? "workspace-revision-mismatch"
                            : applied.error.code === "module-state-schema-version-mismatch"
                                ? "module-state-schema-version-mismatch"
                                : "replay-conflict",
                        message: applied.error.message,
                        details: [applied.error.code]
                    }
                };
            }
            planningProjection = applied.projection;
        }
    }
    return { ok: true, taskProjection, planningProjection };
}
function taskVersionForProjection(projection, taskId) {
    const rows = projection.taskVersions.filter((row) => row.taskId === taskId);
    return rows.at(-1)?.version ?? 0;
}
function checkExpectedTaskVersion(event, projection) {
    const expected = event.expectedTaskVersion;
    if (expected === undefined) {
        return null;
    }
    const taskId = payloadTaskId(event);
    if (!taskId) {
        return null;
    }
    const actual = taskVersionForProjection(projection, taskId);
    if (actual !== expected) {
        return {
            code: "replay-conflict",
            message: `expectedTaskVersion ${expected} does not match replayed version ${actual} for ${taskId}`,
            details: ["stale-task-version"]
        };
    }
    return null;
}
function checkExpectedWorkspaceRevision(event, projection) {
    if (event.kind !== "planning.workspace_status.updated") {
        return null;
    }
    const expected = event.expectedWorkspaceRevision;
    if (expected === undefined) {
        return null;
    }
    const actual = projection.workspaceStatus?.workspaceRevision ?? 0;
    if (actual !== expected) {
        return {
            code: "workspace-revision-mismatch",
            message: `expectedWorkspaceRevision ${expected} does not match replayed workspace revision ${actual}`,
            details: ["stale-workspace-revision"]
        };
    }
    return null;
}
function checkExpectedModuleStateSchemaVersion(event, projection) {
    if (event.kind !== "planning.module_state.updated") {
        return null;
    }
    const payload = event.payload;
    if (payload.expectedStateSchemaVersion === undefined || typeof payload.moduleId !== "string") {
        return null;
    }
    const actual = projection.moduleStateById[payload.moduleId]?.stateSchemaVersion ?? 0;
    if (actual !== payload.expectedStateSchemaVersion) {
        return {
            code: "module-state-schema-version-mismatch",
            message: `expectedStateSchemaVersion ${payload.expectedStateSchemaVersion} does not match replayed module state version ${actual} for ${payload.moduleId}`,
            details: ["stale-module-state-schema-version"]
        };
    }
    return null;
}
function checkLifecycleTransition(event, projection) {
    if (event.kind !== "task.transitioned") {
        return null;
    }
    const payload = event.payload;
    const task = projection.tasksById[payload.taskId];
    if (!task) {
        return {
            code: "task-not-found",
            message: `task.transitioned references unknown task ${payload.taskId}`
        };
    }
    if (task.status !== payload.fromState) {
        return {
            code: "invalid-lifecycle-transition",
            message: `fromState ${payload.fromState} does not match replayed status ${task.status} for ${payload.taskId}`
        };
    }
    if (!isTransitionAllowed(payload.fromState, payload.toState)) {
        return {
            code: "invalid-lifecycle-transition",
            message: `transition ${payload.fromState} -> ${payload.toState} is not allowed`
        };
    }
    const expectedAction = getTransitionAction(payload.fromState, payload.toState);
    if (expectedAction && expectedAction !== payload.action) {
        return {
            code: "invalid-lifecycle-transition",
            message: `action '${payload.action}' does not match expected '${expectedAction}' for ${payload.fromState} -> ${payload.toState}`
        };
    }
    return null;
}
function checkCreated(event, projection) {
    if (event.kind !== "task.created") {
        return null;
    }
    const taskId = payloadTaskId(event);
    if (taskId && projection.tasksById[taskId]) {
        return {
            code: "duplicate-task-id",
            message: `task.created for existing task ${taskId}`
        };
    }
    return null;
}
export function admitCanonicalStateEvent(input, context = {}) {
    if (!isRecord(input)) {
        return {
            ok: false,
            error: { code: "schema-validation-failed", message: "event must be an object" }
        };
    }
    const schemaVersion = input.schemaVersion;
    if (schemaVersion !== TASK_STATE_EVENT_LOG_SUPPORTED_SCHEMA_VERSION) {
        return {
            ok: false,
            error: {
                code: "unsupported-schema-version",
                message: `schemaVersion ${String(schemaVersion)} is not supported (expected ${TASK_STATE_EVENT_LOG_SUPPORTED_SCHEMA_VERSION})`
            }
        };
    }
    const kindRaw = input.kind;
    if (typeof kindRaw === "string" && !CANONICAL_STATE_EVENT_LOG_SUPPORTED_KINDS.includes(kindRaw)) {
        return {
            ok: false,
            error: {
                code: "unknown-event-kind",
                message: `unknown event kind '${kindRaw}'`
            }
        };
    }
    const validated = validateCanonicalStateEvent(input);
    if (!validated.ok) {
        return {
            ok: false,
            error: {
                code: "schema-validation-failed",
                message: "event failed JSON schema validation",
                details: validated.errors
            }
        };
    }
    const event = validated.data;
    const priorEvents = context.priorEvents ?? [];
    const idempotencyErr = checkIdempotency(event, priorEvents);
    if (idempotencyErr) {
        return { ok: false, error: idempotencyErr };
    }
    const projectionResult = buildProjections(priorEvents, context.initialTaskProjection, context.initialPlanningProjection);
    if (!projectionResult.ok) {
        return projectionResult;
    }
    if (isTaskStateEvent(event)) {
        const taskEvent = event;
        const createdErr = checkCreated(taskEvent, projectionResult.taskProjection);
        if (createdErr) {
            return { ok: false, error: createdErr };
        }
        const lifecycleErr = checkLifecycleTransition(taskEvent, projectionResult.taskProjection);
        if (lifecycleErr) {
            return { ok: false, error: lifecycleErr };
        }
        const versionErr = checkExpectedTaskVersion(taskEvent, projectionResult.taskProjection);
        if (versionErr) {
            return { ok: false, error: versionErr };
        }
        const taskId = payloadTaskId(taskEvent);
        if ((taskEvent.kind === "task.updated" || taskEvent.kind === "task.transitioned") &&
            taskId &&
            !projectionResult.taskProjection.tasksById[taskId]) {
            return {
                ok: false,
                error: {
                    code: "task-not-found",
                    message: `${taskEvent.kind} references unknown task ${taskId}`
                }
            };
        }
        const applied = applyTaskStateEvent(projectionResult.taskProjection, taskEvent);
        if (!applied.ok) {
            return {
                ok: false,
                error: {
                    code: "replay-conflict",
                    message: applied.error.message,
                    details: [applied.error.code]
                }
            };
        }
    }
    else if (isPlanningStateEvent(event)) {
        const planningEvent = event;
        const revisionErr = checkExpectedWorkspaceRevision(planningEvent, projectionResult.planningProjection);
        if (revisionErr) {
            return { ok: false, error: revisionErr };
        }
        const moduleStateErr = checkExpectedModuleStateSchemaVersion(planningEvent, projectionResult.planningProjection);
        if (moduleStateErr) {
            return { ok: false, error: moduleStateErr };
        }
        const applied = applyPlanningStateEvent(projectionResult.planningProjection, planningEvent);
        if (!applied.ok) {
            return {
                ok: false,
                error: {
                    code: applied.error.code === "workspace-revision-mismatch"
                        ? "workspace-revision-mismatch"
                        : applied.error.code === "module-state-schema-version-mismatch"
                            ? "module-state-schema-version-mismatch"
                            : "replay-conflict",
                    message: applied.error.message,
                    details: [applied.error.code]
                }
            };
        }
    }
    return { ok: true, event };
}
export function admitCanonicalStateEventStream(inputs, options) {
    const admitted = [...(options?.priorEvents ?? [])];
    const toAppend = [...inputs].sort((a, b) => {
        const sa = isRecord(a) && typeof a.sequence === "number" ? a.sequence : 0;
        const sb = isRecord(b) && typeof b.sequence === "number" ? b.sequence : 0;
        if (sa !== sb)
            return sa - sb;
        const ea = isRecord(a) && typeof a.eventId === "string" ? a.eventId : "";
        const eb = isRecord(b) && typeof b.eventId === "string" ? b.eventId : "";
        return ea.localeCompare(eb);
    });
    for (const input of toAppend) {
        const result = admitCanonicalStateEvent(input, {
            priorEvents: admitted,
            initialTaskProjection: options?.initialTaskProjection,
            initialPlanningProjection: options?.initialPlanningProjection
        });
        if (!result.ok) {
            return result;
        }
        admitted.push(result.event);
    }
    return { ok: true, events: admitted.slice(options?.priorEvents?.length ?? 0) };
}
/** Back-compat: task-only admission delegates to canonical stream. */
export function admitTaskStateEventStreamForCanonical(inputs, options) {
    return admitCanonicalStateEventStream(inputs, {
        priorEvents: options?.priorEvents,
        initialTaskProjection: options?.initialProjection
    });
}
export function createEmptyCanonicalProjections() {
    return {
        taskProjection: createEmptyTaskStateProjection(),
        planningProjection: createEmptyPlanningStateProjection()
    };
}
