/**
 * HMAC-SHA256 request signing and verification for the sync protocol.
 *
 * Provides:
 * - Canonical payload construction
 * - Request signing (client side)
 * - Request verification with replay protection (server side)
 *
 * @module domain/services/sync/SyncAuthService
 */
import LRUCache from "../../utils/LRUCache.js";
import defaultCrypto from "../../utils/defaultCrypto.js";
import nullLogger from "../../utils/nullLogger.js";
import { validateWriterId } from "../../utils/RefLayout.js";
import { hexEncode, hexDecode } from "../../utils/bytes.js";
import SyncError from "../../errors/SyncError.js";
import SyncSecret from "./SyncSecret.js";
import SyncRateLimiter from "./SyncRateLimiter.js";
const SIG_VERSION = '2';
const SIG_PREFIX = 'warp-v2';
const HMAC_ALGO = 'sha256';
export const SYNC_AUTH_SCHEME_HEADER = 'x-warp-auth-scheme';
export const SHARED_SECRET_HMAC_SYNC_AUTH_SCHEME = 'shared-secret-hmac-sha256';
const DEFAULT_NONCE_CAPACITY = 100_000;
const NONCE_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const SIG_HEX_LENGTH = 64;
const HEX_PATTERN = /^[0-9a-f]+$/;
const MAX_TIMESTAMP_DIGITS = 16;
/**
 * Canonicalizes a URL path for signature computation.
 */
export function canonicalizePath(url) {
    const parsed = new URL(url, 'http://localhost');
    return parsed.pathname + (parsed.search || '');
}
/**
 * Builds the canonical string that gets signed.
 */
export function buildCanonicalPayload(params) {
    const { authScheme, keyId, method, path, timestamp, nonce, contentType, bodySha256 } = params;
    const prefix = authScheme === undefined ? SIG_PREFIX : `${SIG_PREFIX}|${authScheme}`;
    return `${prefix}|${keyId}|${method}|${path}|${timestamp}|${nonce}|${contentType}|${bodySha256}`;
}
/**
 * Signs an outgoing sync request.
 */
