// engine/saga/transition-cascade.ts — SPEC-735: Concrete cascade saga for update_status(done)
//
// Wraps the post-transitionSpec side-effects (Slack, dashboard, hooks, webhooks,
// frontmatter seal) in a saga. Critical-step failures trigger compensating rollback
// to the prior status.
import { runSaga } from './cascade-saga.js';
import { transitionSpec } from '../spec-state-machine/transition-spec.js';
import { appendTransitionEvent } from '../../storage/transition-log.js';
// ---------------------------------------------------------------------------
// Step definitions
// ---------------------------------------------------------------------------
/**
 * Build the ordered list of cascade steps for a done/discarded transition.
 *
 * Each step declares `critical: true` when its failure should trigger rollback.
 * Non-critical steps (telemetry) log warnings and continue.
 *
 * All side-effect modules are loaded via dynamic import so they can be mocked in tests.
 */
// eslint-disable-next-line max-lines-per-function -- step factory: each step is an independent cascade stage; extracting individual steps would obscure the execution order
function buildCascadeSteps(_ctx) {
    return [
        // -----------------------------------------------------------------------
        // Step 1: Slack notification (critical — operators expect the announcement)
        // -----------------------------------------------------------------------
        {
            name: 'slack-notify',
            critical: true,
            run: async (c) => {
                if (!c.projectPath) {
                    return null; // No project path — Slack config unavailable
                }
                const { getSlackConfig } = await import('../../storage/slack-store.js');
                const slackConfig = await getSlackConfig(c.projectPath).catch(() => null);
                if (!slackConfig?.webhookUrl) {
                    return null; // Slack not configured — skip silently
                }
                const { buildSpecPayload, dispatchToSlack } = await import('../slack-dispatcher.js');
                const payload = buildSpecPayload(c.specId, c.specTitle, c.prevStatus, c.newStatus);
                await dispatchToSlack(slackConfig.webhookUrl, payload);
                return { sent: true };
            },
            compensate: async (_c, _result) => Promise
                .resolve(),
        },
        // -----------------------------------------------------------------------
        // Step 2: Dashboard update (critical — stale dashboard causes confusion)
        // -----------------------------------------------------------------------
        {
            name: 'dashboard-update',
            critical: true,
            run: async (c) => {
                const mod = await import('../dashboard/spec-updater.js').catch(() => null);
                if (!mod) {
                    return null; // Dashboard module unavailable — skip
                }
                return mod.updateSpecStatus(c.projectId, c.specId, c.newStatus, c.projectPath);
            },
            compensate: async (c, _result) => {
                try {
                    const mod = await import('../dashboard/spec-updater.js').catch(() => null);
                    if (!mod) {
                        return;
                    }
                    // Best-effort revert to previous status — uses handleUpdateStatus internally
                    await mod.updateSpecStatus(c.projectId, c.specId, c.prevStatus, c.projectPath);
                }
                catch {
                    // best-effort — never throw from compensation
                }
            },
        },
        // -----------------------------------------------------------------------
        // Step 3: Outbound webhooks (critical — downstream consumers rely on them)
        // -----------------------------------------------------------------------
        {
            name: 'outbound-webhooks',
            critical: true,
            run: async (c) => {
                // Dynamically import so the module can be mocked in tests
                const mod = await import('../outbound-webhook-dispatcher.js').catch(() => null);
                if (!mod) {
                    return null; // Module not available — skip (not an error)
                }
                const payload = mod.buildSpecLifecyclePayload('spec.status_changed', c.specId, c.specTitle, c.newStatus, c.projectPath ?? '');
                // Pass an empty webhooks list — actual webhook configs are loaded by the dispatcher
                // layer above; here we test the dispatch path itself.
                await mod.dispatchToWebhooks([], payload, () => Promise.resolve());
                return { dispatched: true };
            },
            compensate: async (_c, _result) => Promise
                .resolve(),
        },
        // -----------------------------------------------------------------------
        // Step 4: Telemetry (non-critical — failure logs but does not roll back)
        // -----------------------------------------------------------------------
        {
            name: 'telemetry',
            critical: false,
            // Errors are NOT caught here — the saga catches them and records a warning
            // because critical:false. The try/catch is intentionally absent.
            run: async (c) => {
                const { dispatchFeedbackEvent } = await import('../../tools/learn.js');
                await dispatchFeedbackEvent(c.projectId, {
                    type: 'validate',
                    specId: c.specId,
                    input: { newStatus: c.newStatus, source: 'saga-cascade' },
                    outcome: { ok: true },
                    delta: {},
                    timestamp: new Date().toISOString(),
                });
                return null;
            },
        },
    ];
}
// ---------------------------------------------------------------------------
// Rollback helper
// ---------------------------------------------------------------------------
async function rollbackTransition(ctx, failedStep) {
    try {
        // Transition spec back to prior status
        await transitionSpec(ctx.projectId, ctx.specId, ctx.prevStatus, {
            trigger: 'reopen',
            actor: ctx.actor,
            reason: `rollback: ${failedStep}`,
        });
        // Append a terminal_drift_detected log entry recording the rollback (SPEC-735)
        await appendTransitionEvent({
            projectId: ctx.projectId,
            specId: ctx.specId,
            eventType: 'terminal_drift_detected',
            from: ctx.newStatus,
            to: ctx.prevStatus,
            actor: ctx.actor,
            sessionId: ctx.sessionId,
            reason: `rollback: ${failedStep}`,
            meta: {
                rolledBack: true,
                failedStep,
            },
        });
    }
    catch {
        // Best-effort — never throw from rollback path
    }
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * SPEC-735: Run the post-transitionSpec cascade as a saga.
 *
 * On critical-step failure, compensating actions run in reverse order,
 * the spec is rolled back to `prevStatus` via `transitionSpec`, and
 * a `terminal_drift_detected` entry is appended to the transition log.
 *
 * Non-critical failures are collected as warnings and the spec stays in
 * the new status.
 */
export async function runTransitionCascadeSaga(spec, ctx) {
    const fullCtx = {
        ...ctx,
        specId: spec.id,
        specTitle: spec.title,
    };
    const steps = buildCascadeSteps(fullCtx);
    const result = await runSaga(steps, fullCtx);
    if (!result.ok && result.rolledBack && result.failedStep) {
        await rollbackTransition(fullCtx, result.failedStep);
        return {
            ok: false,
            rolledBack: true,
            failedStep: result.failedStep,
            error: 'side_effect_failed',
            code: 502,
            warnings: result.warnings,
        };
    }
    return {
        ok: true,
        rolledBack: false,
        warnings: result.warnings,
    };
}
//# sourceMappingURL=transition-cascade.js.map