"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PaginationCursorError = exports.PAGINATION_CURSOR_ERROR_CODES = void 0;
exports.hashPaginationValue = hashPaginationValue;
exports.encodePaginationCursor = encodePaginationCursor;
exports.decodePaginationCursor = decodePaginationCursor;
exports.paginateDataLane = paginateDataLane;
const node_crypto_1 = require("node:crypto");
const automation_protocol_1 = require("../automation-protocol");
const collection_pagination_1 = require("./collection-pagination");
exports.PAGINATION_CURSOR_ERROR_CODES = [
    "CURSOR_MALFORMED",
    "CURSOR_PROTOCOL_MISMATCH",
    "CURSOR_COMMAND_MISMATCH",
    "CURSOR_SCOPE_MISMATCH",
    "CURSOR_LANE_MISMATCH",
    "CURSOR_PAGE_SIZE_MISMATCH",
    "CURSOR_STALE",
    "CURSOR_OUT_OF_RANGE",
    "PAGINATION_LANE_MISSING"
];
class PaginationCursorError extends Error {
    code;
    constructor(code, message) {
        super(message);
        this.name = "PaginationCursorError";
        this.code = code;
    }
}
exports.PaginationCursorError = PaginationCursorError;
function hashPaginationValue(value) {
    const serialized = JSON.stringify(value);
    if (serialized === undefined) {
        throw new PaginationCursorError("CURSOR_MALFORMED", "Pagination snapshots require JSON-serializable values.");
    }
    return `sha256:${(0, node_crypto_1.createHash)("sha256").update(serialized).digest("hex")}`;
}
function encodePaginationCursor(payload) {
    validateCursorPayloadShape(payload);
    return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
}
function decodePaginationCursor(token) {
    if (!/^[A-Za-z0-9_-]+$/u.test(token)) {
        throw new PaginationCursorError("CURSOR_MALFORMED", "Pagination cursor is not valid base64url text.");
    }
    try {
        const decoded = Buffer.from(token, "base64url").toString("utf8");
        const canonicalToken = Buffer.from(decoded, "utf8").toString("base64url");
        if (canonicalToken !== token) {
            throw new Error("non-canonical base64url");
        }
        const value = JSON.parse(decoded);
        validateCursorPayloadShape(value);
        return value;
    }
    catch (error) {
        if (error instanceof PaginationCursorError) {
            throw error;
        }
        throw new PaginationCursorError("CURSOR_MALFORMED", "Pagination cursor payload is malformed.");
    }
}
function paginateDataLane(data, request) {
    validateRequest(request);
    if (!isPlainObject(data) || !Array.isArray(data[request.lane])) {
        throw new PaginationCursorError("PAGINATION_LANE_MISSING", `Pagination lane '${request.lane}' is not a top-level array.`);
    }
    const source = data[request.lane];
    const snapshot = hashPaginationValue(source);
    const decoded = request.cursor === undefined ? undefined : decodePaginationCursor(request.cursor);
    const offset = decoded?.offset ?? 0;
    if (decoded !== undefined) {
        validateCursorBindings(decoded, request, snapshot);
    }
    if (offset > source.length || (request.cursor !== undefined && offset === source.length)) {
        throw new PaginationCursorError("CURSOR_OUT_OF_RANGE", `Pagination cursor offset ${offset} is outside lane '${request.lane}' with ${source.length} entries.`);
    }
    const endOffset = Math.min(source.length, offset + request.pageSize);
    const pageEntries = source.slice(offset, endOffset);
    const nextCursor = endOffset < source.length
        ? encodePaginationCursor({
            protocolVersion: (0, automation_protocol_1.getSelectedAutomationProtocolVersion)(),
            command: request.command,
            scope: request.scope,
            lane: request.lane,
            snapshot,
            offset: endOffset,
            pageSize: request.pageSize
        })
        : null;
    const projected = {
        ...data,
        [request.lane]: pageEntries
    };
    return {
        data: projected,
        originalCount: source.length,
        returnedCount: pageEntries.length,
        omittedCount: source.length - pageEntries.length,
        complete: nextCursor === null,
        page: {
            lane: request.lane,
            snapshot,
            recordIds: pageEntries.map((entry) => (0, collection_pagination_1.getStablePaginationRecordId)(entry)),
            startOffset: offset,
            endOffset,
            pageSize: request.pageSize,
            nextCursor
        }
    };
}
function validateCursorBindings(cursor, request, snapshot) {
    const checks = [
        [cursor.protocolVersion === (0, automation_protocol_1.getSelectedAutomationProtocolVersion)(), "CURSOR_PROTOCOL_MISMATCH", "protocol version"],
        [cursor.command === request.command, "CURSOR_COMMAND_MISMATCH", "command"],
        [cursor.scope === request.scope, "CURSOR_SCOPE_MISMATCH", "projection scope"],
        [cursor.lane === request.lane, "CURSOR_LANE_MISMATCH", "lane"],
        [cursor.pageSize === request.pageSize, "CURSOR_PAGE_SIZE_MISMATCH", "page size"],
        [cursor.snapshot === snapshot, "CURSOR_STALE", "snapshot"]
    ];
    for (const [matches, code, label] of checks) {
        if (!matches) {
            throw new PaginationCursorError(code, `Pagination cursor ${label} does not match the current request.`);
        }
    }
}
function validateRequest(request) {
    if (request.command.trim() === "" || request.lane.trim() === "") {
        throw new PaginationCursorError("CURSOR_MALFORMED", "Pagination command and lane are required.");
    }
    if (!/^sha256:[a-f0-9]{64}$/u.test(request.scope)) {
        throw new PaginationCursorError("CURSOR_MALFORMED", "Pagination scope must be a sha256 identity.");
    }
    if (!Number.isSafeInteger(request.pageSize) || request.pageSize < 1) {
        throw new PaginationCursorError("CURSOR_MALFORMED", "Pagination page size must be a positive safe integer.");
    }
}
function validateCursorPayloadShape(value) {
    if (!isPlainObject(value)) {
        throw new PaginationCursorError("CURSOR_MALFORMED", "Pagination cursor payload must be an object.");
    }
    const keys = Object.keys(value);
    const expected = ["protocolVersion", "command", "scope", "lane", "snapshot", "offset", "pageSize"];
    if (keys.length !== expected.length || expected.some((key) => !Object.hasOwn(value, key))) {
        throw new PaginationCursorError("CURSOR_MALFORMED", "Pagination cursor payload fields are invalid.");
    }
    if (!Number.isSafeInteger(value.protocolVersion)
        || typeof value.command !== "string"
        || typeof value.scope !== "string"
        || typeof value.lane !== "string"
        || typeof value.snapshot !== "string"
        || !Number.isSafeInteger(value.offset)
        || value.offset < 0
        || !Number.isSafeInteger(value.pageSize)
        || value.pageSize < 1) {
        throw new PaginationCursorError("CURSOR_MALFORMED", "Pagination cursor payload values are invalid.");
    }
}
function isPlainObject(value) {
    return typeof value === "object"
        && value !== null
        && !Array.isArray(value)
        && Object.getPrototypeOf(value) === Object.prototype;
}
