import { readFileSync, existsSync, mkdirSync } from "fs";
import { writeFile } from "fs/promises";
import { dirname, join } from "path";
import { DEFAULT_AUTHORITY_PROVIDER } from "./types.js";
const READ_ONLY_ROLES = new Set([
    "explorer",
    "researcher",
    "reviewer",
    "qa",
    "tester",
    "documenter",
    "writer",
    "planner",
    "analyst",
    "auditor",
]);
const AUTHORITY_ROLES = new Set([
    "orchestrator",
    "coordinator",
    "merger",
    "integrator",
    "security",
]);
const FILE_AFFECTING_ROLES = new Set([
    "coder",
    "executor",
    "refactorer",
]);
const DEFAULT_CONTEXT_WINDOW = 128_000;
const DEFAULT_LATENCY_BUDGET = 0.75;
const DEFAULT_COST_BUDGET = 0.8;
export function buildProviderStatsKey(tier, role, taskType, complexity) {
    return `${tier}:${role}:${taskType || "unknown"}:${complexity || "unknown"}`;
}
const DEFAULT_STATS_PATH = join(process.cwd(), ".omk", "memory", "provider-model-stats.json");
let pendingSaveTimer = null;
let pendingSavePath = null;
let pendingSaveStats = null;
const SAVE_DEBOUNCE_MS = 100;
export function loadProviderModelStats(filePath = DEFAULT_STATS_PATH) {
    if (!existsSync(filePath)) {
        return { version: 2, entries: {}, updatedAt: Date.now() };
    }
    try {
        const raw = readFileSync(filePath, "utf-8");
        const parsed = JSON.parse(raw);
        if (!isProviderModelStats(parsed)) {
            const migrated = migrateProviderModelStats(parsed);
            if (migrated)
                return migrated;
            return { version: 2, entries: {}, updatedAt: Date.now() };
        }
        return parsed;
    }
    catch {
        return { version: 2, entries: {}, updatedAt: Date.now() };
    }
}
export function saveProviderModelStats(stats, filePath = DEFAULT_STATS_PATH) {
    // Store latest stats and path
    pendingSaveStats = stats;
    pendingSavePath = filePath;
    // Clear existing timer
    if (pendingSaveTimer) {
        clearTimeout(pendingSaveTimer);
        pendingSaveTimer = null;
    }
    // Schedule new write
    pendingSaveTimer = setTimeout(() => {
        flushProviderModelStats();
    }, SAVE_DEBOUNCE_MS);
}
export async function flushProviderModelStats() {
    if (pendingSaveTimer) {
        clearTimeout(pendingSaveTimer);
        pendingSaveTimer = null;
    }
    if (!pendingSaveStats || !pendingSavePath)
        return;
    const dir = dirname(pendingSavePath);
    if (!existsSync(dir)) {
        mkdirSync(dir, { recursive: true });
    }
    try {
        await writeFile(pendingSavePath, JSON.stringify({ ...pendingSaveStats, updatedAt: Date.now() }, null, 2), "utf-8");
    }
    catch {
        // Silently fail — telemetry is best-effort
    }
    pendingSaveStats = null;
    pendingSavePath = null;
}
function isProviderModelStats(value) {
    if (typeof value !== "object" || value === null)
        return false;
    const record = value;
    return (typeof record.version === "number" &&
        record.version >= 2 &&
        typeof record.entries === "object" &&
        record.entries !== null &&
        !Array.isArray(record.entries));
}
function migrateProviderModelStats(value) {
    if (typeof value !== "object" || value === null)
        return null;
    const record = value;
    // v1: flat Record<string, ProviderStats> without version field
    if (record.version === undefined &&
        typeof record.entries !== "object" &&
        Object.keys(record).length > 0) {
        const entries = {};
        for (const [key, entry] of Object.entries(record)) {
            if (typeof entry !== "object" || entry === null)
                continue;
            const e = entry;
            const attempts = typeof e.attempts === "number" ? e.attempts : 0;
            const passes = typeof e.passes === "number" ? e.passes : 0;
            const fallbacks = typeof e.fallbacks === "number" ? e.fallbacks : 0;
            const timeouts = typeof e.timeouts === "number" ? e.timeouts : 0;
            const failures = typeof e.failures === "number" ? e.failures : 0;
            const meanLatencyMs = typeof e.meanLatencyMs === "number" ? e.meanLatencyMs : 0;
            const lastAttemptAt = typeof e.lastAttemptAt === "number" ? e.lastAttemptAt : Date.now();
            const provider = typeof e.provider === "string" ? e.provider : "unknown";
            // Old v1 keys were `${tier}:${role}` — migrate to `${tier}:${role}:unknown:unknown`
            const newKey = key.includes(":") && key.split(":").length === 2
                ? `${key}:unknown:unknown`
                : key;
            entries[newKey] = {
                provider,
                tier: newKey.split(":")[0] ?? "unknown",
                role: newKey.split(":")[1] ?? "unknown",
                taskType: "unknown",
                complexity: "unknown",
                attempts,
                passes,
                failures,
                fallbacks,
                timeouts,
                meanLatencyMs,
                lastAttemptAt,
                evidencePassRate: attempts > 0 ? passes / attempts : 0.5,
                fallbackRate: attempts > 0 ? fallbacks / attempts : 0,
                timeoutRate: attempts > 0 ? timeouts / attempts : 0,
            };
        }
        return { version: 2, entries, updatedAt: Date.now() };
    }
    return null;
}
export function updateProviderModelStats(stats, key, patch) {
    const existing = stats.entries[key];
    const segments = key.split(":");
    const attempts = Math.max(0, (patch.attempts ?? existing?.attempts ?? 0));
    const passes = Math.max(0, (patch.passes ?? existing?.passes ?? 0));
    const failures = Math.max(0, (patch.failures ?? existing?.failures ?? 0));
    const fallbacks = Math.max(0, (patch.fallbacks ?? existing?.fallbacks ?? 0));
    const timeouts = Math.max(0, (patch.timeouts ?? existing?.timeouts ?? 0));
    const meanLatencyMs = patch.meanLatencyMs ?? existing?.meanLatencyMs ?? 0;
    const lastAttemptAt = patch.lastAttemptAt ?? existing?.lastAttemptAt ?? Date.now();
    const entry = {
        provider: existing?.provider ?? segments[0] ?? "unknown",
        tier: segments[0] ?? "unknown",
        role: segments[1] ?? "unknown",
        taskType: segments[2] ?? "unknown",
        complexity: segments[3] ?? "unknown",
        attempts,
        passes,
        failures,
        fallbacks,
        timeouts,
        meanLatencyMs,
        lastAttemptAt,
        evidencePassRate: attempts > 0 ? passes / attempts : 0.5,
        fallbackRate: attempts > 0 ? fallbacks / attempts : 0,
        timeoutRate: attempts > 0 ? timeouts / attempts : 0,
    };
    return {
        ...stats,
        entries: { ...stats.entries, [key]: entry },
        updatedAt: Date.now(),
    };
}
export function computeProviderRouteScore(provider, input) {
    const authority = input.authorityProvider ?? DEFAULT_AUTHORITY_PROVIDER;
    const stats = input.providerStats?.[provider];
    const granularStats = selectProviderModelStats(provider, input);
    // readOnlySafety (0.25): read-only tasks score higher for external providers
    const isReadOnlyTask = input.risk === "read" && !input.needsMcp && !input.needsToolCalling;
    const isSafeForExternal = isReadOnlyTask && (input.readOnly || READ_ONLY_ROLES.has(input.role));
    const readOnlySafety = provider === authority
        ? (isReadOnlyTask ? 0.8 : 1.0)
        : (isSafeForExternal ? 1.0 : 0.2);
    // expectedUtility (0.20): role-based expected fit
    const expectedUtility = computeExpectedUtility(provider, input.role, authority);
    // contextFit (0.15): token count fit for provider context window
    const window = providerContextWindow(provider, input);
    const ratio = input.estimatedTokens / window;
    let contextFit;
    if (ratio < 0.1)
        contextFit = 1.0;
    else if (ratio < 0.5)
        contextFit = 0.9;
    else if (ratio < 0.8)
        contextFit = 0.7;
    else
        contextFit = 0.4;
    // historicalPassRate (0.15): providerStats[provider].passes / attempts
    let historicalPassRate = 0.75;
    if (stats && stats.attempts > 0) {
        historicalPassRate = Math.max(0, Math.min(1, stats.passes / stats.attempts));
    }
    // Prefer granular stats when available
    if (granularStats && granularStats.attempts > 0) {
        historicalPassRate = Math.max(0, Math.min(1, granularStats.evidencePassRate));
    }
    // latencyBudget (0.10): inverse of meanLatencyMs
    let latencyBudget = DEFAULT_LATENCY_BUDGET;
    if (stats && stats.meanLatencyMs > 0) {
        latencyBudget = Math.max(0, Math.min(1, 1 - stats.meanLatencyMs / 10000));
    }
    if (granularStats && granularStats.meanLatencyMs > 0) {
        latencyBudget = Math.max(0, Math.min(1, 1 - granularStats.meanLatencyMs / 10000));
    }
    // costBudget (0.10): neutral until explicit normalized cost metadata exists
    const costBudget = DEFAULT_COST_BUDGET;
    // fallbackReliability (0.05): 1 - fallback rate
    let fallbackReliability = 0.9;
    if (stats && stats.attempts > 0) {
        fallbackReliability = Math.max(0, 1 - stats.fallbacks / stats.attempts);
    }
    if (granularStats && granularStats.attempts > 0) {
        fallbackReliability = Math.max(0, 1 - granularStats.fallbackRate);
    }
    let score = readOnlySafety * 0.25 +
        expectedUtility * 0.20 +
        contextFit * 0.15 +
        historicalPassRate * 0.15 +
        latencyBudget * 0.10 +
        costBudget * 0.10 +
        fallbackReliability * 0.05;
    // riskPenalty: subtract if risk === "write" and provider is not authority
    if (input.risk === "write" && provider !== authority) {
        score -= 0.35;
    }
    // complexity penalty for external providers on complex tasks
    if (provider !== authority && input.complexity === "complex") {
        score -= 0.20;
    }
    score = Math.max(0, Math.min(1, score));
    const reason = `${providerLabel(provider)} scored highest (${clampScore(score)}) for ${input.risk}-risk ${input.role} task (${input.complexity})`;
    return {
        score: clampScore(score),
        reason,
        confidence: clampScore(score),
    };
}
function selectProviderModelStats(provider, input) {
    const entries = input.providerModelStats;
    if (!entries)
        return undefined;
    const providerKey = buildProviderStatsKey(provider, input.role, "unknown", input.complexity);
    const keyedEntry = entries[providerKey];
    if (keyedEntry)
        return keyedEntry;
    const matches = Object.values(entries).filter((entry) => entry.provider === provider &&
        entry.role === input.role &&
        (entry.complexity === input.complexity || entry.complexity === "unknown"));
    return matches.find((entry) => entry.taskType === "unknown") ?? matches[0];
}
function computeExpectedUtility(provider, role, authority) {
    if (AUTHORITY_ROLES.has(role)) {
        return provider === authority ? 1.0 : 0.1;
    }
    if (READ_ONLY_ROLES.has(role)) {
        return provider === authority ? 0.7 : 0.85;
    }
    if (FILE_AFFECTING_ROLES.has(role)) {
        return provider === authority ? 0.95 : 0.4;
    }
    // Generic fallback for unknown roles
    return provider === authority ? 0.7 : 0.75;
}
function providerLabel(provider) {
    if (provider === "qwen")
        return "Qwen";
    if (provider === "codex")
        return "Codex";
    if (provider === "openrouter")
        return "OpenRouter";
    if (provider === "deepseek")
        return "DeepSeek";
    return provider;
}
function clampScore(value) {
    return Math.max(0, Math.min(1, Number(value.toFixed(2))));
}
function providerContextWindow(provider, input) {
    const explicitWindow = input.providerContextWindows?.[provider];
    if (typeof explicitWindow === "number" && Number.isFinite(explicitWindow) && explicitWindow > 0) {
        return explicitWindow;
    }
    return DEFAULT_CONTEXT_WINDOW;
}
