import { NextResponse } from 'next/server';
import { getStripeClientFromDb } from '@server/lib/stripe';
import { db, subscriptions, plans } from '@server/db/drizzle';
import { eq, or, sql } from 'drizzle-orm';
import { initDatabase } from '@server/db/init';
import type Stripe from 'stripe';

import { childLogger } from '@server/logger';
import { wrapRouteHandler } from '@server/logger/request';
const log = childLogger('webhook.stripe');

export const runtime = 'nodejs';

function getSubPeriodEnd(sub: Stripe.Subscription): number | null {
  const item = sub.items?.data?.[0];
  if (item && 'current_period_end' in item) {
    return (item as Stripe.SubscriptionItem & { current_period_end: number }).current_period_end;
  }
  return null;
}

type PlanStatusValue = 'active' | 'inactive' | 'trial' | 'cancelled' | 'past_due';

function normalizeStripeStatus(status: string): PlanStatusValue {
  switch (status) {
    case 'active':       return 'active';
    case 'trialing':     return 'trial';
    case 'past_due':     return 'past_due';
    case 'canceled':
    case 'cancelled':    return 'cancelled';
    case 'unpaid':
    case 'incomplete':   return 'past_due';
    default:             return 'active';
  }
}

async function handleCheckoutSessionCompleted(session: Stripe.Checkout.Session) {
  const restaurantId = session.metadata?.restaurant_id;
  const planId = session.metadata?.plan_id;
  if (!restaurantId || !planId || !session.subscription) return;

  const stripe = await getStripeClientFromDb();
  const subscriptionId = typeof session.subscription === 'string' ? session.subscription : session.subscription.id;
  const stripeSub = await stripe.subscriptions.retrieve(subscriptionId);
  const periodEnd = getSubPeriodEnd(stripeSub);

  const priceId = stripeSub.items.data[0]?.price.id ?? null;
  const customerId = session.customer as string;

  const { rows: existingRows } = await db.execute(sql`
    SELECT stripe_subscription_id FROM subscriptions WHERE restaurant_id = ${restaurantId} LIMIT 1
  `);
  const oldSubId = (existingRows[0] as { stripe_subscription_id: string | null } | undefined)?.stripe_subscription_id;

  if (periodEnd != null) {
    await db.execute(sql`
      INSERT INTO subscriptions (restaurant_id, plan_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, status, current_period_end)
      VALUES (${restaurantId}, ${planId}, ${customerId}, ${stripeSub.id}, ${priceId}, 'active', to_timestamp(${periodEnd}))
      ON CONFLICT (restaurant_id) DO UPDATE SET
        status = 'active', plan_id = ${planId}, stripe_customer_id = ${customerId},
        stripe_subscription_id = ${stripeSub.id}, stripe_price_id = ${priceId},
        current_period_end = to_timestamp(${periodEnd})
    `);
  } else {
    await db.execute(sql`
      INSERT INTO subscriptions (restaurant_id, plan_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, status)
      VALUES (${restaurantId}, ${planId}, ${customerId}, ${stripeSub.id}, ${priceId}, 'active')
      ON CONFLICT (restaurant_id) DO UPDATE SET
        status = 'active', plan_id = ${planId}, stripe_customer_id = ${customerId},
        stripe_subscription_id = ${stripeSub.id}, stripe_price_id = ${priceId}
    `);
  }

  if (oldSubId && oldSubId !== stripeSub.id) {
    try {
      await stripe.subscriptions.cancel(oldSubId);
      log.info({ oldSubId, newSubId: stripeSub.id, restaurantId }, 'cancelled previous subscription after plan change via checkout');
    } catch (err) {
      log.warn({ err, oldSubId }, 'could not cancel old subscription after checkout — may already be cancelled');
    }
  }
}

function resolveSubscriptionId(invoice: Stripe.Invoice): string | null {
  const fromParent = invoice.parent?.subscription_details?.subscription;
  if (fromParent) {
    return typeof fromParent === 'string' ? fromParent : (fromParent as Stripe.Subscription).id;
  }
  const direct = (invoice as unknown as Record<string, unknown>).subscription;
  if (direct) {
    return typeof direct === 'string' ? direct : (direct as Stripe.Subscription).id;
  }
  return null;
}

async function handleInvoicePaymentSucceeded(invoice: Stripe.Invoice) {
  const customerId = typeof invoice.customer === 'string' ? invoice.customer : (invoice.customer as Stripe.Customer)?.id;
  if (!customerId) return;

  const rows = await db.execute(sql`
    SELECT restaurant_id, billing_contact_email FROM subscriptions WHERE stripe_customer_id = ${customerId} LIMIT 1
  `);
  const dbSubRaw = rows.rows[0] as { restaurant_id: string; billing_contact_email: string | null } | undefined;
  if (!dbSubRaw) return;
  const dbSub = { restaurantId: dbSubRaw.restaurant_id };

  const subId = resolveSubscriptionId(invoice);
  if (!subId) {
    await db.update(subscriptions)
      .set({ status: 'active' })
      .where(eq(subscriptions.restaurantId, dbSub.restaurantId));
    return;
  }

  const stripe = await getStripeClientFromDb();
  const stripeSub = await stripe.subscriptions.retrieve(subId);
  const periodEnd = getSubPeriodEnd(stripeSub);

  if (dbSubRaw.billing_contact_email) {
    try {
      const { sendInvoiceNotificationEmail } = await import('@server/services/email.service');
      const customer = await stripe.customers.retrieve(customerId) as import('stripe').default.Customer;
      const invNum = (invoice as unknown as Record<string, unknown>).number as string ?? invoice.id;
      const pStart = invoice.period_start ? new Date(invoice.period_start * 1000).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '';
      const pEnd = invoice.period_end ? new Date(invoice.period_end * 1000).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '';
      const invCurrency = (invoice.currency ?? 'usd').toUpperCase();
      const invAmount = ((invoice.amount_paid ?? 0) / 100).toFixed(2);
      const restName = (customer.metadata?.restaurant_name as string | undefined) ?? 'RestroAgent';
      const hostedUrl = (invoice as unknown as Record<string, unknown>).hosted_invoice_url as string | undefined;
      const pdfUrl = (invoice as unknown as Record<string, unknown>).invoice_pdf as string | undefined;
      await sendInvoiceNotificationEmail({
        to: dbSubRaw.billing_contact_email,
        restaurantName: restName,
        invoiceNumber: invNum,
        amount: invAmount,
        currency: invCurrency,
        periodStart: pStart,
        periodEnd: pEnd,
        hostedUrl,
        pdfUrl,
      }, dbSub.restaurantId);
    } catch (err) {
      log.warn({ err }, 'invoice.payment_succeeded: failed to send billing contact CC email');
    }
  }

  if (periodEnd != null) {
    await db.execute(sql`
      UPDATE subscriptions
      SET status = 'active', current_period_end = to_timestamp(${periodEnd}), overdue_at = NULL
      WHERE restaurant_id = ${dbSub.restaurantId}
    `);
  } else {
    await db.execute(sql`
      UPDATE subscriptions SET status = 'active', overdue_at = NULL WHERE restaurant_id = ${dbSub.restaurantId}
    `);
  }
}

