import { DwnErrorCode } from './dwn-error.js';
import { Encoder } from '../utils/encoder.js';
import { EncryptionProtocol } from '../protocols/encryption.js';
import { Message } from './message.js';
import { ROLE_AUDIENCE_DERIVATION_SCHEME } from '../utils/encryption.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
import { isCrossProtocolRef, parseCrossProtocolRef } from '../utils/protocols.js';
/**
 * Converts a regular handler reply into the structured result consumed by
 * replication sync. The DWN handler remains the dependency authority; this
 * adapter only gives the sync transport a typed way to distinguish missing
 * dependencies from terminal invalid messages.
 */
export function replicationApplyResultFromReply(message, reply, context = {}) {
    const { code, detail = '' } = reply.status;
    if (code === 202 || code === 204) {
        return {
            kind: 'Applied',
            ...(code === 204 ? { ancestryOnly: true } : {}),
            ...(reply.position === undefined ? {} : { position: reply.position }),
        };
    }
    if (code === 409) {
        return { kind: 'Superseded' };
    }
    if (getDwnErrorCode(detail) === DwnErrorCode.RecordsWriteNotAllowedAfterDelete) {
        return { kind: 'Superseded' };
    }
    if (isResolverFailure(detail)) {
        return { kind: 'Deferred', reason: 'resolver-unavailable' };
    }
    const missing = dependencyRefsFromStatus(message, code, detail, context);
    if (missing.length > 0) {
        return { kind: 'Incomplete', missing };
    }
    if (code >= 500) {
        return { kind: 'Deferred', reason: 'storage' };
    }
    return { kind: 'Invalid', reason: detail || `replicated message rejected with status ${code}` };
}
/**
 * Computes the complete set of locally-missing ancestor recordIds for a replicated message that
 * failed on a missing ancestor — either the immediate parent referential check
 * (`ProtocolAuthorizationParentRecordNotFound`) or record-chain construction
 * (`ProtocolAuthorizationParentNotFoundConstructingRecordChain`).
 *
 * Both failures name a single ancestor, but the message's `contextId` names the full ancestor
 * recordId chain, so every segment the failed check could not reach is presence-checked here and
 * all absent segments are returned in one batch (ordered root-first, named ancestor last).
 * Feeding the result to `replicationApplyResultFromReply()` via `ReplicationApplyResultContext`
 * lets sync fetch the entire ancestry in a single pass instead of one level per retry.
 *
 * @returns the missing ancestor recordIds, or `undefined` when the reply is not a
 *          missing-ancestor failure or the chain cannot be determined from the message (callers
 *          then fall back to emitting the single ancestor named by the failure).
 */
