import { FilterUtility } from './filter.js';
import { getRuleSetAtPath } from './protocols.js';
import { lexicographicalCompare } from './string.js';
import { ProtocolRecordLimitStrategy } from '../types/protocols-types.js';
import { Records } from './records.js';
import { SortDirection } from '../types/query-types.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
/**
 * Queries records with bounded `$recordLimit` occupancy projection when every limited filter targets one concrete scope.
 *
 * Broad filters keep the store's native query path. Projecting those without a store-level grouping primitive would require
 * scanning the full matching set, which is worse than leaving them unprojected until a bounded broad-query strategy exists.
 */
export async function queryRecordsWithRecordLimitOccupancy(input) {
    const filterResolution = await resolveRecordLimitFilters(input);
    if (filterResolution === undefined) {
        const { messages, cursor } = await input.messageStore.query(input.tenant, input.filters, input.messageSort, input.pagination);
        return { messages: messages.filter(Records.isRecordsWrite), cursor };
    }
    if (filterResolution.projectedFilters.length === 0) {
        return { messages: [] };
    }
    const { messages, cursor } = await input.messageStore.query(input.tenant, filterResolution.projectedFilters, input.messageSort, input.pagination);
    return { messages: messages.filter(Records.isRecordsWrite), cursor };
}
/**
 * Counts records with bounded `$recordLimit` occupancy projection when every limited filter targets one concrete scope.
 */
export async function countRecordsWithRecordLimitOccupancy(input) {
    const filterResolution = await resolveRecordLimitFilters(input);
    if (filterResolution === undefined) {
        return input.messageStore.count(input.tenant, input.filters, input.messageSort);
    }
    if (filterResolution.projectedFilters.length === 0) {
        return 0;
    }
    return input.messageStore.count(input.tenant, filterResolution.projectedFilters, input.messageSort);
}
/**
 * Returns true when the latest RecordsWrite is visible under the `$recordLimit` projection.
 */
