/**
 * Stripe integration routes — checkout, webhook, customer portal.
 *
 * POST   /stripe/checkout          — create Stripe checkout session
 * POST   /stripe/webhook           — handle Stripe webhook events
 * POST   /stripe/portal            — create Stripe customer portal session
 *
 * Multi-tenant: productId is embedded in Stripe metadata and used to route
 * webhook events. Supports per-product Stripe keys via STRIPE_SECRET_KEY_<PRODUCTID>.
 */

import { randomUUID } from 'node:crypto';
import type { FastifyInstance, FastifyRequest } from 'fastify';
import Stripe from 'stripe';
import { DEFAULT_PRODUCT_ID } from '../../lib/product-config.js';
import { getRequestProductId } from '../../lib/request-context.js';
import { getStripeForProduct, getPriceIds } from '../../lib/stripe.js';
import { BadRequestError } from '../../lib/errors.js';
import * as authRepo from '../auth/repository.js';
import * as subRepo from '../subscriptions/repository.js';
import type { SubscriptionDoc } from '../subscriptions/types.js';

export async function stripeRoutes(app: FastifyInstance) {
  // ── Checkout ──────────────────────────────────────────────
  app.post('/stripe/checkout', async (req, reply) => {
    const { userId, plan, successUrl, cancelUrl, trialDays, promoCode } = req.body as {
      userId: string;
      plan: string;
      successUrl: string;
      cancelUrl: string;
      trialDays?: number;
      promoCode?: string;
    };

    if (!userId || !plan || !successUrl || !cancelUrl) {
      throw new BadRequestError('userId, plan, successUrl, cancelUrl are required');
    }

    const productId = getRequestProductId(req);
    const stripe = getStripeForProduct(productId);
    const priceIds = getPriceIds();
    const priceId = priceIds[plan];
    if (!priceId) throw new BadRequestError(`No price configured for plan: ${plan}`);

    const params: Stripe.Checkout.SessionCreateParams = {
      mode: 'subscription',
      line_items: [{ price: priceId, quantity: 1 }],
      success_url: successUrl,
      cancel_url: cancelUrl,
      metadata: { userId, productId, plan },
      ...(trialDays &&
        trialDays > 0 && {
          subscription_data: { trial_period_days: trialDays },
        }),
      ...(promoCode && { allow_promotion_codes: true }),
    };

    const session = await stripe.checkout.sessions.create(params);
    reply.code(201);
    return { sessionId: session.id, url: session.url };
  });

  // ── Webhook ───────────────────────────────────────────────
  // Register a raw content-type parser so we receive the untouched body
  // for Stripe signature verification.
  app.addContentTypeParser('application/json', { parseAs: 'string' }, (_req, body, done) => {
    done(null, body);
  });

  app.post('/stripe/webhook', async (req: FastifyRequest, reply) => {
    const sig = req.headers['stripe-signature'] as string;
    const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
    if (!sig || !webhookSecret) {
      throw new BadRequestError('Missing stripe-signature or webhook secret');
    }

    const stripe = getStripeForProduct(DEFAULT_PRODUCT_ID);
    let event: Stripe.Event;

    try {
      // req.body is the raw string thanks to the content-type parser above
      const rawBody = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
      event = stripe.webhooks.constructEvent(rawBody, sig, webhookSecret);
    } catch (err) {
      app.log.error(`Webhook signature verification failed: ${err}`);
      reply.code(400).send({ error: 'Invalid signature' });
      return;
    }

    // Route by productId in metadata (multi-tenant)
    const metadata = getEventMetadata(event);
    const eventProductId = metadata?.productId || DEFAULT_PRODUCT_ID;

    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object as Stripe.Checkout.Session;
        const userId = session.metadata?.userId;
        const plan = normalizePlan(session.metadata?.plan) ?? 'pro';
        if (userId && session.customer) {
          const existing = await subRepo.getByUserId(userId, eventProductId);
          const now = new Date();
          const periodEnd = new Date(now);
          periodEnd.setMonth(periodEnd.getMonth() + 1);

          if (existing) {
            await subRepo.updateSubscription(existing.id, userId, {
              plan: plan as SubscriptionDoc['plan'],
              status: 'active',
              stripeCustomerId: String(session.customer),
              stripeSubscriptionId: session.subscription ? String(session.subscription) : undefined,
              currentPeriodStart: now.toISOString(),
              currentPeriodEnd: periodEnd.toISOString(),
            });
            await authRepo.updatePlan(userId, eventProductId, plan as SubscriptionDoc['plan']);
          } else {
            await subRepo.createSubscription({
              id: `sub_${userId}_${Date.now()}`,
              productId: eventProductId,
              userId,
              plan: plan as SubscriptionDoc['plan'],
              status: 'active',
              currentPeriodStart: now.toISOString(),
              currentPeriodEnd: periodEnd.toISOString(),
              cancelAtPeriodEnd: false,
              monthlyPrice: (session.amount_total ?? 0) / 100,
              tokensIncluded: 0, // Will be set by plan config
              tokensUsed: 0,
              stripeCustomerId: String(session.customer),
              stripeSubscriptionId: session.subscription ? String(session.subscription) : undefined,
              createdAt: now.toISOString(),
              updatedAt: now.toISOString(),
            });
            await authRepo.updatePlan(userId, eventProductId, plan as SubscriptionDoc['plan']);
          }

          // Record payment
          if (session.amount_total && session.amount_total > 0) {
            await subRepo.createPayment({
              id: `pay_${randomUUID()}`,
              productId: eventProductId,
              userId,
              amount: session.amount_total,
              currency: session.currency || 'usd',
              status: 'succeeded',
              description: `${plan} plan subscription`,
              method: 'card',
              createdAt: now.toISOString(),
            });
          }
        }
        break;
      }

      case 'customer.subscription.updated': {
        const sub = event.data.object as Stripe.Subscription;
        const customerId = typeof sub.customer === 'string' ? sub.customer : sub.customer.id;
        const existing = metadata?.productId
          ? await subRepo.getByStripeCustomerId(customerId, eventProductId)
          : await subRepo.getByStripeCustomerIdAnyProduct(customerId);
        if (existing) {
          const effectiveProductId = existing.productId;
          const updatedPlan = getPlanFromSubscription(sub) ?? existing.plan;
          const newStatus = sub.cancel_at_period_end
            ? 'cancelled'
            : sub.status === 'active'
              ? 'active'
              : 'past_due';
          await subRepo.updateSubscription(existing.id, existing.userId, {
            plan: updatedPlan,
            status: newStatus,
            cancelAtPeriodEnd: sub.cancel_at_period_end,
            currentPeriodEnd: new Date(getSubscriptionPeriodEnd(sub) * 1000).toISOString(),
          });
          await authRepo.updatePlan(existing.userId, effectiveProductId, updatedPlan);
        }
        break;
      }

      case 'customer.subscription.deleted': {
        const sub = event.data.object as Stripe.Subscription;
        const customerId = typeof sub.customer === 'string' ? sub.customer : sub.customer.id;
        const existing = metadata?.productId
          ? await subRepo.getByStripeCustomerId(customerId, eventProductId)
          : await subRepo.getByStripeCustomerIdAnyProduct(customerId);
        if (existing) {
          const effectiveProductId = existing.productId;
          await subRepo.updateSubscription(existing.id, existing.userId, {
            status: 'cancelled',
            plan: 'free',
            cancelAtPeriodEnd: false,
          });
          await authRepo.updatePlan(existing.userId, effectiveProductId, 'free');
        }
        break;
      }

      case 'invoice.payment_succeeded': {
        const invoice = event.data.object as Stripe.Invoice;
        const customerId =
          typeof invoice.customer === 'string' ? invoice.customer : invoice.customer?.id;
        if (customerId) {
          const existing = metadata?.productId
            ? await subRepo.getByStripeCustomerId(customerId, eventProductId)
            : await subRepo.getByStripeCustomerIdAnyProduct(customerId);
          if (existing && invoice.amount_paid > 0) {
            await subRepo.createPayment({
              id: `pay_${randomUUID()}`,
              productId: existing.productId,
              userId: existing.userId,
              amount: invoice.amount_paid,
              currency: invoice.currency,
              status: 'succeeded',
              description: 'Subscription renewal',
              method: 'card',
              invoiceUrl: invoice.hosted_invoice_url ?? undefined,
              createdAt: new Date().toISOString(),
            });
          }
        }
        break;
      }

      default:
        app.log.info(`Unhandled Stripe event type: ${event.type}`);
    }

    return { received: true };
  });

  // ── Customer Portal ───────────────────────────────────────
  app.post('/stripe/portal', async req => {
    const { userId, returnUrl } = req.body as { userId?: string; returnUrl?: string };
    if (!userId || !returnUrl) {
      throw new BadRequestError('userId and returnUrl are required');
    }

    const productId = getRequestProductId(req);
    const sub = await subRepo.getByUserId(userId, productId);
    if (!sub?.stripeCustomerId) {
      throw new BadRequestError('No Stripe customer found for this user');
    }

    const stripe = getStripeForProduct(productId);
    const session = await stripe.billingPortal.sessions.create({
      customer: sub.stripeCustomerId,
      return_url: returnUrl,
    });

    return { url: session.url };
  });
}