export async function missingAncestorRecordIdsFromReply(tenant, message, reply, validationStateReader) {
    const detail = reply.status.detail ?? '';
    const errorCode = getDwnErrorCode(detail);
    let namedRecordId;
    if (errorCode === DwnErrorCode.ProtocolAuthorizationParentRecordNotFound) {
        namedRecordId = parentRecordIdFromMessage(message, detail);
    }
    else if (errorCode === DwnErrorCode.ProtocolAuthorizationParentNotFoundConstructingRecordChain) {
        namedRecordId = ancestorRecordIdFromDetail(detail);
    }
    else {
        return undefined;
    }
    const contextId = message.contextId;
    if (namedRecordId === undefined || typeof contextId !== 'string') {
        return undefined;
    }
    // `contextId` is the ancestor recordId chain ending with the record's own recordId. The failed
    // check stopped at `namedRecordId`, so every segment below it is locally present; only the
    // segments above it (closer to the root) still need a presence check. Presence means an initial
    // write exists — exactly what record-chain construction reads for the segments above the
    // immediate parent.
    const ancestorRecordIds = contextId.split('/').slice(0, -1);
    const namedIndex = ancestorRecordIds.indexOf(namedRecordId);
    if (namedIndex === -1) {
        return undefined;
    }
    const missingRecordIds = [];
    for (const ancestorRecordId of ancestorRecordIds.slice(0, namedIndex)) {
        const initialWrite = await validationStateReader.fetchInitialWrite(tenant, ancestorRecordId);
        if (initialWrite === undefined) {
            missingRecordIds.push(ancestorRecordId);
        }
    }
    missingRecordIds.push(namedRecordId);
    return missingRecordIds;
}
function dependencyRefsFromStatus(message, code, detail, context) {
    const errorCode = getDwnErrorCode(detail);
    switch (errorCode) {
        case DwnErrorCode.ProtocolAuthorizationProtocolNotFound:
        case DwnErrorCode.ProtocolsConfigureComposedProtocolNotInstalled:
            return toRefList(protocolDependencyFromMessage(message, detail));
        case DwnErrorCode.ProtocolAuthorizationParentRecordNotFound:
            return parentDependenciesFromMessage(message, detail, context);
        case DwnErrorCode.ProtocolAuthorizationCrossProtocolParentNotFound:
            return toRefList(parentDependencyFromMessage(message, detail));
        case DwnErrorCode.ProtocolAuthorizationParentNotFoundConstructingRecordChain:
            return ancestorDependenciesFromMessage(message, detail, context);
        case DwnErrorCode.RecordsWriteGetInitialWriteNotFound:
            return toRefList(initialWriteDependencyFromMessage(message));
        case DwnErrorCode.GrantAuthorizationGrantMissing:
            return toRefList(grantDependencyFromMessage(message));
        case DwnErrorCode.ProtocolAuthorizationMatchingRoleRecordNotFound:
        case DwnErrorCode.EncryptionProtocolValidateAudienceWriterUnauthorized:
            return toRefList(roleDependencyFromMessage(message, context));
        case DwnErrorCode.EncryptionProtocolValidateAudienceEpochMissing:
            return toRefList(encryptionProtocolDependencyFromMessage(message, EncryptionProtocol.audienceEpochPath));
        case DwnErrorCode.ProtocolAuthorizationEncryptionRoleAudienceEpochMissing:
            return toRefList(encryptionProtocolDependencyFromRoleAudienceEntry(message, detail));
        case DwnErrorCode.EncryptionProtocolValidateAudienceKeyRoleRecordMissing:
            return toRefList(roleDependencyFromEncryptionAudienceRecipient(message));
        case DwnErrorCode.RecordsWriteMissingDataInPrevious:
        case DwnErrorCode.RecordsWriteMissingEncodedDataInPrevious:
            return toRefList(recordDataDependencyFromMessage(message));
        default:
            break;
    }
    if (code === 404 && isRecordsDelete(message)) {
        return [{ type: 'InitialWrite', recordId: getRecordsDeleteRecordId(message) }];
    }
    return [];
}
function toRefList(ref) {
    return ref === undefined ? [] : [ref];
}
function getDwnErrorCode(detail) {
    const delimiter = detail.indexOf(':');
    return delimiter === -1 ? undefined : detail.slice(0, delimiter);
}
function isResolverFailure(detail) {
    return getDwnErrorCode(detail) === DwnErrorCode.GeneralJwsVerifierGetPublicKeyNotFound;
}
function protocolDependencyFromMessage(message, detail) {
    const composedProtocol = /composed protocol '([^']+)'/.exec(detail)?.[1];
    if (composedProtocol !== undefined) {
        return { type: 'Protocol', protocol: composedProtocol };
    }
    const protocol = dependencyProtocolFromMessage(message);
    return typeof protocol === 'string' ? { type: 'Protocol', protocol } : undefined;
}
/**
 * Emits the refs for an immediate-parent referential failure. The missing parent keeps its
 * existing `Parent` ref; when the apply context carries the layer-batched missing-ancestor set
 * (see `missingAncestorRecordIdsFromReply()`), an `Ancestor` ref is additionally emitted for
 * every locally-absent segment above the parent, so the entire ancestry resolves in a single
 * fetch pass instead of one level per retry.
 */
