/**
 * Knowledge Mix — controls how much the assistant leans on Second Brain
 * evidence vs. the model's own general knowledge for a given query.
 *
 * The single integer "secondBrainWeight" (0–100) drives three things:
 *
 *   1. RAG chunk budget    — how many brain files we feed the model.
 *   2. Retrieval ratio     — what fraction of the context budget RAG can claim.
 *   3. Prompt policy       — natural-language instruction injected into the
 *                            system prompt telling the model how to balance
 *                            its own knowledge against the evidence shown.
 *
 * Per-agent overrides (AgentKnowledgeEntry.secondBrainWeight) win over the
 * global config (g1nation.knowledgeMix.secondBrainWeight). Both fall back to
 * the default `DEFAULT_WEIGHT` (balanced) when nothing is set.
 *
 * Keeping this module isolated and pure makes it trivial to unit-test the
 * mapping curve and to extend it later (e.g. add a "creative" axis) without
 * touching retrieval or prompt assembly.
 */
import { getConfig } from '../config';
import { getOrCreateAgentEntry } from '../skills/agentKnowledgeMap';

export const DEFAULT_WEIGHT = 50;

/** Where the resolved weight came from — surfaced in `_lastRetrievalInfo` for UX. */
export type KnowledgeMixSource = 'agent' | 'global' | 'default';

export interface ResolvedKnowledgeMix {
    /** Integer in [0, 100]. */
    weight: number;
    source: KnowledgeMixSource;
    /** Agent name when `source === 'agent'`, else undefined. */
    agent?: string;
}

/**
 * Resolve the effective weight for the active turn.
 *
 * Precedence: per-agent → global config → default. Out-of-range values from
 * either source are clamped, never silently zeroed (a typo in JSON should not
 * disable retrieval entirely).
 */
export function resolveKnowledgeMix(agentFileOrName?: string): ResolvedKnowledgeMix {
    if (agentFileOrName && agentFileOrName !== 'none') {
        try {
            const entry = getOrCreateAgentEntry(agentFileOrName);
            if (typeof entry.secondBrainWeight === 'number' && Number.isFinite(entry.secondBrainWeight)) {
                return {
                    weight: _clamp(entry.secondBrainWeight),
                    source: 'agent',
                    agent: entry.name,
                };
            }
        } catch {
            // Map missing or unreadable — fall through to global.
        }
    }
    try {
        const cfg = getConfig();
        if (typeof cfg.knowledgeMixSecondBrainWeight === 'number') {
            return { weight: _clamp(cfg.knowledgeMixSecondBrainWeight), source: 'global' };
        }
    } catch {
        // getConfig should never throw in practice, but keep this safe in tests.
    }
    return { weight: DEFAULT_WEIGHT, source: 'default' };
}

function _clamp(n: number): number {
    if (!Number.isFinite(n)) return DEFAULT_WEIGHT;
    return Math.max(0, Math.min(100, Math.round(n)));
}

/**
 * Map a weight to the maximum number of brain files (long-term memory) the
 * retriever is allowed to consider for this turn.
 *
 * Curve was chosen so that:
 *   - 0    fully disables brain-file retrieval (model-only mode).
 *   - 50   maps to roughly the existing default (`memoryLongTermFiles`), so
 *          behaviour without any per-agent setting matches the status quo.
 *   - 100  pushes the cap toward `selectWithinBudget`'s hard ceiling (12).
 *
 * The configured `memoryLongTermFiles` is treated as the "balanced" baseline:
 * it's scaled up at high weights and damped at low weights.
 */
export function mapWeightToBrainFileLimit(weight: number, configuredLimit: number): number {
    const w = _clamp(weight);
    if (w === 0) return 0;
    const baseline = Math.max(1, configuredLimit || 6);
    // Linear interpolation:
    //  w=0   → 0
    //  w=25  → baseline * 0.5
    //  w=50  → baseline
    //  w=75  → baseline * 1.5
    //  w=100 → baseline * 2  (capped at 12 elsewhere)
    const scaled = Math.round((w / 50) * baseline);
    // Honour the orchestrator's hard cap (12) so we never blow the budget.
    return Math.max(0, Math.min(12, scaled));
}

/**
 * Map a weight to the retrieval ratio (fraction of the context-budget that
 * RAG can claim). Lower weights mean RAG gets a smaller slice and leaves more
 * room for conversation history / system prompt.
 */
export function mapWeightToRetrievalRatio(weight: number): number {
    const w = _clamp(weight);
    // 0 → 0.05 (still room for tiny lessons block), 50 → 0.40 (status quo), 100 → 0.60.
    return Math.max(0.05, Math.min(0.6, 0.05 + (w / 100) * 0.55));
}

/**
 * Build the natural-language policy block injected into the system prompt.
 * Returns `''` when the weight is exactly the default (50) — at the midpoint
 * there's nothing useful to tell the model, and quieter prompts behave better.
 */
export function buildKnowledgeMixPolicy(mix: ResolvedKnowledgeMix): string {
    const w = _clamp(mix.weight);
    if (w === DEFAULT_WEIGHT) return '';
    const header = `[KNOWLEDGE MIX POLICY]\nSecond Brain reliance: ${w}% (model knowledge: ${100 - w}%).`;
    let body: string;
    if (w === 0) {
        body = [
            'Second Brain retrieval is disabled for this turn.',
            'Answer from your general knowledge and the conversation history alone.',
            'Do not invent file citations.',
        ].join('\n');
    } else if (w < 25) {
        body = [
            'Rely primarily on your own general knowledge.',
            'Treat any Second Brain notes shown below as light reference material only.',
            'Brainstorming, broad explanations and creative synthesis are encouraged.',
        ].join('\n');
    } else if (w < 50) {
        body = [
            'Lean on your general knowledge; use Second Brain notes as supporting context.',
            'Cite Brain files only when they materially shape the answer.',
        ].join('\n');
    } else if (w < 75) {
        body = [
            'Prefer Second Brain evidence when it is present.',
            'Use your general knowledge to connect, explain, and fill harmless background.',
            'Do not override explicit Second Brain evidence with model assumptions.',
        ].join('\n');
    } else if (w < 100) {
        body = [
            'Treat Second Brain notes as the primary evidence for this answer.',
            'Cite Brain files for any non-trivial claim and quote relevant lines when needed.',
            'If the notes do not cover a point, say so explicitly instead of guessing.',
        ].join('\n');
    } else {
        body = [
            'Second Brain notes are the only authoritative source for this answer.',
            'Cite Brain files for every substantive claim.',
            'If a point is not in the notes, reply that it is outside the recorded knowledge — do not fall back to general knowledge.',
        ].join('\n');
    }
    return `${header}\n${body}`;
}
