import QueryError from "../../errors/QueryError.js";
import { computeChecksum } from "../../utils/checksumUtils.js";
import { createStateReader } from "../state/StateReader.js";
import { computeStateHash } from "../state/StateSerializer.js";
import { scopeMaterializedState, scopePatchEntries, } from "../VisibleStateScope.js";
export function compareStrings(a, b) {
    return a < b ? -1 : a > b ? 1 : 0;
}
export function normalizeLamportCeiling(value, field) {
    if (value === undefined || value === null) {
        return null;
    }
    assertValidLamport(value, field);
    return value;
}
function assertValidLamport(value, field) {
    if (!Number.isInteger(value) || value < 0) {
        throw new QueryError(`${field} must be a non-negative integer or null`, {
            code: 'invalid_coordinate', context: { field, value },
        });
    }
}
export function normalizeOptionalString(value, field) {
    if (value === undefined || value === null) {
        return null;
    }
    if (typeof value !== 'string' || value.trim().length === 0) {
        throw new QueryError(`${field} must be a non-empty string when provided`, {
            code: 'invalid_coordinate', context: { field, valueType: typeof value },
        });
    }
    return value.trim();
}
export function normalizeRequiredString(value, field) {
    const normalized = normalizeOptionalString(value, field);
    if (normalized === null) {
        throw new QueryError(`${field} must be a non-empty string`, {
            code: 'invalid_coordinate', context: { field },
        });
    }
    return normalized;
}
function frontierEntries(frontier) {
    if (frontier instanceof Map) {
        return [...frontier.entries()];
    }
    return Object.entries(frontier);
}
function assertFrontierEntry(writerId, sha, field) {
    if (typeof writerId !== 'string' || writerId.length === 0) {
        throw new QueryError(`${field} contains an invalid writer id`, {
            code: 'invalid_coordinate', context: { field, writerId },
        });
    }
    if (typeof sha !== 'string' || sha.length === 0) {
        throw new QueryError(`${field} contains an invalid patch sha`, {
            code: 'invalid_coordinate', context: { field, writerId, shaType: typeof sha },
        });
    }
}
export function normalizeFrontierRecord(frontier, field) {
    if (frontier === null || frontier === undefined
        || (typeof frontier !== 'object')) {
        throw new QueryError(`${field} must be a frontier map or record`, {
            code: 'invalid_coordinate', context: { field },
        });
    }
    const entries = frontierEntries(frontier);
    const record = {};
    for (const [writerId, sha] of entries.sort(([a], [b]) => compareStrings(a, b))) {
        assertFrontierEntry(writerId, sha, field);
        record[writerId] = sha;
    }
    return record;
}
export function frontierRecordToMap(frontierRecord) {
    return new Map(Object.entries(frontierRecord).sort(([a], [b]) => compareStrings(a, b)));
}
export function optionalCeiling(ceiling) {
    return ceiling === null ? {} : { ceiling };
}
export function combineCeilings(left, right) {
    if (left === null) {
        return right;
    }
    if (right === null) {
        return left;
    }
    return Math.min(left, right);
}
export function buildCoordinateRequest(frontierRecord, ceiling) {
    return { frontier: { ...frontierRecord }, ceiling };
}
function updateWriterHighestPatch(byWriter, writerId, patchInfo) {
    const current = byWriter.get(writerId);
    const isNewer = !current || patchInfo.lamport > current.lamport ||
        (patchInfo.lamport === current.lamport && compareStrings(patchInfo.sha, current.sha) > 0);
    if (isNewer) {
        byWriter.set(writerId, patchInfo);
    }
}
export function patchFrontierFromEntries(entries) {
    const byWriter = new Map();
    for (const entry of entries) {
        updateWriterHighestPatch(byWriter, entry.patch.writer, {
            lamport: entry.patch.lamport ?? 0, sha: entry.sha,
        });
    }
    const sorted = [...byWriter.entries()].sort(([a], [b]) => compareStrings(a, b));
    const record = {};
    for (const [writerId, info] of sorted) {
        record[writerId] = info.sha;
    }
    return record;
}
export function lamportFrontierFromEntries(entries) {
    const byWriter = new Map();
    for (const entry of entries) {
        const lamport = entry.patch.lamport ?? 0;
        const current = byWriter.get(entry.patch.writer);
        if (current === undefined || lamport > current) {
            byWriter.set(entry.patch.writer, lamport);
        }
    }
    return Object.fromEntries([...byWriter.entries()].sort(([a], [b]) => compareStrings(a, b)));
}
export function uniqueSortedPatchShas(entries) {
    return [...new Set(entries.map(({ sha }) => sha))].sort(compareStrings);
}
async function collectWriterEntries(graph, params) {
    const entries = [];
    const writerEntries = await graph._loadPatchChainFromSha(params.tipSha);
    for (const entry of writerEntries) {
        if (params.ceiling === null || (entry.patch.lamport ?? 0) <= params.ceiling) {
            entries.push(entry);
        }
    }
    return entries;
}
export async function collectPatchEntriesForFrontier(graph, frontierRecord, ceiling) {
    const frontier = frontierRecordToMap(frontierRecord);
    const results = [];
    for (const tipSha of frontier.values()) {
        if (tipSha) {
            results.push(await collectWriterEntries(graph, { tipSha, ceiling }));
        }
    }
    return results.flat();
}
export function buildStrandMetadata(strandId, descriptor) {
    const readOverlays = descriptor.braid?.readOverlays ?? [];
    return {
        strandId,
        baseLamportCeiling: descriptor.baseObservation.lamportCeiling,
        overlayHeadPatchSha: descriptor.overlay.headPatchSha,
        overlayPatchCount: descriptor.overlay.patchCount,
        overlayWritable: descriptor.overlay.writable ?? true,
        braid: {
            readOverlayCount: readOverlays.length,
            braidedStrandIds: readOverlays.map((o) => o.strandId).sort(compareStrings),
        },
    };
}
export async function computeStateHashForGraph(graph, state) {
    if (graph._stateHashService) {
        return await graph._stateHashService.compute(state);
    }
    return await computeStateHash(state, { crypto: graph._crypto, codec: graph._codec });
}
export class ResolvedComparisonSide {
    requested;
    resolved;
    state;
    patchEntries;
    constructor(params) {
        this.requested = params.requested;
        this.resolved = params.resolved;
        this.state = params.state;
        this.patchEntries = [...params.patchEntries];
        Object.freeze(this);
    }
}
export async function finalizeSide(graph, params, scope) {
    const scoped = await scopedFinalizedSide(graph, params, scope);
    const resolved = await buildResolvedPayload(graph, params, scoped);
    return new ResolvedComparisonSide({
        requested: params.requested,
        state: scoped.state,
        patchEntries: [...scoped.patchEntries],
        resolved,
    });
}
async function scopedFinalizedSide(graph, params, scope) {
    const scopedState = scopeMaterializedState(params.state, scope);
    const scopedPatchEntries = scopePatchEntries([...params.patchEntries], scope);
    return {
        state: scopedState,
        patchEntries: scopedPatchEntries,
        patchFrontier: patchFrontierFromEntries(scopedPatchEntries),
        lamportFrontier: lamportFrontierFromEntries(scopedPatchEntries),
        stateHash: await computeStateHashForGraph(graph, scopedState),
        patchShas: uniqueSortedPatchShas(scopedPatchEntries),
    };
}
async function buildResolvedPayload(graph, params, scoped) {
    const reader = createStateReader(scoped.state);
    return {
        coordinateKind: params.coordinateKind,
        patchFrontier: scoped.patchFrontier,
        patchFrontierDigest: await computeChecksum(scoped.patchFrontier, graph._crypto),
        lamportFrontier: scoped.lamportFrontier,
        lamportFrontierDigest: await computeChecksum(scoped.lamportFrontier, graph._crypto),
        lamportCeiling: params.lamportCeiling,
        stateHash: scoped.stateHash,
        patchUniverseDigest: await computeChecksum({ patches: scoped.patchShas }, graph._crypto),
        summary: summarizeVisibleState(reader, scoped.patchEntries.length),
        ...(params.strand !== undefined ? { strand: params.strand } : {}),
    };
}
function summarizeVisibleState(reader, patchCount) {
    const nodes = reader.getNodes();
    const edges = reader.getEdges();
    let nodePropertyCount = 0;
    for (const nodeId of nodes) {
        nodePropertyCount += Object.keys(reader.getNodeProps(nodeId) ?? {}).length;
    }
    let edgePropertyCount = 0;
    for (const edge of edges) {
        edgePropertyCount += Object.keys(edge.props ?? {}).length;
    }
    return { nodeCount: nodes.length, edgeCount: edges.length, nodePropertyCount, edgePropertyCount, patchCount };
}
