/**
 * COM execute_code executor — local XLL-backed execution lifecycle.
 *
 * Owns the COM-specific execution flow:
 * 1. Send code to the local XLL via xllFetch
 * 2. Parse and validate the response
 * 3. Run the approval / revert / cleanup flow
 *
 * Used by:
 * - `createExcelExecTool()` (COM ToolDefinition wrapper)
 */
import { log } from '../../../../shared/logging/agent-log.js';
import { APPROVAL } from '../../../approvals/types.js';
import { getApprovalSurface, resolveApprovalExecutionDecision } from '../../../permissions/approval-surface.js';
import { getEffectiveExcelHttpUrl } from '../../../xll/endpoint.js';
import { xllFetch } from '../../../xll/session.js';
import { getEditModeAbortMessage, isEditModeAbortError, resolveEditModeRetry } from './edit-mode-retry.js';
import { analyzeComWriteCode } from './write-analyzer.js';
class ExcelShapeError extends Error {
}
// ---------------------------------------------------------------------------
// Validation
// ---------------------------------------------------------------------------
/** Validate that a parsed JSON value has the COM execute result shape. */
export function isComExecResult(v) {
    if (typeof v !== 'object' || v === null || !('ok' in v))
        return false;
    const r = v;
    return (typeof r.ok === 'boolean' &&
        (r.output === undefined || typeof r.output === 'string') &&
        (r.error === undefined || typeof r.error === 'string') &&
        (r.editMode === undefined || typeof r.editMode === 'boolean'));
}
// ---------------------------------------------------------------------------
// Control constants
// ---------------------------------------------------------------------------
const CONTROL = {
    REVERT: 'revert',
    CLEANUP: 'cleanup'
};
function describeExcelApprovalChoice(choice) {
    switch (choice) {
        case APPROVAL.ACCEPT:
            return 'User approved this Excel operation.';
        case APPROVAL.ACCEPT_ALL:
            return 'User enabled session auto-approve for Excel.';
        default:
            return undefined;
    }
}
function appendApprovalSummary(approvalSummaries, choice) {
    const summary = describeExcelApprovalChoice(choice);
    if (!summary) {
        return approvalSummaries;
    }
    return [...(approvalSummaries ?? []), summary];
}
function isDangerousExcelLifecycleBypassed(ctx) {
    return ctx?.permissionPolicy?.dangerouslySkipRuntimePermissions === true;
}
async function requestAlwaysConfirmApproval(approval, ctx, dangerousOperations, observability) {
    if (isDangerousExcelLifecycleBypassed(ctx)) {
        return APPROVAL.ACCEPT;
    }
    const request = {
        kind: 'execute_code',
        title: 'Confirm Excel operation',
        alwaysRequireExplicitApproval: true,
        payload: {
            kind: 'dangerous',
            title: 'Confirm Excel operation',
            summary: 'This action can close Excel or close a workbook, so it always requires approval.',
            operations: dangerousOperations
        }
    };
    const decision = resolveApprovalExecutionDecision({
        surface: getApprovalSurface(ctx),
        gateAvailable: approval !== undefined,
        isSafe: false,
        isAutoApproved: approval?.isAutoApproved() ?? false,
        alwaysRequireExplicitApproval: true,
        allowHeadlessApproval: ctx?.permissionPolicy?.spreadsheet.allowHeadlessApproval ?? false
    });
    if (decision !== 'prompt' || !approval || !ctx) {
        observability?.onRejected?.(request);
        return APPROVAL.REJECT;
    }
    observability?.onRequested?.(request);
    const choice = await approval.requestApproval(request, ctx);
    if (choice === APPROVAL.REJECT || choice === undefined) {
        observability?.onRejected?.(request);
        return choice;
    }
    observability?.onAccepted?.(request, choice);
    return choice;
}
// ---------------------------------------------------------------------------
// Approval resolution
// ---------------------------------------------------------------------------
/**
 * Resolve approval for cell changes: show UI if needed, then revert or cleanup.
 */
