import * as path from 'path';
import { nexusEventBus } from '../../engines/event-bus.js';
import { resolveWorkspaceContext } from '../../engines/workspace-resolver.js';
import { execAsync } from '../../utils/exec-async.js';
import { recordFirstInteraction } from '../../engines/telemetry.js';
import { pushDispatch, cancelDispatch, listActiveDispatches } from '../../engines/dispatch/push-dispatch.js';
import { invokerRegistry } from '../../invokers/registry.js';
import { completeRun, createRun, failRun, getRun as getStoredRun, listRecentRuns, startSpan, endSpan, updateStage, } from '../../engines/orchestrator/store.js';
function normalizeLifetimeTokenPayload(record) {
    const savedTokens = Number(record?.totalSavedTokens ?? 0);
    const grossInputTokens = Number(record?.totalGrossInputTokens ?? 0);
    const compressedTokens = Number(record?.totalCompressedTokens ?? 0);
    const compressionPct = grossInputTokens > 0 ? Math.round((savedTokens / grossInputTokens) * 100) : 0;
    return {
        ...record,
        savedTokens,
        grossInputTokens,
        compressedTokens,
        compressionPct,
        totalSaved: savedTokens,
        totalGrossInput: grossInputTokens,
        totalCompressed: compressedTokens,
        lifetime: {
            ...record,
            savedTokens,
            grossInputTokens,
            compressedTokens,
            compressionPct,
        },
    };
}
function normalizeTokenSummaryPayload(record) {
    const savedTokens = Number(record?.savedTokens ?? record?.totalSavedTokens ?? record?.saved ?? 0);
    const grossInputTokens = Number(record?.grossInputTokens ?? record?.totalGrossInputTokens ?? record?.gross ?? 0);
    const compressedTokens = Number(record?.compressedTokens ?? record?.totalCompressedTokens ?? record?.net ?? 0);
    const forwardedTokens = Number(record?.forwardedTokens ?? record?.tokensForwarded ?? compressedTokens);
    const compressionPct = Number.isFinite(Number(record?.compressionPct))
        ? Number(record.compressionPct)
        : (grossInputTokens > 0 ? Math.round((savedTokens / grossInputTokens) * 100) : 0);
    return {
        ...record,
        savedTokens,
        grossInputTokens,
        compressedTokens,
        forwardedTokens,
        compressionPct,
        saved: savedTokens,
        gross: grossInputTokens,
        net: compressedTokens,
    };
}
function mergeDashboardRuns(liveRuns, storedRuns, limit) {
    const seen = new Set();
    return [...(liveRuns ?? []), ...(storedRuns ?? [])]
        .map((run) => {
        const runId = run?.runId ?? run?.id;
        return runId ? { ...run, runId, id: run.id ?? runId } : null;
    })
        .filter((run) => {
        if (!run?.runId || seen.has(run.runId))
            return false;
        seen.add(run.runId);
        return true;
    })
        .sort((left, right) => {
        const leftTs = Number(left.updatedAt ?? left.completedAt ?? left.createdAt ?? left.startedAt ?? 0);
        const rightTs = Number(right.updatedAt ?? right.completedAt ?? right.createdAt ?? right.startedAt ?? 0);
        return rightTs - leftTs;
    })
        .slice(0, Math.max(1, limit));
}
function summarizeDashboardRun(run, fallbackGoal) {
    const state = run?.state ?? run?.status ?? 'completed';
    const summary = run?.summary ?? run?.resultSummary ?? run?.message ?? '';
    return String(summary || `${state}: ${fallbackGoal}`).slice(0, 1024);
}
async function tokenLedgerFallback(ctx) {
    const memory = ctx.getMemory?.();
    if (!memory)
        return null;
    try {
        const { TokenAnalyticsEngine } = await import('../../engines/token-analytics.js');
        const report = new TokenAnalyticsEngine(memory).getLifetimeReport();
        const optimized = Number(report.totalTokensOptimized ?? 0);
        const saved = Number(report.totalTokensSaved ?? 0);
        const forwarded = Number(report.totalTokensForwarded ?? Math.max(0, optimized - saved));
        if (optimized <= 0 && saved <= 0 && forwarded <= 0)
            return null;
        return {
            totalSavedTokens: saved,
            totalGrossInputTokens: optimized,
            totalCompressedTokens: forwarded,
            totalRuns: Number(report.totalSessions ?? 0),
            lastUpdatedAt: Date.now(),
            tokenLedger: report,
        };
    }
    catch {
        return null;
    }
}
function mergeLifetimeTokenRecords(fileRecord, ledgerRecord) {
    if (!ledgerRecord)
        return fileRecord;
    return {
        ...fileRecord,
        totalSavedTokens: Math.max(Number(fileRecord?.totalSavedTokens ?? 0), Number(ledgerRecord.totalSavedTokens ?? 0)),
        totalGrossInputTokens: Math.max(Number(fileRecord?.totalGrossInputTokens ?? 0), Number(ledgerRecord.totalGrossInputTokens ?? 0)),
        totalCompressedTokens: Math.max(Number(fileRecord?.totalCompressedTokens ?? 0), Number(ledgerRecord.totalCompressedTokens ?? 0)),
        totalRuns: Math.max(Number(fileRecord?.totalRuns ?? 0), Number(ledgerRecord.totalRuns ?? 0)),
        lastUpdatedAt: Math.max(Number(fileRecord?.lastUpdatedAt ?? 0), Number(ledgerRecord.lastUpdatedAt ?? 0)),
        sources: {
            lifetimeFile: fileRecord,
            tokenLedger: ledgerRecord.tokenLedger,
        },
    };
}
async function readGitBranch(repoRoot) {
    try {
        const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
            cwd: repoRoot,
            timeout: 2_000,
        });
        const branch = stdout.trim();
        return branch === 'HEAD' ? null : (branch || null);
    }
    catch {
        return null;
    }
}
export const handleRuntimeRoutes = async (ctx, req, res, url) => {
    if (req.method === 'GET' && url.pathname === '/api/runs') {
        const limit = parseInt(url.searchParams.get('limit') || '20', 10);
        const liveRuns = ctx.getRuntime()?.listRuns(limit) ?? [];
        const storedRuns = listRecentRuns(limit);
        ctx.respondJson(res, mergeDashboardRuns(liveRuns, storedRuns, limit));
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/usage') {
        await ctx.respondCachedJson(res, `usage:${url.search}`, 2_000, () => Promise.resolve(ctx.collectUsageSnapshot(url)));
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/orchestration/session') {
        await ctx.respondCachedJson(res, `orchestration-session:${url.search}`, 2_000, async () => {
            const snapshot = ctx.resolveRuntimeSnapshot(url);
            const orchestrator = ctx.getOrchestrator();
            return snapshot?.orchestration ?? orchestrator?.getSessionState?.() ?? {};
        });
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/orchestration/ledger') {
        await ctx.respondCachedJson(res, `orchestration-ledger:${url.search}`, 2_000, async () => {
            const snapshot = ctx.resolveRuntimeSnapshot(url);
            return snapshot?.executionLedger ?? ctx.getRuntime()?.getExecutionLedger?.() ?? {};
        });
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/orchestration/worker-plan') {
        await ctx.respondCachedJson(res, `orchestration-worker-plan:${url.search}`, 2_000, async () => {
            const snapshot = ctx.resolveRuntimeSnapshot(url);
            return snapshot?.workerPlan ?? ctx.getRuntime()?.getUsageSnapshot?.()?.workerPlan ?? {};
        });
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/orchestration/artifact-outcomes') {
        await ctx.respondCachedJson(res, `orchestration-artifact-outcomes:${url.search}`, 2_000, async () => {
            const snapshot = ctx.resolveRuntimeSnapshot(url);
            return snapshot?.artifactOutcome ?? ctx.getRuntime()?.getUsageSnapshot?.()?.artifactOutcome ?? {};
        });
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/instruction-packet') {
        await ctx.respondCachedJson(res, `instruction-packet:${url.search}`, 2_000, async () => {
            const snapshot = ctx.resolveRuntimeSnapshot(url);
            return snapshot?.instructionPacket ?? ctx.getRuntime()?.getInstructionPacket?.() ?? {};
        });
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/worktree-health') {
        await ctx.respondCachedJson(res, `worktree-health:${url.search}`, 2_000, async () => {
            const snapshot = ctx.resolveRuntimeSnapshot(url);
            return snapshot?.worktreeHealth ?? ctx.getRuntime()?.getUsageSnapshot?.()?.worktreeHealth ?? {};
        });
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/tokens/summary') {
        await ctx.respondCachedJson(res, `tokens-summary:${url.search}`, 2_000, async () => {
            const snapshot = ctx.resolveRuntimeSnapshot(url);
            return normalizeTokenSummaryPayload(snapshot?.tokens ?? ctx.getRuntime()?.getTokenTelemetrySummary?.() ?? {});
        });
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/tokens/by-source') {
        await ctx.respondCachedJson(res, `tokens-by-source:${url.search}`, 2_000, async () => {
            const snapshot = ctx.resolveRuntimeSnapshot(url);
            return snapshot?.tokens?.bySourceClass ?? ctx.getRuntime()?.getTokenTelemetrySummary?.()?.bySourceClass ?? {};
        });
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/tokens/timeline') {
        const limit = parseInt(url.searchParams.get('limit') || '20', 10);
        await ctx.respondCachedJson(res, `tokens-timeline:${url.search}`, 2_000, async () => {
            const snapshot = ctx.resolveRuntimeSnapshot(url);
            const timeline = snapshot?.tokens?.timeline ?? ctx.getRuntime()?.getTokenTelemetryTimeline?.(limit) ?? [];
            return timeline.slice(0, Math.max(1, limit));
        });
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/tokens/lifetime') {
        try {
            const { readLifetimeTokens } = await import('../../engines/lifetime-tokens.js');
            const merged = mergeLifetimeTokenRecords(readLifetimeTokens(), await tokenLedgerFallback(ctx));
            ctx.respondJson(res, { ok: true, data: normalizeLifetimeTokenPayload(merged) });
        }
        catch (err) {
            ctx.respondJson(res, { ok: false, error: err?.message ?? 'Failed to read lifetime tokens' });
        }
        return true;
    }
    if (req.method === 'GET' && url.pathname.startsWith('/api/tokens/runs/')) {
        const runId = decodeURIComponent(url.pathname.replace('/api/tokens/runs/', ''));
        const snapshot = ctx.resolveRuntimeSnapshot(url);
        const entry = ctx.getRuntime()?.getRuntimeId?.() === snapshot?.runtimeId
            ? await ctx.getRuntime()?.getTokenTelemetryForRun?.(runId)
            : snapshot?.tokens?.timeline?.find((item) => item.runId === runId);
        ctx.respondJson(res, entry ?? { error: 'run-token-telemetry-not-found', runId }, entry ? 200 : 404);
        return true;
    }
    const userWorkflowMatch = req.method === 'GET'
        ? /^\/api\/runs\/(.+)\/user-workflow$/.exec(url.pathname)
        : null;
    if (userWorkflowMatch) {
        const runId = decodeURIComponent(userWorkflowMatch[1]);
        const trace = await ctx.getRuntime()?.getUserWorkflowTrace?.(runId);
        ctx.respondJson(res, trace ?? { error: 'user-workflow-trace-not-found', runId }, trace ? 200 : 404);
        return true;
    }
    if (req.method === 'GET' && url.pathname.startsWith('/api/runs/')) {
        const runId = decodeURIComponent(url.pathname.replace('/api/runs/', ''));
        const run = await ctx.getRuntime()?.getRun?.(runId) ?? getStoredRun(runId);
        ctx.respondJson(res, run ?? { error: 'run-not-found', runId }, run ? 200 : 404);
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/repo-tree') {
        await ctx.respondCachedJson(res, 'repo-tree', 45_000, () => Promise.resolve(ctx.repoTreeGenerator.generate()));
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/workspace') {
        await ctx.respondCachedJson(res, 'workspace', 30_000, async () => {
            const fallbackWorkspace = ctx.workspaceContextProvider?.()
                ?? resolveWorkspaceContext({ workspaceRoot: ctx.repoRoot });
            const repoIdentity = ctx.getSelectedRepoIdentity(url);
            const repoRoot = typeof repoIdentity?.repoRoot === 'string' && repoIdentity.repoRoot.trim()
                ? repoIdentity.repoRoot.trim()
                : fallbackWorkspace.repoRoot;
            const workspaceRoot = typeof repoIdentity?.workspaceRoot === 'string' && repoIdentity.workspaceRoot.trim()
                ? repoIdentity.workspaceRoot.trim()
                : fallbackWorkspace.workspaceRoot;
            const repoName = typeof repoIdentity?.repoName === 'string' && repoIdentity.repoName.trim()
                ? repoIdentity.repoName.trim()
                : fallbackWorkspace.repoName || path.basename(repoRoot) || 'workspace';
            const branch = await readGitBranch(repoRoot);
            return {
                workspaceRoot,
                repoRoot,
                repoName,
                stateKey: typeof repoIdentity?.workspaceStateKey === 'string' && repoIdentity.workspaceStateKey.trim()
                    ? repoIdentity.workspaceStateKey.trim()
                    : fallbackWorkspace.stateKey,
                branch,
                remoteUrl: repoIdentity?.remoteUrl ?? fallbackWorkspace.remoteUrl ?? null,
            };
        });
        return true;
    }
    if (req.method === 'POST' && url.pathname === '/api/runtime/execute') {
        const body = await ctx.readJsonBody(req);
        const orchestrator = ctx.getOrchestrator();
        if (!orchestrator) {
            ctx.respondJson(res, { error: 'orchestrator-unavailable' }, 503);
            return true;
        }
        const goal = typeof body.goal === 'string' ? body.goal : (typeof body.message === 'string' ? body.message : '');
        if (!goal.trim()) {
            ctx.respondJson(res, { error: 'goal-required' }, 400);
            return true;
        }
        const dashboardRunId = `dash_${Date.now()}`;
        createRun(dashboardRunId, goal, 'dashboard-chat');
        updateStage(dashboardRunId, 'orchestrating', 35);
        const spanId = startSpan(dashboardRunId, 'dashboard.runtime.execute', undefined, { source: 'dashboard-chat' });
        try {
            const run = await orchestrator.orchestrate(goal, { ...body, goal, source: body.source ?? 'dashboard-chat' });
            const actualRunId = run?.runId ?? dashboardRunId;
            if (actualRunId !== dashboardRunId) {
                createRun(actualRunId, goal, 'dashboard-chat');
                updateStage(actualRunId, 'orchestrating', 80);
            }
            completeRun(dashboardRunId, summarizeDashboardRun(run, goal));
            if (actualRunId !== dashboardRunId)
                completeRun(actualRunId, summarizeDashboardRun(run, goal));
            endSpan(spanId, run?.state === 'failed' ? 'error' : 'ok', { actualRunId, state: run?.state ?? 'completed' });
            nexusEventBus.emit('dashboard.action', {
                action: 'runtime.execute',
                status: run.state,
                target: actualRunId,
            });
            ctx.respondJson(res, run, 201);
        }
        catch (err) {
            const message = err?.message ?? String(err);
            failRun(dashboardRunId, message);
            endSpan(spanId, 'error', { error: message });
            nexusEventBus.emit('dashboard.action', {
                action: 'runtime.execute.failed',
                status: 'fail',
                target: dashboardRunId,
            });
            ctx.respondJson(res, { error: message, runId: dashboardRunId }, 500);
        }
        return true;
    }
    if (req.method === 'POST' && url.pathname === '/api/runtime/plan') {
        const body = await ctx.readJsonBody(req);
        const runtime = ctx.getRuntime();
        if (!runtime) {
            ctx.respondJson(res, { error: 'runtime-unavailable' }, 503);
            return true;
        }
        if (!body.goal || typeof body.goal !== 'string') {
            ctx.respondJson(res, { error: 'goal-required' }, 400);
            return true;
        }
        const plan = await runtime.planExecution(body);
        nexusEventBus.emit('dashboard.action', {
            action: 'runtime.plan',
            status: 'ready',
            target: plan.selectedCrew?.crewId,
        });
        ctx.respondJson(res, plan);
        return true;
    }
    // ── Push-mode dispatch ────────────────────────────────────────────────────
    if (req.method === 'GET' && url.pathname === '/api/invokers') {
        const availability = await invokerRegistry.listAvailability();
        ctx.respondJson(res, availability);
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/dispatch') {
        ctx.respondJson(res, listActiveDispatches());
        return true;
    }
    if (req.method === 'POST' && url.pathname === '/api/dispatch') {
        const body = await ctx.readJsonBody(req);
        const goal = typeof body.goal === 'string' ? body.goal.trim() : '';
        if (!goal) {
            ctx.respondJson(res, { error: 'goal-required' }, 400);
            return true;
        }
        const operativeId = typeof body.operativeId === 'string' ? body.operativeId : 'dashboard-operator';
        const specialistId = typeof body.specialistId === 'string' ? body.specialistId : 'engineering.rapid-prototyper';
        const budgetCapUsd = typeof body.budgetCapUsd === 'number' ? body.budgetCapUsd : 2;
        let workingDir = '', repoName = '';
        try {
            const repoIdentity = ctx.getSelectedRepoIdentity(url);
            const ws = resolveWorkspaceContext({
                workspaceRoot: typeof repoIdentity?.repoRoot === 'string' && repoIdentity.repoRoot.trim()
                    ? repoIdentity.repoRoot.trim()
                    : ctx.repoRoot,
            });
            workingDir = ws.repoRoot;
            repoName = typeof repoIdentity?.repoName === 'string' && repoIdentity.repoName.trim()
                ? repoIdentity.repoName.trim()
                : ws.repoName || path.basename(ws.repoRoot) || path.basename(process.cwd());
        }
        catch { /* standalone mode: fallback to cwd */
            workingDir = process.cwd();
            repoName = path.basename(process.cwd());
        }
        const result = await pushDispatch({
            goal, specialistId, operativeId, budgetCapUsd,
            workingDir, repoName,
            timeoutMs: typeof body.timeoutMs === 'number' ? body.timeoutMs : undefined,
            preferredRuntime: typeof body.preferredRuntime === 'string' ? body.preferredRuntime : undefined,
        }).catch((err) => ({ error: err instanceof Error ? err.message : String(err) }));
        if ('error' in result) {
            ctx.respondJson(res, result, 503);
        }
        else {
            nexusEventBus.emit('dashboard.action', { action: 'dispatch.start', status: 'queued', target: result.runId });
            ctx.respondJson(res, result, 202);
        }
        return true;
    }
    if (req.method === 'POST' && /^\/api\/dispatch\/[^/]+\/cancel$/.test(url.pathname)) {
        const runId = url.pathname.split('/')[3] ?? '';
        const cancelled = await cancelDispatch(runId);
        ctx.respondJson(res, { cancelled, runId }, cancelled ? 200 : 404);
        return true;
    }
    if (req.method === 'POST' && url.pathname === '/api/orchestrate') {
        const body = await ctx.readJsonBody(req);
        const goal = typeof body?.goal === 'string' ? body.goal.trim() : '';
        if (!goal) {
            ctx.respondJson(res, { error: 'goal-required' }, 400);
            return true;
        }
        const orchestrator = ctx.getOrchestrator();
        if (!orchestrator) {
            ctx.respondJson(res, { error: 'nexus-unavailable', code: 'nexus-unavailable', hint: 'Nexus runtime is not attached to this dashboard process.' }, 503);
            return true;
        }
        const runId = `run_${Date.now()}`;
        createRun(runId, goal, 'dashboard');
        updateStage(runId, 'queued', 10);
        const spanId = startSpan(runId, 'dashboard.orchestrate', undefined, { source: body.source ?? 'dashboard' });
        // TTV: first goal submission from the dashboard counts as first interaction.
        void recordFirstInteraction().catch(() => { });
        // Fire-and-forget: orchestration runs in the background; caller polls /api/runs/:id.
        orchestrator.orchestrate(goal, { source: body.source ?? 'dashboard', dashboardRunId: runId })
            .then((run) => {
            const actualRunId = run?.runId ?? runId;
            if (actualRunId !== runId) {
                createRun(actualRunId, goal, 'dashboard');
                updateStage(actualRunId, 'orchestrating', 80);
            }
            const summary = summarizeDashboardRun(run, goal);
            completeRun(runId, summary);
            if (actualRunId !== runId)
                completeRun(actualRunId, summary);
            endSpan(spanId, run?.state === 'failed' ? 'error' : 'ok', { actualRunId, state: run?.state ?? 'completed' });
            nexusEventBus.emit('dashboard.action', {
                action: 'orchestrate.complete',
                status: run?.state === 'failed' ? 'fail' : 'ok',
                target: actualRunId,
            });
        })
            .catch((err) => {
            const message = err instanceof Error ? err.message : String(err);
            failRun(runId, message);
            endSpan(spanId, 'error', { error: message });
            nexusEventBus.emit('dashboard.action', {
                action: 'orchestrate.failed',
                status: 'fail',
                target: runId,
            });
        });
        nexusEventBus.emit('dashboard.action', { action: 'orchestrate.enqueue', status: 'queued', target: runId });
        ctx.respondJson(res, { queued: true, runId }, 202);
        return true;
    }
    return false;
};