async function handleInvoicePaymentFailed(invoice: Stripe.Invoice) {
  const customerId = typeof invoice.customer === 'string' ? invoice.customer : (invoice.customer as Stripe.Customer)?.id;
  if (!customerId) return;

  await db.update(subscriptions)
    .set({ status: 'past_due' })
    .where(eq(subscriptions.stripeCustomerId, customerId));
}

async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
  const periodEnd = getSubPeriodEnd(subscription);
  const priceId = subscription.items.data[0]?.price.id ?? null;

  let planRow: { id: string } | undefined;
  if (priceId) {
    const rows = await db.select({ id: plans.id })
      .from(plans)
      .where(or(eq(plans.stripePriceIdMonthly, priceId), eq(plans.stripePriceIdAnnual, priceId)))
      .limit(1);
    planRow = rows[0];
  }
  if (!planRow) {
    const metaPlanId = subscription.metadata?.plan_id;
    if (metaPlanId) {
      const rows = await db.select({ id: plans.id })
        .from(plans)
        .where(eq(plans.id, metaPlanId))
        .limit(1);
      planRow = rows[0];
    }
  }

  const normalizedStatus = normalizeStripeStatus(subscription.status);
  if (periodEnd != null && planRow) {
    await db.execute(sql`
      UPDATE subscriptions SET status = ${normalizedStatus}, stripe_price_id = ${priceId}, plan_id = ${planRow.id},
        current_period_end = to_timestamp(${periodEnd})
      WHERE stripe_subscription_id = ${subscription.id}
    `);
  } else if (periodEnd != null) {
    await db.execute(sql`
      UPDATE subscriptions SET status = ${normalizedStatus}, stripe_price_id = ${priceId},
        current_period_end = to_timestamp(${periodEnd})
      WHERE stripe_subscription_id = ${subscription.id}
    `);
  } else {
    await db.update(subscriptions)
      .set({ status: normalizedStatus, stripePriceId: priceId })
      .where(eq(subscriptions.stripeSubscriptionId, subscription.id));
  }
}

async function handleSubscriptionDeleted(subscription: Stripe.Subscription) {
  await db.update(subscriptions)
    .set({ status: 'cancelled', stripeSubscriptionId: null })
    .where(eq(subscriptions.stripeSubscriptionId, subscription.id));
}

async function handleStripeWebhook(req: Request) {
  await initDatabase();

  const body = await req.text();
  const sig = req.headers.get('stripe-signature');
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;

  if (!webhookSecret) {
    log.error('STRIPE_WEBHOOK_SECRET is not configured — rejecting request');
    return NextResponse.json({ error: 'Webhook secret not configured' }, { status: 500 });
  }

  let event: Stripe.Event;
  try {
    const stripe = await getStripeClientFromDb();
    event = stripe.webhooks.constructEvent(body, sig ?? '', webhookSecret);
  } catch (err) {
    log.warn({ err, signatureValid: false, type: 'webhook.received' }, 'webhook.received');
    return NextResponse.json({ error: 'Webhook signature verification failed' }, { status: 400 });
  }

  log.info(
    { signatureValid: true, event: event.type, id: event.id, type: 'webhook.received' },
    'webhook.received'
  );

  try {
    switch (event.type) {
      case 'checkout.session.completed':
        await handleCheckoutSessionCompleted(event.data.object as Stripe.Checkout.Session);
        break;
      case 'invoice.payment_succeeded':
        await handleInvoicePaymentSucceeded(event.data.object as Stripe.Invoice);
        break;
      case 'invoice.payment_failed':
        await handleInvoicePaymentFailed(event.data.object as Stripe.Invoice);
        break;
      case 'customer.subscription.updated':
        await handleSubscriptionUpdated(event.data.object as Stripe.Subscription);
        break;
      case 'customer.subscription.deleted':
        await handleSubscriptionDeleted(event.data.object as Stripe.Subscription);
        break;
      default:
        break;
    }
  } catch (err) {
    log.error({ err, event: event.type, id: event.id }, 'stripe webhook handler error');
    return NextResponse.json({ error: 'Webhook handler failed — Stripe should retry' }, { status: 500 });
  }

  return NextResponse.json({ received: true });
}

export const POST = wrapRouteHandler(handleStripeWebhook);