export async function resolveApproval(result, approval, postControl, ctx, observability, priorApprovalSummaries) {
    const { diff, cfTxId } = result;
    let approvalSummaries = priorApprovalSummaries;
    if (diff) {
        const request = {
            kind: 'execute_code',
            title: 'Review Excel changes',
            payload: {
                kind: 'diff',
                diff
            }
        };
        const decision = resolveApprovalExecutionDecision({
            surface: getApprovalSurface(ctx),
            gateAvailable: approval !== undefined,
            isSafe: false,
            isAutoApproved: approval?.isAutoApproved() ?? false,
            alwaysRequireExplicitApproval: false,
            allowHeadlessApproval: ctx.permissionPolicy?.spreadsheet.allowHeadlessApproval ?? false
        });
        if (decision === 'prompt') {
            if (!approval) {
                observability?.onRejected?.(request);
                try {
                    await postControl({ control: CONTROL.REVERT, cfTxId });
                    observability?.onReverted?.(request);
                    return { outcome: 'reverted' };
                }
                catch {
                    if (cfTxId != null) {
                        await postControl({ control: CONTROL.CLEANUP, cfTxId }).catch(() => { });
                    }
                    observability?.onRevertFailed?.(request);
                    return { outcome: 'revert-failed' };
                }
            }
            observability?.onRequested?.(request);
            const choice = await approval.requestApproval(request, ctx);
            if (choice === APPROVAL.REJECT || choice === undefined) {
                observability?.onRejected?.(request);
                try {
                    await postControl({ control: CONTROL.REVERT, cfTxId });
                    observability?.onReverted?.(request);
                    return { outcome: 'reverted' };
                }
                catch {
                    if (cfTxId != null) {
                        await postControl({ control: CONTROL.CLEANUP, cfTxId }).catch(() => { });
                    }
                    observability?.onRevertFailed?.(request);
                    return { outcome: 'revert-failed' };
                }
            }
            observability?.onAccepted?.(request, choice);
            if (choice === APPROVAL.ACCEPT_ALL) {
                approval.onAcceptAll();
            }
            approvalSummaries = appendApprovalSummary(approvalSummaries, choice);
        }
        else if (decision === 'deny') {
            observability?.onRejected?.(request);
            try {
                await postControl({ control: CONTROL.REVERT, cfTxId });
                observability?.onReverted?.(request);
                return { outcome: 'reverted' };
            }
            catch {
                if (cfTxId != null) {
                    await postControl({ control: CONTROL.CLEANUP, cfTxId }).catch(() => { });
                }
                observability?.onRevertFailed?.(request);
                return { outcome: 'revert-failed' };
            }
        }
    }
    // Accepted (explicitly or auto) — clean up CF highlights.
    if (cfTxId != null) {
        await postControl({ control: CONTROL.CLEANUP, cfTxId }).catch((e) => {
            const msg = e instanceof Error ? e.message : String(e);
            log.error('excel-exec', `CF cleanup failed for tx ${cfTxId}: ${msg}`);
        });
    }
    return { outcome: 'accepted', approvalSummaries };
}
// ---------------------------------------------------------------------------
// Core executor
// ---------------------------------------------------------------------------
/**
 * Execute code against the local XLL and run approval.
 *
 * This is the reusable core for desktop-local COM execution.
 * The XLL endpoint (`httpUrl`) is always local to the machine running the agent.
 */