export async function signSyncRequest(params, deps = {}) {
    const c = deps.crypto ?? defaultCrypto;
    const timestamp = String(params.lamport);
    const nonce = globalThis.crypto.randomUUID();
    const authScheme = params.authScheme ?? SHARED_SECRET_HMAC_SYNC_AUTH_SCHEME;
    const bodySha256 = await c.hash('sha256', params.body);
    const canonical = buildCanonicalPayload({
        authScheme,
        keyId: params.keyId,
        method: params.method.toUpperCase(),
        path: params.path,
        timestamp,
        nonce,
        contentType: params.contentType,
        bodySha256,
    });
    const hmacBuf = await params.secret.hmac(c, HMAC_ALGO, canonical);
    const signature = hexEncode(hmacBuf);
    return {
        [SYNC_AUTH_SCHEME_HEADER]: authScheme,
        'x-warp-sig-version': SIG_VERSION,
        'x-warp-key-id': params.keyId,
        'x-warp-timestamp': timestamp,
        'x-warp-nonce': nonce,
        'x-warp-signature': signature,
    };
}
function fail(reason, status) {
    return { ok: false, reason, status };
}
function _freshMetrics() {
    return {
        authFailCount: 0,
        replayRejectCount: 0,
        nonceEvictions: 0,
        clockSkewRejects: 0,
        malformedRejects: 0,
        logOnlyPassthroughs: 0,
        forbiddenWriterRejects: 0,
        rateLimitRejects: 0,
    };
}
function _checkHeaderFormats(timestamp, nonce, signature) {
    if (!/^\d+$/.test(timestamp) || timestamp.length > MAX_TIMESTAMP_DIGITS) {
        return fail('MALFORMED_TIMESTAMP', 400);
    }
    if (!NONCE_PATTERN.test(nonce)) {
        return fail('MALFORMED_NONCE', 400);
    }
    if (signature.length !== SIG_HEX_LENGTH || !HEX_PATTERN.test(signature)) {
        return fail('MALFORMED_SIGNATURE', 400);
    }
    return { ok: true };
}
function _validateAuthScheme(headers) {
    const authScheme = headers[SYNC_AUTH_SCHEME_HEADER];
    if (authScheme === undefined) {
        return { ok: true, authScheme: null };
    }
    if (authScheme === SHARED_SECRET_HMAC_SYNC_AUTH_SCHEME) {
        return { ok: true, authScheme };
    }
    return fail('UNSUPPORTED_AUTH_SCHEME', 400);
}
function _validateKeys(keys) {
    if (!keys || typeof keys !== 'object' || Object.keys(keys).length === 0) {
        throw new SyncError('SyncAuthService requires a non-empty keys map', { code: 'E_SYNC_AUTH_NO_KEYS' });
    }
    for (const secret of Object.values(keys)) {
        if (!(secret instanceof SyncSecret)) {
            throw new SyncError('SyncAuthService requires SyncSecret values', { code: 'E_SYNC_AUTH_SECRET_TYPE' });
        }
    }
}
function _validateAllowedWriters(allowedWriters) {
    if (!allowedWriters) {
        return null;
    }
    if (allowedWriters.length === 0) {
        throw new SyncError('allowedWriters must be a non-empty array when provided', { code: 'E_SYNC_AUTH_EMPTY_ALLOWED_WRITERS' });
    }
    for (const w of allowedWriters) {
        validateWriterId(w);
    }
    return new Set(allowedWriters);
}
export default class SyncAuthService {
    _keys;
    _mode;
    _crypto;
    _logger;
    _nonceCache;
    _lastSeenLamport;
    _allowedWriters;
    _rateLimiter;
    _metrics;
    constructor(options) {
        const { keys, mode = 'enforce', nonceCapacity, crypto, logger, allowedWriters, rateLimit } = options ?? {};
        _validateKeys(keys);
        this._keys = keys;
        this._mode = mode;
        this._crypto = crypto ?? defaultCrypto;
        this._logger = logger ?? nullLogger;
        this._nonceCache = new LRUCache(typeof nonceCapacity === 'number' && nonceCapacity > 0 ? nonceCapacity : DEFAULT_NONCE_CAPACITY);
        this._lastSeenLamport = new Map();
        this._metrics = _freshMetrics();
        this._allowedWriters = _validateAllowedWriters(allowedWriters);
        this._rateLimiter = rateLimit === undefined ? null : new SyncRateLimiter(rateLimit);
    }
    get mode() {
        return this._mode;
    }
    _validateHeaders(headers) {
        const authSchemeResult = _validateAuthScheme(headers);
        if (!authSchemeResult.ok) {
            return authSchemeResult;
        }
        const sigVersion = headers['x-warp-sig-version'];
        if (sigVersion !== SIG_VERSION) {
            return fail('INVALID_VERSION', 400);
        }
        const keyId = headers['x-warp-key-id'];
        const signature = headers['x-warp-signature'];
        const timestamp = headers['x-warp-timestamp'];
        const nonce = headers['x-warp-nonce'];
        if (keyId === undefined || keyId === '' || signature === undefined || signature === '' || timestamp === undefined || timestamp === '' || nonce === undefined || nonce === '') {
            return fail('MISSING_AUTH', 401);
        }
        const formatCheck = _checkHeaderFormats(timestamp, nonce, signature);
        if (!formatCheck.ok) {
            return formatCheck;
        }
        return { ok: true, sigVersion, signature, timestamp, nonce, keyId, authScheme: authSchemeResult.authScheme };
    }
    _validateFreshness(timestamp, keyId) {
        const lamport = Number(timestamp);
        if (!Number.isFinite(lamport) || lamport < 0) {
            this._metrics.clockSkewRejects += 1;
            return fail('INVALID_LAMPORT', 400);
        }
        const lastSeen = this._lastSeenLamport.get(keyId) ?? -1;
        if (lamport <= lastSeen) {
            this._metrics.clockSkewRejects += 1;
            return fail('STALE_LAMPORT', 403);
        }
        this._lastSeenLamport.set(keyId, lamport);
        return { ok: true };
    }
    _reserveNonce(nonce) {
        if (this._nonceCache.has(nonce)) {
            this._metrics.replayRejectCount += 1;
            return fail('REPLAY', 403);
        }
        const sizeBefore = this._nonceCache.size;
        this._nonceCache.set(nonce, true);
        if (this._nonceCache.size <= sizeBefore && sizeBefore >= this._nonceCache.maxSize) {
            this._metrics.nonceEvictions += 1;
        }
        return { ok: true };
    }
    _resolveKey(keyId) {
        const secret = this._keys[keyId];
        if (secret === undefined) {
            return fail('UNKNOWN_KEY_ID', 401);
        }
        return { ok: true, secret };
    }
    _consumeRateLimit(keyId) {
        if (this._rateLimiter === null) {
            return { ok: true };
        }
        if (this._rateLimiter.tryConsume(keyId)) {
            return { ok: true };
        }
        this._metrics.rateLimitRejects += 1;
        return fail('RATE_LIMITED', 429);
    }
    async _verifySignature(params) {
        const { request, secret, keyId, timestamp, nonce, authScheme } = params;
        const body = request.body ?? new Uint8Array(0);
        const bodySha256 = await this._crypto.hash('sha256', body);
        const contentType = request.headers['content-type'] !== undefined ? request.headers['content-type'] : '';
        const path = canonicalizePath(request.url);
        const canonical = buildCanonicalPayload({
            ...(authScheme !== null ? { authScheme } : {}),
            keyId,
            method: request.method.toUpperCase(),
            path,
            timestamp,
            nonce,
            contentType,
            bodySha256,
        });
        const expectedBuf = await secret.hmac(this._crypto, HMAC_ALGO, canonical);
        const receivedHex = request.headers['x-warp-signature'] ?? '';
        let receivedBuf;
        try {
            receivedBuf = hexDecode(receivedHex);
        }
        catch {
            return fail('INVALID_SIGNATURE', 401);
        }
        if (receivedBuf.length !== expectedBuf.length) {
            return fail('INVALID_SIGNATURE', 401);
        }
        let equal;
        try {
            equal = this._crypto.timingSafeEqual(expectedBuf, receivedBuf);
        }
        catch {
            return fail('INVALID_SIGNATURE', 401);
        }
        if (!equal) {
            return fail('INVALID_SIGNATURE', 401);
        }
        return { ok: true };
    }
    async verify(request) {
        const { headers } = request;
        const headerResult = this._validateHeaders(headers);
        if (!headerResult.ok) {
            this._metrics.malformedRejects += 1;
            return this._fail('header validation failed', { reason: headerResult.reason }, headerResult);
        }
        const { timestamp, nonce, keyId, authScheme } = headerResult;
        const freshnessResult = this._validateFreshness(timestamp, keyId);
        if (!freshnessResult.ok) {
            return this._fail('lamport freshness rejected', { keyId, timestamp }, freshnessResult);
        }
        const keyResult = this._resolveKey(keyId);
        if (!keyResult.ok) {
            return this._fail('unrecognized key-id', { keyId }, keyResult);
        }
        const sigResult = await this._verifySignature({
            request, secret: keyResult.secret, keyId, timestamp, nonce, authScheme,
        });
        if (!sigResult.ok) {
            return this._fail('signature mismatch', { keyId }, sigResult);
        }
        const nonceResult = this._reserveNonce(nonce);
        if (!nonceResult.ok) {
            return this._fail('replay detected', { keyId, nonce }, nonceResult);
        }
        const rateLimitResult = this._consumeRateLimit(keyId);
        if (!rateLimitResult.ok) {
            return this._fail('rate limit exceeded', { keyId }, rateLimitResult);
        }
        return { ok: true };
    }
    verifyWriters(writerIds) {
        if (!this._allowedWriters) {
            return { ok: true };
        }
        const forbidden = writerIds.filter(id => !this._allowedWriters.has(id));
        if (forbidden.length > 0) {
            this._metrics.forbiddenWriterRejects += 1;
            this._logger.warn('sync auth: forbidden writers rejected', { forbidden });
            return fail('FORBIDDEN_WRITER', 403);
        }
        return { ok: true };
    }
    enforceWriters(writerIds) {
        const result = this.verifyWriters(writerIds);
        if (!result.ok && this._mode !== 'enforce') {
            this._metrics.logOnlyPassthroughs += 1;
            return { ok: true };
        }
        return result;
    }
    _fail(message, context, result) {
        this._metrics.authFailCount += 1;
        this._logger.warn(`sync auth: ${message}`, context);
        return result;
    }
    recordLogOnlyPassthrough() {
        this._metrics.logOnlyPassthroughs += 1;
    }
    getMetrics() {
        return { ...this._metrics };
    }
}
