/**
 * PayPal Webhook Routes
 *
 * Handles PayPal webhook events for subscription lifecycle management
 */

import { Router, Request, Response } from 'express';
import { PrismaClient, Prisma } from '@prisma/client';
import { logger } from '../utils/logger';
import { PayPalWebhookEvent } from '../types';
import { verifyPayPalWebhookSignature } from '../utils/paypalCert';
import { getPayPalConfig } from '../config';
import { tierService } from '../services/tierService';
import { withIdempotency } from '../utils/webhookIdempotency';
import { parsePlanId } from '../services/paypalService';
import { calculatePeriodEnd } from '../utils/dateUtils';

const router = Router();
const prisma = new PrismaClient();

/**
 * Verify PayPal webhook signature
 *
 * Validates that the webhook event came from PayPal using cryptographic verification
 * This is a wrapper around the paypalCert utility that handles webhook ID retrieval
 *
 * @param headers Webhook request headers
 * @param body Raw webhook request body (string)
 * @returns Promise<boolean> True if signature is valid
 */
async function verifyWebhookSignature(
  headers: Record<string, string | string[] | undefined>,
  body: string
): Promise<boolean> {
  try {
    // Get PayPal webhook ID from configuration
    const paypalConfig = getPayPalConfig();
    const webhookId = paypalConfig.webhookId;

    if (!webhookId) {
      logger.error('PayPal webhook ID not configured');
      return false;
    }

    // Delegate to the full signature verification utility
    return await verifyPayPalWebhookSignature(headers, body, webhookId);
  } catch (error) {
    logger.error('PayPal webhook signature verification failed', { error });
    return false;
  }
}

/**
 * Handle PayPal webhook events
 *
 * POST /v1/webhooks/paypal
 *
 * Processes PayPal subscription lifecycle events:
 * - BILLING.SUBSCRIPTION.ACTIVATED
 * - BILLING.SUBSCRIPTION.CANCELLED
 * - BILLING.SUBSCRIPTION.SUSPENDED
 * - BILLING.SUBSCRIPTION.UPDATED
 * - PAYMENT.SALE.COMPLETED
 */
router.post('/paypal', async (req: Request, res: Response) => {
  const startTime = Date.now();
  const requestId = req.requestId || 'unknown';

  try {
    // Get raw body for signature verification
    const rawBody = JSON.stringify(req.body);

    // Verify webhook signature
    const isValid = await verifyWebhookSignature(req.headers as Record<string, string>, rawBody);

    if (!isValid) {
      logger.warn('PayPal webhook signature verification failed', {
        requestId,
        eventType: req.body?.event_type,
        headers: {
          hasTransmissionId: !!req.headers['paypal-transmission-id'],
          hasTransmissionTime: !!req.headers['paypal-transmission-time'],
          hasTransmissionSig: !!req.headers['paypal-transmission-sig'],
          hasCertUrl: !!req.headers['paypal-cert-url'],
          hasAuthAlgo: !!req.headers['paypal-auth-algo'],
        },
      });
      // Return 401 Unauthorized for invalid signatures to indicate authentication failure
      return res.status(401).json({
        error: {
          code: 'INVALID_SIGNATURE',
          message: 'Webhook signature verification failed'
        }
      });
    }

    const event = req.body as PayPalWebhookEvent;

    logger.info('PayPal webhook received', {
      requestId,
      eventId: event.id,
      eventType: event.event_type,
      resourceType: event.resource_type,
      createTime: event.create_time,
    });

    // Check idempotency — skip if this event was already processed
    const isDuplicate = await withIdempotency(
      event.id,
      event.event_type,
      event.resource?.id,
      event,
      async () => {
        // Handle different event types
        switch (event.event_type) {
          case 'BILLING.SUBSCRIPTION.ACTIVATED':
            await handleSubscriptionActivated(event);
            break;
          case 'BILLING.SUBSCRIPTION.CANCELLED':
            await handleSubscriptionCancelled(event);
            break;
          case 'BILLING.SUBSCRIPTION.SUSPENDED':
            await handleSubscriptionSuspended(event);
            break;
          case 'BILLING.SUBSCRIPTION.UPDATED':
            await handleSubscriptionUpdated(event);
            break;
          case 'PAYMENT.SALE.COMPLETED':
            await handlePaymentCompleted(event);
            break;
          case 'PAYMENT.SALE.REFUNDED':
            await handlePaymentRefunded(event);
            break;
          case 'CUSTOMER.DISPUTE.CREATED':
            await handleDisputeCreated(event);
            break;
          case 'CUSTOMER.DISPUTE.RESOLVED':
            await handleDisputeResolved(event);
            break;
          case 'CUSTOMER.DISPUTE.UPDATED':
            await handleDisputeUpdated(event);
            break;
          default:
            logger.info('Unhandled PayPal webhook event type', {
              requestId,
              eventType: event.event_type,
            });
        }
      }
    );

    if (isDuplicate) {
      return res.status(200).json({ received: true, duplicate: true });
    }

    const duration = Date.now() - startTime;
    logger.info('PayPal webhook processed successfully', {
      requestId,
      eventId: event.id,
      eventType: event.event_type,
      duration,
    });

    // Always return 200 to acknowledge receipt
    return res.status(200).json({ received: true });
  } catch (error) {
    const duration = Date.now() - startTime;
    logger.error('PayPal webhook processing failed', {
      requestId,
      error: error instanceof Error ? error.message : 'Unknown error',
      stack: error instanceof Error ? error.stack : undefined,
      duration,
    });

    // Return 200 to prevent retries for processing errors
    // The event is logged and can be manually reprocessed if needed
    return res.status(200).json({ received: true, error: 'Processing error' });
  }
});

