import { ENCRYPTION_PROTOCOL_URI } from './constants.js';
import { FilterUtility } from '../utils/filter.js';
import { PermissionGrant } from '../protocols/permission-grant.js';
import { PermissionsProtocol } from '../protocols/permissions.js';
import { RecordsWrite } from '../interfaces/records-write.js';
import { SortDirection } from '../types/query-types.js';
import { DwnError, DwnErrorCode } from './dwn-error.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
import { fetchInitialRecordsWrite, fetchInitialRecordsWriteMessage, getInitialWrite } from '../interfaces/records-write-query.js';
/**
 * The store-backed `ValidationStateReader` — the single place where validation-time state reads
 * touch the `MessageStore`/`DataStore`.
 */
export class StoreValidationStateReader {
    messageStore;
    dataStore;
    coreProtocols;
    constructor(input) {
        this.messageStore = input.messageStore;
        this.dataStore = input.dataStore;
        this.coreProtocols = input.coreProtocols;
    }
    /** @inheritdoc */
    async fetchInitialRecordsWrite(tenant, recordId) {
        return fetchInitialRecordsWrite(this.messageStore, tenant, recordId);
    }
    /** @inheritdoc */
    async fetchInitialWrite(tenant, recordId) {
        const query = {
            interface: DwnInterfaceName.Records,
            method: DwnMethodName.Write,
            recordId: recordId
        };
        const { messages } = await this.messageStore.query(tenant, [query]);
        if (messages.length === 0) {
            return undefined;
        }
        const initialWrite = await getInitialWrite(messages);
        return initialWrite;
    }
    /** @inheritdoc */
    async constructRecordChain(tenant, descendantRecordId) {
        if (descendantRecordId === undefined) {
            return [];
        }
        const recordChain = [];
        // keep walking up the chain from the inbound message's parent, until there is no more parent
        let currentRecordId = descendantRecordId;
        while (currentRecordId !== undefined) {
            const initialWrite = await this.fetchInitialWrite(tenant, currentRecordId);
            // RecordsWrite needed should be available since we perform necessary checks at the time of writes,
            // eg. check the immediate parent in `verifyProtocolPathAndContextId` at the time of writing,
            // so if this condition is triggered, it means there is an unexpected bug that caused an incomplete chain.
            // We add additional defensive check here because returning an unexpected/incorrect record chain could lead to security vulnerabilities.
            if (initialWrite === undefined) {
                throw new DwnError(DwnErrorCode.ProtocolAuthorizationParentNotFoundConstructingRecordChain, `Unexpected error that should never trigger: no parent found with ID ${currentRecordId} when constructing record chain.`);
            }
            recordChain.push(initialWrite);
            currentRecordId = initialWrite.descriptor.parentId;
        }
        return recordChain.reverse(); // root record first
    }
    /** @inheritdoc */
    async fetchParentRecord(input) {
        const { tenant, parentProtocolUri, parentId } = input;
        const latestStateQuery = {
            isLatestBaseState: true, // NOTE: this filter is critical, to ensure are are not returning a deleted parent
            interface: DwnInterfaceName.Records,
            method: DwnMethodName.Write,
            protocol: parentProtocolUri,
            recordId: parentId
        };
        const { messages: parentMessages } = await this.messageStore.query(tenant, [latestStateQuery]);
        const latestParent = parentMessages[0];
        if (latestParent !== undefined) {
            return latestParent;
        }
        const initialWrite = await fetchInitialRecordsWriteMessage(this.messageStore, tenant, parentId);
        if (initialWrite?.descriptor.protocol !== parentProtocolUri) {
            return undefined;
        }
        if (await this.recordHasLocalTombstone(tenant, parentId)) {
            return undefined;
        }
        return initialWrite;
    }
    /** @inheritdoc */
    async hasMatchingRoleRecord(input) {
        const latestStateFilter = StoreValidationStateReader.constructRoleRecordFilter({ ...input, latestStateOnly: true });
        const { messages: matchingMessages } = await this.messageStore.query(input.tenant, [latestStateFilter]);
        if (matchingMessages.length > 0) {
            return true;
        }
        const anyWriteFilter = StoreValidationStateReader.constructRoleRecordFilter({ ...input, latestStateOnly: false });
        const { messages: candidates } = await this.messageStore.query(input.tenant, [anyWriteFilter]);
        for (const candidate of candidates) {
            if (!await RecordsWrite.isInitialWrite(candidate)) {
                continue;
            }
            if (!await this.recordHasLocalTombstone(input.tenant, candidate.recordId)) {
                return true;
            }
        }
        return false;
    }
    /** @inheritdoc */
    async queryLatestRoleRecords(input) {
        const filter = StoreValidationStateReader.constructRoleRecordFilter({ ...input, latestStateOnly: true });
        const { messages: matchingMessages } = await this.messageStore.query(input.tenant, [filter]);
        return matchingMessages;
    }
    /** @inheritdoc */
    async queryAudienceEpochs(input) {
        const filter = {
            interface: DwnInterfaceName.Records,
            method: DwnMethodName.Write,
            isLatestBaseState: true,
            protocol: ENCRYPTION_PROTOCOL_URI,
            protocolPath: 'audienceEpoch',
            'tag.protocol': input.protocol,
            'tag.contextId': input.contextId,
            'tag.role': input.role,
            'tag.epoch': input.epoch,
        };
        if (input.keyId !== undefined) {
            filter['tag.keyId'] = input.keyId;
        }
        const { messages } = await this.messageStore.query(input.tenant, [filter]);
        return messages;
    }
    /** @inheritdoc */
    async fetchGrant(tenant, permissionGrantId) {
        const grantQuery = {
            recordId: permissionGrantId,
            isLatestBaseState: true
        };
        const { messages } = await this.messageStore.query(tenant, [grantQuery]);
        const possibleGrantMessage = messages[0];
        const dwnInterface = possibleGrantMessage?.descriptor.interface;
        const dwnMethod = possibleGrantMessage?.descriptor.method;
        if (dwnInterface !== DwnInterfaceName.Records ||
            dwnMethod !== DwnMethodName.Write ||
            possibleGrantMessage.descriptor.protocolPath !== PermissionsProtocol.grantPath) {
            throw new DwnError(DwnErrorCode.GrantAuthorizationGrantMissing, `Could not find permission grant with record ID ${permissionGrantId}.`);
        }
        const permissionGrantMessage = possibleGrantMessage;
        const permissionGrant = PermissionGrant.parse(permissionGrantMessage);
        return permissionGrant;
    }
    /** @inheritdoc */
    async fetchOldestGrantRevocation(tenant, permissionGrantId) {
        const query = PermissionsProtocol.grantRevocationFilter(permissionGrantId);
        const { messages } = await this.messageStore.query(tenant, [query], { messageTimestamp: SortDirection.Ascending }, { limit: 1 });
        return messages[0];
    }
    /** @inheritdoc */
    async fetchNewestRecordsWrite(tenant, recordId) {
        return RecordsWrite.fetchNewestRecordsWrite(this.messageStore, tenant, recordId);
    }
    /** @inheritdoc */
    async fetchProtocolDefinition(tenant, protocolUri, messageTimestamp) {
        // if the protocol is a registered core protocol, return the definition directly without a store query
        if (this.coreProtocols !== undefined) {
            const coreDefinition = this.coreProtocols.getDefinition(protocolUri);
            if (coreDefinition !== undefined) {
                return coreDefinition;
            }
        }
        // fetch the corresponding protocol definition
        const query = {
            interface: DwnInterfaceName.Protocols,
            method: DwnMethodName.Configure,
            protocol: protocolUri,
        };
        if (messageTimestamp === undefined) {
            // default: return only the latest protocol definition
            query.isLatestBaseState = true;
        }
        else {
            // temporal lookup: find the protocol definition active at the given timestamp
            query.messageTimestamp = { lte: messageTimestamp };
        }
        let { messages: protocols } = await this.messageStore.query(tenant, [query], { messageTimestamp: SortDirection.Descending }, { limit: 1 });
        if (protocols.length === 0 && messageTimestamp !== undefined) {
            // A record can be authored before the protocol's earliest retained config yet still have
            // been admitted under that config: admission order, not timestamp order, governed the
            // source. When no config predates the record, reconstruct the historical answer from the
            // earliest retained config. A protocol that is genuinely not installed still has zero
            // configs and fails below.
            ({ messages: protocols } = await this.messageStore.query(tenant, [{
                    interface: DwnInterfaceName.Protocols,
                    method: DwnMethodName.Configure,
                    protocol: protocolUri,
                }], { messageTimestamp: SortDirection.Ascending }, { limit: 1 }));
        }
        if (protocols.length === 0) {
            throw new DwnError(DwnErrorCode.ProtocolAuthorizationProtocolNotFound, `unable to find protocol definition for ${protocolUri}`);
        }
        const protocolMessage = protocols[0];
        return protocolMessage.descriptor.definition;
    }
    /** @inheritdoc */
    async fetchLatestSquashRecordAtScope(input) {
        const filter = {
            interface: DwnInterfaceName.Records,
            method: DwnMethodName.Write,
            isLatestBaseState: true,
            protocol: input.protocol,
            protocolPath: input.protocolPath,
            squash: true,
        };
        if (input.contextIdPrefix !== undefined) {
            filter.contextId = FilterUtility.constructPrefixFilterAsRangeFilter(input.contextIdPrefix);
        }
        const { messages } = await this.messageStore.query(input.tenant, [filter], { messageTimestamp: SortDirection.Descending }, { limit: 1 });
        return messages[0];
    }
    /** @inheritdoc */
    async hasStoredData(tenant, recordId, dataCid) {
        const dataStoreGetResult = await this.dataStore.get(tenant, recordId, dataCid);
        return dataStoreGetResult !== undefined;
    }
    /**
     * Checks whether a `RecordsDelete` tombstone for the given record is locally present.
     * Retained initial writes can prove immutable parent/role facts, but a tombstone still wins.
     */
    async recordHasLocalTombstone(tenant, recordId) {
        const tombstoneQuery = {
            interface: DwnInterfaceName.Records,
            method: DwnMethodName.Delete,
            recordId: recordId
        };
        const { messages } = await this.messageStore.query(tenant, [tombstoneQuery]);
        return messages.length > 0;
    }
    /**
     * Constructs the role-record selector filter shared by the invoked-role lookup and the
     * duplicate-role-recipient uniqueness check.
     */
    static constructRoleRecordFilter(input) {
        const filter = {
            interface: DwnInterfaceName.Records,
            method: DwnMethodName.Write,
            protocol: input.protocol,
            protocolPath: input.protocolPath,
            recipient: input.recipient,
        };
        if (input.latestStateOnly) {
            filter.isLatestBaseState = true;
        }
        if (input.contextIdPrefix !== undefined) {
            filter.contextId = FilterUtility.constructPrefixFilterAsRangeFilter(input.contextIdPrefix);
        }
        return filter;
    }
}
//# sourceMappingURL=validation-state-reader.js.map