import { Router } from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
import { ApiError } from "../shared/errors";
import { successResponse } from "../shared/middleware/errorMiddleware";
import { readIdempotency, writeIdempotency } from "../shared/idempotency/idempotencyStore";
import { addTransactionEvent, findTransactionByPartnerReference, getTransactionEvents, updateTransactionStatus } from "../shared/store/transactionStore";
import { emitTransactionPaid } from "../shared/events/transactionEvents";
import { env } from "../config/env";
import { createAuditLog } from "../shared/store/auditLogStore";
const router = Router();
function parsePaymentStatus(rawStatus) {
    const normalized = String(rawStatus || "").toLowerCase();
    if (["paid", "success", "successful", "settled", "completed"].includes(normalized)) {
        return { status: "paid" };
    }
    if (["failed", "declined", "rejected", "error", "cancelled"].includes(normalized)) {
        return { status: "failed" };
    }
    if (["expired", "timeout", "stale"].includes(normalized)) {
        return { status: "expired" };
    }
    return { status: "failed", reason: "UNKNOWN_STATUS" };
}
function verifySignature(payload, signature) {
    if (!signature) {
        throw new ApiError("WEBHOOK_SIGNATURE_INVALID", "missing X-Partner-Signature", 401);
    }
    const { signature: _ignoredSignature, ...signaturePayload } = payload;
    const secret = env.INTEGRATION_WEBHOOK_SECRET;
    const expected = createHmac("sha256", secret)
        .update(JSON.stringify(signaturePayload))
        .digest("hex");
    const a = Buffer.from(signature.toLowerCase(), "utf8");
    const b = Buffer.from(expected.toLowerCase(), "utf8");
    if (a.length !== b.length || !timingSafeEqual(a, b)) {
        throw new ApiError("WEBHOOK_SIGNATURE_INVALID", "invalid X-Partner-Signature", 401);
    }
}
function parseCallback(payload) {
    const partnerReference = payload.partner_reference;
    if (!partnerReference) {
        throw new ApiError("CALLBACK_PARTNER_DATA_INVALID", "partner_reference is required", 400);
    }
    const amount = Number(payload.amount || 0);
    if (!Number.isFinite(amount) || amount <= 0) {
        throw new ApiError("CALLBACK_PARTNER_DATA_INVALID", "amount must be > 0", 400);
    }
    const parsed = {
        partnerReference,
        partnerTxnId: payload.partner_txn_id,
        status: parsePaymentStatus(payload.payment_status || payload.status),
        amount,
        paidAt: payload.paid_at,
        currency: typeof payload.currency === "string" && payload.currency.length > 0 ? payload.currency.toUpperCase() : "IDR"
    };
    return parsed;
}
function makeIdempotencyKey(payload, headerValue) {
    if (headerValue) {
        return headerValue;
    }
    return `${payload.partnerReference}:${payload.partnerTxnId || ""}:${payload.status.status}`;
}
function buildCallbackResponse(req, transactionId, eventId, note, reason) {
    const response = {
        status: "accepted",
        event_id: eventId,
        transaction_id: transactionId,
        request_id: req.requestId,
        timestamp: new Date().toISOString()
    };
    if (note) {
        response.note = note;
    }
    if (reason) {
        response.reason = reason;
    }
    return response;
}
function writeCallbackResult(idempotencyKey, response, transactionId) {
    writeIdempotency("callback.processing", idempotencyKey, { response, transaction_id: transactionId }, env.IDEMPOTENCY_TTL_MS);
}
async function auditWebhookAction(req, payload) {
    await createAuditLog({
        actor_type: "webhook",
        actor_id: "qris_partner",
        action: payload.action,
        entity_type: "transaction",
        entity_id: payload.entity_id,
        before_json: payload.before_json,
        after_json: payload.after_json,
        source_ip: req.ip,
        request_id: req.requestId,
        trace_id: req.traceId
    });
}
async function makeResponseEventId(txId, fallbackTag) {
    const events = await getTransactionEvents(txId);
    return events.at(-1)?.id || `${fallbackTag}_${Date.now()}`;
}
router.post("/qris/callback", async (req, res, next) => {
    const incoming = req.body;
    const signature = req.header("X-Partner-Signature");
    let parsed;
    try {
        verifySignature(incoming, signature);
        parsed = parseCallback(incoming);
    }
    catch (error) {
        return next(error);
    }
    const idempotencyKey = makeIdempotencyKey(parsed, req.header("Idempotency-Key") || undefined);
    const cache = readIdempotency("callback.processing", idempotencyKey);
    if (cache && typeof cache === "object" && "response" in cache) {
        const cached = cache;
        const transactionId = typeof cached.transaction_id === "string" ? cached.transaction_id : null;
        if (transactionId) {
            await addTransactionEvent({
                transaction_id: transactionId,
                event_type: "CALLBACK_DUPLICATE",
                source: "webhook",
                payload_json: {
                    idempotency_key: idempotencyKey,
                    source_payload: incoming
                }
            });
        }
        return res.json(successResponse(req, cached.response));
    }
    const tx = await findTransactionByPartnerReference(parsed.partnerReference);
    if (!tx) {
        const response = buildCallbackResponse(req, null, `callback_no_tx_${Date.now()}`, "TRANSACTION_NOT_FOUND");
        writeCallbackResult(idempotencyKey, response, null);
        return res.json(successResponse(req, response));
    }
    if (parsed.amount !== tx.amount) {
        const response = buildCallbackResponse(req, tx.id, `callback_amount_mismatch_${Date.now()}`, "AMOUNT_MISMATCH");
        await addTransactionEvent({
            transaction_id: tx.id,
            event_type: "CALLBACK_REJECTED",
            source: "webhook",
            payload_json: {
                idempotency_key: idempotencyKey,
                received_amount: parsed.amount,
                expected_amount: tx.amount
            }
        });
        writeCallbackResult(idempotencyKey, response, tx.id);
        return res.status(409).json(successResponse(req, response));
    }
    await addTransactionEvent({
        transaction_id: tx.id,
        event_type: "CALLBACK_RECEIVED",
        source: "webhook",
        payload_json: {
            idempotency_key: idempotencyKey,
            partner_reference: parsed.partnerReference,
            partner_txn_id: parsed.partnerTxnId,
            candidate_status: parsed.status.status,
            status_reason: parsed.status.reason,
            partner_payload: incoming
        }
    });
    const eventContext = {
        partner_reference: parsed.partnerReference,
        partner_txn_id: parsed.partnerTxnId,
        idempotency_key: idempotencyKey,
        candidate_currency: parsed.currency,
        reason: parsed.status.reason
    };
    if (parsed.status.status === "paid") {
        const wasPaid = tx.status === "paid";
        let updated;
        try {
            updated = await updateTransactionStatus(tx.id, "paid", {
                source: "webhook",
                eventContext,
                paid_at: parsed.paidAt
            });
        }
        catch (error) {
            if (error instanceof Error && error.message.startsWith("INVALID_TRANSACTION_STATE_TRANSITION")) {
                const response = buildCallbackResponse(req, tx.id, `callback_transition_${Date.now()}`, "transaction state transition rejected");
                writeCallbackResult(idempotencyKey, response, tx.id);
                return res.status(409).json(successResponse(req, response));
            }
            throw error;
        }
        if (!wasPaid) {
            await auditWebhookAction(req, {
                action: "transaction.mark_paid",
                entity_id: updated.id,
                before_json: tx,
                after_json: updated
            });
            emitTransactionPaid({
                transaction_id: updated.id,
                merchant_id: updated.merchant_id,
                outlet_id: updated.outlet_id,
                terminal_id: updated.terminal_id,
                device_id: updated.device_id,
                amount: updated.amount,
                currency: updated.currency,
                partner_reference: updated.partner_reference,
                paid_at: updated.paid_at
            });
            await addTransactionEvent({
                transaction_id: updated.id,
                event_type: "PUSH_QUEUED",
                source: "system",
                payload_json: {
                    event_type: "transaction.paid",
                    transaction_id: updated.id,
                    partner_reference: updated.partner_reference,
                    device_id: updated.device_id
                }
            });
        }
        const response = buildCallbackResponse(req, updated.id, await makeResponseEventId(updated.id, "tx_event"));
        writeCallbackResult(idempotencyKey, response, updated.id);
        return res.json(successResponse(req, response));
    }
    if (parsed.status.status === "expired") {
        let updated;
        try {
            updated = await updateTransactionStatus(tx.id, "expired", {
                source: "webhook",
                eventContext,
                expired_at: new Date().toISOString()
            });
        }
        catch (error) {
            if (error instanceof Error && error.message.startsWith("INVALID_TRANSACTION_STATE_TRANSITION")) {
                const response = buildCallbackResponse(req, tx.id, `callback_transition_${Date.now()}`, "transaction state transition rejected");
                writeCallbackResult(idempotencyKey, response, tx.id);
                return res.status(409).json(successResponse(req, response));
            }
            throw error;
        }
        await auditWebhookAction(req, {
            action: "transaction.mark_expired",
            entity_id: updated.id,
            before_json: tx,
            after_json: updated
        });
        const response = buildCallbackResponse(req, updated.id, await makeResponseEventId(updated.id, "tx_event"));
        writeCallbackResult(idempotencyKey, response, updated.id);
        return res.json(successResponse(req, response));
    }
    let updated;
    try {
        updated = await updateTransactionStatus(tx.id, "failed", {
            source: "webhook",
            eventContext
        });
    }
    catch (error) {
        if (error instanceof Error && error.message.startsWith("INVALID_TRANSACTION_STATE_TRANSITION")) {
            const response = buildCallbackResponse(req, tx.id, `callback_transition_${Date.now()}`, "transaction state transition rejected");
            writeCallbackResult(idempotencyKey, response, tx.id);
            return res.status(409).json(successResponse(req, response));
        }
        throw error;
    }
    await auditWebhookAction(req, {
        action: "transaction.mark_failed",
        entity_id: updated.id,
        before_json: tx,
        after_json: updated
    });
    const response = buildCallbackResponse(req, updated.id, await makeResponseEventId(updated.id, "tx_event"), undefined, parsed.status.reason);
    writeCallbackResult(idempotencyKey, response, updated.id);
    return res.json(successResponse(req, response));
});
export default router;