/**
 * Handle subscription activation
 *
 * Two paths:
 *   1. Session-backed (canonical): custom_id on the PayPal resource is a
 *      CheckoutSession UUID we created at /v1/subscriptions/checkout. We use
 *      that row's `user_id` and `plugin` as authoritative — the payload never
 *      overrides server-side intent. Session flips to `paid` so the plugin's
 *      poll endpoint can mint the license + api_key.
 *   2. Legacy email-lookup (fallback): if no session id is present, find the
 *      user by subscriber email and default to plugin='translate'. Kept for
 *      any subscriptions created before the session flow landed.
 */
async function handleSubscriptionActivated(event: PayPalWebhookEvent): Promise<void> {
  const subscriptionId = event.resource.id;
  const planId = event.resource.plan_id;
  const email = event.resource.subscriber?.email_address;
  const customId = (event.resource as { custom_id?: string }).custom_id;

  if (!subscriptionId || !planId) {
    logger.warn('Missing required fields in subscription activation event', {
      subscriptionId,
      planId,
    });
    return;
  }

  logger.info('Processing subscription activation', {
    subscriptionId,
    planId,
    email,
    customId,
  });

  // Session-backed lookup — custom_id is a UUID that matches a CheckoutSession.
  // (UUID format is the gate; user-controlled strings hitting this endpoint
  // will not match a row.)
  let session = customId
    ? await prisma.checkoutSession.findUnique({ where: { id: customId } })
    : null;

  if (!session && subscriptionId) {
    // Second chance: session may have been linked at checkout time via
    // paypal_subscription_id. Useful if custom_id was lost in transit.
    session = await prisma.checkoutSession.findUnique({
      where: { paypal_subscription_id: subscriptionId },
    });
  }

  let userId: string;
  let plugin: string;

  if (session) {
    userId = session.user_id;
    plugin = session.plugin;
  } else {
    if (!email) {
      logger.warn('No checkout session and no subscriber email — cannot route activation', {
        subscriptionId,
      });
      return;
    }
    const user = await prisma.user.findUnique({ where: { email } });
    if (!user) {
      logger.warn('User not found for subscription activation', { email });
      return;
    }
    userId = user.id;
    plugin = 'translate';
  }

  const { planTier: tier, billingCycle: cycle } = await parsePlanId(planId, plugin);

  if (!tier || !cycle) {
    logger.warn('Unable to parse plan ID', { planId });
    return;
  }

  // Atomic transaction: activate subscription + allocate credits + mark session paid
  await prisma.$transaction(async (tx) => {
    await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${`credits:${userId}`}, 0))`;
    if (session) {
      await tx.checkoutSession.updateMany({
        where: { id: session.id, status: 'pending' },
        data: { status: 'paid', paypal_subscription_id: subscriptionId },
      });
    }

    // 1. Update or create subscription
    await tx.subscription.upsert({
      where: { user_id_plugin: { user_id: userId, plugin } },
      update: {
        plan_tier: tier,
        billing_cycle: cycle,
        status: 'active',
        paypal_subscription_id: subscriptionId,
        current_period_start: new Date(),
        current_period_end: calculatePeriodEnd(new Date(), cycle),
        cancel_at_period_end: false,
      },
      create: {
        user_id: userId,
        plugin,
        plan_tier: tier,
        billing_cycle: cycle,
        status: 'active',
        paypal_subscription_id: subscriptionId,
        current_period_start: new Date(),
        current_period_end: calculatePeriodEnd(new Date(), cycle),
      },
    });

    // 2. Get current balance from latest transaction
    const latestTransaction = await tx.creditTransaction.findFirst({
      where: { user_id: userId },
      orderBy: { ledger_sequence: 'desc' },
      select: { balance_after: true },
    });

    const currentBalance = latestTransaction?.balance_after ?? 0;

    // 3. Calculate new balance
    const creditAllocation = await tierService.getCreditAllocation(tier, plugin);
    const newBalance = currentBalance + creditAllocation;

    // 4. Create credit transaction
    await tx.creditTransaction.create({
      data: {
        user_id: userId,
        type: 'allocation',
        amount: creditAllocation,
        balance_after: newBalance,
        description: `Monthly credit allocation for ${tier} plan`,
      },
    });

    // 5. Update user timestamp
    await tx.user.update({
      where: { id: userId },
      data: {
        updated_at: new Date(),
      },
    });
  });

  logger.info('Subscription activated successfully', {
    userId: userId,
    subscriptionId,
    tier,
    cycle,
    creditsAllocated: await tierService.getCreditAllocation(tier, plugin),
  });
}

/**
 * Handle subscription cancellation
 */
async function handleSubscriptionCancelled(event: PayPalWebhookEvent): Promise<void> {
  const subscriptionId = event.resource.id;

  if (!subscriptionId) {
    logger.warn('Missing subscription ID in cancellation event');
    return;
  }

  logger.info('Processing subscription cancellation', { subscriptionId });

  // Update subscription status
  const subscription = await prisma.subscription.findUnique({
    where: { paypal_subscription_id: subscriptionId },
  });

  if (!subscription) {
    logger.warn('Subscription not found for cancellation', { subscriptionId });
    return;
  }

  await prisma.subscription.update({
    where: { id: subscription.id },
    data: {
      status: 'cancelled',
      cancel_at_period_end: true,
      grace_period_end_at: subscription.current_period_end,
    },
  });

  logger.info('Subscription cancelled successfully', {
    userId: subscription.user_id,
    subscriptionId,
  });
}

/**
 * Handle subscription suspension
 */
async function handleSubscriptionSuspended(event: PayPalWebhookEvent): Promise<void> {
  const subscriptionId = event.resource.id;

  if (!subscriptionId) {
    logger.warn('Missing subscription ID in suspension event');
    return;
  }

  logger.info('Processing subscription suspension', { subscriptionId });

  // Update subscription status
  const subscription = await prisma.subscription.findUnique({
    where: { paypal_subscription_id: subscriptionId },
  });

  if (!subscription) {
    logger.warn('Subscription not found for suspension', { subscriptionId });
    return;
  }

  await prisma.subscription.update({
    where: { id: subscription.id },
    data: {
      status: 'suspended',
      grace_period_end_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    },
  });

  logger.info('Subscription suspended successfully', {
    userId: subscription.user_id,
    subscriptionId,
  });
}

/**
 * Handle subscription update
 */
async function handleSubscriptionUpdated(event: PayPalWebhookEvent): Promise<void> {
  const subscriptionId = event.resource.id;
  const planId = event.resource.plan_id;

  if (!subscriptionId) {
    logger.warn('Missing subscription ID in update event');
    return;
  }

  logger.info('Processing subscription update', { subscriptionId, planId });

  const subscription = await prisma.subscription.findUnique({
    where: { paypal_subscription_id: subscriptionId },
  });

  if (!subscription) {
    logger.warn('Subscription not found for update', { subscriptionId });
    return;
  }

  // If plan changed, update tier and cycle
  if (planId) {
    const { planTier: tier, billingCycle: cycle } = await parsePlanId(planId, subscription.plugin);

    if (tier && cycle) {
      await prisma.subscription.update({
        where: { id: subscription.id },
        data: {
          plan_tier: tier,
          billing_cycle: cycle,
        },
      });

      logger.info('Subscription updated successfully', {
        userId: subscription.user_id,
        subscriptionId,
        newTier: tier,
        newCycle: cycle,
      });
    }
  }
}

/**
 * Handle payment completion
 *
 * For PAYMENT.SALE.COMPLETED events, PayPal provides:
 * - resource.id: sale/payment ID
 * - resource.billing_agreement_id: subscription ID (for recurring payments)
 * - resource.amount.total: payment amount
 * - resource.amount.currency: currency code
 *
 * This handler records the payment AND allocates credits for the new billing period.
 */
async function handlePaymentCompleted(event: PayPalWebhookEvent): Promise<void> {
  const paymentId = event.resource.id;
  const billingAgreementId = event.resource.billing_agreement_id as string | undefined;
  const paymentAmount = (event.resource.amount as { total?: string; currency?: string })?.total;
  const paymentCurrency = (event.resource.amount as { total?: string; currency?: string })?.currency || 'USD';

  if (!paymentId) {
    logger.warn('Missing payment ID in payment completion event');
    return;
  }

  logger.info('Processing payment completion', {
    paymentId,
    billingAgreementId,
    amount: paymentAmount,
  });

  // Find subscription by PayPal subscription/billing agreement ID
  const subscriptionId = billingAgreementId || (event.resource.id as string);
  if (!subscriptionId) {
    logger.warn('No subscription identifier found in payment event', { paymentId });
    return;
  }

  const subscription = await prisma.subscription.findUnique({
    where: { paypal_subscription_id: subscriptionId },
  });

  if (!subscription) {
    // If billing_agreement_id didn't match, this may be a non-subscription payment
    if (billingAgreementId) {
      logger.warn('Subscription not found for payment', { paymentId, billingAgreementId });
    }
    return;
  }

  await prisma.$transaction(async (tx) => {
    await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${`credits:${subscription.user_id}`}, 0))`;
    // Check for duplicate payment
    const existingPayment = await tx.payment.findUnique({
      where: { paypal_payment_id: paymentId },
    });
    if (existingPayment) {
      logger.warn('Duplicate payment detected, skipping', { paymentId });
      await tx.payment.update({
        where: { id: existingPayment.id },
        data: { retry_count: { increment: 1 } },
      });
      return;
    }

    // 1. Record payment
    const payment = await tx.payment.create({
      data: {
        user_id: subscription.user_id,
        paypal_payment_id: paymentId,
        amount: paymentAmount ? parseFloat(paymentAmount) : 0,
        currency: paymentCurrency,
        status: 'completed',
        type: 'subscription_payment',
        subscription_id: subscription.id,
        raw_webhook_payload: event as unknown as Prisma.InputJsonValue,
      },
    });

    // 1b. Back-link any orphan disputes that arrived before this payment was recorded.
    // Idempotent: only updates rows where payment_id is still null.
    await tx.paymentDispute.updateMany({
      where: { paypal_sale_id: paymentId, payment_id: null },
      data: { payment_id: payment.id },
    });

    // 2. Advance billing period and clear any grace period (user is back in good standing)
    const cycle = subscription.billing_cycle as 'monthly' | 'annual';
    await tx.subscription.update({
      where: { id: subscription.id },
      data: {
        current_period_start: new Date(),
        current_period_end: calculatePeriodEnd(new Date(), cycle),
        grace_period_end_at: null,
      },
    });

    // 3. Allocate credits for the new billing period
    const creditAllocation = await tierService.getCreditAllocation(
      subscription.plan_tier,
      subscription.plugin
    );

    const latestTransaction = await tx.creditTransaction.findFirst({
      where: { user_id: subscription.user_id },
      orderBy: { ledger_sequence: 'desc' },
      select: { balance_after: true },
    });

    const currentBalance = latestTransaction?.balance_after ?? 0;
    const newBalance = currentBalance + creditAllocation;

    await tx.creditTransaction.create({
      data: {
        user_id: subscription.user_id,
        type: 'allocation',
        amount: creditAllocation,
        balance_after: newBalance,
        description: `Monthly credit allocation for ${subscription.plan_tier} plan (recurring payment)`,
        related_payment_id: paymentId,
      },
    });

    // 4. Create invoice for this payment (tax/legal compliance)
    // Use an atomic UPSERT on invoice_counters to guarantee gap-free sequential
    // numbering. PostgreSQL sequences are non-transactional (nextval advances
    // even on rollback), so a count()-based approach or a sequence would leave
    // gaps. The UPSERT runs inside the same tx, so a rolled-back payment also
    // rolls back the counter increment — no number is ever consumed without an
    // invoice being committed.
    const issuedYear = new Date().getUTCFullYear();
    const counterRows = await tx.$queryRaw<{ last_number: number }[]>`
      INSERT INTO invoice_counters (year, last_number, updated_at)
      VALUES (${issuedYear}, 1, NOW())
      ON CONFLICT (year) DO UPDATE
        SET last_number = invoice_counters.last_number + 1,
            updated_at  = NOW()
      RETURNING last_number
    `;
    const invoiceSeq = String(counterRows[0].last_number).padStart(6, '0');
    const invoiceNumber = `PZ-${issuedYear}-${invoiceSeq}`;
    const invoiceAmount = paymentAmount ? parseFloat(paymentAmount) : 0;

    await tx.invoice.create({
      data: {
        user_id: subscription.user_id,
        payment_id: payment.id,
        subscription_id: subscription.id,
        invoice_number: invoiceNumber,
        amount: invoiceAmount,
        currency: paymentCurrency,
        status: 'issued',
        line_items: [
          {
            description: `${subscription.plan_tier} subscription (${subscription.billing_cycle})`,
            quantity: 1,
            unit_price: invoiceAmount,
            amount: invoiceAmount,
          },
        ],
      },
    });

    logger.info('Payment recorded and credits allocated for recurring payment', {
      userId: subscription.user_id,
      paymentId,
      subscriptionId,
      tier: subscription.plan_tier,
      creditAllocation,
      newBalance,
      invoiceNumber,
    });
  });
}

