// Conversation budget estimator (Issue #151 / Epic #142).
//
// Pure, deterministic helper that estimates how much of a chat model's context
// window is consumed by a conversation turn. The UI uses this for the
// pre-send pressure indicator (AC#1) and the "clear history" affordance
// (AC#4). The breakdown carries connected-context byte counts so they can
// be disclosed and removable from the next call (AC#5).
//
// Engineering note (from the issue): token counting is APPROXIMATE here.
// We use bytes/4 as a rough proxy. UI copy and tests MUST state this
// precisely — do not present byte estimates as exact tokens.
// Average ASCII byte ≈ 4 chars per token in mainstream BPE tokenizers. This
// is an over-simplification; we surface it as APPROXIMATE everywhere.
const APPROX_BYTES_PER_TOKEN = 4;
// Pressure thresholds, applied against the fraction of availableInputTokens
// the approximate token count consumes.
const PRESSURE_LOW = 0.5;
const PRESSURE_MODERATE = 0.75;
const PRESSURE_HIGH = 0.95;
function coerceNonNegativeInt(value) {
    if (value === undefined)
        return 0;
    if (!Number.isFinite(value))
        return 0;
    if (value <= 0)
        return 0;
    return Math.floor(value);
}
function utf8ByteLength(text) {
    // Browsers and Node both expose TextEncoder. Falls back to char length
    // when (in some legacy harness) it is absent — acceptable since the
    // result is documented approximate.
    if (typeof TextEncoder !== "undefined") {
        return new TextEncoder().encode(text).length;
    }
    return text.length;
}
function sumHistoryBytes(history) {
    let total = 0;
    for (const message of history) {
        total += utf8ByteLength(message.role);
        total += utf8ByteLength(message.content);
    }
    return total;
}
function sumDocumentBytes(documents) {
    if (documents === undefined)
        return 0;
    let total = 0;
    for (const document of documents) {
        total += coerceNonNegativeInt(document.extractedBytes);
    }
    return total;
}
function classifyPressure(tokens, availableInputTokens) {
    if (availableInputTokens <= 0)
        return "exceeded";
    const ratio = tokens / availableInputTokens;
    if (ratio > PRESSURE_HIGH)
        return "exceeded";
    if (ratio > PRESSURE_MODERATE)
        return "high";
    if (ratio > PRESSURE_LOW)
        return "moderate";
    return "low";
}
export function estimateConversationBudget(input) {
    const contextWindowTokens = coerceNonNegativeInt(input.modelContextWindow);
    const reservedOutputTokens = coerceNonNegativeInt(input.modelMaxOutputTokens);
    const availableInputTokens = Math.max(0, contextWindowTokens - reservedOutputTokens);
    const draftBytes = utf8ByteLength(input.userDraftText);
    const historyBytes = sumHistoryBytes(input.conversationHistory);
    const documentBytes = sumDocumentBytes(input.documentContext);
    const repoContextBytes = coerceNonNegativeInt(input.repoContextPackBytes);
    const knowledgeBytes = coerceNonNegativeInt(input.knowledgeCapsuleBytes);
    const memoryBytes = coerceNonNegativeInt(input.memoryContextBytes);
    const breakdown = {
        draftBytes,
        historyBytes,
        documentBytes,
        repoContextBytes,
        knowledgeBytes,
        memoryBytes,
    };
    const approximateBytes = draftBytes + historyBytes + documentBytes + repoContextBytes + knowledgeBytes + memoryBytes;
    const approximateTokens = Math.ceil(approximateBytes / APPROX_BYTES_PER_TOKEN);
    const pressure = classifyPressure(approximateTokens, availableInputTokens);
    return {
        approximateBytes,
        approximateTokens,
        contextWindowTokens,
        reservedOutputTokens,
        availableInputTokens,
        pressure,
        breakdown,
    };
}
