import { OverflowHandler } from '../../tools/shared/overflow.js';
// ─── Default Policies ────────────────────────────────────────────────────────
/** Approximate bytes-per-token ratio used for the soft token estimate. */
const BYTES_PER_TOKEN = 4;
/**
 * DEFAULT_POLICIES — one policy per tool class.
 *
 * Byte limits are calibrated to stay well under typical LLM context windows:
 * - read    : 200 KB — file reads can be large; spill remainder to disk
 * - write   : 32 KB  — confirmations should be concise; inline truncation
 * - execute : 128 KB — command output can be chatty; keep tail (most recent)
 * - network : 256 KB — remote responses can be large; spill to disk
 * - analyze : 64 KB  — structured summaries; inline truncation
 */
export const DEFAULT_POLICIES = {
    read: {
        toolClass: 'read',
        maxBytes: 200 * 1024,
        maxTokens: Math.round((200 * 1024) / BYTES_PER_TOKEN),
        truncationMode: 'tail',
        spillMode: 'file',
        auditMetadata: true,
    },
    write: {
        toolClass: 'write',
        maxBytes: 32 * 1024,
        maxTokens: Math.round((32 * 1024) / BYTES_PER_TOKEN),
        truncationMode: 'tail',
        spillMode: 'inline',
        auditMetadata: true,
    },
    execute: {
        toolClass: 'execute',
        maxBytes: 128 * 1024,
        maxTokens: Math.round((128 * 1024) / BYTES_PER_TOKEN),
        truncationMode: 'head',
        spillMode: 'file',
        auditMetadata: true,
    },
    network: {
        toolClass: 'network',
        maxBytes: 256 * 1024,
        maxTokens: Math.round((256 * 1024) / BYTES_PER_TOKEN),
        truncationMode: 'tail',
        spillMode: 'file',
        auditMetadata: true,
    },
    analyze: {
        toolClass: 'analyze',
        maxBytes: 64 * 1024,
        maxTokens: Math.round((64 * 1024) / BYTES_PER_TOKEN),
        truncationMode: 'tail',
        spillMode: 'inline',
        auditMetadata: true,
    },
};
// ─── Policy Lookup ───────────────────────────────────────────────────────────
/**
 * Returns the output policy for the given tool class.
 * Always returns a defined policy — falls back to the `read` policy if
 * an unrecognised class is supplied (should never happen in strict TypeScript
 * but guards against runtime extension points).
 *
 * @param toolClass - The class of the tool whose policy to retrieve.
 */
export function getPolicy(toolClass) {
    return DEFAULT_POLICIES[toolClass] ?? DEFAULT_POLICIES.read;
}
// ─── Truncation Helpers ──────────────────────────────────────────────────────
/**
 * Apply the configured truncation mode to `content`, cutting to `maxBytes`.
 * Returns the truncated string.
 */
function truncate(content, maxBytes, mode) {
    const encoder = new TextEncoder();
    const encoded = encoder.encode(content);
    if (encoded.length <= maxBytes) {
        return content;
    }
    // Byte length of the full content (for truncation messages only)
    const byteLen = encoded.length;
    switch (mode) {
        case 'head': {
            // Keep the tail (most recent output) — slice at string level to avoid
            // splitting multi-byte UTF-8 code points that byte-slicing would corrupt.
            const dropChars = Math.floor(content.length * (1 - maxBytes / byteLen));
            const tail = content.slice(dropChars);
            const droppedBytes = byteLen - encoder.encode(tail).length;
            return `[... truncated ${droppedBytes} bytes from start]\n` + tail;
        }
        case 'middle': {
            // Keep equal portions from start and end at string level.
            const keepRatio = maxBytes / byteLen;
            const halfChars = Math.floor((content.length * keepRatio) / 2);
            const start = content.slice(0, halfChars);
            const end = content.slice(content.length - halfChars);
            const dropped = byteLen - encoder.encode(start).length - encoder.encode(end).length;
            return start + `\n[... ${dropped} bytes omitted ...]\n` + end;
        }
        case 'summary':
        // Future: replace with AI-generated summary; fall through to tail for now
        // falls through
        case 'tail':
        default: {
            // Keep the head — slice at string level to avoid corrupting multi-byte chars.
            const keepRatio = maxBytes / byteLen;
            const keepChars = Math.floor(content.length * keepRatio);
            const head = content.slice(0, keepChars);
            const droppedBytes = byteLen - encoder.encode(head).length;
            return head + `\n[... truncated ${droppedBytes} bytes]`;
        }
    }
}
// ─── Policy Enforcement ──────────────────────────────────────────────────────
/**
 * Applies the output policy to a tool result, enforcing byte limits and
 * attaching an audit record.
 *
 * The input `result` is **mutated in place** (output field updated) and also
 * returned as `ToolResultWithAudit` so callers can use the return value
 * directly without re-reading `record.result`.
 *
 * @param result - The raw tool result to enforce limits on.
 * @param policy - The policy to apply (obtain via `getPolicy`).
 * @returns The mutated result with `_policyAudit` attached, plus a standalone
 *          `OutputPolicyResult` audit record.
 */
export function applyOutputPolicy(result, policy, overflowHandler) {
    const output = typeof result.output === 'string' ? result.output : '';
    const encoder = new TextEncoder();
    const originalSize = encoder.encode(output).length;
    const audit = {
        policyId: policy.toolClass,
        actionTaken: 'none',
        originalSize,
        resultSize: originalSize,
    };
    const auditResult = result;
    // Within limits — no transformation needed
    if (originalSize <= policy.maxBytes) {
        if (policy.auditMetadata) {
            auditResult._policyAudit = audit;
        }
        return { result: auditResult, audit };
    }
    // Over limit — enforce according to spillMode
    switch (policy.spillMode) {
        case 'file': {
            const overflowResult = overflowHandler.handle(output, {
                // Convert bytes to a conservative char estimate (worst-case 4 bytes per
                // UTF-8 char). The overflow handler works in chars, not bytes.
                maxChars: Math.floor(policy.maxBytes / 4),
                label: policy.toolClass,
            });
            result.output = overflowResult.content;
            const actionTaken = overflowResult.overflowRef
                ? 'spilled'
                : 'truncated';
            audit.actionTaken = actionTaken;
            audit.resultSize = encoder.encode(overflowResult.content).length;
            if (overflowResult.spillBackend) {
                audit.spillBackend = overflowResult.spillBackend;
            }
            break;
        }
        case 'reference': {
            const sizeKb = (originalSize / 1024).toFixed(1);
            result.output = `[output omitted: ${sizeKb} KB — ${policy.toolClass} tool output exceeded policy limit of ${policy.maxBytes} bytes]`;
            audit.actionTaken = 'referenced';
            audit.resultSize = encoder.encode(result.output).length;
            break;
        }
        case 'inline':
        default: {
            result.output = truncate(output, policy.maxBytes, policy.truncationMode);
            audit.actionTaken = 'truncated';
            audit.resultSize = encoder.encode(result.output).length;
            break;
        }
    }
    if (policy.auditMetadata) {
        auditResult._policyAudit = audit;
    }
    return { result: auditResult, audit };
}
