/**
 * Interactive `/proposal` command flow. Surfaces pending skill proposals,
 * lets the user open a proposal folder in their editor, then offers
 * Create-or-Replace / Save as Version / Reject / Close. Filesystem mutations are
 * delegated to `app/skill-proposals/review.ts`; this module only orchestrates
 * UI and reload-after-apply.
 */
import stripAnsi from 'strip-ansi';
import { PROPOSAL_KIND, countSkillProposals, listSkillProposals } from '../../app/skill-proposals/catalog.js';
import { getSkillProposalRoots, proposalDir } from '../../app/skill-proposals/proposal-fs.js';
import { readSkillProposalReviewSnapshot, rejectSkillProposal, replaceSkillWithProposal, saveProposalAsVersion } from '../../app/skill-proposals/review.js';
import { emitSkillProposalStatus } from '../../app/skill-proposals/status.js';
import { openInEditor } from '../../shared/platform/open.js';
import { Spacer, Text } from '../../tui/index.js';
import { DynamicBorder } from '../components/primitives/dynamic-border.js';
import { theme } from '../theme/runtime.js';
const PROPOSAL_REVIEW_ACTION = {
    APPLY_UNSUFFIXED: 'replace',
    SAVE_VERSION: 'version',
    REJECT: 'reject',
    CLOSE: 'close'
};
export class ProposalsWorkflow {
    deps;
    roots;
    /** Wire the workflow to its UI dependencies and resolve the proposal roots. */
    constructor(deps) {
        this.deps = deps;
        this.roots = deps.roots ?? getSkillProposalRoots();
    }
    /** Entry point for `/proposal` and `/proposal scan ...`; lists, scans, or reviews proposals. */
    async handleProposalsCommand(args = '') {
        const trimmedArgs = args.trim();
        const [subcommand, ...rest] = trimmedArgs.split(/\s+/);
        if (subcommand === 'scan') {
            if (!this.deps.scanProposals) {
                this.deps.showWarning('Skill proposal scan is unavailable in this shell.');
                return;
            }
            const manualRequest = rest.join(' ').trim() || undefined;
            const started = await this.deps.scanProposals(manualRequest);
            if (started === false) {
                this.deps.showWarning('Skill proposal scan could not start because no model is selected.');
            }
            else {
                this.deps.showStatus('Skill proposal scan started.');
            }
            return;
        }
        if (trimmedArgs.length > 0) {
            this.deps.showWarning('Usage: /proposal [scan [request]]');
            return;
        }
        let proposals;
        try {
            proposals = listSkillProposals(this.roots);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            this.deps.showWarning(`Could not list skill proposals: ${message}`);
            return;
        }
        if (proposals.length === 0) {
            this.deps.showStatus('No skill proposals.');
            return;
        }
        const selected = await this.deps.selectOption('Skill Proposals', proposals.map((proposal) => ({
            value: proposal.name,
            label: proposal.name,
            description: proposal.kind === PROPOSAL_KIND.CREATE ? 'Create proposal' : 'Update proposal'
        })));
        if (!selected)
            return;
        await this.reviewProposal(selected);
    }
    /** Open one proposal in an editor and prompt the user to replace, version, reject, or close. */
    async reviewProposal(name) {
        let failurePrefix = 'Proposal review failed';
        try {
            const folder = proposalDir(this.roots, name);
            const snapshot = readSkillProposalReviewSnapshot(this.roots, name);
            // Opening the folder in an editor is a convenience; if it fails (no editor,
            // headless terminal, sandboxed env) we still want the user to be able to
            // review the rationale and apply or reject the proposal.
            await this.openProposalFolder(folder);
            this.renderProposalReview(name, folder, snapshot.kind, snapshot.rationale);
            const action = (await this.deps.selectOption(`Review "${name}"`, [
                {
                    value: PROPOSAL_REVIEW_ACTION.APPLY_UNSUFFIXED,
                    label: snapshot.kind === PROPOSAL_KIND.CREATE ? 'Create' : 'Replace',
                    description: snapshot.kind === PROPOSAL_KIND.CREATE
                        ? 'Install as the unsuffixed live skill'
                        : 'Replace the unsuffixed live skill'
                },
                {
                    value: PROPOSAL_REVIEW_ACTION.SAVE_VERSION,
                    label: 'Save as Version',
                    description: 'Install as the next available suffixed skill'
                },
                {
                    value: PROPOSAL_REVIEW_ACTION.REJECT,
                    label: 'Reject',
                    description: 'Delete the proposal only'
                },
                {
                    value: PROPOSAL_REVIEW_ACTION.CLOSE,
                    label: 'Close',
                    description: 'Make no filesystem changes'
                }
            ]));
            if (!action || action === PROPOSAL_REVIEW_ACTION.CLOSE) {
                this.deps.showStatus('Closed proposal reviewer without applying changes.');
                return;
            }
            if (action === PROPOSAL_REVIEW_ACTION.APPLY_UNSUFFIXED) {
                failurePrefix = 'Proposal apply failed';
                await replaceSkillWithProposal(this.roots, name);
                await this.reloadSkillsAfterApply(`Applied proposal "${name}".`);
                this.deps.showStatus(`Applied proposal "${name}".`);
            }
            else if (action === PROPOSAL_REVIEW_ACTION.SAVE_VERSION) {
                failurePrefix = 'Proposal apply failed';
                const versionName = await saveProposalAsVersion(this.roots, name);
                await this.reloadSkillsAfterApply(`Saved proposal "${name}" as "${versionName}".`);
                this.deps.showStatus(`Saved proposal "${name}" as "${versionName}".`);
            }
            else if (action === PROPOSAL_REVIEW_ACTION.REJECT) {
                failurePrefix = 'Proposal reject failed';
                await rejectSkillProposal(this.roots, name);
                this.deps.showStatus(`Rejected proposal "${name}".`);
            }
            emitSkillProposalStatus({ phase: 'checked', count: countSkillProposals(this.roots) });
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            this.deps.showWarning(`${failurePrefix}: ${sanitizeDisplayText(message, 240)}`);
        }
        finally {
            this.deps.requestRender();
        }
    }
    /** Trigger a skills reload after a successful apply, surfacing any reload errors. */
    async reloadSkillsAfterApply(successMessage) {
        try {
            await this.deps.reloadSkills();
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            this.deps.showWarning(`${successMessage} Skill reload failed: ${sanitizeDisplayText(message, 180)}`);
        }
    }
    /** Render the proposal review summary block (name, kind, rationale) into the chat container. */
    renderProposalReview(name, folder, kind, rationale) {
        const lines = [];
        lines.push(`${theme.bold(name)} (${kind})`);
        lines.push(`Folder: ${folder}`);
        lines.push('');
        lines.push(theme.bold('Rationale'));
        lines.push(...this.formatRationale(rationale));
        this.deps.chatContainer.addChild(new Spacer(1));
        this.deps.chatContainer.addChild(new DynamicBorder());
        this.deps.chatContainer.addChild(new Text(theme.bold(theme.fg('accent', 'Skill Proposal')), 1, 0));
        this.deps.chatContainer.addChild(new Text(lines.join('\n'), 1, 0));
        this.deps.chatContainer.addChild(new Spacer(1));
        this.deps.chatContainer.addChild(new DynamicBorder());
        this.deps.requestRender();
    }
    /** Render rationale text (or a placeholder) for display in the proposal review block. */
    formatRationale(rationale) {
        const trimmed = rationale?.trim();
        if (!trimmed)
            return [theme.fg('dim', 'No RATIONALE.md in this proposal.')];
        return [
            sanitizeRationale(trimmed),
            theme.fg('dim', 'Full rationale is available in RATIONALE.md in the opened folder.')
        ];
    }
    /** Open the proposal folder in the user's editor; report a warning instead of failing the flow. */
    async openProposalFolder(path) {
        try {
            await Promise.resolve(this.deps.openProposalFolder ? this.deps.openProposalFolder(path) : openInEditor(path));
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            this.deps.showWarning(`Could not open proposal folder in editor (${message}); review actions are still available below.`);
        }
    }
}
/** Collapse a rationale block into a single inline line, dropping diff noise and truncating to 500 chars. */
function sanitizeRationale(rationale) {
    const lines = rationale
        .replace(/\r\n/g, '\n')
        .replace(/\r/g, '\n')
        .split('\n')
        .map((line) => line.trim())
        .filter((line) => line.length > 0)
        .filter((line) => !isDiffNoiseLine(line));
    const text = lines.join(' ');
    return sanitizeDisplayText(text, 500);
}
/** Strip ANSI/control characters and ellipsize `value` to at most `maxChars`. */
function sanitizeDisplayText(value, maxChars) {
    const stripped = stripAnsi(value)
        .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, '')
        .replace(/\s+/g, ' ')
        .trim();
    return stripped.length <= maxChars ? stripped : `${stripped.slice(0, maxChars - 3)}...`;
}
/** True if the line looks like diff scaffolding (headers, hunks, or removed lines) we want to drop. */
function isDiffNoiseLine(line) {
    const normalized = line.replace(/^>\s*/, '');
    return (normalized.startsWith('diff --git ') ||
        normalized.startsWith('@@ ') ||
        normalized.startsWith('--- ') ||
        normalized.startsWith('+++ ') ||
        normalized.startsWith('+') ||
        /^-\s*(removed|deleted|old)\b/i.test(normalized) ||
        /^-[^\s]/.test(normalized));
}
//# sourceMappingURL=proposals-workflow.js.map