import SyncError from "../../errors/SyncError.js";
export function normalizeSyncPageRequest(page) {
    if (page === undefined) {
        return { maxPatches: null, cursorOffset: 0 };
    }
    return {
        maxPatches: requirePositiveMaxPatches(page.maxPatches),
        cursorOffset: cursorOffsetFor(page.cursor ?? null),
    };
}
export function appendPatchForPage(options) {
    if (options.seenPatches < options.page.cursorOffset) {
        return { seenPatches: options.seenPatches + 1, hasMore: false, cursor: null };
    }
    if (options.page.maxPatches !== null && options.patches.length >= options.page.maxPatches) {
        return { seenPatches: options.seenPatches, hasMore: true, cursor: options.seenPatches.toString() };
    }
    options.patches.push(options.entry);
    return { seenPatches: options.seenPatches + 1, hasMore: false, cursor: null };
}
export function normalizeObservedLatencyMs(value) {
    if (value === undefined) {
        return null;
    }
    if (Number.isFinite(value) && value >= 0) {
        return value;
    }
    throw new SyncError('Sync response observed latency must be non-negative when provided', {
        code: 'E_SYNC_METRICS_INVALID',
        context: { field: 'observedLatencyMs', value },
    });
}
export function estimateResponsePayloadBytes(input) {
    let bytes = 'sync-response'.length + estimateFrontierBytes(input.frontier);
    for (const entry of input.patches) {
        bytes += entry.writerId.length + entry.sha.length + estimatePatchBytes(entry.patch);
    }
    bytes += estimateSkippedWriterBytes(input.skippedWriters);
    bytes += (input.page.cursor ?? '').length + String(input.page.maxPatches ?? '').length;
    bytes += String(input.page.hasMore).length + input.page.returnedPatches.toString().length;
    return bytes;
}
export function logResponseMetrics(input) {
    input.logger.info('Sync response metrics', {
        code: 'SYNC_RESPONSE_METRICS',
        graphName: input.graphName,
        patchCount: input.metrics.patchCount,
        skippedWriterCount: input.metrics.skippedWriterCount,
        estimatedPayloadBytes: input.metrics.estimatedPayloadBytes,
        latencyMs: input.metrics.latencyMs,
        syncResponseCursor: input.page.cursor,
        syncResponseHasMore: input.page.hasMore,
        syncResponseMaxPatches: input.page.maxPatches,
    });
}
function requirePositiveMaxPatches(value) {
    if (Number.isInteger(value) && value > 0) {
        return value;
    }
    throw new SyncError('Sync response page maxPatches must be a positive integer', {
        code: 'E_SYNC_PAGING_INVALID',
        context: { field: 'maxPatches', value },
    });
}
function cursorOffsetFor(cursor) {
    if (cursor === null) {
        return 0;
    }
    const cursorOffset = Number.parseInt(cursor, 10);
    if (Number.isInteger(cursorOffset) && cursorOffset >= 0 && cursorOffset.toString() === cursor) {
        return cursorOffset;
    }
    throw new SyncError('Sync response page cursor must be a non-negative integer string', {
        code: 'E_SYNC_PAGING_INVALID',
        context: { field: 'cursor', value: cursor },
    });
}
function estimateFrontierBytes(frontier) {
    let bytes = 0;
    for (const [writerId, sha] of Object.entries(frontier)) {
        bytes += writerId.length + sha.length;
    }
    return bytes;
}
function estimateStringArrayBytes(values) {
    if (values === undefined) {
        return 0;
    }
    return values.reduce((total, value) => total + value.length, 0);
}
function estimatePatchBytes(patch) {
    let bytes = patch.writer.length + patch.lamport.toString().length + patch.ops.length;
    if (patch.schema !== undefined) {
        bytes += patch.schema.toString().length;
    }
    for (const op of patch.ops) {
        bytes += op.type.length;
    }
    bytes += estimateStringArrayBytes(patch.reads);
    bytes += estimateStringArrayBytes(patch.writes);
    return bytes;
}
function estimateSkippedWriterBytes(skippedWriters) {
    let bytes = 0;
    for (const skipped of skippedWriters) {
        bytes += skipped.writerId.length + skipped.reason.length + skipped.localSha.length;
        bytes += skipped.remoteSha === null ? 0 : skipped.remoteSha.length;
    }
    return bytes;
}
