// Wire-safe entity types for the BFF (Back-For-Frontend) layer (ADR-0013 D4). These types
// travel over the HTTP wire between the BFF and the React UI. The DI port interfaces
// (UiStore, UiStoreFactoryOptions) remain in src/ui/store/types.ts to avoid a contracts→ui
// circular dependency.
// GroundedAnswerContextPackSummary projects the connected-context pack into a counts-only,
// browser-safe shape (Issue #187 / ADR-0022). The connected-context module is a pure-data
// peer; importing it does not pull in any IO or redaction code.
import { CANDIDATE_OMISSION_REASONS, CONNECTED_CONTEXT_SCHEMA_VERSION, } from "./connected-context.js";
export const DEFAULT_GROUNDING_LIMITS = {
    maxConnectedSources: 16,
    maxLocalKnowledgeSources: 16,
    maxPromptReferences: 8,
    maxExcerptChars: 900,
    referenceBudget: 10,
    hybridMaxCandidates: 24,
    hybridMaxExcerptBytes: 131_072,
};
// Hard safety ceilings: an operator config may TUNE a limit but never raise it past these
// (preserves the original DoS-bounding intent of the fan-out caps).
export const GROUNDING_LIMIT_CEILINGS = {
    maxConnectedSources: 64,
    maxLocalKnowledgeSources: 64,
    maxPromptReferences: 64,
    maxExcerptChars: 20_000,
    referenceBudget: 256,
    hybridMaxCandidates: 256,
    hybridMaxExcerptBytes: 524_288,
};
// Pure resolver: fill each field from `partial` when it is a positive integer, else the default;
// then clamp to the ceiling. Invalid (non-positive / non-integer) present fields fall back to the
// default here (the gateway parse layer is responsible for REJECTING malformed config explicitly).
export function resolveGroundingLimits(partial) {
    const pick = (key) => {
        const supplied = partial?.[key];
        const resolved = supplied !== undefined && Number.isInteger(supplied) && supplied >= 1
            ? supplied
            : DEFAULT_GROUNDING_LIMITS[key];
        return Math.min(resolved, GROUNDING_LIMIT_CEILINGS[key]);
    };
    return {
        maxConnectedSources: pick("maxConnectedSources"),
        maxLocalKnowledgeSources: pick("maxLocalKnowledgeSources"),
        maxPromptReferences: pick("maxPromptReferences"),
        maxExcerptChars: pick("maxExcerptChars"),
        referenceBudget: pick("referenceBudget"),
        hybridMaxCandidates: pick("hybridMaxCandidates"),
        hybridMaxExcerptBytes: pick("hybridMaxExcerptBytes"),
    };
}
// Epic #532 — a sane cap on the number of folders/files one chat may connect at once. The BFF
// rejects PATCHes whose connectedScopes list exceeds this; the store enforces the same as a
// defense-in-depth subset. Bounds retrieval fan-out cost (each source runs an independent,
// budget-split retrieval pass), so total work stays bounded regardless of N.
// Kept for back-compat; new code should read from a resolved GroundingLimits instance instead.
export const MAX_CONNECTED_SOURCES = DEFAULT_GROUNDING_LIMITS.maxConnectedSources;
// Epic #189 — sibling of MAX_CONNECTED_SOURCES for the local-knowledge (connector) source list. A
// chat may bind multiple capsules/capsule-sets at once; the BFF rejects PATCHes whose
// localKnowledgeScopes list exceeds this, and the store enforces the same as a defense-in-depth
// subset. Bounds the connector fan-out cost in the hybrid grounded path (Slice 2).
// Kept for back-compat; new code should read from a resolved GroundingLimits instance instead.
export const MAX_LOCAL_KNOWLEDGE_SOURCES = DEFAULT_GROUNDING_LIMITS.maxLocalKnowledgeSources;
function buildOmittedCounts(pack) {
    const counts = {};
    for (const reason of CANDIDATE_OMISSION_REASONS) {
        counts[reason] = 0;
    }
    for (const entry of pack.omitted) {
        counts[entry.reason] += 1;
    }
    return counts;
}
function hashString32(value) {
    let hash = 0x811c9dc5;
    for (let i = 0; i < value.length; i += 1) {
        hash ^= value.charCodeAt(i);
        hash = Math.imul(hash, 0x01000193);
    }
    return (hash >>> 0).toString(16).padStart(8, "0");
}
function displayScopeId(scopeId) {
    return `scope-${hashString32(scopeId)}`;
}
// Pure builder: derives a GroundedAnswerContextPackSummary from the source pack plus the
// BFF-computed citation count and total elapsed wall time. No IO, no redaction (the only
// scope-derived string carried is a deterministic display fingerprint); allocates one fresh object.
export function buildGroundedAnswerContextPackSummary(pack, citationCount, elapsedMs) {
    return {
        schemaVersion: CONNECTED_CONTEXT_SCHEMA_VERSION,
        scopeId: displayScopeId(pack.scope.scopeId),
        scopeKind: pack.scope.kind,
        fileCount: pack.scope.kind === "workspace-root" ? -1 : pack.scope.relativePaths.length,
        queryKind: pack.query.kind,
        usage: pack.usage,
        budget: pack.budget,
        citationCount,
        omittedCount: pack.omitted.length,
        omittedCounts: buildOmittedCounts(pack),
        uncertaintyCount: pack.uncertainty.length,
        elapsedMs,
    };
}