/**
 * Handle payment refund
 *
 * Reverses credit allocation when a payment is refunded through PayPal.
 */
async function handlePaymentRefunded(event: PayPalWebhookEvent): Promise<void> {
  const refundId = event.resource.id;
  const saleId = (event.resource as any).sale_id;

  if (!refundId) {
    logger.warn('Missing refund ID in payment refund event');
    return;
  }

  logger.info('Processing payment refund', { refundId, saleId });

  // Find the original payment by the sale ID
  const originalPayment = await prisma.payment.findUnique({
    where: { paypal_payment_id: saleId },
  });

  if (!originalPayment) {
    logger.warn('Original payment not found for refund', { refundId, saleId });
    return;
  }

  await prisma.$transaction(async (tx) => {
    await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${`credits:${originalPayment.user_id}`}, 0))`;
    // 1. Update original payment status to refunded
    await tx.payment.update({
      where: { id: originalPayment.id },
      data: { status: 'refunded' },
    });

    // 2. Create refund payment record
    const refundAmount = (event.resource.amount as { total?: string })?.total;
    await tx.payment.create({
      data: {
        user_id: originalPayment.user_id,
        paypal_payment_id: refundId,
        amount: refundAmount ? parseFloat(refundAmount) : Number(originalPayment.amount),
        currency: originalPayment.currency,
        status: 'completed',
        type: 'refund',
        subscription_id: originalPayment.subscription_id,
        raw_webhook_payload: event as unknown as Prisma.InputJsonValue,
      },
    });

    // 3. Reverse credit allocation
    // Find the credit allocation linked to this payment
    const creditAllocation = await tx.creditTransaction.findFirst({
      where: {
        user_id: originalPayment.user_id,
        related_payment_id: saleId,
        type: 'allocation',
      },
    });

    if (creditAllocation) {
      const latestTransaction = await tx.creditTransaction.findFirst({
        where: { user_id: originalPayment.user_id },
        orderBy: { ledger_sequence: 'desc' },
        select: { balance_after: true },
      });

      const currentBalance = latestTransaction?.balance_after ?? 0;
      const deductAmount = creditAllocation.amount;
      const newBalance = currentBalance - deductAmount;

      await tx.creditTransaction.create({
        data: {
          user_id: originalPayment.user_id,
          type: 'deduction',
          amount: -deductAmount,
          balance_after: Math.max(0, newBalance),
          description: `Credits reversed due to payment refund (${saleId})`,
          related_payment_id: refundId,
        },
      });
    }

    logger.info('Payment refund processed', {
      userId: originalPayment.user_id,
      refundId,
      saleId,
      creditsReversed: creditAllocation?.amount ?? 0,
    });
  });
}

/**
 * Map a raw PayPal dispute reason string to our DisputeReason enum value.
 * Falls back to 'other' for any unrecognised reason code.
 */
function mapDisputeReason(rawReason: string | undefined): 'chargeback' | 'fraud' | 'unrecognized' | 'product_not_received' | 'product_not_as_described' | 'duplicate' | 'other' {
  switch ((rawReason || '').toUpperCase()) {
    case 'CHARGEBACK':
      return 'chargeback';
    case 'FRAUD':
      return 'fraud';
    case 'UNRECOGNIZED':
      return 'unrecognized';
    case 'ITEM_NOT_RECEIVED':
    case 'PRODUCT_NOT_RECEIVED':
      return 'product_not_received';
    case 'MERCHANDISE_OR_SERVICE_NOT_AS_DESCRIBED':
    case 'PRODUCT_NOT_AS_DESCRIBED':
      return 'product_not_as_described';
    case 'DUPLICATE_TRANSACTION':
    case 'DUPLICATE':
      return 'duplicate';
    default:
      return 'other';
  }
}

/**
 * Handle dispute created
 *
 * For CUSTOMER.DISPUTE.CREATED events, PayPal provides:
 * - resource.id: dispute ID
 * - resource.disputed_transactions[0].seller_transaction_id: original sale/payment ID
 * - resource.reason: dispute reason string
 * - resource.dispute_amount.value + .currency_code: disputed amount
 */
async function handleDisputeCreated(event: PayPalWebhookEvent): Promise<void> {
  const disputeId = event.resource.id;
  const resource = event.resource as any;
  const transactionId: string | undefined =
    resource.disputed_transactions?.[0]?.seller_transaction_id ??
    resource.disputed_transactions?.[0]?.buyer_transaction_id;
  const rawReason: string | undefined = resource.reason;
  const disputeAmount: string | undefined =
    resource.dispute_amount?.value ?? resource.amount?.total;
  const disputeCurrency: string =
    resource.dispute_amount?.currency_code ?? resource.amount?.currency ?? 'USD';

  if (!disputeId) {
    logger.warn('CUSTOMER.DISPUTE.CREATED: missing dispute ID in payload');
    return;
  }

  logger.info('Processing dispute created', { disputeId, transactionId, rawReason });

  // Look up the underlying payment via the PayPal transaction id
  const originalPayment = transactionId
    ? await prisma.payment.findUnique({ where: { paypal_payment_id: transactionId } })
    : null;

  if (!originalPayment) {
    // Payment may not have arrived yet (race condition). Record as an orphan dispute so
    // we don't lose the event. The back-link to payment_id will be filled in by
    // handlePaymentCompleted once the PAYMENT.SALE.COMPLETED webhook arrives.
    logger.info('CUSTOMER.DISPUTE.CREATED: payment not found yet — recording as orphan dispute for later back-linking', {
      disputeId,
      transactionId,
    });
  }

  await prisma.paymentDispute.upsert({
    where: { paypal_dispute_id: disputeId },
    create: {
      payment_id: originalPayment?.id ?? null,
      paypal_dispute_id: disputeId,
      paypal_sale_id: transactionId ?? null,
      reason: mapDisputeReason(rawReason),
      status: 'open',
      amount: disputeAmount ? parseFloat(disputeAmount) : (originalPayment ? Number(originalPayment.amount) : 0),
      currency: disputeCurrency,
      raw_webhook_payload: event as unknown as Prisma.InputJsonValue,
    },
    update: {
      raw_webhook_payload: event as unknown as Prisma.InputJsonValue,
      // Don't overwrite payment_id, paypal_sale_id, or status — update runs only on replay.
    },
  });

  logger.info('Dispute created and recorded', {
    disputeId,
    paymentId: originalPayment?.id ?? null,
    userId: originalPayment?.user_id ?? null,
    orphan: !originalPayment,
    reason: mapDisputeReason(rawReason),
  });
}

/**
 * Handle dispute resolved
 *
 * For CUSTOMER.DISPUTE.RESOLVED events, PayPal provides:
 * - resource.id: dispute ID
 * - resource.status: RESOLVED
 * - resource.dispute_outcome.outcome_code: RESOLVED_BUYER_FAVOUR / RESOLVED_SELLER_FAVOUR / RESOLVED_WITH_PAYOUT etc.
 */
async function handleDisputeResolved(event: PayPalWebhookEvent): Promise<void> {
  const disputeId = event.resource.id;
  const resource = event.resource as any;
  const outcomeCode: string | undefined = resource.dispute_outcome?.outcome_code;

  if (!disputeId) {
    logger.warn('CUSTOMER.DISPUTE.RESOLVED: missing dispute ID in payload');
    return;
  }

  logger.info('Processing dispute resolved', { disputeId, outcomeCode });

  const dispute = await prisma.paymentDispute.findUnique({
    where: { paypal_dispute_id: disputeId },
  });

  if (!dispute) {
    logger.warn('CUSTOMER.DISPUTE.RESOLVED: dispute row not found', { disputeId });
    return;
  }

  // Map outcome code to our status
  let finalStatus: 'won' | 'lost' | 'closed';
  if (outcomeCode === 'RESOLVED_SELLER_FAVOUR') {
    finalStatus = 'won';
  } else if (
    outcomeCode === 'RESOLVED_BUYER_FAVOUR' ||
    outcomeCode === 'RESOLVED_WITH_PAYOUT'
  ) {
    finalStatus = 'lost';
  } else {
    finalStatus = 'closed';
  }

  await prisma.$transaction(async (tx) => {
    // Update dispute record
    await tx.paymentDispute.update({
      where: { id: dispute.id },
      data: {
        status: finalStatus,
        resolved_at: new Date(),
        raw_webhook_payload: event as unknown as Prisma.InputJsonValue,
      },
    });

    // If we lost the dispute, flag the original payment as refunded.
    // Credit reversal is handled by the PAYMENT.SALE.REFUNDED webhook when
    // PayPal auto-refunds the disputed amount — do NOT reverse credits here.
    if (finalStatus === 'lost' && dispute.payment_id) {
      const payment = await tx.payment.findUnique({ where: { id: dispute.payment_id } });
      if (payment && payment.status === 'completed') {
        await tx.payment.update({
          where: { id: dispute.payment_id },
          data: { status: 'refunded' },
        });
        logger.info('Payment flagged as refunded after lost dispute', {
          paymentId: dispute.payment_id,
          disputeId,
        });
      }
    }
  });

  logger.info('Dispute resolved', {
    disputeId,
    paymentId: dispute.payment_id,
    finalStatus,
    outcomeCode,
  });
}

/**
 * Handle dispute updated (status change, evidence submitted, etc.)
 *
 * For CUSTOMER.DISPUTE.UPDATED events we simply update the raw payload
 * and any status change on the existing dispute row.
 */
async function handleDisputeUpdated(event: PayPalWebhookEvent): Promise<void> {
  const disputeId = event.resource.id;
  const resource = event.resource as any;

  if (!disputeId) {
    logger.warn('CUSTOMER.DISPUTE.UPDATED: missing dispute ID in payload');
    return;
  }

  logger.info('Processing dispute updated', { disputeId });

  const dispute = await prisma.paymentDispute.findUnique({
    where: { paypal_dispute_id: disputeId },
  });

  if (!dispute) {
    logger.warn('CUSTOMER.DISPUTE.UPDATED: dispute row not found — skipping', { disputeId });
    return;
  }

  // Map PayPal status to our enum when present (OPEN → open, UNDER_REVIEW → under_review)
  const statusMap: Record<string, 'open' | 'under_review' | 'won' | 'lost' | 'closed'> = {
    OPEN: 'open',
    UNDER_REVIEW: 'under_review',
    WAITING_FOR_BUYER_RESPONSE: 'under_review',
    WAITING_FOR_SELLER_RESPONSE: 'under_review',
    RESOLVED: 'closed',
    OTHER: 'closed',
  };
  const paypalStatus: string | undefined = resource.status;
  const mappedStatus = paypalStatus ? statusMap[paypalStatus.toUpperCase()] : undefined;

  await prisma.paymentDispute.update({
    where: { id: dispute.id },
    data: {
      ...(mappedStatus ? { status: mappedStatus } : {}),
      raw_webhook_payload: event as unknown as Prisma.InputJsonValue,
    },
  });

  logger.info('Dispute updated', { disputeId, paypalStatus, mappedStatus });
}

export default router;
