import { resolveHubProtocolSpec } from './protocol-spec.js';
import { normalizeProviderProtocolTokenWithNative } from '../../../router/virtual-router/engine-selection/native-hub-pipeline-req-inbound-semantics.js';
let hubPolicyRuntime = undefined;
export function setHubPolicyRuntimePolicy(policy) {
    hubPolicyRuntime = policy;
}
function resolveEffectivePolicy(policy) {
    return policy ?? hubPolicyRuntime;
}
function shouldSample(rate) {
    if (rate === undefined)
        return true;
    if (!Number.isFinite(rate))
        return true;
    if (rate <= 0)
        return false;
    if (rate >= 1)
        return true;
    return Math.random() < rate;
}
function isJsonRecord(value) {
    return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function hasGeminiCliRequestWrapper(payload) {
    const request = payload.request;
    if (!isJsonRecord(request)) {
        return false;
    }
    const req = request;
    return (Object.prototype.hasOwnProperty.call(req, 'contents') ||
        Object.prototype.hasOwnProperty.call(req, 'systemInstruction') ||
        Object.prototype.hasOwnProperty.call(req, 'tools') ||
        Object.prototype.hasOwnProperty.call(req, 'toolConfig') ||
        Object.prototype.hasOwnProperty.call(req, 'generationConfig') ||
        Object.prototype.hasOwnProperty.call(req, 'safetySettings'));
}
function isGeminiCliPayload(payload) {
    const record = payload;
    return (typeof record.project === 'string' ||
        typeof record.requestType === 'string' ||
        typeof record.userAgent === 'string' ||
        typeof record.requestId === 'string' ||
        hasGeminiCliRequestWrapper(payload));
}
function normalizeHubProviderProtocol(providerProtocol) {
    return normalizeProviderProtocolTokenWithNative(providerProtocol) ?? providerProtocol;
}
function extendGeminiAllowlistIfNeeded(providerProtocol, payload, allowedTopLevelKeys) {
    if (providerProtocol !== 'gemini-chat') {
        return allowedTopLevelKeys;
    }
    if (!isGeminiCliPayload(payload)) {
        return allowedTopLevelKeys;
    }
    const extended = allowedTopLevelKeys ? new Set(allowedTopLevelKeys) : new Set();
    extended.add('request');
    extended.add('project');
    extended.add('requestId');
    extended.add('requestType');
    extended.add('userAgent');
    extended.add('action');
    return extended;
}
function applyProviderOutboundPolicy(providerProtocol, payload) {
    const removedTopLevelKeys = [];
    const flattenedWrappers = [];
    const spec = resolveHubProtocolSpec(providerProtocol);
    if (!spec.providerOutbound.enforceEnabled) {
        return {
            payload,
            changed: false,
            removedTopLevelKeys,
            flattenedWrappers
        };
    }
    let out = payload;
    const ensureOutClone = () => {
        if (out === payload) {
            out = { ...payload };
        }
    };
    const allowedTopLevelKeys = Array.isArray(spec.providerOutbound.allowedTopLevelKeys) &&
        spec.providerOutbound.allowedTopLevelKeys.length > 0 &&
        spec.providerOutbound.enforceAllowedTopLevelKeys === true
        ? new Set(spec.providerOutbound.allowedTopLevelKeys)
        : undefined;
    const isGeminiCli = providerProtocol === 'gemini-chat' && isGeminiCliPayload(payload);
    const allowedTopLevelKeysResolved = extendGeminiAllowlistIfNeeded(providerProtocol, payload, allowedTopLevelKeys);
    // Reserved/private keys must never be sent upstream.
    for (const key of Object.keys(payload)) {
        if (spec.providerOutbound.reservedKeyPrefixes.some((prefix) => key.startsWith(prefix))) {
            ensureOutClone();
            delete out[key];
            removedTopLevelKeys.push(key);
        }
    }
    // Flatten accidental wrappers that have caused upstream 400s.
    for (const rule of spec.providerOutbound.flattenWrappers) {
        const wrapperKey = rule.wrapperKey;
        if (!wrapperKey || typeof wrapperKey !== 'string') {
            continue;
        }
        if (isGeminiCli && wrapperKey === 'request') {
            continue;
        }
        if (!isJsonRecord(out[wrapperKey])) {
            continue;
        }
        ensureOutClone();
        const inner = { ...out[wrapperKey] };
        const alias = rule.aliasKeys || {};
        for (const [from, to] of Object.entries(alias)) {
            if (inner[to] === undefined && inner[from] !== undefined) {
                inner[to] = inner[from];
            }
        }
        const allow = Array.isArray(rule.allowKeys) ? new Set(rule.allowKeys) : null;
        const onlyIfMissing = rule.onlyIfTargetMissing !== false;
        for (const [key, value] of Object.entries(inner)) {
            if (allow && !allow.has(key)) {
                continue;
            }
            if (!onlyIfMissing || out[key] === undefined) {
                out[key] = value;
            }
        }
        delete out[wrapperKey];
        flattenedWrappers.push(wrapperKey);
    }
    // Enforce protocol allowlist (top-level). Only runs when explicitly enabled
    // for this protocol, and only after wrapper flatten so allowed fields are
    // present at the correct level.
    if (allowedTopLevelKeysResolved) {
        for (const key of Object.keys(out)) {
            if (allowedTopLevelKeysResolved.has(key))
                continue;
            ensureOutClone();
            delete out[key];
            removedTopLevelKeys.push(key);
        }
    }
    return {
        payload: out,
        changed: out !== payload || removedTopLevelKeys.length > 0 || flattenedWrappers.length > 0,
        removedTopLevelKeys,
        flattenedWrappers
    };
}
function observeProviderPayload(options) {
    const violations = [];
    // Observe-only: detect known layout anti-patterns and reserved keys.
    // Do NOT modify payload here.
    const spec = resolveHubProtocolSpec(options.providerProtocol);
    const allowlistEnabled = options.phase === 'provider_outbound';
    const isGeminiCli = options.providerProtocol === 'gemini-chat' && isGeminiCliPayload(options.payload);
    const allowedTopLevelKeys = allowlistEnabled && Array.isArray(spec.providerOutbound.allowedTopLevelKeys)
        ? new Set(spec.providerOutbound.allowedTopLevelKeys)
        : undefined;
    const allowedTopLevelKeysResolved = extendGeminiAllowlistIfNeeded(options.providerProtocol, options.payload, allowedTopLevelKeys);
    for (const rule of spec.providerOutbound.forbidWrappers) {
        if (rule.code !== 'forbid_wrapper') {
            continue;
        }
        if (isGeminiCli && rule.path === 'request') {
            continue;
        }
        if (rule.path in options.payload && isJsonRecord(options.payload[rule.path])) {
            violations.push({
                code: 'unexpected_wrapper',
                path: rule.path,
                detail: rule.detail
            });
        }
    }
    // Always record unknown private wrapper keys (best-effort, conservative).
    for (const key of Object.keys(options.payload)) {
        if (spec.providerOutbound.reservedKeyPrefixes.some((prefix) => key.startsWith(prefix))) {
            violations.push({
                code: 'unexpected_field',
                path: key
            });
            continue;
        }
        if (allowedTopLevelKeysResolved && !allowedTopLevelKeysResolved.has(key)) {
            violations.push({
                code: 'unexpected_field',
                path: key,
                detail: `Top-level key is not in protocol allowlist: ${options.providerProtocol}`
            });
        }
    }
    const unexpectedFieldCount = violations.filter((v) => v.code === 'unexpected_field').length;
    return {
        phase: options.phase,
        providerProtocol: options.providerProtocol,
        violations,
        summary: {
            totalViolations: violations.length,
            unexpectedFieldCount
        }
    };
}
export function recordHubPolicyObservation(options) {
    if (!options.stageRecorder) {
        return;
    }
    const effectivePolicy = resolveEffectivePolicy(options.policy);
    const mode = effectivePolicy?.mode ?? 'off';
    // Keep observing in enforce mode (best-effort) so operators can monitor
    // violations while gradually turning enforcement on.
    if (mode !== 'observe' && mode !== 'enforce') {
        return;
    }
    const compatibilityProfile = typeof options.compatibilityProfile === 'string' ? options.compatibilityProfile.trim().toLowerCase() : '';
    if (compatibilityProfile === 'chat:deepseek-web') {
        return;
    }
    if (!shouldSample(effectivePolicy?.sampleRate)) {
        return;
    }
    const normalizedProviderProtocol = normalizeHubProviderProtocol(options.providerProtocol);
    try {
        const phase = options.phase ?? 'provider_outbound';
        const observation = observeProviderPayload({
            phase,
            providerProtocol: normalizedProviderProtocol,
            payload: options.payload
        });
        if (observation.summary.totalViolations <= 0) {
            return;
        }
        const stage = `hub_policy.observe.${phase}`;
        options.stageRecorder.record(stage, {
            requestId: options.requestId,
            ...observation
        });
    }
    catch {
        // observe-only must never break the pipeline
    }
}
export function applyHubProviderOutboundPolicy(options) {
    const effectivePolicy = resolveEffectivePolicy(options.policy);
    const mode = effectivePolicy?.mode ?? 'off';
    if (mode !== 'enforce') {
        return options.payload;
    }
    const normalizedProviderProtocol = normalizeHubProviderProtocol(options.providerProtocol);
    const compatibilityProfile = typeof options.compatibilityProfile === 'string' ? options.compatibilityProfile.trim().toLowerCase() : '';
    if (compatibilityProfile === 'chat:deepseek-web') {
        return options.payload;
    }
    const result = applyProviderOutboundPolicy(normalizedProviderProtocol, options.payload);
    if (!result.changed) {
        return options.payload;
    }
    try {
        options.stageRecorder?.record('hub_policy.enforce.provider_outbound', {
            requestId: options.requestId,
            providerProtocol: normalizedProviderProtocol,
            removedTopLevelKeys: result.removedTopLevelKeys,
            flattenedWrappers: result.flattenedWrappers
        });
    }
    catch {
        // policy enforcement recording must not break the pipeline
    }
    return result.payload;
}