export async function isRecordLimitOccupant(input) {
    const recordLimit = await getRecordLimitForMessage({
        messageStore: input.messageStore,
        validationStateReader: input.validationStateReader,
        tenant: input.tenant,
        message: input.message,
        messageTimestamp: input.messageTimestamp,
        recordLimitDefinitions: new Map(),
    });
    if (recordLimit === undefined) {
        return true;
    }
    const occupantRecordIds = await findOccupantRecordIds({
        messageStore: input.messageStore,
        tenant: input.tenant,
        scope: getRecordLimitScope(input.message),
        recordLimit,
    });
    return occupantRecordIds.has(input.message.recordId);
}
async function resolveRecordLimitFilters(input) {
    const recordLimitDefinitions = new Map();
    const projectedFilters = [];
    let projectionApplied = false;
    for (const filter of input.filters) {
        const recordLimit = await getRecordLimitForFilter({
            messageStore: input.messageStore,
            validationStateReader: input.validationStateReader,
            tenant: input.tenant,
            filter,
            messageTimestamp: input.messageTimestamp,
            recordLimitDefinitions,
        });
        if (recordLimit === undefined) {
            projectedFilters.push(filter);
            continue;
        }
        const scope = getRecordLimitScopeFromFilter(filter);
        if (scope === undefined) {
            return undefined;
        }
        const occupantRecordIds = await findOccupantRecordIds({
            messageStore: input.messageStore,
            tenant: input.tenant,
            scope,
            recordLimit,
        });
        const projectedFilter = buildProjectedFilter(filter, occupantRecordIds);
        if (projectedFilter !== undefined) {
            projectedFilters.push(projectedFilter);
        }
        projectionApplied = true;
    }
    return projectionApplied ? { projectedFilters } : undefined;
}
async function getRecordLimitForMessage(input) {
    const { protocol, protocolPath } = input.message.descriptor;
    if (protocol === undefined || protocolPath === undefined) {
        return undefined;
    }
    return getRecordLimit({
        validationStateReader: input.validationStateReader,
        tenant: input.tenant,
        protocol,
        protocolPath,
        messageTimestamp: input.messageTimestamp,
        recordLimitDefinitions: input.recordLimitDefinitions,
    });
}
async function getRecordLimitForFilter(input) {
    if (input.filter.interface !== DwnInterfaceName.Records ||
        input.filter.method !== DwnMethodName.Write ||
        input.filter.isLatestBaseState !== true) {
        return undefined;
    }
    const { protocol, protocolPath } = input.filter;
    if (typeof protocol !== 'string' || typeof protocolPath !== 'string') {
        return undefined;
    }
    return getRecordLimit({
        validationStateReader: input.validationStateReader,
        tenant: input.tenant,
        protocol,
        protocolPath,
        messageTimestamp: input.messageTimestamp,
        recordLimitDefinitions: input.recordLimitDefinitions,
    });
}
async function getRecordLimit(input) {
    const key = `${input.protocol}\u0000${input.protocolPath}`;
    if (!input.recordLimitDefinitions.has(key)) {
        let protocolDefinition;
        try {
            protocolDefinition = await input.validationStateReader.fetchProtocolDefinition(input.tenant, input.protocol, input.messageTimestamp);
        }
        catch (error) {
            if (error instanceof DwnError && error.code === DwnErrorCode.ProtocolAuthorizationProtocolNotFound) {
                input.recordLimitDefinitions.set(key, undefined);
                return undefined;
            }
            throw error;
        }
        const ruleSet = getRuleSetAtPath(input.protocolPath, protocolDefinition.structure);
        const recordLimit = ruleSet?.$recordLimit;
        input.recordLimitDefinitions.set(key, recordLimit?.strategy === ProtocolRecordLimitStrategy.Reject ? recordLimit : undefined);
    }
    return input.recordLimitDefinitions.get(key);
}
async function findOccupantRecordIds(input) {
    const scopeFilter = buildRecordLimitScopeFilter(input.scope);
    const messageSort = { dateCreated: SortDirection.Ascending };
    const { messages: firstPage } = await input.messageStore.query(input.tenant, [scopeFilter], messageSort, { limit: input.recordLimit.max });
    const firstPageCandidates = firstPage.filter(Records.isRecordsWrite);
    if (firstPageCandidates.length === 0) {
        return new Set();
    }
    const boundaryDateCreated = firstPageCandidates.at(-1).descriptor.dateCreated;
    const beforeBoundary = firstPageCandidates.filter((message) => message.descriptor.dateCreated < boundaryDateCreated);
    let boundaryCandidates = firstPageCandidates.filter((message) => message.descriptor.dateCreated === boundaryDateCreated);
    if (firstPageCandidates.length === input.recordLimit.max) {
        const { messages } = await input.messageStore.query(input.tenant, [{ ...scopeFilter, dateCreated: boundaryDateCreated }], messageSort);
        boundaryCandidates = messages.filter(Records.isRecordsWrite);
    }
    boundaryCandidates.sort(compareRecordLimitCandidates);
    const rankedCandidates = [
        ...beforeBoundary,
        ...boundaryCandidates,
    ];
    return new Set(rankedCandidates
        .slice(0, input.recordLimit.max)
        .map((message) => message.recordId));
}
function buildRecordLimitScopeFilter(scope) {
    const filter = {
        interface: DwnInterfaceName.Records,
        method: DwnMethodName.Write,
        isLatestBaseState: true,
        protocol: scope.protocol,
        protocolPath: scope.protocolPath,
    };
    if (scope.parentContextId !== '') {
        filter.contextId = FilterUtility.constructPrefixFilterAsRangeFilter(scope.parentContextId);
    }
    return filter;
}
function getRecordLimitScopeFromFilter(filter) {
    const { protocol, protocolPath } = filter;
    if (typeof protocol !== 'string' || typeof protocolPath !== 'string') {
        return undefined;
    }
    if (!protocolPath.includes('/')) {
        return { protocol, protocolPath, parentContextId: '' };
    }
    const parentContextId = getExactParentContextIdFromFilter(filter);
    if (parentContextId === undefined) {
        return undefined;
    }
    return { protocol, protocolPath, parentContextId };
}
function getRecordLimitScope(message) {
    return {
        protocol: message.descriptor.protocol,
        protocolPath: message.descriptor.protocolPath,
        parentContextId: Records.getParentContextFromOfContextId(message.contextId) ?? '',
    };
}
function getExactParentContextIdFromFilter(filter) {
    const { contextId } = filter;
    if (contextId === undefined || !FilterUtility.isRangeFilter(contextId)) {
        return undefined;
    }
    if (typeof contextId.gte !== 'string' || contextId.lt !== `${contextId.gte}\uffff`) {
        return undefined;
    }
    return contextId.gte;
}
function buildProjectedFilter(filter, occupantRecordIds) {
    let projectedRecordIds = Array.from(occupantRecordIds);
    const existingRecordIdFilter = filter.recordId;
    if (typeof existingRecordIdFilter === 'string') {
        projectedRecordIds = occupantRecordIds.has(existingRecordIdFilter) ? [existingRecordIdFilter] : [];
    }
    else if (Array.isArray(existingRecordIdFilter)) {
        projectedRecordIds = existingRecordIdFilter
            .filter((recordId) => typeof recordId === 'string' && occupantRecordIds.has(recordId));
    }
    if (projectedRecordIds.length === 0) {
        return undefined;
    }
    return {
        ...filter,
        recordId: projectedRecordIds,
    };
}
function compareRecordLimitCandidates(left, right) {
    const dateComparison = lexicographicalCompare(left.descriptor.dateCreated, right.descriptor.dateCreated);
    if (dateComparison !== 0) {
        return dateComparison;
    }
    return lexicographicalCompare(left.recordId, right.recordId);
}
//# sourceMappingURL=record-limit-occupancy.js.map