import fs from 'fs/promises';
import path from 'path';
import { LEARNING_FEEDBACK_FILE, LEARNING_PACKET_FILE, LEARNING_POLICY_DELTAS_FILE, LEARNING_SIGNALS_FILE, buildLearningGraph, buildLearningNetworkEnvelope, learningSignalsToJsonl, } from '../../engines/learning-network.js';
import { getRun as getStoredRun, listRecentRuns } from '../../engines/orchestrator/store.js';
import { resolveNexusStateDir } from '../../engines/runtime-registry.js';
import { CONTEXT_LOG_FILE, DECISION_LOG_FILE, REQUEST_BRIEF_FILE, SELECTION_PLAN_FILE, } from '../../engines/orchestrator/decision-spine.js';
import { listWorkspaceArtifactRuns, readWorkspaceArtifactRun, runTimestamp } from './run-artifacts.js';
async function readJson(run, filename, fallback) {
    if (!run?.artifactsPath)
        return fallback;
    try {
        return JSON.parse(await fs.readFile(path.join(run.artifactsPath, filename), 'utf8'));
    }
    catch {
        return fallback;
    }
}
async function readJsonl(run, filename, fallback = []) {
    if (!run?.artifactsPath)
        return fallback;
    try {
        const text = await fs.readFile(path.join(run.artifactsPath, filename), 'utf8');
        return text
            .split('\n')
            .map((line) => line.trim())
            .filter(Boolean)
            .map((line) => JSON.parse(line));
    }
    catch {
        return fallback;
    }
}
async function writeLearningArtifacts(run, envelope) {
    if (!run?.artifactsPath)
        return;
    await fs.mkdir(run.artifactsPath, { recursive: true });
    await Promise.all([
        fs.writeFile(path.join(run.artifactsPath, LEARNING_PACKET_FILE), JSON.stringify(envelope, null, 2), 'utf8'),
        fs.writeFile(path.join(run.artifactsPath, LEARNING_SIGNALS_FILE), learningSignalsToJsonl(envelope.signals), 'utf8'),
        fs.writeFile(path.join(run.artifactsPath, LEARNING_POLICY_DELTAS_FILE), JSON.stringify(envelope.policyDeltas, null, 2), 'utf8'),
    ]);
}
function fallbackFeedbackPath(runId) {
    const safeRunId = runId.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 120);
    return path.join(resolveNexusStateDir(), 'learning-feedback', `${safeRunId}.jsonl`);
}
async function readLearningFeedback(run) {
    const runId = String(run?.runId ?? run?.id ?? '');
    const artifactFeedback = await readJsonl(run, LEARNING_FEEDBACK_FILE, []);
    if (!runId)
        return artifactFeedback;
    try {
        const text = await fs.readFile(fallbackFeedbackPath(runId), 'utf8');
        return [
            ...artifactFeedback,
            ...text
                .split('\n')
                .map((line) => line.trim())
                .filter(Boolean)
                .map((line) => JSON.parse(line)),
        ];
    }
    catch {
        return artifactFeedback;
    }
}
async function appendLearningFeedback(run, feedback) {
    const runId = String(run?.runId ?? run?.id ?? feedback.runId ?? '');
    const target = run?.artifactsPath
        ? path.join(run.artifactsPath, LEARNING_FEEDBACK_FILE)
        : fallbackFeedbackPath(runId);
    await fs.mkdir(path.dirname(target), { recursive: true });
    await fs.appendFile(target, JSON.stringify(feedback) + '\n', 'utf8');
}
function selectedRepoStateKey(ctx, url) {
    const identity = ctx.getSelectedRepoIdentity(url);
    return typeof identity?.workspaceStateKey === 'string' ? identity.workspaceStateKey : undefined;
}
async function resolveRun(ctx, url, runId) {
    const snapshot = ctx.resolveRuntimeSnapshot(url);
    const runtime = ctx.getRuntime();
    const liveRun = snapshot?.runtimeId && runtime?.getRuntimeId?.() === snapshot.runtimeId
        ? await runtime?.getRun?.(runId)
        : await runtime?.getRun?.(runId);
    return liveRun
        ?? await readWorkspaceArtifactRun(selectedRepoStateKey(ctx, url), runId)
        ?? getStoredRun(runId);
}
async function collectRunEvidence(run, extraFeedback = []) {
    const [requestBrief, selectionPlan, contextLog, decisionLog, workerResults, verificationResults, feedback] = await Promise.all([
        readJson(run, REQUEST_BRIEF_FILE, run?.requestBrief ?? null),
        readJson(run, SELECTION_PLAN_FILE, run?.selectionPlan ?? null),
        readJsonl(run, CONTEXT_LOG_FILE, Array.isArray(run?.contextLog) ? run.contextLog : []),
        readJsonl(run, DECISION_LOG_FILE, Array.isArray(run?.decisionLog) ? run.decisionLog : []),
        readJson(run, 'worker-results.json', run?.workerResults ?? []),
        readJson(run, 'verification-results.json', run?.verificationResults ?? []),
        readLearningFeedback(run),
    ]);
    return {
        runId: String(run?.runId ?? run?.id ?? ''),
        goal: run?.goal ?? requestBrief?.rawPrompt,
        state: run?.state ?? run?.status,
        source: run?.source ?? run?.client,
        createdAt: run?.createdAt ?? run?.startedAt ?? runTimestamp(run),
        artifactsPath: run?.artifactsPath ?? null,
        requestBrief,
        selectionPlan,
        contextLog,
        decisionLog,
        workerResults: Array.isArray(workerResults) ? workerResults : [],
        verificationResults: Array.isArray(verificationResults) ? verificationResults : [],
        memoryChecks: run?.memoryChecks ?? [],
        backendEvidence: run?.backendEvidence,
        tokenTelemetry: run?.tokenTelemetry,
        userWorkflowTrace: run?.userWorkflowTrace,
        userFeedback: [...feedback, ...extraFeedback],
    };
}
async function compileRunLearning(run, extraFeedback = [], persist = true) {
    const envelope = buildLearningNetworkEnvelope(await collectRunEvidence(run, extraFeedback));
    if (persist) {
        await writeLearningArtifacts(run, envelope).catch(() => undefined);
    }
    return envelope;
}
async function listCandidateRuns(ctx, url, limit) {
    const runtime = ctx.getRuntime();
    const liveRuns = runtime?.listRuns?.(limit) ?? [];
    const artifactRuns = await listWorkspaceArtifactRuns(selectedRepoStateKey(ctx, url), limit);
    const storedRuns = listRecentRuns(limit);
    const seen = new Set();
    return [...liveRuns, ...artifactRuns, ...storedRuns]
        .filter((run) => {
        const candidate = run;
        const id = String(candidate?.runId ?? candidate?.id ?? '');
        if (!id || seen.has(id))
            return false;
        seen.add(id);
        return true;
    })
        .sort((left, right) => runTimestamp(right) - runTimestamp(left))
        .slice(0, Math.max(1, limit));
}
export const handleLearningRoutes = async (ctx, req, res, url) => {
    const runLearningMatch = req.method === 'GET'
        ? /^\/api\/runs\/([^/]+)\/learning$/.exec(url.pathname)
        : null;
    if (runLearningMatch) {
        const runId = decodeURIComponent(runLearningMatch[1]);
        const run = await resolveRun(ctx, url, runId);
        if (!run) {
            ctx.respondJson(res, { error: 'learning-run-not-found', runId }, 404);
            return true;
        }
        ctx.respondJson(res, await compileRunLearning(run));
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/learning/packets') {
        const limit = parseInt(url.searchParams.get('limit') || '20', 10);
        const runs = await listCandidateRuns(ctx, url, limit);
        const envelopes = await Promise.all(runs.map((run) => compileRunLearning(run).catch(() => null)));
        const valid = envelopes.filter((envelope) => Boolean(envelope));
        ctx.respondJson(res, {
            generatedAt: Date.now(),
            packets: valid.flatMap((envelope) => envelope.packets.map((packet) => ({ ...packet, runId: envelope.runId }))),
            envelopes: valid,
            graph: buildLearningGraph(valid),
            quality: {
                runs: valid.length,
                packets: valid.reduce((sum, envelope) => sum + envelope.packets.length, 0),
                promoted: valid.reduce((sum, envelope) => sum + envelope.quality.promotedCount, 0),
                testing: valid.reduce((sum, envelope) => sum + envelope.quality.testCount, 0),
            },
        });
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/learning/graph') {
        const limit = parseInt(url.searchParams.get('limit') || '30', 10);
        const runs = await listCandidateRuns(ctx, url, limit);
        const envelopes = (await Promise.all(runs.map((run) => compileRunLearning(run).catch(() => null))))
            .filter((envelope) => Boolean(envelope));
        ctx.respondJson(res, { generatedAt: Date.now(), ...buildLearningGraph(envelopes) });
        return true;
    }
    if (req.method === 'POST' && url.pathname === '/api/learning/feedback') {
        const body = await ctx.readJsonBody(req);
        const runId = typeof body.runId === 'string' ? body.runId.trim() : '';
        const rating = typeof body.rating === 'string' ? body.rating : '';
        if (!runId || !['accepted', 'rejected', 'useful', 'ignore'].includes(rating)) {
            ctx.respondJson(res, { error: 'runId-and-valid-rating-required' }, 400);
            return true;
        }
        const run = await resolveRun(ctx, url, runId);
        if (!run) {
            ctx.respondJson(res, { error: 'learning-run-not-found', runId }, 404);
            return true;
        }
        const feedback = {
            id: `lf_${Date.now().toString(36)}`,
            runId,
            packetId: typeof body.packetId === 'string' ? body.packetId : undefined,
            lessonId: typeof body.lessonId === 'string' ? body.lessonId : undefined,
            rating: rating,
            note: typeof body.note === 'string' ? body.note.slice(0, 500) : undefined,
            createdAt: Date.now(),
        };
        await appendLearningFeedback(run, feedback);
        const envelope = await compileRunLearning(run);
        ctx.respondJson(res, { ok: true, feedback, envelope }, 201);
        return true;
    }
    return false;
};
