// tools/github-release-handler.ts — Handlers for create_release and generate_changelog.
// SPEC-729: Release publish step wrapped with withEscalation for bounded retries.
import { createPlatformClient, getRemoteUrl } from '../engine/github/client.js';
import { detectAuth } from '../engine/github/auth.js';
import { fetchMergedPRs, formatChangelog, formatChangelogJson, } from '../engine/github/changelog.js';
import { execFile as nodeExecFile } from 'node:child_process';
import { readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { withEscalation } from '../engine/escalator/with-escalation.js';
const defaultExec = async (cmd, args) => {
    return promisify(nodeExecFile)(cmd, args, { maxBuffer: 10 * 1024 * 1024 });
};
// ---------------------------------------------------------------------------
// SPEC-774: Stuck-spec gate
// ---------------------------------------------------------------------------
/**
 * Returns a ToolResult error if stuck specs are detected and no bypass flag
 * is set, or null if release may proceed. Handles autoFixDrift by closing
 * specs and forceRelease by logging an audit entry.
 *
 * Exported for use in the `stuckSpecGateResult` field of integration tests.
 */
export async function runStuckSpecGate(forceRelease, autoFixDrift) {
    let stuckSpecs;
    try {
        const { detectStuckSpecs } = await import('../engine/release/stuck-spec-detector.js');
        stuckSpecs = await detectStuckSpecs(process.cwd());
    }
    catch {
        // If detection fails (e.g., no git, no project), skip gate silently
        return null;
    }
    if (stuckSpecs.length === 0) {
        return null;
    }
    if (!forceRelease && !autoFixDrift) {
        const list = stuckSpecs
            .map((s) => `  • ${s.id}: "${s.title}" (branch: ${s.branch}, status: ${s.status})`)
            .join('\n');
        return {
            content: [
                {
                    type: 'text',
                    text: `Cannot release: ${String(stuckSpecs.length)} spec(s) are stuck ` +
                        `(code merged but status not done).\n${list}\n\n` +
                        `Run update_status(done, specId=SPEC-XXX) for each before releasing, ` +
                        `or pass autoFixDrift: true to auto-close.`,
                },
            ],
            isError: true,
        };
    }
    if (autoFixDrift) {
        const { autoCloseStuckSpec } = await import('../engine/release/stuck-spec-detector.js');
        await Promise.all(stuckSpecs.map((s) => autoCloseStuckSpec(s, process.cwd())));
    }
    else if (forceRelease) {
        const { logForceReleaseAudit } = await import('../engine/release/stuck-spec-detector.js');
        void logForceReleaseAudit(stuckSpecs.map((s) => s.id), process.cwd());
    }
    // Store stuck specs on the module-level so release notes can pick them up
    currentStuckSpecs = stuckSpecs;
    currentForceRelease = forceRelease ?? false;
    currentAutoFixDrift = autoFixDrift ?? false;
    return null;
}
// Module-level state for stuck specs detected during the current release call
// (used to inject ## Status Drift section in release notes)
let currentStuckSpecs = [];
let currentForceRelease = false;
let currentAutoFixDrift = false;
/**
 * SPEC-545: Pre-flight check — stage planu/ and abort if non-planu changes are uncommitted.
 * Returns an error message string if the repo is dirty, or null if clean/staged.
 */
async function runReleasePreflight(exec) {
    try {
        const cwd = process.cwd();
        // Stage all Planu-managed files (spec.md, session-context.md, status.json, etc.)
        try {
            await exec('git', ['add', 'planu/']);
        }
        catch {
            // planu/ may not exist — continue to status check
        }
        const { stdout } = await exec('git', ['status', '--porcelain']);
        const nonPlanuLines = stdout
            .trim()
            .split('\n')
            .filter((line) => line.trim().length > 0 && !line.includes('planu/'));
        if (nonPlanuLines.length > 0) {
            return (`Release aborted: uncommitted changes detected outside planu/ in ${cwd}.\n` +
                `Files:\n${nonPlanuLines.map((l) => `  ${l}`).join('\n')}\n` +
                `Commit or stash them before creating a release.`);
        }
        return null;
    }
    catch {
        // Not a git repo or git unavailable — skip pre-flight silently
        return null;
    }
}
export async function handleCreateRelease(args, exec = defaultExec) {
    // SPEC-774: Detect specs whose branches are merged but status is still approved/implementing
    const stuckGateResult = await runStuckSpecGate(args.forceRelease, args.autoFixDrift);
    if (stuckGateResult !== null) {
        return stuckGateResult;
    }
    // SPEC-545: Ensure repo is clean before creating a release
    const preflightError = await runReleasePreflight(exec);
    if (preflightError !== null) {
        return { content: [{ type: 'text', text: preflightError }], isError: true };
    }
    const auth = await detectAuth(exec);
    if (auth.method === 'none') {
        return {
            content: [{ type: 'text', text: `GitHub auth required.\n${auth.message ?? ''}` }],
            isError: true,
        };
    }
    const remoteUrl = await getRemoteUrl(exec);
    const client = createPlatformClient(remoteUrl, exec);
    const result = await client.createRelease({
        version: args.version,
        generateNotes: args.generateNotes,
        draft: args.draft,
        prerelease: args.prerelease,
    });
    // SPEC-496: Regenerate session context on release
    try {
        const { generateSessionContext } = await import('../engine/session-context-generator.js');
        const releasePath = process.cwd();
        const { resolveProjectIdOrAutoDetect } = await import('./resolve-project-id.js');
        const resolved = await resolveProjectIdOrAutoDetect({ projectPath: releasePath });
        if (resolved.ok) {
            void generateSessionContext(releasePath, resolved.projectId).catch(() => {
                /* best-effort */
            });
        }
    }
    catch {
        /* best-effort */
    }
    // SPEC-585: Autopush release commit + tag if configured (best-effort)
    let autopushLine = null;
    try {
        const releasePath = process.cwd();
        const fsPromises = await import('node:fs/promises');
        const nodePath = await import('node:path');
        const { readPlanuConfig } = await import('../engine/planu-config-writer.js');
        const configPath = nodePath.join(releasePath, 'planu', 'config.json');
        const planuConfig = await readPlanuConfig(configPath, fsPromises.readFile);
        if (planuConfig?.autopush === true) {
            const { safeAutopush } = await import('../engine/session-safeguard/autopush.js');
            const branches = planuConfig.autopushBranches ?? ['feat/*', 'fix/*', 'chore/*'];
            const pushResult = await safeAutopush(releasePath, branches);
            if (pushResult.pushed) {
                autopushLine = `  Autopush: ${String(pushResult.commitsPushed)} commit(s) pushed to ${pushResult.ref}`;
                // Also push the tag
                try {
                    await exec('git', ['push', 'origin', result.tagName]);
                    autopushLine += ` (tag ${result.tagName} pushed)`;
                }
                catch {
                    /* best-effort tag push */
                }
            }
            else {
                autopushLine = `  Autopush: skipped (${pushResult.reason})`;
            }
        }
    }
    catch {
        /* best-effort — autopush never blocks release */
    }
    const outputLines = [
        `Release created successfully!`,
        `  Tag: ${result.tagName}`,
        `  URL: ${result.url}`,
        `  Draft: ${String(args.draft)}`,
        `  Prerelease: ${String(args.prerelease)}`,
    ];
    if (autopushLine) {
        outputLines.push(autopushLine);
    }
    // SPEC-774: Append Status Drift or auto-close summary if stuck specs were detected
    if (currentStuckSpecs.length > 0) {
        if (currentAutoFixDrift) {
            const ids = currentStuckSpecs.map((s) => s.id).join(', ');
            outputLines.push(`  Auto-closed ${String(currentStuckSpecs.length)} stuck specs: ${ids}`);
        }
        else if (currentForceRelease) {
            outputLines.push('');
            outputLines.push('## Status Drift');
            outputLines.push('The following specs were stuck (merged branch, non-done status) and bypassed:');
            for (const s of currentStuckSpecs) {
                outputLines.push(`  • ${s.id}: "${s.title}" (branch: ${s.branch})`);
            }
        }
        // Reset module-level state after use
        currentStuckSpecs = [];
        currentForceRelease = false;
        currentAutoFixDrift = false;
    }
    // SPEC-751: Auto-housekeeping after successful release (best-effort, non-blocking)
    try {
        const releasePath = process.cwd();
        const { runHousekeepingSweep } = await import('../engine/housekeeping/index.js');
        const sweepReport = await runHousekeepingSweep({
            projectPath: releasePath,
            dryRun: false,
            aggressive: false,
            includeStashes: false,
        });
        if (sweepReport.deleted.branches > 0 || sweepReport.deleted.worktrees > 0) {
            outputLines.push(`  Housekeeping: deleted ${String(sweepReport.deleted.branches)} branch(es), ` +
                `${String(sweepReport.deleted.worktrees)} worktree(s)`);
        }
    }
    catch {
        /* housekeeping is best-effort — never block release output */
    }
    // SPEC-776: Fire release_completed cascade so statusJson + sessionContext hooks regenerate
    void (async () => {
        try {
            const releasePath = process.cwd();
            const { resolveProjectIdOrAutoDetect } = await import('./resolve-project-id.js');
            const resolved = await resolveProjectIdOrAutoDetect({ projectPath: releasePath });
            if (!resolved.ok) {
                return;
            }
            const { specStore } = await import('../storage/index.js');
            const { runCascade } = await import('../engine/cascade-hooks/runner.js');
            const allSpecs = await specStore.listSpecs(resolved.projectId);
            const dummySpec = allSpecs[0];
            if (!dummySpec) {
                return;
            }
            await runCascade({
                specId: result.tagName,
                spec: dummySpec,
                newStatus: 'done',
                currentStatus: 'done',
                projectId: resolved.projectId,
                projectPath: releasePath,
                validateScore: null,
                allSpecs,
            }, { disabledByConfig: new Set(), disabledByEnv: new Set() });
            // SPEC-790: Auto-trigger housekeeping_sweep after release to clean branches/worktrees
            try {
                const { runHousekeepingSweep } = await import('../engine/housekeeping/index.js');
                void runHousekeepingSweep({ projectPath: releasePath, dryRun: false });
            }
            catch {
                /* best-effort — housekeeping never blocks the release */
            }
        }
        catch {
            /* best-effort — release_completed cascade never blocks the release response */
        }
    })();
    // SPEC-1003: Auto-merge develop to main after release (best-effort)
    void (async () => {
        try {
            const releasePath = process.cwd();
            const run = promisify(nodeExecFile);
            const { stdout: currentBranch } = await run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
                cwd: releasePath,
            });
            if (currentBranch.trim() === 'develop') {
                await run('git', ['checkout', 'main'], { cwd: releasePath });
                await run('git', ['merge', 'develop', '--no-edit'], { cwd: releasePath });
                await run('git', ['push', '--no-verify', 'origin', 'main'], { cwd: releasePath });
                await run('git', ['checkout', 'develop'], { cwd: releasePath });
                outputLines.push('  Auto-merge: develop → main');
            }
        }
        catch {
            /* best-effort — auto-merge never blocks release */
        }
    })();
    return {
        content: [
            {
                type: 'text',
                text: outputLines.join('\n'),
            },
        ],
    };
}
export async function handleGenerateChangelog(args, exec = defaultExec) {
    const entries = await fetchMergedPRs(args.since, args.until, exec);
    if (entries.length === 0) {
        return {
            content: [{ type: 'text', text: `No merged PRs found since ${args.since}.` }],
        };
    }
    const output = args.format === 'json'
        ? formatChangelogJson(entries)
        : formatChangelog(entries, args.since, args.until);
    // SPEC-545: Optionally write/prepend to CHANGELOG.md
    if (args.writeToFile === true && args.format !== 'json') {
        try {
            const changelogPath = join(process.cwd(), 'CHANGELOG.md');
            let existing = '';
            try {
                existing = await readFile(changelogPath, 'utf-8');
            }
            catch {
                // File doesn't exist yet — create it
            }
            await writeFile(changelogPath, `${output}\n\n${existing}`.trimEnd() + '\n', 'utf-8');
            return {
                content: [{ type: 'text', text: `${output}\n\n✅ Written to CHANGELOG.md` }],
            };
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            return {
                content: [
                    { type: 'text', text: `${output}\n\n⚠️ Could not write to CHANGELOG.md: ${msg}` },
                ],
            };
        }
    }
    return {
        content: [{ type: 'text', text: output }],
    };
}
// ---------------------------------------------------------------------------
// SPEC-729: Release publish wrapped with withEscalation (bounded retries)
// ---------------------------------------------------------------------------
/**
 * SPEC-729: Run the GitHub release creation step with bounded retry logic.
 *
 * Wraps the publish step in `withEscalation('releaser', ...)` so that repeated
 * publish failures escalate to a human decision (abort is the default for Releaser).
 *
 * @param args        - Release creation arguments.
 * @param projectId   - Project identifier for retry counter persistence.
 * @param specId      - Spec identifier for retry counter keying.
 * @param frontmatterSha - Frontmatter SHA for retry counter keying.
 * @param exec        - Optional exec function (injectable for testing).
 */
export async function handleCreateReleaseWithEscalation(args, projectId, specId, frontmatterSha, exec = defaultExec) {
    return withEscalation({ role: 'releaser', specId, frontmatterSha, projectId }, async () => {
        const result = await handleCreateRelease(args, exec);
        if (result.isError === true) {
            const errorText = result.content[0]?.type === 'text' ? result.content[0].text : 'Release failed';
            return { ok: false, reason: errorText };
        }
        return { ok: true, result };
    });
}
//# sourceMappingURL=github-release-handler.js.map