"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OUTPUT_DETAILS = void 0;
exports.detectOutputProjectionOptions = detectOutputProjectionOptions;
exports.validateOutputProjectionOptions = validateOutputProjectionOptions;
exports.projectOutput = projectOutput;
const pagination_1 = require("./pagination");
const budget_1 = require("./budget");
exports.OUTPUT_DETAILS = ["compact", "summary", "full"];
const SEVERITIES = new Set([
    "informational",
    "warning",
    "error"
]);
const SAFE_FIELD_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/u;
const SAFE_FINDING_CODE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/u;
function detectOutputProjectionOptions(argv) {
    const options = {};
    const findingCodes = [];
    const severities = [];
    const fields = [];
    const limits = {};
    for (let index = 2; index < argv.length; index += 1) {
        const token = argv[index];
        const [optionName, inlineValue] = token.includes("=")
            ? token.split(/=(.*)/su, 2)
            : [token, undefined];
        if (![
            "--detail",
            "--finding-code",
            "--severity",
            "--fields",
            "--limit",
            "--page-lane",
            "--page-size",
            "--page-cursor",
            "--output-budget",
            "--json-spacing"
        ].includes(optionName)) {
            continue;
        }
        const value = inlineValue ?? argv[index + 1];
        if (value === undefined || value.startsWith("--")) {
            throw new TypeError(`Output option '${optionName}' requires a value.`);
        }
        if (inlineValue === undefined) {
            index += 1;
        }
        if (optionName === "--detail") {
            options.detail = value;
        }
        else if (optionName === "--finding-code") {
            findingCodes.push(value);
        }
        else if (optionName === "--severity") {
            severities.push(value);
        }
        else if (optionName === "--fields") {
            fields.push(...value.split(","));
        }
        else if (optionName === "--limit") {
            const separator = value.indexOf("=");
            if (separator <= 0 || separator === value.length - 1) {
                throw new TypeError("Output limits must use '<lane>=<count>'.");
            }
            const lane = value.slice(0, separator);
            const countToken = value.slice(separator + 1);
            if (!/^\d+$/u.test(countToken)) {
                throw new TypeError(`Output lane '${lane}' requires a non-negative safe integer limit.`);
            }
            limits[lane] = Number(countToken);
        }
        else if (optionName === "--page-lane") {
            options.pageLane = value;
        }
        else if (optionName === "--page-size") {
            if (!/^\d+$/u.test(value)) {
                throw new TypeError("Pagination page size must be a positive safe integer.");
            }
            options.pageSize = Number(value);
        }
        else {
            if (optionName === "--page-cursor") {
                options.pageCursor = value;
            }
            else if (optionName === "--output-budget") {
                if (!/^\d+$/u.test(value)) {
                    throw new TypeError("Output budget must be a safe integer byte count.");
                }
                options.outputBudget = Number(value);
            }
            else {
                options.jsonSpacing = value;
            }
        }
    }
    return validateOutputProjectionOptions({
        ...options,
        findingCodes,
        severities,
        fields,
        limits
    });
}
function validateOutputProjectionOptions(options) {
    const detail = options.detail ?? "full";
    if (!exports.OUTPUT_DETAILS.includes(detail)) {
        throw new TypeError(`Unsupported output detail '${String(detail)}'.`);
    }
    const findingCodes = normalizeTokens(options.findingCodes, "finding code", SAFE_FINDING_CODE_PATTERN);
    const fields = normalizeTokens(options.fields, "field", SAFE_FIELD_PATTERN);
    const severities = [...(options.severities ?? [])];
    const pageLane = options.pageLane?.trim();
    const pageSize = options.pageSize;
    const pageCursor = options.pageCursor;
    const outputBudget = options.outputBudget;
    const jsonSpacing = options.jsonSpacing;
    for (const severity of severities) {
        if (!SEVERITIES.has(severity)) {
            throw new TypeError(`Unsupported finding severity '${String(severity)}'.`);
        }
    }
    const limits = {};
    for (const [lane, limit] of Object.entries(options.limits ?? {})) {
        if (!SAFE_FIELD_PATTERN.test(lane)) {
            throw new TypeError(`Invalid output lane '${lane}'.`);
        }
        if (!Number.isSafeInteger(limit) || limit < 0) {
            throw new TypeError(`Output lane '${lane}' requires a non-negative safe integer limit.`);
        }
        limits[lane] = limit;
    }
    const paginationControlCount = [pageLane, pageSize, pageCursor]
        .filter((value) => value !== undefined).length;
    if (paginationControlCount > 0) {
        if (pageLane === undefined || !SAFE_FIELD_PATTERN.test(pageLane)) {
            throw new TypeError("Pagination requires a safe --page-lane value.");
        }
        if (!Number.isSafeInteger(pageSize) || pageSize < 1) {
            throw new TypeError("Pagination requires --page-size with a positive safe integer.");
        }
        if (pageCursor !== undefined && !/^[A-Za-z0-9_-]+$/u.test(pageCursor)) {
            throw new TypeError("Pagination cursor is not valid base64url text.");
        }
        if (limits[pageLane] !== undefined) {
            throw new TypeError("A paginated lane cannot also use a simple lane limit.");
        }
    }
    if (outputBudget !== undefined && (!Number.isSafeInteger(outputBudget)
        || outputBudget < budget_1.MINIMUM_OUTPUT_BUDGET_BYTES)) {
        throw new TypeError(`Output budget must be at least ${budget_1.MINIMUM_OUTPUT_BUDGET_BYTES} bytes.`);
    }
    if (jsonSpacing !== undefined && !budget_1.JSON_SPACING_VALUES.includes(jsonSpacing)) {
        throw new TypeError(`JSON spacing must be one of: ${budget_1.JSON_SPACING_VALUES.join(", ")}.`);
    }
    return {
        detail,
        findingCodes,
        severities,
        fields,
        limits,
        ...(pageLane === undefined ? {} : { pageLane }),
        ...(pageSize === undefined ? {} : { pageSize }),
        ...(pageCursor === undefined ? {} : { pageCursor }),
        ...(outputBudget === undefined ? {} : { outputBudget }),
        ...(jsonSpacing === undefined ? {} : { jsonSpacing }),
        ...(options.pagination === undefined ? {} : { pagination: options.pagination }),
        ...(options.compactCollectionProfile === undefined
            ? {}
            : { compactCollectionProfile: options.compactCollectionProfile })
    };
}
function projectOutput(data, findings, rawOptions = {}) {
    const options = validateOutputProjectionOptions(rawOptions);
    const detail = options.detail ?? "full";
    const codeFilter = new Set(options.findingCodes);
    const severityFilter = new Set(options.severities);
    const selectedFindings = findings.filter((finding) => {
        if (finding.severity === "error") {
            return true;
        }
        return (codeFilter.size === 0 || codeFilter.has(finding.code))
            && (severityFilter.size === 0 || severityFilter.has(finding.severity));
    });
    const findingGroups = groupFindings(selectedFindings);
    const detailFindings = detail === "full"
        ? [...selectedFindings]
        : deduplicateNonBlockingFindings(selectedFindings).map((finding) => (detail === "compact" ? compactFinding(finding) : finding));
    const returnedFindings = limitFindings(detailFindings, options.limits?.findings);
    const lanes = {
        findings: laneMetadata(findings.length, returnedFindings.length)
    };
    const dataProjection = projectData(data, options, lanes);
    const incomplete = Object.values(lanes).some((lane) => !lane.complete);
    return {
        data: dataProjection.data,
        findings: returnedFindings,
        output: {
            detail,
            complete: !incomplete,
            truncated: incomplete,
            lanes,
            findingGroups,
            ...(dataProjection.page === undefined ? {} : { page: dataProjection.page })
        }
    };
}
function projectData(data, options, lanes) {
    if (!isPlainObject(data)) {
        return { data };
    }
    const fieldFilter = new Set(options.fields);
    const sourceEntries = Object.entries(data);
    const selectedEntries = fieldFilter.size === 0
        ? sourceEntries
        : sourceEntries.filter(([key]) => fieldFilter.has(key));
    if (fieldFilter.size > 0) {
        lanes.dataFields = laneMetadata(sourceEntries.length, selectedEntries.length);
    }
    let projectedData = Object.fromEntries(selectedEntries);
    let page;
    if (options.pagination !== undefined) {
        if (options.limits?.[options.pagination.lane] !== undefined) {
            throw new TypeError("A paginated lane cannot also use a simple lane limit.");
        }
        const paginated = (0, pagination_1.paginateDataLane)(projectedData, options.pagination);
        projectedData = paginated.data;
        if (options.compactCollectionProfile !== undefined) {
            projectedData = options.compactCollectionProfile.projectData(projectedData);
        }
        page = paginated.page;
        lanes[options.pagination.lane] = {
            originalCount: paginated.originalCount,
            returnedCount: paginated.returnedCount,
            omittedCount: paginated.omittedCount,
            complete: paginated.complete
        };
    }
    const projectedEntries = Object.entries(projectedData).map(([key, value]) => {
        if (!Array.isArray(value) || options.limits?.[key] === undefined) {
            return [key, value];
        }
        const projected = value.slice(0, options.limits[key]);
        lanes[key] = laneMetadata(value.length, projected.length);
        return [key, projected];
    });
    return {
        data: Object.fromEntries(projectedEntries),
        ...(page === undefined ? {} : { page })
    };
}
function deduplicateNonBlockingFindings(findings) {
    const seen = new Set();
    const result = [];
    for (const finding of findings) {
        if (finding.severity === "error") {
            result.push(finding);
            continue;
        }
        const key = findingGroupKey(finding);
        if (!seen.has(key)) {
            seen.add(key);
            result.push(finding);
        }
    }
    return result;
}
function limitFindings(findings, limit) {
    if (limit === undefined || findings.length <= limit) {
        return [...findings];
    }
    const blockers = findings.filter((finding) => finding.severity === "error");
    const advisoryCapacity = Math.max(0, limit - blockers.length);
    const advisory = findings
        .filter((finding) => finding.severity !== "error")
        .slice(0, advisoryCapacity);
    const retained = new Set([...blockers, ...advisory]);
    return findings.filter((finding) => retained.has(finding));
}
function compactFinding(finding) {
    return {
        ...(finding.id === undefined ? {} : { id: finding.id }),
        code: finding.code,
        severity: finding.severity,
        message: finding.message
    };
}
function groupFindings(findings) {
    const groups = new Map();
    for (const finding of findings) {
        const key = findingGroupKey(finding);
        const existing = groups.get(key);
        if (existing) {
            existing.count += 1;
        }
        else {
            groups.set(key, {
                code: finding.code,
                severity: finding.severity,
                count: 1
            });
        }
    }
    return [...groups.values()];
}
function findingGroupKey(finding) {
    return `${finding.code}\u0000${finding.severity}`;
}
function laneMetadata(originalCount, returnedCount) {
    const omittedCount = Math.max(0, originalCount - returnedCount);
    return {
        originalCount,
        returnedCount,
        omittedCount,
        complete: omittedCount === 0
    };
}
function normalizeTokens(values, label, pattern) {
    const normalized = [];
    const seen = new Set();
    for (const rawValue of values ?? []) {
        const value = rawValue.trim();
        if (value.length === 0 || (pattern !== undefined && !pattern.test(value))) {
            throw new TypeError(`Invalid output ${label} '${rawValue}'.`);
        }
        if (!seen.has(value)) {
            seen.add(value);
            normalized.push(value);
        }
    }
    return normalized;
}
function isPlainObject(value) {
    return typeof value === "object"
        && value !== null
        && !Array.isArray(value)
        && Object.getPrototypeOf(value) === Object.prototype;
}