export async function executeComLocally(excelHttpUrl, code, options) {
    const resolvedExcelHttpUrl = excelHttpUrl ?? getEffectiveExcelHttpUrl();
    const { approval, ctx, signal, observability, editModeTiming } = options ?? {};
    let approvalSummaries;
    const postControl = (body, sig) => xllFetch(`${resolvedExcelHttpUrl}/exec`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
        signal: sig
    });
    const parseExecResult = async (response) => {
        const body = await response.text();
        try {
            const parsed = JSON.parse(body);
            if (!isComExecResult(parsed)) {
                throw new ExcelShapeError(`Excel returned unexpected shape (HTTP ${response.status}): ${body.slice(0, 200)}`);
            }
            return parsed;
        }
        catch (e) {
            if (e instanceof ExcelShapeError)
                throw e;
            throw new Error(`Excel returned non-JSON (HTTP ${response.status}): ${body.slice(0, 200)}`);
        }
    };
    const sendExecRequest = async () => {
        let response;
        try {
            response = await postControl({ code, autoApprove }, signal);
        }
        catch (e) {
            if (signal?.aborted || isEditModeAbortError(e)) {
                throw new Error(getEditModeAbortMessage());
            }
            const msg = e instanceof Error ? e.message : String(e);
            throw new Error(`Failed to connect to Excel at ${resolvedExcelHttpUrl}: ${msg}`);
        }
        return parseExecResult(response);
    };
    // Static analysis: skip approval for code with no detectable writes.
    const analysis = analyzeComWriteCode(code);
    const dangerousLifecycleBypassed = analysis.alwaysRequireApproval && isDangerousExcelLifecycleBypassed(ctx);
    if (analysis.alwaysRequireApproval) {
        const choice = await requestAlwaysConfirmApproval(approval, ctx, analysis.dangerousOperations, observability);
        if (choice !== APPROVAL.ACCEPT && choice !== APPROVAL.ACCEPT_ALL) {
            return {
                result: { ok: true, output: 'Excel operation rejected by user.' },
                outcome: 'rejected'
            };
        }
        if (choice === APPROVAL.ACCEPT_ALL) {
            approval?.onAcceptAll();
        }
        approvalSummaries = appendApprovalSummary(approvalSummaries, choice);
    }
    const autoApprove = analysis.isReadOnly || analysis.alwaysRequireApproval || (approval?.isAutoApproved() ?? false);
    const executionDecision = dangerousLifecycleBypassed
        ? 'allow'
        : resolveApprovalExecutionDecision({
            surface: getApprovalSurface(ctx),
            gateAvailable: approval !== undefined,
            isSafe: analysis.isReadOnly,
            isAutoApproved: approval?.isAutoApproved() ?? false,
            alwaysRequireExplicitApproval: false,
            allowHeadlessApproval: ctx?.permissionPolicy?.spreadsheet.allowHeadlessApproval ?? false
        });
    if (executionDecision === 'deny') {
        return {
            result: { ok: true, output: 'Excel operation rejected by user.' },
            outcome: 'rejected'
        };
    }
    let result = await sendExecRequest();
    if (result.editMode) {
        result = await resolveEditModeRetry(result, {
            ctx,
            signal,
            send: sendExecRequest,
            timing: editModeTiming
        });
    }
    if (!result.ok) {
        return { result, outcome: 'failed' };
    }
    if (ctx) {
        const { outcome, approvalSummaries: resolvedApprovalSummaries } = await resolveApproval(result, approval, (b) => postControl(b), ctx, observability, approvalSummaries);
        return { result, outcome, approvalSummaries: resolvedApprovalSummaries };
    }
    if (result.diff) {
        try {
            await postControl({ control: CONTROL.REVERT, cfTxId: result.cfTxId });
            return {
                result: { ...result, output: 'Excel operation rejected by user.' },
                outcome: 'reverted'
            };
        }
        catch {
            if (result.cfTxId != null) {
                await postControl({ control: CONTROL.CLEANUP, cfTxId: result.cfTxId }).catch(() => { });
            }
            return {
                result: { ...result, output: 'Excel operation rejected by user.' },
                outcome: 'revert-failed'
            };
        }
    }
    if (result.cfTxId != null) {
        await postControl({ control: CONTROL.CLEANUP, cfTxId: result.cfTxId }).catch((e) => {
            const msg = e instanceof Error ? e.message : String(e);
            log.error('excel-exec', `CF cleanup failed for tx ${result.cfTxId}: ${msg}`);
        });
    }
    return { result, outcome: 'accepted', approvalSummaries };
}
//# sourceMappingURL=executor.js.map