// tools/approval-handler.ts — Handlers for approval workflow tools (SPEC-283, SPEC-722)
import { hashProjectPath, projectDataDir } from '../storage/base-store.js';
import * as approvalStore from '../storage/approval-store.js';
// eslint-disable-next-line no-restricted-imports -- approval-handler is a SPEC-722 migration bridge; __internalSetStatus use is intentional
import { getSpec, __internalSetStatus } from '../storage/spec-store.js';
import { buildApprovalStatus } from '../engine/approval-workflow.js';
import { appendEvent } from '../engine/audit-trail/index.js';
import { join } from 'node:path';
import { issueReviewerToken, verifyReviewerToken } from '../engine/reviewer-tokens/index.js';
import { readPlanuConfig } from '../engine/planu-config-writer.js';
import { readFile } from 'node:fs/promises';
// ---------------------------------------------------------------------------
// Path helpers
// ---------------------------------------------------------------------------
function auditLogPath(projectId) {
    return join(projectDataDir(projectId), 'audit-log.jsonl');
}
// ---------------------------------------------------------------------------
// Legacy shim: build a ReviewerIdentity from a raw string (no token)
// ---------------------------------------------------------------------------
function legacyIdentity(displayName) {
    return { displayName, verified: false, legacy: true };
}
// ---------------------------------------------------------------------------
// Read planu.json legacyReviewMode flag
// ---------------------------------------------------------------------------
async function isLegacyReviewMode(projectPath) {
    try {
        const planuConfigPath = join(projectPath, 'planu.json');
        const config = await readPlanuConfig(planuConfigPath, readFile);
        return config?.legacyReviewMode === true;
    }
    catch {
        return false;
    }
}
// ---------------------------------------------------------------------------
// SPEC-722 rejection message
// ---------------------------------------------------------------------------
function buildTokenRejectionMessage(reason) {
    return (`[SPEC-722] Approval rejected — ${reason}\n\n` +
        'To approve a spec, use one of these three paths:\n' +
        '  (a) A different Opus session: call `issue_reviewer_token` with the plannerToken ' +
        'emitted by `create_spec`, then pass the resulting `reviewerToken` to `approve_spec`.\n' +
        '  (b) `multi_teammate_review`: run a heterogeneous panel and pass the multiTeammateReviewId.\n' +
        '  (c) Legacy mode (deprecated, v2.x only): set `legacyReviewMode: true` in planu.json. ' +
        'This flag will be removed in v3. [DEPRECATION WARNING]');
}
// ---------------------------------------------------------------------------
// configure_approval_policy
// ---------------------------------------------------------------------------
export async function handleConfigureApprovalPolicy(args) {
    const projectId = hashProjectPath(args.projectPath);
    const policy = await approvalStore.savePolicy(projectId, {
        requiredApprovals: args.requiredApprovals,
        reviewSLAHours: args.reviewSLAHours,
        ...(args.escalateTo !== undefined ? { escalateTo: args.escalateTo } : {}),
        ...(args.requireRoles !== undefined ? { requireRoles: args.requireRoles } : {}),
    });
    return {
        content: [
            {
                type: 'text',
                text: `Approval policy configured: ${String(policy.requiredApprovals)} approval(s) required, ` +
                    `SLA ${String(policy.reviewSLAHours)}h.` +
                    (policy.escalateTo !== undefined ? ` Escalate to: ${policy.escalateTo}.` : '') +
                    (policy.requireRoles !== undefined && policy.requireRoles.length > 0
                        ? ` Required roles: ${policy.requireRoles.join(', ')}.`
                        : ''),
            },
        ],
        structuredContent: { policy },
    };
}
// ---------------------------------------------------------------------------
// issue_reviewer_token
// ---------------------------------------------------------------------------
export async function handleIssueReviewerToken(args) {
    try {
        const reviewerToken = await issueReviewerToken(args.projectPath, args.plannerToken, args.sessionId, args.modelId, args.host);
        return {
            content: [
                {
                    type: 'text',
                    text: `Reviewer token issued for session ${args.sessionId} (${args.modelId} / ${args.host}). ` +
                        `Pass this token as \`reviewerToken\` when calling \`approve_spec\`. ` +
                        `Token is valid for 7 days.`,
                },
            ],
            structuredContent: { reviewerToken },
        };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        return {
            content: [{ type: 'text', text: `Failed to issue reviewer token: ${message}` }],
            isError: true,
        };
    }
}
// ---------------------------------------------------------------------------
// approve_spec
// ---------------------------------------------------------------------------
export async function handleApproveSpec(args) {
    const projectId = hashProjectPath(args.projectPath);
    const spec = await getSpec(projectId, args.specId);
    if (spec === null) {
        return {
            content: [{ type: 'text', text: `Spec ${args.specId} not found.` }],
            isError: true,
        };
    }
    // -------------------------------------------------------------------------
    // SPEC-722: Token verification gate
    // -------------------------------------------------------------------------
    let reviewerIdentity;
    const hasReviewerToken = args.reviewerToken !== undefined && args.reviewerToken.length > 0;
    const hasPlannerToken = args.plannerToken !== undefined && args.plannerToken.length > 0;
    if (hasReviewerToken && hasPlannerToken) {
        // Happy path: both tokens provided — verify.
        // args.reviewerToken and args.plannerToken are guaranteed non-empty by the hasXxx guards above.
        const verificationResult = await verifyReviewerToken(args.projectPath, args.reviewerToken, // eslint-disable-line @typescript-eslint/no-non-null-assertion
        args.plannerToken, // eslint-disable-line @typescript-eslint/no-non-null-assertion
        { multiTeammateReviewId: args.multiTeammateReviewId });
        if (!verificationResult.valid || verificationResult.payload === undefined) {
            const reason = verificationResult.reason ?? 'Token verification failed.';
            return {
                content: [{ type: 'text', text: buildTokenRejectionMessage(reason) }],
                isError: true,
            };
        }
        reviewerIdentity = {
            displayName: args.reviewer,
            sessionId: verificationResult.payload.sessionId,
            modelId: verificationResult.payload.modelId,
            host: verificationResult.payload.host,
            verified: true,
            legacy: false,
        };
    }
    else {
        // No token provided — check if legacy mode is enabled.
        const legacyMode = await isLegacyReviewMode(args.projectPath);
        if (!legacyMode) {
            return {
                content: [
                    {
                        type: 'text',
                        text: buildTokenRejectionMessage('no reviewerToken was provided and legacyReviewMode is not enabled.'),
                    },
                ],
                isError: true,
            };
        }
        // Legacy mode: accept the string reviewer but warn loudly.
        reviewerIdentity = legacyIdentity(args.reviewer);
    }
    // -------------------------------------------------------------------------
    // Persist the approval record
    // -------------------------------------------------------------------------
    const record = {
        specId: args.specId,
        reviewer: args.reviewer,
        reviewerIdentity,
        action: 'approved',
        timestamp: new Date().toISOString(),
        ...(args.role !== undefined ? { role: args.role } : {}),
        ...(args.comment !== undefined ? { comment: args.comment } : {}),
    };
    const records = await approvalStore.appendRecord(projectId, args.specId, record);
    const policy = await approvalStore.getPolicy(projectId);
    const status = buildApprovalStatus(args.specId, records, policy, undefined);
    // Audit trail — fire-and-forget
    appendEvent(auditLogPath(projectId), {
        eventType: 'spec.approved',
        specId: args.specId,
        userId: args.reviewer,
        action: 'approve_spec',
        details: {
            reviewer: args.reviewer,
            reviewerIdentity,
            role: args.role ?? null,
            comment: args.comment ?? null,
            approvalCount: status.approvalCount,
            gateOpen: status.gateOpen,
            tokenVerified: reviewerIdentity.verified,
            legacyMode: reviewerIdentity.legacy,
        },
    }).catch(() => {
        // audit failure must never block the tool
    });
    const gateMsg = status.gateOpen
        ? ` Approval gate is now OPEN (${String(status.approvalCount)}/${String(policy?.requiredApprovals ?? status.approvalCount)}).`
        : ` Gate still needs ${String(status.approvalsNeeded)} more approval(s).`;
    const legacyWarning = reviewerIdentity.legacy
        ? '\n\n⚠ [DEPRECATION] legacyReviewMode is active. Token-based review will be mandatory in v3.'
        : '';
    return {
        content: [
            {
                type: 'text',
                text: `${args.reviewer} approved ${args.specId}.${gateMsg}${legacyWarning}`,
            },
        ],
        structuredContent: { record, status },
    };
}
// ---------------------------------------------------------------------------
// request_changes
// ---------------------------------------------------------------------------
export async function handleRequestChanges(args) {
    const projectId = hashProjectPath(args.projectPath);
    const spec = await getSpec(projectId, args.specId);
    if (spec === null) {
        return {
            content: [{ type: 'text', text: `Spec ${args.specId} not found.` }],
            isError: true,
        };
    }
    const record = {
        specId: args.specId,
        reviewer: args.reviewer,
        reviewerIdentity: legacyIdentity(args.reviewer),
        action: 'changes_requested',
        timestamp: new Date().toISOString(),
        comment: args.comment,
        ...(args.role !== undefined ? { role: args.role } : {}),
    };
    // Append the changes_requested record and clear prior approvals for this spec
    await approvalStore.appendRecord(projectId, args.specId, record);
    await approvalStore.clearRecords(projectId, args.specId);
    // Re-append only the changes_requested record so the history is preserved
    const records = await approvalStore.appendRecord(projectId, args.specId, record);
    // Move spec back to draft
    // TODO(SPEC-722): Migrate to handleUpdateStatus with reviewer identity token.
    // Using __internalSetStatus as a temporary bridge until SPEC-722 lands.
    await __internalSetStatus(projectId, args.specId, 'draft');
    // Audit trail — fire-and-forget
    appendEvent(auditLogPath(projectId), {
        eventType: 'spec.status_changed',
        specId: args.specId,
        userId: args.reviewer,
        action: 'request_changes',
        details: {
            reviewer: args.reviewer,
            role: args.role ?? null,
            comment: args.comment,
            changesRequested: true,
        },
    }).catch(() => {
        // audit failure must never block the tool
    });
    void records;
    return {
        content: [
            {
                type: 'text',
                text: `${args.reviewer} requested changes on ${args.specId}. ` +
                    `Spec moved back to draft. Comment: "${args.comment}"`,
            },
        ],
        structuredContent: { record, specStatus: 'draft' },
    };
}
// ---------------------------------------------------------------------------
// approval_status
// ---------------------------------------------------------------------------
export async function handleApprovalStatus(args) {
    const projectId = hashProjectPath(args.projectPath);
    const [spec, records, policy] = await Promise.all([
        getSpec(projectId, args.specId),
        approvalStore.getRecords(projectId, args.specId),
        approvalStore.getPolicy(projectId),
    ]);
    if (spec === null) {
        return {
            content: [{ type: 'text', text: `Spec ${args.specId} not found.` }],
            isError: true,
        };
    }
    // Determine when spec entered review (use updatedAt as proxy if in review)
    const reviewStartedAt = spec.status === 'review' ? spec.updatedAt : undefined;
    const status = buildApprovalStatus(args.specId, records, policy, reviewStartedAt);
    const lines = [
        `Approval Status — ${args.specId}`,
        `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
        `Approvals: ${String(status.approvalCount)} / ${String(policy?.requiredApprovals ?? '(no policy)')}`,
        `Gate: ${status.gateOpen ? 'OPEN' : `BLOCKED — needs ${String(status.approvalsNeeded)} more`}`,
    ];
    if (status.approvers.length > 0) {
        lines.push(`Approved by: ${status.approvers.join(', ')}`);
    }
    if (policy !== null) {
        lines.push(`SLA: ${String(policy.reviewSLAHours)}h`);
        if (spec.status === 'review') {
            lines.push(`In review: ${String(status.hoursInReview)}h`);
            if (status.slaBreached) {
                lines.push(`⚠ SLA BREACHED by ${String(status.slaBreachHours)}h` +
                    (policy.escalateTo !== undefined ? ` — escalate to ${policy.escalateTo}` : ''));
            }
        }
    }
    const changeRequests = records.filter((r) => r.action === 'changes_requested');
    if (changeRequests.length > 0) {
        lines.push(`Change requests: ${String(changeRequests.length)}`);
    }
    return {
        content: [{ type: 'text', text: lines.join('\n') }],
        structuredContent: { status },
    };
}
//# sourceMappingURL=approval-handler.js.map