function parentDependenciesFromMessage(message, detail, context) {
    const parentRef = parentDependencyFromMessage(message, detail);
    if (parentRef === undefined) {
        return [];
    }
    const protocol = dependencyProtocolFromMessage(message);
    const protocolProperty = typeof protocol === 'string' ? { protocol } : {};
    const ancestorRefs = (context.missingAncestorRecordIds ?? [])
        .filter((recordId) => recordId !== parentRef.recordId)
        .map((recordId) => ({ type: 'Ancestor', recordId, ...protocolProperty }));
    return [...ancestorRefs, parentRef];
}
function parentDependencyFromMessage(message, detail) {
    const parentId = parentRecordIdFromMessage(message, detail);
    const protocol = /in protocol '([^']+)'/.exec(detail)?.[1] ?? dependencyProtocolFromMessage(message);
    if (parentId === undefined || typeof protocol !== 'string') {
        return undefined;
    }
    return { type: 'Parent', recordId: parentId, protocol };
}
function parentRecordIdFromMessage(message, detail) {
    const descriptor = message.descriptor;
    return typeof descriptor.parentId === 'string'
        ? descriptor.parentId
        : /parent record '([^']+)'/.exec(detail)?.[1];
}
/**
 * Emits `Ancestor` refs for a record-chain construction failure. When the apply context carries
 * the layer-batched missing-ancestor set (see `missingAncestorRecordIdsFromReply()`), one ref is
 * emitted per missing segment so the entire ancestry resolves in a single fetch pass; otherwise
 * falls back to the single ancestor named by the failure detail (e.g. for messages that carry no
 * `contextId` chain).
 */
