import { Env } from '../config/env.js';
import { Logger } from '../config/logger.js';
import { securityConfig } from '../config/security.js';
import { isMaxLength, isMinLength, isNonEmptyString } from '../helper/index.js';
import { RequestContext } from '../http/RequestContext.js';
import { respondWithMiddlewareFailure, } from './MiddlewareFailureResponder.js';
import { JwtManager } from '../security/JwtManager.js';
import { JwtSessions } from '../security/JwtSessions.js';
import { NonceReplay } from '../security/NonceReplay.js';
import { SignedRequest } from '../security/SignedRequest.js';
const getHeaderString = (req, name) => {
    const header = req.getHeader(name);
    if (Array.isArray(header))
        return typeof header[0] === 'string' ? header[0] : '';
    return typeof header === 'string' ? header : '';
};
const getBearerToken = (authorizationHeader) => {
    const trimmed = authorizationHeader.trim();
    if (trimmed === '')
        return null;
    const [scheme, ...rest] = trimmed.split(/\s+/);
    if (typeof scheme !== 'string' || scheme.toLowerCase() !== 'bearer')
        return null;
    const token = rest.join(' ').trim();
    return token === '' ? null : token;
};
const pickOptionalStringClaim = (payload, keys) => {
    const record = payload;
    for (const key of keys) {
        const value = record[key];
        if (typeof value === 'string' && value.trim() !== '')
            return value.trim();
    }
    return undefined;
};
const toBodyForSigning = (req) => {
    const rawText = req.context?.['rawBodyText'];
    if (typeof rawText === 'string')
        return rawText;
    const rawBytes = req.context?.['rawBodyBytes'];
    if (rawBytes instanceof Uint8Array)
        return rawBytes;
    if (rawBytes instanceof ArrayBuffer)
        return new Uint8Array(rawBytes);
    return '';
};
const toUrlForSigning = (req) => {
    const raw = req.getRaw?.();
    const rawUrl = typeof raw?.url === 'string' ? raw.url : undefined;
    const path = rawUrl ?? req.getPath?.() ?? '/';
    try {
        return new URL(path);
    }
    catch {
        return new URL(path.startsWith('/') ? path : `/${path}`, 'http://localhost');
    }
};
const markAuthContext = (req, ctx) => {
    req.context ??= {};
    req.context['auth'] = ctx;
    req.context['authStrategy'] = 'bulletproof';
};
const respond401 = async (req, res, message, responder, reason = 'unauthorized', error) => {
    await respondWithMiddlewareFailure(req, res, responder, {
        middleware: 'bulletproof',
        reason,
        statusCode: 401,
        message,
        body: { error: message },
        error,
    });
};
const parseBulletproofHeaders = (params) => {
    const deviceId = getHeaderString(params.req, params.deviceIdHeader).trim();
    if (params.requireDeviceId) {
        const ok = isNonEmptyString(deviceId) && isMaxLength(deviceId, 128);
        if (!ok)
            return { ok: false, message: 'Missing or invalid device id' };
    }
    const timezone = getHeaderString(params.req, params.timezoneHeader).trim();
    if (params.requireTimezone) {
        const ok = isNonEmptyString(timezone) && isMaxLength(timezone, 64);
        if (!ok)
            return { ok: false, message: 'Missing or invalid timezone' };
    }
    const signingHeaders = {
        'x-zt-key-id': getHeaderString(params.req, 'x-zt-key-id').trim() || undefined,
        'x-zt-timestamp': getHeaderString(params.req, 'x-zt-timestamp').trim() || undefined,
        'x-zt-nonce': getHeaderString(params.req, 'x-zt-nonce').trim() || undefined,
        'x-zt-body-sha256': getHeaderString(params.req, 'x-zt-body-sha256').trim() || undefined,
        'x-zt-signature': getHeaderString(params.req, 'x-zt-signature').trim() || undefined,
    };
    return { ok: true, deviceId, timezone, signingHeaders };
};
const verifySignedRequest = async (params) => {
    const baseParams = {
        method: params.req.getMethod?.() ?? 'GET',
        url: toUrlForSigning(params.req),
        body: toBodyForSigning(params.req),
        headers: params.signingHeaders,
        nowMs: Date.now(),
        windowMs: params.windowMs,
        verifyNonce: async (keyId, nonce, ttlMs) => {
            const ok = isNonEmptyString(nonce) && isMinLength(nonce, 8) && isMaxLength(nonce, 128);
            if (!ok)
                return false;
            return params.verifyNonce(keyId, nonce, ttlMs);
        },
    };
    const staticSecrets = params.staticSecrets
        ?.map((s) => (typeof s === 'string' ? s.trim() : ''))
        .filter((s) => s !== '');
    const signed = staticSecrets !== undefined && staticSecrets.length > 0
        ? await (async () => {
            const attempts = await Promise.all(staticSecrets.map(async (secret) => SignedRequest.verify({
                ...baseParams,
                getSecretForKeyId: () => secret,
            })));
            // Preserve preference order based on provided secret list.
            for (const attempt of attempts) {
                if (attempt.ok === true)
                    return attempt;
            }
            // If any attempt failed for a reason other than signature mismatch, return that.
            for (const attempt of attempts) {
                if (attempt.ok === false && attempt.code !== 'INVALID_SIGNATURE')
                    return attempt;
            }
            return { ok: false, code: 'INVALID_SIGNATURE', message: 'Invalid signature' };
        })()
        : await SignedRequest.verify({
            ...baseParams,
            getSecretForKeyId: async (keyId) => {
                const secretForKey = await params.getSecretForKeyId(keyId, params.req);
                const normalized = typeof secretForKey === 'string' ? secretForKey.trim() : '';
                return normalized === '' ? undefined : normalized;
            },
        });
    if (!signed.ok) {
        Logger.debug('Bulletproof auth signed-request verification failed', {
            code: signed.code,
            message: signed.message,
        });
        return { ok: false, message: 'Unauthorized' };
    }
    return {
        ok: true,
        keyId: signed.keyId,
        timestampMs: signed.timestampMs,
        nonce: signed.nonce,
    };
};
const verifyJwtPayload = (params) => {
    try {
        return { ok: true, payload: params.jwt.verify(params.token, params.algorithm) };
    }
    catch (error) {
        Logger.debug('Bulletproof auth JWT verification failed', {
            algorithm: params.algorithm,
            error: error instanceof Error ? error.message : String(error),
        });
        return { ok: false };
    }
};
const enforceDeviceBindings = (params) => {
    if (params.requireDeviceId && params.deviceId !== '' && params.deviceId !== params.keyId) {
        return { ok: false, message: 'Device mismatch' };
    }
    if (params.requireDeviceClaimMatch) {
        const claimedDevice = pickOptionalStringClaim(params.payload, params.deviceClaimKeys);
        const mismatch = claimedDevice === undefined || (params.deviceId !== '' && claimedDevice !== params.deviceId);
        if (mismatch)
            return { ok: false, message: 'Device mismatch' };
    }
    return { ok: true };
};
const enforceTimezoneBindings = (params) => {
    if (!params.requireTimezoneClaimMatch || params.timezone === '') {
        return { ok: true };
    }
    const claimedTimezone = pickOptionalStringClaim(params.payload, params.timezoneClaimKeys);
    if (claimedTimezone !== undefined && claimedTimezone !== params.timezone) {
        return { ok: false, message: 'Timezone mismatch' };
    }
    return { ok: true };
};
const enforceUserAgentBindings = async (params) => {
    if (!params.requireUserAgentHashMatch)
        return { ok: true };
    const claimedUaHash = pickOptionalStringClaim(params.payload, params.userAgentHashClaimKeys);
    if (claimedUaHash === undefined)
        return { ok: true };
    const userAgent = getHeaderString(params.req, 'user-agent').trim();
    const uaHash = await SignedRequest.sha256Hex(userAgent);
    if (uaHash !== claimedUaHash) {
        return { ok: false, message: 'User agent mismatch' };
    }
    return { ok: true };
};
const enforceJwtBindings = async (params) => {
    const device = enforceDeviceBindings({
        payload: params.payload,
        deviceId: params.deviceId,
        keyId: params.keyId,
        requireDeviceId: params.requireDeviceId,
        requireDeviceClaimMatch: params.requireDeviceClaimMatch,
        deviceClaimKeys: params.deviceClaimKeys,
    });
    if (!device.ok)
        return device;
    const tz = enforceTimezoneBindings({
        payload: params.payload,
        timezone: params.timezone,
        requireTimezoneClaimMatch: params.requireTimezoneClaimMatch,
        timezoneClaimKeys: params.timezoneClaimKeys,
    });
    if (!tz.ok)
        return tz;
    return enforceUserAgentBindings({
        req: params.req,
        payload: params.payload,
        requireUserAgentHashMatch: params.requireUserAgentHashMatch,
        userAgentHashClaimKeys: params.userAgentHashClaimKeys,
    });
};
const resolveJwtVerifier = (options) => {
    const algorithm = options.algorithm ?? securityConfig.jwt.algorithm;
    const secret = options.secret ?? securityConfig.jwt.secret;
    const windowMs = options.windowMs ?? 60_000;
    const jwt = JwtManager.create();
    if (algorithm === 'HS256' || algorithm === 'HS512') {
        jwt.setHmacSecret(secret);
    }
    return { algorithm, jwt, windowMs };
};
const resolveHeaderConfig = (options) => {
    return {
        deviceIdHeader: (options.deviceIdHeader ?? 'x-zt-device-id').toLowerCase(),
        timezoneHeader: (options.timezoneHeader ?? 'x-zt-timezone').toLowerCase(),
        requireDeviceId: options.requireDeviceId ?? true,
        requireTimezone: options.requireTimezone ?? false,
    };
};
const resolveClaimConfig = (options) => {
    return {
        requireDeviceClaimMatch: options.requireDeviceClaimMatch ?? true,
        deviceClaimKeys: options.deviceClaimKeys ?? ['deviceId', 'device_id', 'did'],
        requireTimezoneClaimMatch: options.requireTimezoneClaimMatch ?? true,
        timezoneClaimKeys: options.timezoneClaimKeys ?? ['tz', 'timezone', 'timeZone'],
        requireUserAgentHashMatch: options.requireUserAgentHashMatch ?? true,
        userAgentHashClaimKeys: options.userAgentHashClaimKeys ?? ['uaHash', 'uah', 'userAgentHash'],
    };
};
const parseBackupSecrets = (raw) => {
    const value = raw.trim();
    if (value === '')
        return [];
    // JSON array support: ["a","b"]
    if (value.startsWith('[')) {
        try {
            const parsed = JSON.parse(value);
            if (!Array.isArray(parsed))
                return [];
            return parsed
                .filter((v) => typeof v === 'string')
                .map((s) => s.trim())
                .filter((s) => s !== '');
        }
        catch {
            return [];
        }
    }
    // Comma-separated support: a,b,c
    return value
        .split(',')
        .map((s) => s.trim())
        .filter((s) => s !== '');
};
const dedupeSecrets = (secrets) => {
    const out = [];
    const seen = new Set();
    for (const secret of secrets) {
        const trimmed = typeof secret === 'string' ? secret.trim() : '';
        if (trimmed === '')
            continue;
        if (seen.has(trimmed))
            continue;
        seen.add(trimmed);
        out.push(trimmed);
    }
    return out;
};
const resolveSigningConfig = (options) => {
    const appKeyFallback = (Env.get('AUTH_KEY', '') || Env.get('APP_KEY', '')).trim();
    const signingSecretFromEnv = Env.get('BULLETPROOF_SIGNING_SECRET', appKeyFallback).trim();
    const signingSecret = (options.signingSecret ?? signingSecretFromEnv).trim();
    const backupSecrets = parseBackupSecrets(Env.get('BULLETPROOF_SIGNING_SECRET_BK', ''));
    const hasCustomResolver = typeof options.getSecretForKeyId === 'function';
    const getSecretForKeyId = hasCustomResolver
        ? options.getSecretForKeyId
        : (_keyId) => (signingSecret === '' ? undefined : signingSecret);
    const verifyNonce = options.verifyNonce ?? NonceReplay.createMemoryVerifier();
    const staticSigningSecrets = hasCustomResolver
        ? undefined
        : dedupeSecrets([signingSecret, ...backupSecrets]);
    return { getSecretForKeyId, verifyNonce, staticSigningSecrets };
};
const resolveBulletproof = (options) => {
    return {
        ...resolveJwtVerifier(options),
        ...resolveHeaderConfig(options),
        ...resolveClaimConfig(options),
        ...resolveSigningConfig(options),
    };
};
const getTokenOrFail = (req) => {
    const authorizationHeader = getHeaderString(req, 'authorization');
    if (authorizationHeader === '')
        return { ok: false, message: 'Missing authorization header' };
    const token = getBearerToken(authorizationHeader);
    if (token === null)
        return { ok: false, message: 'Invalid authorization header format' };
    return { ok: true, token };
};
const authenticate = async (params) => {
    const tokenResult = getTokenOrFail(params.req);
    if (!tokenResult.ok)
        return tokenResult;
    const parsed = parseBulletproofHeaders({
        req: params.req,
        deviceIdHeader: params.resolved.deviceIdHeader,
        timezoneHeader: params.resolved.timezoneHeader,
        requireDeviceId: params.resolved.requireDeviceId,
        requireTimezone: params.resolved.requireTimezone,
    });
    if (!parsed.ok)
        return { ok: false, message: parsed.message };
    const signed = await verifySignedRequest({
        req: params.req,
        signingHeaders: parsed.signingHeaders,
        windowMs: params.resolved.windowMs,
        verifyNonce: params.resolved.verifyNonce,
        getSecretForKeyId: params.resolved.getSecretForKeyId,
        staticSecrets: params.resolved.staticSigningSecrets,
    });
    if (!signed.ok)
        return { ok: false, message: signed.message };
    const jwtResult = verifyJwtPayload({
        jwt: params.resolved.jwt,
        token: tokenResult.token,
        algorithm: params.resolved.algorithm,
    });
    if (!jwtResult.ok)
        return { ok: false, message: 'Invalid or expired token' };
    // Session allowlist: token must exist in the sessions store to be accepted.
    if (!(await JwtSessions.isActive(tokenResult.token))) {
        return { ok: false, message: 'Invalid or expired token' };
    }
    const binding = await enforceJwtBindings({
        req: params.req,
        payload: jwtResult.payload,
        deviceId: parsed.deviceId,
        timezone: parsed.timezone,
        keyId: signed.keyId,
        requireDeviceId: params.resolved.requireDeviceId,
        requireDeviceClaimMatch: params.resolved.requireDeviceClaimMatch,
        deviceClaimKeys: params.resolved.deviceClaimKeys,
        requireTimezoneClaimMatch: params.resolved.requireTimezoneClaimMatch,
        timezoneClaimKeys: params.resolved.timezoneClaimKeys,
        requireUserAgentHashMatch: params.resolved.requireUserAgentHashMatch,
        userAgentHashClaimKeys: params.resolved.userAgentHashClaimKeys,
    });
    if (!binding.ok)
        return { ok: false, message: binding.message };
    return {
        ok: true,
        token: tokenResult.token,
        payload: jwtResult.payload,
        parsed,
        signed,
    };
};
const attachAuth = (req, result) => {
    req.user = result.payload;
    if (typeof result.payload.sub === 'string' && result.payload.sub.trim() !== '') {
        RequestContext.setUserId(req, result.payload.sub);
    }
    const anyPayload = result.payload;
    const tenantId = anyPayload['tenantId'] ?? anyPayload['tenant_id'];
    if ((typeof tenantId === 'string' && tenantId.trim() !== '') ||
        (typeof tenantId === 'number' && Number.isFinite(tenantId))) {
        RequestContext.setTenantId(req, String(tenantId));
    }
    markAuthContext(req, {
        strategy: 'bulletproof',
        deviceId: result.parsed.deviceId === '' ? result.signed.keyId : result.parsed.deviceId,
        keyId: result.signed.keyId,
        signedRequest: {
            timestampMs: result.signed.timestampMs,
            nonce: result.signed.nonce,
        },
    });
    if (result.parsed.timezone !== '') {
        req.context ??= {};
        req.context['timezone'] = result.parsed.timezone;
    }
};
const createHandler = (resolved, onUnauthorized) => {
    return async (req, res, next) => {
        if (req.context?.['authStrategy'] === 'bulletproof' && req.user !== undefined) {
            await next();
            return;
        }
        const result = await authenticate({ req, resolved });
        if (!result.ok) {
            await respond401(req, res, result.message, onUnauthorized);
            return;
        }
        attachAuth(req, result);
        await next();
    };
};
export const BulletproofAuthMiddleware = Object.freeze({
    create(options = {}) {
        const resolved = resolveBulletproof(options);
        return createHandler(resolved, options.onUnauthorized);
    },
});
export default BulletproofAuthMiddleware;