/** Extract metadata from various Stripe event object types. */
function getEventMetadata(event: Stripe.Event): Record<string, string> | null {
  const obj = event.data.object as unknown as Record<string, unknown>;
  if (obj.metadata && typeof obj.metadata === 'object') {
    return obj.metadata as Record<string, string>;
  }
  return null;
}

function getPlanFromSubscription(sub: Stripe.Subscription): SubscriptionDoc['plan'] | null {
  const priceId = sub.items.data[0]?.price?.id;
  if (!priceId) return null;
  const priceIds = getPriceIds();
  if (priceId === priceIds.enterprise) return 'enterprise';
  if (priceId === priceIds.pro) return 'pro';
  return null;
}

/**
 * Stripe moved `current_period_end` off the Subscription onto each subscription
 * item. The subscription-level period end is the latest item period end.
 */
function getSubscriptionPeriodEnd(sub: Stripe.Subscription): number {
  const ends = sub.items.data
    .map(i => i.current_period_end)
    .filter((n): n is number => typeof n === 'number');
  return ends.length > 0 ? Math.max(...ends) : Math.floor(Date.now() / 1000);
}

function normalizePlan(plan: string | undefined): SubscriptionDoc['plan'] | null {
  if (plan === 'free' || plan === 'pro' || plan === 'enterprise') return plan;
  return null;
}