function ancestorDependenciesFromMessage(message, detail, context) {
    const protocol = dependencyProtocolFromMessage(message);
    const protocolProperty = typeof protocol === 'string' ? { protocol } : {};
    const batchedRecordIds = context.missingAncestorRecordIds;
    if (batchedRecordIds !== undefined && batchedRecordIds.length > 0) {
        return batchedRecordIds.map((recordId) => ({ type: 'Ancestor', recordId, ...protocolProperty }));
    }
    const namedRecordId = ancestorRecordIdFromDetail(detail);
    if (namedRecordId === undefined) {
        return [];
    }
    return [{ type: 'Ancestor', recordId: namedRecordId, ...protocolProperty }];
}
function ancestorRecordIdFromDetail(detail) {
    return /ID ([^ ]+)/.exec(detail)?.[1];
}
function initialWriteDependencyFromMessage(message) {
    if (!isRecordsWrite(message)) {
        return undefined;
    }
    const protocol = dependencyProtocolFromMessage(message);
    return {
        type: 'InitialWrite',
        recordId: message.recordId,
        ...(typeof protocol === 'string' ? { protocol } : {}),
    };
}
function grantDependencyFromMessage(message) {
    if (isEncryptionProtocolMessage(message)) {
        const tags = message.descriptor.tags;
        const grantId = isRecordObject(tags) ? tags.grantId : undefined;
        if (typeof grantId === 'string') {
            return { type: 'Grant', permissionGrantId: grantId };
        }
    }
    const descriptorGrantId = message.descriptor.permissionGrantId;
    if (typeof descriptorGrantId === 'string') {
        return { type: 'Grant', permissionGrantId: descriptorGrantId };
    }
    const payloadGrantId = getSignaturePayload(message)?.permissionGrantId;
    return typeof payloadGrantId === 'string'
        ? { type: 'Grant', permissionGrantId: payloadGrantId }
        : undefined;
}
function recordDataDependencyFromMessage(message) {
    if (!isRecordsWrite(message)) {
        return undefined;
    }
    const descriptor = message.descriptor;
    const dataCid = descriptor.dataCid;
    const protocol = dependencyProtocolFromMessage(message);
    return typeof dataCid === 'string'
        ? {
            type: 'RecordData',
            recordId: message.recordId,
            dataCid,
            ...(typeof protocol === 'string' ? { protocol } : {}),
        }
        : undefined;
}
function roleDependencyFromMessage(message, context) {
    const encryptionRoleDependency = roleDependencyFromEncryptionAudienceWriter(message, context);
    if (encryptionRoleDependency !== undefined) {
        return encryptionRoleDependency;
    }
    const descriptor = message.descriptor;
    const filter = descriptor.filter;
    const protocol = descriptor.protocol ?? filter?.protocol;
    const protocolRole = getSignaturePayload(message)?.protocolRole;
    const recipient = Message.getAuthor(message);
    if (typeof protocol !== 'string' || typeof protocolRole !== 'string' || recipient === undefined) {
        return undefined;
    }
    let roleProtocol = protocol;
    let roleProtocolPath = protocolRole;
    if (isCrossProtocolRef(protocolRole)) {
        const parsed = parseCrossProtocolRef(protocolRole);
        const referencedProtocol = parsed === undefined ? undefined : context.protocolDefinition?.uses?.[parsed.alias];
        if (parsed !== undefined && referencedProtocol !== undefined) {
            roleProtocol = referencedProtocol;
            roleProtocolPath = parsed.protocolPath;
        }
    }
    let contextId;
    if (typeof descriptor.contextId === 'string') {
        contextId = descriptor.contextId;
    }
    else if (typeof filter?.contextId === 'string') {
        contextId = filter.contextId;
    }
    const roleSegments = roleProtocolPath.split('/').length - 1;
    const contextPrefix = roleSegments > 0 && contextId !== undefined
        ? contextId.split('/').slice(0, roleSegments).join('/')
        : undefined;
    return {
        type: 'Role',
        protocol: roleProtocol,
        protocolPath: roleProtocolPath,
        recipient,
        ...(contextPrefix === undefined ? {} : { contextPrefix }),
    };
}
function encryptionProtocolDependencyFromMessage(message, protocolPath) {
    const tags = encryptionAudienceTagsFromMessage(message);
    return tags === undefined
        ? undefined
        : { type: 'EncryptionProtocol', protocolPath, tags };
}
function encryptionProtocolDependencyFromRoleAudienceEntry(message, detail) {
    const descriptor = message.descriptor;
    const messageContextId = message.contextId;
    const contextId = typeof messageContextId === 'string' ? messageContextId : descriptor.contextId;
    const keyEncryption = message.encryption?.keyEncryption;
    if (typeof contextId !== 'string' || !Array.isArray(keyEncryption)) {
        return undefined;
    }
    const missingRole = /role '([^']+)'/.exec(detail)?.[1];
    const entry = keyEncryption.find((candidate) => candidate.derivationScheme === ROLE_AUDIENCE_DERIVATION_SCHEME &&
        typeof candidate.protocol === 'string' &&
        typeof candidate.role === 'string' &&
        (missingRole === undefined || candidate.role === missingRole) &&
        typeof candidate.epoch === 'number' &&
        typeof candidate.keyId === 'string');
    if (entry === undefined) {
        return undefined;
    }
    const role = entry.role;
    const audienceContextId = roleAudienceContextId(role, contextId);
    if (audienceContextId === undefined) {
        return undefined;
    }
    return {
        type: 'EncryptionProtocol',
        protocolPath: EncryptionProtocol.audienceEpochPath,
        tags: {
            protocol: entry.protocol,
            contextId: audienceContextId,
            role,
            epoch: entry.epoch,
            keyId: entry.keyId,
        },
    };
}
function roleDependencyFromEncryptionAudienceRecipient(message) {
    const tags = encryptionAudienceTagsFromMessage(message);
    const recipient = message.descriptor.recipient;
    if (tags === undefined || typeof recipient !== 'string') {
        return undefined;
    }
    return {
        type: 'Role',
        protocol: tags.protocol,
        protocolPath: tags.role,
        recipient,
        ...(tags.contextId === '' ? {} : { contextPrefix: tags.contextId }),
    };
}
function roleDependencyFromEncryptionAudienceWriter(message, context) {
    const tags = encryptionAudienceTagsFromMessage(message);
    const protocolRole = getSignaturePayload(message)?.protocolRole;
    const recipient = Message.getAuthor(message);
    if (tags === undefined || typeof protocolRole !== 'string' || recipient === undefined) {
        return undefined;
    }
    let roleProtocol = tags.protocol;
    let roleProtocolPath = protocolRole;
    if (isCrossProtocolRef(protocolRole)) {
        const parsed = parseCrossProtocolRef(protocolRole);
        const referencedProtocol = parsed === undefined ? undefined : context.protocolDefinition?.uses?.[parsed.alias];
        if (parsed === undefined || referencedProtocol === undefined) {
            return undefined;
        }
        roleProtocol = referencedProtocol;
        roleProtocolPath = parsed.protocolPath;
    }
    const contextPrefix = roleContextPrefix(roleProtocolPath, tags.contextId);
    return {
        type: 'Role',
        protocol: roleProtocol,
        protocolPath: roleProtocolPath,
        recipient,
        ...(contextPrefix === undefined ? {} : { contextPrefix }),
    };
}
function encryptionAudienceTagsFromMessage(message) {
    if (!isEncryptionProtocolMessage(message)) {
        return undefined;
    }
    const tags = message.descriptor.tags;
    if (!isRecordObject(tags)) {
        return undefined;
    }
    const { protocol, contextId, role, epoch, keyId } = tags;
    if (typeof protocol !== 'string' ||
        typeof contextId !== 'string' ||
        typeof role !== 'string' ||
        typeof epoch !== 'number' ||
        typeof keyId !== 'string') {
        return undefined;
    }
    return { protocol, contextId, role, epoch, keyId };
}
function dependencyProtocolFromMessage(message) {
    if (isEncryptionProtocolMessage(message)) {
        const tags = message.descriptor.tags;
        const protocol = isRecordObject(tags) ? tags.protocol : undefined;
        return typeof protocol === 'string' ? protocol : undefined;
    }
    const descriptor = message.descriptor;
    const filter = descriptor.filter;
    const protocol = descriptor.protocol ?? filter?.protocol;
    return typeof protocol === 'string' ? protocol : undefined;
}
function isEncryptionProtocolMessage(message) {
    const descriptor = message.descriptor;
    return descriptor.interface === DwnInterfaceName.Records &&
        descriptor.protocol === EncryptionProtocol.uri;
}
function roleContextPrefix(rolePath, contextId) {
    const roleSegments = rolePath.split('/').length - 1;
    if (roleSegments === 0 || contextId === '') {
        return undefined;
    }
    return contextId.split('/').slice(0, roleSegments).join('/');
}
function roleAudienceContextId(rolePath, contextId) {
    const roleSegments = rolePath.split('/').length - 1;
    if (roleSegments === 0) {
        return '';
    }
    const contextSegments = contextId.split('/');
    if (contextSegments.length < roleSegments) {
        return undefined;
    }
    return contextSegments.slice(0, roleSegments).join('/');
}
function isRecordObject(value) {
    return typeof value === 'object' && value !== null;
}
function isRecordsWrite(message) {
    return message.descriptor.interface === DwnInterfaceName.Records &&
        message.descriptor.method === DwnMethodName.Write &&
        typeof message.recordId === 'string';
}
function isRecordsDelete(message) {
    return message.descriptor.interface === DwnInterfaceName.Records &&
        message.descriptor.method === DwnMethodName.Delete;
}
function getRecordsDeleteRecordId(message) {
    return message.descriptor.recordId;
}
function getSignaturePayload(message) {
    const payload = message.authorization?.signature.payload;
    if (payload === undefined) {
        return undefined;
    }
    try {
        return Encoder.base64UrlToObject(payload);
    }
    catch {
        return undefined;
    }
}
//# sourceMappingURL=replication-apply.js.map