// engine/multi-teammate-review/panel-orchestrator.ts — Fan-out + arbitration logic (SPEC-593)
// SPEC-705: RAM-aware parallelism guardrail applied before specialist fan-out.
// SPEC-727: Heterogeneity gate + disagreement counter + arbitrator activation.
// SPEC-729: Reviewer round wrapped with withEscalation for bounded retries.
import { join } from 'node:path';
import { SECURITY_PROMPT, CORRECTNESS_PROMPT, PERFORMANCE_PROMPT, MAINTAINABILITY_PROMPT, ARCHITECTURE_PROMPT, } from './specialist-prompts.js';
import { ARBITRATOR_PROMPT } from './arbitrator-prompt.js';
import { formatAsMarkdown, formatAsJson } from './report-formatter.js';
import { decideAgentParallelism } from '../system-resources/spawn-policy.js';
import { sendTelemetryEvent } from '../telemetry/telemetry-client.js';
import { checkHeterogeneity } from './heterogeneity-gate.js';
import { markDisagreement } from './disagreement-counter.js';
import { runArbitrator, ArbitratorOutputInvalid } from './arbitrator-runner.js';
import { atomicWriteFile } from '../safety/atomic-write-file.js';
import { appendTransitionEvent } from '../../storage/transition-log.js';
import { withEscalation } from '../escalator/with-escalation.js';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const SPECIALIST_ROLES = [
    'security',
    'correctness',
    'performance',
    'maintainability',
    'architecture',
];
const SPECIALIST_PROMPTS = {
    security: SECURITY_PROMPT,
    correctness: CORRECTNESS_PROMPT,
    performance: PERFORMANCE_PROMPT,
    maintainability: MAINTAINABILITY_PROMPT,
    architecture: ARCHITECTURE_PROMPT,
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function buildSpecialistUserMessage(input) {
    const lines = [];
    if (input.diff) {
        lines.push('## Diff to review\n');
        lines.push('```diff');
        lines.push(input.diff);
        lines.push('```');
    }
    else {
        lines.push('No diff provided — review based on the context only.');
    }
    if (input.spec) {
        lines.push('\n## Linked spec\n');
        lines.push(input.spec);
    }
    return lines.join('\n');
}
function buildArbitratorUserMessage(input, specialistReports) {
    const sections = [];
    sections.push('## Original diff\n');
    sections.push(input.diff ?? 'No diff provided');
    sections.push('\n## Linked spec\n');
    sections.push(input.spec ?? 'No spec provided');
    sections.push('\n## Specialist reports\n');
    for (const role of SPECIALIST_ROLES) {
        sections.push(`### ${role}\n`);
        sections.push(JSON.stringify(specialistReports[role], null, 2));
    }
    return sections.join('\n');
}
function coerceStr(v) {
    return typeof v === 'string' ? v : '';
}
function coerceNum(v) {
    return typeof v === 'number' ? v : 0;
}
function coerceSeverity(v) {
    if (v === 'critical' || v === 'suggestion' || v === 'nit' || v === 'false-positive') {
        return v;
    }
    return 'suggestion';
}
function parseSpecialistResponse(raw, role) {
    try {
        const match = /\[[\s\S]*\]/.exec(raw);
        if (!match) {
            return [];
        }
        const parsed = JSON.parse(match[0]);
        return parsed.map((item) => {
            const f = item;
            return {
                role,
                file: coerceStr(f.file),
                line: coerceNum(f.line),
                message: coerceStr(f.message),
                recommendation: coerceStr(f.recommendation),
                severity: coerceSeverity(f.severity),
                reasoning: coerceStr(f.reasoning),
            };
        });
    }
    catch {
        return [];
    }
}
function parseArbitratorResponse(raw) {
    try {
        const match = /\{[\s\S]*\}/.exec(raw);
        if (!match) {
            return [];
        }
        const parsed = JSON.parse(match[0]);
        if (!Array.isArray(parsed.decisions)) {
            return [];
        }
        return parsed.decisions.map((item) => {
            const d = item;
            return {
                severity: coerceSeverity(d.severity),
                file: coerceStr(d.file),
                line: coerceNum(d.line),
                message: coerceStr(d.message),
                recommendation: coerceStr(d.recommendation),
                contributors: Array.isArray(d.contributors)
                    ? d.contributors.filter((r) => SPECIALIST_ROLES.includes(r))
                    : [],
                justification: coerceStr(d.justification),
            };
        });
    }
    catch {
        return [];
    }
}
// ---------------------------------------------------------------------------
// Concurrency helpers (SPEC-705)
// ---------------------------------------------------------------------------
/**
 * Run tasks with a maximum concurrency limit (p-limit-style).
 * Returns PromiseSettledResult[] in the same order as items.
 */
async function runWithConcurrencyLimit(items, concurrency, fn) {
    if (concurrency >= items.length) {
        return Promise.allSettled(items.map(fn));
    }
    // Using Array.from to avoid unsafe `new Array()` assignment (no-unsafe-assignment)
    const results = Array.from({ length: items.length });
    let nextIndex = 0;
    async function worker() {
        while (nextIndex < items.length) {
            const idx = nextIndex++;
            const item = items[idx];
            if (item === undefined) {
                break;
            }
            try {
                results[idx] = { status: 'fulfilled', value: await fn(item) };
            }
            catch (reason) {
                results[idx] = { status: 'rejected', reason };
            }
        }
    }
    const workers = Array.from({ length: Math.max(1, concurrency) }, () => worker());
    await Promise.all(workers);
    return results;
}
// ---------------------------------------------------------------------------
// Constants (SPEC-727)
// ---------------------------------------------------------------------------
/** Disagreement count threshold that triggers arbitrator activation. */
const ARBITRATOR_THRESHOLD = 2;
// ---------------------------------------------------------------------------
// Main orchestrator
// ---------------------------------------------------------------------------
// eslint-disable-next-line max-lines-per-function -- orchestrator: each section handles a discrete review phase; extraction would require threading 10+ shared variables
export async function runReviewPanel(input, invoker) {
    // SPEC-727: Heterogeneity gate — abort before invoking any specialist.
    if (input.specialists && input.specialists.length > 0) {
        const hetResult = checkHeterogeneity(input.specialists);
        if (!hetResult.ok) {
            // Fire telemetry event (fire-and-forget)
            sendTelemetryEvent({
                event: 'panel_heterogeneity_violation',
                installationId: 'anonymous',
                properties: {
                    duplicateCount: hetResult.duplicates.length,
                    specId: input.specId ?? 'unknown',
                },
            });
            return {
                aborted: true,
                reason: 'HETEROGENEITY_VIOLATION',
                duplicates: hetResult.duplicates,
                // Required fields — empty since panel never ran
                specialistReports: {
                    security: [],
                    correctness: [],
                    performance: [],
                    maintainability: [],
                    architecture: [],
                },
                decisions: [],
                markdownReport: '',
                jsonSummary: {
                    totalFindings: 0,
                    bySeverity: { critical: 0, suggestion: 0, nit: 0, 'false-positive': 0 },
                    criticalFindings: [],
                    allFindings: [],
                },
                reviewedAt: new Date().toISOString(),
            };
        }
    }
    // SPEC-727: Check disagreement counter — route to arbitrator if threshold reached.
    const projectId = input.projectId;
    const specId = input.specId;
    const frontmatterSha = input.frontmatterSha;
    if (projectId !== undefined && specId !== undefined && frontmatterSha !== undefined) {
        const dKey = { specId, frontmatterSha };
        // Increment counter to record this reviewer round's disagreement
        const newCount = await markDisagreement(projectId, dKey);
        if (newCount >= ARBITRATOR_THRESHOLD) {
            // Route to arbitrator instead of running specialists
            const arbitratorMessage = buildArbitratorUserMessage(input, {
                security: [],
                correctness: [],
                performance: [],
                maintainability: [],
                architecture: [],
            });
            try {
                const arbitratorDecisions = await runArbitrator({
                    userMessage: arbitratorMessage,
                    invoker,
                });
                // Persist arbitrator decision JSON
                const sha = frontmatterSha.slice(0, 16);
                const decisionPath = join('planu', 'data', 'projects', projectId, 'arbitrator-decisions', `${specId}-${sha}.json`);
                await atomicWriteFile(decisionPath, JSON.stringify(arbitratorDecisions, null, 2));
                // Record in transition log
                await appendTransitionEvent({
                    projectId,
                    specId,
                    eventType: 'transition',
                    frontmatterSha,
                    reason: 'arbitrator-decision',
                    meta: { arbitratorDecisionPath: decisionPath },
                });
                const reviewedAt = new Date().toISOString();
                const markdownReport = formatAsMarkdown(arbitratorDecisions, input);
                const jsonSummary = formatAsJson(arbitratorDecisions);
                return {
                    specialistReports: {
                        security: [],
                        correctness: [],
                        performance: [],
                        maintainability: [],
                        architecture: [],
                    },
                    decisions: arbitratorDecisions,
                    markdownReport,
                    jsonSummary,
                    reviewedAt,
                };
            }
            catch (err) {
                if (err instanceof ArbitratorOutputInvalid) {
                    return {
                        blocked: true,
                        reason: 'arbitrator_output_invalid',
                        specialistReports: {
                            security: [],
                            correctness: [],
                            performance: [],
                            maintainability: [],
                            architecture: [],
                        },
                        decisions: [],
                        markdownReport: '',
                        jsonSummary: {
                            totalFindings: 0,
                            bySeverity: { critical: 0, suggestion: 0, nit: 0, 'false-positive': 0 },
                            criticalFindings: [],
                            allFindings: [],
                        },
                        reviewedAt: new Date().toISOString(),
                    };
                }
                throw err;
            }
        }
    }
    const userMessage = buildSpecialistUserMessage(input);
    // SPEC-705: Check RAM before spawning specialists in parallel.
    const ramDecision = await decideAgentParallelism({ requested: SPECIALIST_ROLES.length });
    let ramDowngradeWarning;
    if (ramDecision.allowed < ramDecision.requested) {
        // Emit telemetry for downgrade (fire-and-forget — never throws)
        sendTelemetryEvent({
            event: 'ram_guardrail_downgrade',
            installationId: 'anonymous',
            properties: {
                requested: ramDecision.requested,
                allowed: ramDecision.allowed,
                freeGBObserved: ramDecision.freeGBObserved,
                pressureLevel: ramDecision.pressureLevel,
                context: 'multi_teammate_review',
            },
        });
        // Surface downgrade warning (also available via input.ramDowngradeWarning for callers that pre-set it)
        ramDowngradeWarning =
            input.ramDowngradeWarning ??
                `⚠️ Parallelism reduced from ${ramDecision.requested} to ${ramDecision.allowed} due to host memory (${ramDecision.freeGBObserved?.toFixed(1) ?? 'unknown'}GB free)`;
    }
    // Fan out specialists with RAM-aware concurrency limit (p-limit style)
    const concurrency = ramDecision.allowed;
    const specialistResults = await runWithConcurrencyLimit(SPECIALIST_ROLES, concurrency, async (role) => {
        const raw = await invoker.invoke(SPECIALIST_PROMPTS[role], userMessage);
        return { role, findings: parseSpecialistResponse(raw, role) };
    });
    // Collect specialist reports — pre-fill with empty arrays so all roles are always present
    const specialistReports = {
        security: [],
        correctness: [],
        performance: [],
        maintainability: [],
        architecture: [],
    };
    for (const result of specialistResults) {
        if (result.status === 'fulfilled') {
            specialistReports[result.value.role] = result.value.findings;
        }
    }
    // Invoke arbitrator with all specialist reports
    const arbitratorMessage = buildArbitratorUserMessage(input, specialistReports);
    const decisions = await (async () => {
        try {
            const rawArbitrator = await invoker.invoke(ARBITRATOR_PROMPT, arbitratorMessage);
            return parseArbitratorResponse(rawArbitrator);
        }
        catch {
            // Arbitrator failure: degrade to raw specialist findings as suggestions
            return SPECIALIST_ROLES.flatMap((role) => specialistReports[role].map((f) => ({
                severity: f.severity,
                file: f.file,
                line: f.line,
                message: f.message,
                recommendation: f.recommendation,
                contributors: [role],
                justification: f.reasoning,
            })));
        }
    })();
    const reviewedAt = new Date().toISOString();
    const markdownReport = formatAsMarkdown(decisions, input);
    const jsonSummary = formatAsJson(decisions);
    return {
        specialistReports,
        decisions,
        markdownReport,
        jsonSummary,
        reviewedAt,
        ...(ramDowngradeWarning !== undefined && { ramDowngradeWarning }),
    };
}
// ---------------------------------------------------------------------------
// SPEC-729: Reviewer round with escalation wrapper
// ---------------------------------------------------------------------------
/**
 * Run the review panel with bounded retry logic (SPEC-729).
 *
 * Wraps `runReviewPanel` in `withEscalation('reviewer', ...)` so that repeated
 * panel failures escalate to a human decision after budget exhaustion.
 *
 * The panel is considered "failed" when `report.blocked === true` or
 * `report.aborted === true`.
 *
 * @param input     - Panel input (must include specId, frontmatterSha, projectId).
 * @param invoker   - LLM invoker.
 * @returns Either a successful EscalateResult wrapping the report, or an escalation sentinel.
 */
export async function runReviewPanelWithEscalation(input, invoker) {
    const specId = input.specId ?? 'unknown';
    const frontmatterSha = input.frontmatterSha ?? 'unknown';
    const projectId = input.projectId ?? 'unknown';
    return withEscalation({ role: 'reviewer', specId, frontmatterSha, projectId }, async () => {
        const report = await runReviewPanel(input, invoker);
        if (report.aborted === true || report.blocked === true) {
            return {
                ok: false,
                reason: report.reason ?? 'Panel aborted or blocked without reason',
            };
        }
        return { ok: true, result: report };
    });
}
//# sourceMappingURL=panel-orchestrator.js.map