import { randomUUID } from "node:crypto";
import { redactSecretText } from "../utils/redactSecrets.js";
import { extractOpenCodePermissionRequest } from "../runtime/opencode/OpenCodePermissionEventExtractor.js";
export class PermissionRequestConflictError extends Error {
    requestId;
    constructor(requestId) {
        super(`Permission request ${requestId} is already answered.`);
        this.requestId = requestId;
        this.name = "PermissionRequestConflictError";
    }
}
export class PermissionRuntimeReplyError extends Error {
    request;
    constructor(message, request) {
        super(message);
        this.request = request;
        this.name = "PermissionRuntimeReplyError";
    }
}
export class PermissionReplyValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "PermissionReplyValidationError";
    }
}
export class PermissionBridgeService {
    localStore;
    policy;
    runtime;
    audit;
    decisionRegistry;
    constructor(localStore, policy, runtime, audit, decisionRegistry) {
        this.localStore = localStore;
        this.policy = policy;
        this.runtime = runtime;
        this.audit = audit;
        this.decisionRegistry = decisionRegistry;
    }
    async handleRuntimeEvent(rawEvent, storedEvent) {
        const extracted = extractOpenCodePermissionRequest(rawEvent);
        if (!extracted)
            return undefined;
        const run = this.localStore.getRunByRuntimeSessionId(extracted.runtimeSessionId);
        if (!run) {
            this.audit.append({
                type: "permission.request.unmatched_session",
                severity: "warning",
                message: "OpenCode permission request did not match a PatchTrail run.",
                metadata: { runtimeSessionId: extracted.runtimeSessionId },
            });
            return undefined;
        }
        const context = {
            workspaceId: run.workspaceId,
            sessionId: run.id,
        };
        const evaluation = this.policy.evaluate(extracted.toolCall, context);
        const request = createStoredPermissionRequest(extracted.toolCall, evaluation);
        const upsertInput = {
            id: `prq_${randomUUID()}`,
            workspaceId: run.workspaceId,
            runId: run.id,
            provider: "opencode",
            runtimeSessionId: extracted.runtimeSessionId,
            sourceRuntimeEventId: storedEvent.id,
            request,
            evaluation,
        };
        const stored = this.localStore.upsertPermissionRequest({
            ...upsertInput,
            ...(extracted.providerRequestId
                ? { providerRequestId: extracted.providerRequestId }
                : {}),
            ...(extracted.providerPermissionId
                ? { providerPermissionId: extracted.providerPermissionId }
                : {}),
        });
        if (stored.status !== "pending" && stored.status !== "reply_failed")
            return stored;
        if (evaluation.verdict === "allowed") {
            return this.replyToStoredRequest(stored, {
                option: evaluation.decision.option,
                capabilities: evaluation.decision.capabilities,
            });
        }
        if (evaluation.verdict === "blocked") {
            return this.replyToStoredRequest(stored, {
                option: "deny",
                capabilities: ["local_tools"],
                message: evaluation.reason,
            });
        }
        return stored;
    }
    async replyToPermissionRequest(id, input) {
        const request = this.localStore.findPermissionRequest(id);
        if (!request)
            return undefined;
        if (request.status === "answered")
            throw new PermissionRequestConflictError(id);
        return this.replyToStoredRequest(request, input);
    }
    async replyToStoredRequest(request, input) {
        if (request.status === "answered" || request.status === "replying") {
            throw new PermissionRequestConflictError(request.id);
        }
        if (request.status === "reply_failed" &&
            request.selectedOption !== input.option) {
            throw new PermissionReplyValidationError("Failed permission replies can only retry the original option.");
        }
        const permissionRequest = parsePermissionRequest(request.requestJson);
        const evaluation = parsePermissionEvaluation(request.evaluationJson);
        validatePermissionReplyInput(permissionRequest, input, evaluation.verdict);
        const capabilities = allowedCapabilitiesFor(permissionRequest, input);
        const opencodeReply = optionToOpenCodeReply(input.option);
        const claimed = this.localStore.claimPermissionRequestForReply(request.id);
        if (!claimed)
            throw new PermissionRequestConflictError(request.id);
        try {
            await this.replyToOpenCode(claimed, opencodeReply, input.message);
        }
        catch (error) {
            const message = redactSecretText(error instanceof Error ? error.message : String(error));
            const failed = this.localStore.markPermissionRequestReplyFailed(claimed.id, message, input.option, opencodeReply);
            this.audit.append({
                type: "permission.reply_failed",
                severity: "warning",
                message: "Failed to reply to OpenCode permission request.",
                metadata: { requestId: claimed.id, error: message },
            });
            throw new PermissionRuntimeReplyError(message, failed);
        }
        let decisionAuditId;
        if (input.option === "allow_for_session" &&
            evaluation.verdict === "requires_approval") {
            const context = {
                workspaceId: claimed.workspaceId,
                sessionId: claimed.runId ?? claimed.runtimeSessionId,
            };
            const record = this.decisionRegistry
                ? this.decisionRegistry.recordAllowForSession({
                    toolCall: permissionRequest.toolCall,
                    context,
                    capabilities,
                    runtimeSessionId: claimed.runtimeSessionId,
                    sourcePermissionRequestId: claimed.id,
                })
                : undefined;
            const decision = record
                ? { auditId: record.auditId }
                : this.policy.recordDecision(permissionRequest.toolCall, input.option, context, capabilities);
            decisionAuditId = decision.auditId;
        }
        const answered = this.localStore.markPermissionRequestAnswered({
            id: claimed.id,
            selectedOption: input.option,
            opencodeReply,
            ...(decisionAuditId ? { decisionAuditId } : {}),
        });
        this.audit.append({
            type: "permission.request.answered",
            severity: opencodeReply === "reject" ? "info" : "warning",
            message: `Permission request ${claimed.id} answered with ${input.option}.`,
            metadata: {
                requestId: claimed.id,
                runId: claimed.runId,
                workspaceId: claimed.workspaceId,
                opencodeReply,
            },
        });
        return answered;
    }
    async replyToOpenCode(request, reply, message) {
        if (request.providerPermissionId) {
            const ok = await this.runtime.respondToSessionPermission(request.runtimeSessionId, request.providerPermissionId, reply);
            if (!ok)
                throw new Error("OpenCode session permission reply returned false.");
            return;
        }
        if (request.providerRequestId) {
            const ok = await this.runtime.replyToPermission(request.providerRequestId, reply, message);
            if (!ok)
                throw new Error("OpenCode global permission reply returned false.");
            return;
        }
        throw new Error("Permission request is missing an OpenCode reply id.");
    }
}
function optionToOpenCodeReply(option) {
    if (option === "deny" || option === "deny_and_remember")
        return "reject";
    return "once";
}
function validatePermissionReplyInput(request, input, evaluationVerdict) {
    if (!request.options.includes(input.option)) {
        throw new PermissionReplyValidationError("Permission option is not available for this request.");
    }
    if (evaluationVerdict === "blocked" && input.option !== "deny") {
        throw new PermissionReplyValidationError("Blocked permission requests can only be denied.");
    }
    if (evaluationVerdict === "requires_approval") {
        if (!isEnabledManualReplyOption(input.option)) {
            throw new PermissionReplyValidationError("Permission option is not enabled in the current approval UX.");
        }
    }
    if (isBroadPermissionOption(input.option) && !input.breakGlassConfirmed) {
        throw new PermissionReplyValidationError("Broad permission options require explicit break-glass confirmation.");
    }
    if (request.externalServiceMutation && input.option !== "deny") {
        throw new PermissionReplyValidationError("External-service mutation requests are deny-only in the current approval UX.");
    }
    if (input.capabilities?.includes("external_service_mutations")) {
        throw new PermissionReplyValidationError("External-service mutation capability is not enabled in the current approval UX.");
    }
}
function allowedCapabilitiesFor(request, input) {
    const capabilities = ["local_tools"];
    if (request.externalServiceMutation &&
        input.externalServiceMutationConfirmed &&
        input.capabilities?.includes("external_service_mutations")) {
        capabilities.push("external_service_mutations");
    }
    return capabilities;
}
function isEnabledManualReplyOption(option) {
    return (option === "approve_once" ||
        option === "allow_for_session" ||
        option === "deny");
}
function isBroadPermissionOption(option) {
    return (option === "always_allow" ||
        option === "always_allow_tool_class" ||
        option === "full_escape_hatch_run" ||
        option === "full_escape_hatch_project" ||
        option === "full_escape_hatch_global");
}
function parsePermissionRequest(json) {
    return JSON.parse(json);
}
function parsePermissionEvaluation(json) {
    return JSON.parse(json);
}
function createStoredPermissionRequest(toolCall, evaluation) {
    if (evaluation.verdict === "requires_approval")
        return evaluation.request;
    if (evaluation.verdict === "allowed") {
        return createSyntheticRequest(toolCall, "Matched an existing PatchTrail permission decision.", "allow", [evaluation.decision.option]);
    }
    return createSyntheticRequest(toolCall, "Blocked by PatchTrail permission policy.", "deny", ["deny"], {
        externalServiceMutation: evaluation.externalServiceMutation,
        ...(evaluation.denyOnlyDiagnostics
            ? { denyOnlyDiagnostics: evaluation.denyOnlyDiagnostics }
            : {}),
    });
}
function createSyntheticRequest(toolCall, reason, policyRecommendation, options, diagnostics = {}) {
    return {
        id: randomUUID(),
        toolCall,
        reason,
        options,
        policyRecommendation,
        capabilities: ["local_tools"],
        ...diagnostics,
    };
}
