import { validatePhaseCatalogKey } from "../persistence/phase-catalog-store.js";
import { isModuleStatePlanningSyncAllowed } from "./module-state-planning-sync-allowlist.js";
import { planningEventKindToSyncDomain } from "../persistence/planning-canonical-sync-domains.js";
import { eventSnapshotToPhaseNoteRow, eventSnapshotToPhaseNoteSuggestionRow } from "./planning-phase-note-event-utils.js";
import { eventSnapshotToWorkflowIdeaRow } from "./planning-idea-event-utils.js";
export function createEmptyPlanningStateProjection(lastUpdated = "1970-01-01T00:00:00.000Z") {
    return {
        schemaVersion: 1,
        phaseCatalogByKey: {},
        phaseNotesById: {},
        phaseNoteSuggestionsById: {},
        ideasById: {},
        moduleStateById: {},
        workspaceStatus: null,
        workspaceStatusAudits: [],
        appliedWorkspaceMutationIds: new Set(),
        appliedNoteIdempotencyKeys: new Set(),
        appliedSuggestionMutationIds: new Set(),
        appliedIdeaMutationIds: new Set(),
        appliedModuleStateMutationIds: new Set(),
        lastEventSequence: 0,
        lastUpdated
    };
}
function bumpSequence(projection, event) {
    projection.lastEventSequence = Math.max(projection.lastEventSequence, event.sequence);
    projection.lastUpdated = event.recordedAt;
}
function noteIdempotencyKey(event, note) {
    const fromNote = note.idempotencyKey?.trim();
    if (fromNote) {
        return fromNote;
    }
    const fromEvent = event.clientMutationId?.trim();
    return fromEvent && fromEvent.length > 0 ? fromEvent : null;
}
function cloneNoteRow(note) {
    return { ...note, refs: note.refs.map((r) => ({ ...r })) };
}
function applyCatalogUpsert(projection, event, payload) {
    const key = validatePhaseCatalogKey(payload.phaseKey);
    if (!key) {
        return {
            code: "phase-catalog-key-invalid",
            message: `invalid phaseKey '${payload.phaseKey}'`,
            eventId: event.eventId
        };
    }
    projection.phaseCatalogByKey[key] = {
        phaseKey: key,
        shortDescription: payload.shortDescription,
        updatedAt: payload.updatedAt
    };
    bumpSequence(projection, event);
    return null;
}
function applyCatalogRemoved(projection, event, payload) {
    const key = validatePhaseCatalogKey(payload.phaseKey);
    if (!key) {
        return {
            code: "phase-catalog-key-invalid",
            message: `invalid phaseKey '${payload.phaseKey}'`,
            eventId: event.eventId
        };
    }
    delete projection.phaseCatalogByKey[key];
    bumpSequence(projection, event);
    return null;
}
function applyPhaseNoteCreated(projection, event, payload) {
    const idemKey = noteIdempotencyKey(event, payload.note);
    if (idemKey && projection.appliedNoteIdempotencyKeys.has(idemKey)) {
        bumpSequence(projection, event);
        return null;
    }
    if (projection.phaseNotesById[payload.note.id]) {
        bumpSequence(projection, event);
        return null;
    }
    projection.phaseNotesById[payload.note.id] = eventSnapshotToPhaseNoteRow(payload.note);
    if (idemKey) {
        projection.appliedNoteIdempotencyKeys.add(idemKey);
    }
    bumpSequence(projection, event);
    return null;
}
function applyPhaseNoteUpdated(projection, event, payload) {
    const mutationKey = event.clientMutationId?.trim();
    if (mutationKey && projection.appliedNoteIdempotencyKeys.has(`update:${mutationKey}`)) {
        bumpSequence(projection, event);
        return null;
    }
    projection.phaseNotesById[payload.note.id] = eventSnapshotToPhaseNoteRow(payload.note);
    if (mutationKey) {
        projection.appliedNoteIdempotencyKeys.add(`update:${mutationKey}`);
    }
    bumpSequence(projection, event);
    return null;
}
function applyPhaseNoteArchived(projection, event, payload) {
    const mutationKey = event.clientMutationId?.trim();
    if (mutationKey && projection.appliedNoteIdempotencyKeys.has(`archive:${mutationKey}`)) {
        bumpSequence(projection, event);
        return null;
    }
    projection.phaseNotesById[payload.note.id] = eventSnapshotToPhaseNoteRow(payload.note);
    if (mutationKey) {
        projection.appliedNoteIdempotencyKeys.add(`archive:${mutationKey}`);
    }
    bumpSequence(projection, event);
    return null;
}
function applySuggestionCreated(projection, event, payload) {
    const mutationKey = event.clientMutationId?.trim();
    if (mutationKey && projection.appliedSuggestionMutationIds.has(mutationKey)) {
        bumpSequence(projection, event);
        return null;
    }
    const row = eventSnapshotToPhaseNoteSuggestionRow(payload.suggestion);
    projection.phaseNoteSuggestionsById[row.id] = row;
    if (mutationKey) {
        projection.appliedSuggestionMutationIds.add(mutationKey);
    }
    bumpSequence(projection, event);
    return null;
}
function applySuggestionUpdated(projection, event, payload) {
    const mutationKey = event.clientMutationId?.trim();
    if (mutationKey && projection.appliedSuggestionMutationIds.has(mutationKey)) {
        bumpSequence(projection, event);
        return null;
    }
    const row = eventSnapshotToPhaseNoteSuggestionRow(payload.suggestion);
    projection.phaseNoteSuggestionsById[row.id] = row;
    if (mutationKey) {
        projection.appliedSuggestionMutationIds.add(mutationKey);
    }
    bumpSequence(projection, event);
    return null;
}
function applySuggestionRemoved(projection, event, payload) {
    const mutationKey = event.clientMutationId?.trim();
    if (mutationKey && projection.appliedSuggestionMutationIds.has(mutationKey)) {
        bumpSequence(projection, event);
        return null;
    }
    delete projection.phaseNoteSuggestionsById[payload.suggestionId];
    for (const [id, row] of Object.entries(projection.phaseNoteSuggestionsById)) {
        if (row.noteId === payload.noteId && id !== payload.suggestionId) {
            delete projection.phaseNoteSuggestionsById[id];
        }
    }
    if (mutationKey) {
        projection.appliedSuggestionMutationIds.add(mutationKey);
    }
    bumpSequence(projection, event);
    return null;
}
function applyIdeaCreated(projection, event, payload) {
    const mutationKey = event.clientMutationId?.trim();
    if (mutationKey && projection.appliedIdeaMutationIds.has(mutationKey)) {
        bumpSequence(projection, event);
        return null;
    }
    if (projection.ideasById[payload.idea.id]) {
        bumpSequence(projection, event);
        return null;
    }
    projection.ideasById[payload.idea.id] = eventSnapshotToWorkflowIdeaRow(payload.idea);
    if (mutationKey) {
        projection.appliedIdeaMutationIds.add(mutationKey);
    }
    bumpSequence(projection, event);
    return null;
}
function applyIdeaUpdated(projection, event, payload) {
    const mutationKey = event.clientMutationId?.trim();
    if (mutationKey && projection.appliedIdeaMutationIds.has(`update:${mutationKey}`)) {
        bumpSequence(projection, event);
        return null;
    }
    if (payload.removed) {
        delete projection.ideasById[payload.idea.id];
    }
    else {
        projection.ideasById[payload.idea.id] = eventSnapshotToWorkflowIdeaRow(payload.idea);
    }
    if (mutationKey) {
        projection.appliedIdeaMutationIds.add(`update:${mutationKey}`);
    }
    bumpSequence(projection, event);
    return null;
}
function applyModuleStateUpdated(projection, event, payload) {
    if (!isModuleStatePlanningSyncAllowed(payload.moduleId)) {
        return {
            code: "replay-conflict",
            message: `moduleId '${payload.moduleId}' is not allowlisted for planning.module_state.updated`,
            eventId: event.eventId
        };
    }
    const mutationKey = event.clientMutationId?.trim();
    if (mutationKey && projection.appliedModuleStateMutationIds.has(mutationKey)) {
        bumpSequence(projection, event);
        return null;
    }
    const currentVersion = projection.moduleStateById[payload.moduleId]?.stateSchemaVersion ?? 0;
    const expected = payload.expectedStateSchemaVersion ?? currentVersion;
    if (currentVersion !== expected) {
        return {
            code: "module-state-schema-version-mismatch",
            message: `expectedStateSchemaVersion ${expected} does not match replayed version ${currentVersion} for ${payload.moduleId}`,
            eventId: event.eventId
        };
    }
    if (payload.removed) {
        delete projection.moduleStateById[payload.moduleId];
    }
    else {
        projection.moduleStateById[payload.moduleId] = {
            moduleId: payload.moduleId,
            stateSchemaVersion: payload.stateSchemaVersion,
            state: { ...payload.state },
            updatedAt: payload.updatedAt
        };
    }
    if (mutationKey) {
        projection.appliedModuleStateMutationIds.add(mutationKey);
    }
    bumpSequence(projection, event);
    return null;
}
function applyWorkspaceStatusUpdated(projection, event, payload) {
    const expected = event.expectedWorkspaceRevision ??
        (projection.workspaceStatus?.workspaceRevision ?? 0);
    const currentRevision = projection.workspaceStatus?.workspaceRevision ?? 0;
    if (currentRevision !== expected) {
        return {
            code: "workspace-revision-mismatch",
            message: `expectedWorkspaceRevision ${expected} does not match replayed revision ${currentRevision}`,
            eventId: event.eventId
        };
    }
    const mutationKey = event.clientMutationId?.trim();
    if (mutationKey && projection.appliedWorkspaceMutationIds.has(mutationKey)) {
        bumpSequence(projection, event);
        return null;
    }
    const beforeRevision = currentRevision;
    const after = payload.after;
    if (after.workspaceRevision !== beforeRevision + 1) {
        return {
            code: "replay-conflict",
            message: `after.workspaceRevision ${after.workspaceRevision} is not beforeRevision + 1`,
            eventId: event.eventId
        };
    }
    const next = {
        workspaceRevision: after.workspaceRevision,
        currentKitPhase: after.currentKitPhase,
        nextKitPhase: after.nextKitPhase,
        activeFocus: after.activeFocus,
        lastUpdated: after.lastUpdated,
        blockers: [...after.blockers],
        pendingDecisions: [...after.pendingDecisions],
        nextAgentActions: [...after.nextAgentActions],
        updatedAt: after.updatedAt
    };
    projection.workspaceStatus = next;
    const rolloverPhase = payload.previousCurrentKitPhase !== undefined && payload.previousCurrentKitPhase !== null
        ? String(payload.previousCurrentKitPhase).trim()
        : "";
    const eventKind = rolloverPhase.length > 0 || event.command.name === "set-current-phase"
        ? "set_current_phase"
        : event.command.name;
    const details = JSON.stringify({
        patchKeys: Object.keys(payload.patch),
        clientMutationId: mutationKey ?? undefined,
        payloadDigest: payload.payloadDigest,
        ...(rolloverPhase.length > 0 ? { previousCurrentKitPhase: rolloverPhase } : {})
    });
    projection.workspaceStatusAudits.push({
        eventKind,
        actor: event.actor?.id ?? null,
        command: event.command.name,
        revisionBefore: beforeRevision,
        revisionAfter: after.workspaceRevision,
        detailsJson: details,
        createdAt: event.recordedAt,
        clientMutationId: mutationKey
    });
    if (mutationKey) {
        projection.appliedWorkspaceMutationIds.add(mutationKey);
    }
    bumpSequence(projection, event);
    return null;
}
export function applyPlanningStateEvent(projection, event, options) {
    if (options?.enabledDomains) {
        const domain = planningEventKindToSyncDomain(event.kind);
        if (!options.enabledDomains.has(domain)) {
            return { ok: true, projection };
        }
    }
    const next = {
        ...projection,
        phaseCatalogByKey: { ...projection.phaseCatalogByKey },
        phaseNotesById: Object.fromEntries(Object.entries(projection.phaseNotesById).map(([id, note]) => [id, cloneNoteRow(note)])),
        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)
    };
    let err = null;
    switch (event.kind) {
        case "planning.phase_catalog.upserted":
            err = applyCatalogUpsert(next, event, event.payload);
            break;
        case "planning.phase_catalog.removed":
            err = applyCatalogRemoved(next, event, event.payload);
            break;
        case "planning.workspace_status.updated":
            err = applyWorkspaceStatusUpdated(next, event, event.payload);
            break;
        case "planning.phase_note.created":
            err = applyPhaseNoteCreated(next, event, event.payload);
            break;
        case "planning.phase_note.updated":
            err = applyPhaseNoteUpdated(next, event, event.payload);
            break;
        case "planning.phase_note.archived":
            err = applyPhaseNoteArchived(next, event, event.payload);
            break;
        case "planning.phase_note_suggestion.created":
            err = applySuggestionCreated(next, event, event.payload);
            break;
        case "planning.phase_note_suggestion.updated":
            err = applySuggestionUpdated(next, event, event.payload);
            break;
        case "planning.phase_note_suggestion.removed":
            err = applySuggestionRemoved(next, event, event.payload);
            break;
        case "planning.idea.created":
            err = applyIdeaCreated(next, event, event.payload);
            break;
        case "planning.idea.updated":
            err = applyIdeaUpdated(next, event, event.payload);
            break;
        case "planning.module_state.updated":
            err = applyModuleStateUpdated(next, event, event.payload);
            break;
        default:
            err = {
                code: "replay-conflict",
                message: `unknown planning kind ${event.kind}`,
                eventId: event.eventId
            };
    }
    if (err) {
        return { ok: false, error: err };
    }
    return { ok: true, projection: next };
}
export function replayPlanningStateEvents(events, options) {
    let projection = createEmptyPlanningStateProjection();
    for (const event of events) {
        const applied = applyPlanningStateEvent(projection, event, options);
        if (!applied.ok) {
            return applied;
        }
        projection = applied.projection;
    }
    return { ok: true, projection };
}
