import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import crypto from "crypto";
import { eq, sql } from "drizzle-orm";
import { db, isDatabaseAvailable, schema } from "@/lib/db";
import { sendDiscordSaleAlert } from "@/lib/notifications/discord";
import { getPlanConfig, isPlanId, resolvePlan, type PlanId } from "@/lib/billing/pricing";
import {
  isAllowedEventType,
  isPaymentConfirmed,
  sessionModeMatchesPlan,
  subscriptionExpiresAt,
  verifyPaidAmount,
} from "@/lib/billing/webhookPolicy";
import { MAX_JSON_BODY_BYTES } from "@/lib/limits";
import { guardBodySize } from "@/lib/http/requestSize";

const stripe = process.env.STRIPE_SECRET_KEY
  ? new Stripe(process.env.STRIPE_SECRET_KEY, {
      apiVersion: "2025-01-27.acacia" as any,
      // See src/app/api/checkout/route.ts — same rationale: retry transient
      // connection blips instead of failing the webhook outright. The calls
      // this route makes to Stripe are read-only lookups, so retries are safe.
      maxNetworkRetries: 2,
    })
  : null;

export const runtime = "nodejs";

/** Generic client-safe message — internal details only ever go to the logs. */
const GENERIC_WEBHOOK_ERROR = "Webhook processing failed";

/**
 * Retrieve the freshest session state from Stripe. Falls back to the
 * signature-verified event payload when the API is temporarily unreachable so
 * a Stripe-side outage cannot permanently strand a paying customer.
 */
async function resolveCheckoutSession(
  eventSession: Stripe.Checkout.Session
): Promise<Stripe.Checkout.Session> {
  if (!stripe) return eventSession;
  try {
    return await stripe.checkout.sessions.retrieve(eventSession.id);
  } catch (err) {
    console.warn(
      "Failed to retrieve checkout session, using verified event payload:",
      err
    );
    return eventSession;
  }
}

/**
 * Resolve the subscription referenced by a checkout session (if any).
 * Returns `null` when there is none or it cannot be retrieved — the
 * confirmation gate then refuses to grant.
 */
async function resolveSubscription(
  session: Stripe.Checkout.Session
): Promise<Stripe.Subscription | null> {
  const subId =
    typeof session.subscription === "string"
      ? session.subscription
      : session.subscription?.id || null;
  if (!subId || !stripe) return null;
  try {
    return await stripe.subscriptions.retrieve(subId);
  } catch (err) {
    console.error("Failed to retrieve subscription for checkout session:", err);
    return null;
  }
}

export async function POST(req: NextRequest) {
  try {
    if (!stripe) {
      return NextResponse.json(
        { error: "Stripe is not configured" },
        { status: 500 }
      );
    }

    const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
    if (!webhookSecret) {
      return NextResponse.json(
        { error: "Stripe webhook secret is not configured" },
        { status: 500 }
      );
    }

    // Reject oversized payloads from the Content-Length header BEFORE the body
    // is buffered — the signature is verified against the raw text below, so
    // the read must not be able to exhaust memory.
    const tooLarge = guardBodySize(req, MAX_JSON_BODY_BYTES);
    if (tooLarge) return tooLarge;

    const rawBody = await req.text();
    const sig = req.headers.get("stripe-signature");

    if (!sig) {
      return NextResponse.json(
        { error: "Missing stripe-signature header" },
        { status: 400 }
      );
    }

    // Sole entry gate: nothing below runs without a cryptographically valid
    // signature. `constructEvent` verifies the raw body against the signing
    // secret and throws on any tampering or replay of a stale payload.
    let event: Stripe.Event;
    try {
      event = stripe.webhooks.constructEvent(rawBody, sig, webhookSecret);
    } catch (err) {
      console.error("Webhook signature verification failed:", err);
      return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
    }

    if (!isAllowedEventType(event.type)) {
      // Signed but irrelevant event — acknowledge without side effects.
      return NextResponse.json({ received: true });
    }

    const hasDb = await isDatabaseAvailable().catch(() => false);

    switch (event.type) {
      case "checkout.session.completed": {
        const eventSession = event.data.object as Stripe.Checkout.Session;
        const session = await resolveCheckoutSession(eventSession);

        // Plan validation instead of a blind cast: unknown/absent metadata
        // falls back to the annual default (same contract as the checkout
        // route and `resolvePlan`), and a plan that was present but unknown is
        // logged so operators notice a mismatch.
        const rawPlan = session.metadata?.plan;
        const plan: PlanId = resolvePlan(rawPlan);
        if (rawPlan && !isPlanId(rawPlan)) {
          console.warn("Unknown plan in checkout metadata, falling back to annual:", {
            sessionId: session.id,
            plan: rawPlan,
          });
        }
        const planConfig = getPlanConfig(plan);

        const subscription = await resolveSubscription(session);

        // Gate 1 — payment confirmation. One-time payments must be `paid`;
        // subscriptions must resolve to `active` or `trialing`. Anything else
        // is acknowledged but NEVER granted a license (no retry spam, log only).
        if (!isPaymentConfirmed(session, subscription)) {
          console.warn("Checkout session not confirmed paid — license NOT granted:", {
            sessionId: session.id,
            mode: session.mode,
            paymentStatus: session.payment_status,
            subscriptionStatus: subscription?.status ?? null,
          });
          return NextResponse.json({ received: true });
        }

        // Gate 2 — the metadata plan must describe the session mode Stripe
        // actually created (payment session ⇔ one-time plan, and vice versa).
        if (session.mode && !sessionModeMatchesPlan(session.mode, plan)) {
          console.error("Plan/mode mismatch — license NOT granted:", {
            sessionId: session.id,
            plan,
            sessionMode: session.mode,
          });
          return NextResponse.json({ received: true });
        }

        // Gate 3 — amount cross-check against the shared catalog (one-time
        // payments only; subscription trials legitimately total 0).
        if (!verifyPaidAmount(session, plan)) {
          console.error("Amount mismatch — license NOT granted:", {
            sessionId: session.id,
            plan,
            expectedMinor: planConfig.unitAmountMinor,
            actualMinor: session.amount_total ?? null,
          });
          return NextResponse.json({ received: true });
        }

        // Expiry from the real subscription period, never from Date.now().
        let expiresAt: Date | null = null;
        if (planConfig.mode === "subscription") {
          expiresAt = subscriptionExpiresAt(subscription);
        }

        const metadataUserId =
          session.metadata?.userId ||
          (session.client_reference_id && session.client_reference_id.startsWith("usr_")
            ? session.client_reference_id
            : null);
        const customerId =
          typeof session.customer === "string"
            ? session.customer
            : session.customer?.id || null;
        const customerEmail =
          session.customer_details?.email || session.customer_email || undefined;

        // Generate persistent license key
        const licenseKey = `RS-PRO-${crypto.randomBytes(4).toString("hex").toUpperCase()}-${crypto.randomBytes(4).toString("hex").toUpperCase()}`;

        // Persist license in PostgreSQL if database is reachable. A transaction
        // with an advisory lock serializes concurrent deliveries of the same
        // checkout.session.completed event, so parallel duplicates can never
        // create two licenses for one checkout session.
        if (hasDb) {
          try {
            const licenseCreated = await db.transaction(async (tx) => {
              await tx.execute(
                sql`SELECT pg_advisory_xact_lock(hashtextextended(${session.id}, 0))`
              );

              const existingLicense = await tx
                .select({ id: schema.licenses.id })
                .from(schema.licenses)
                .where(eq(schema.licenses.stripeCheckoutSessionId, session.id))
                .limit(1);

              if (existingLicense.length > 0) {
                return false; // duplicate delivery — already processed
              }

              let verifiedUserId: string | null = null;
              if (metadataUserId) {
                const userRows = await tx
                  .select({ id: schema.users.id })
                  .from(schema.users)
                  .where(eq(schema.users.id, metadataUserId))
                  .limit(1);
                if (userRows.length > 0) {
                  verifiedUserId = metadataUserId;
                } else {
                  console.warn(
                    "Checkout session references unknown user, skipping user link:",
                    metadataUserId
                  );
                }
              }

              await tx.insert(schema.licenses).values({
                id: `lic_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`,
                userId: verifiedUserId,
                licenseKey,
                plan,
                status: "active",
                stripeCustomerId: customerId,
                stripeSubscriptionId: subscription?.id ?? null,
                stripeCheckoutSessionId: session.id,
                activatedAt: new Date(),
                expiresAt,
                createdAt: new Date(),
                updatedAt: new Date(),
              });

              // Update user record if user ID is known and verified
              if (verifiedUserId) {
                await tx
                  .update(schema.users)
                  .set({
                    plan,
                    stripeCustomerId: customerId,
                    stripeSubscriptionId: subscription?.id ?? null,
                    expiresAt,
                    updatedAt: new Date(),
                  })
                  .where(eq(schema.users.id, verifiedUserId));
              }

              return true;
            });

            if (!licenseCreated) {
              console.log("Duplicate checkout.session.completed event ignored:", session.id);
            }
          } catch (dbErr) {
            console.error("Database error while persisting license:", dbErr);
          }
        }

        // Trigger real-time Discord sale alert (only confirmed, amount-verified
        // sales reach this point).
        const amountTotal = (session.amount_total || 0) / 100;
        const amountFormatted = `${amountTotal.toFixed(2)} €`;

        await sendDiscordSaleAlert({
          planName: planConfig.label,
          amountFormatted,
          currency: session.currency?.toUpperCase() || "EUR",
          customerEmail,
          sessionId: session.id,
        }).catch((err) => {
          console.error("Discord alert error:", err);
        });

        return NextResponse.json({
          received: true,
          licenseKey,
          persisted: hasDb,
        });
      }

      case "customer.subscription.updated": {
        const subscription = event.data.object as Stripe.Subscription;
        const subscriptionId = subscription.id;
        const subStatus = subscription.status;

        let mappedStatus = "active";
        if (subStatus === "active" || subStatus === "trialing") {
          mappedStatus = "active";
        } else if (subStatus === "past_due") {
          mappedStatus = "past_due";
        } else if (
          subStatus === "canceled" ||
          subStatus === "unpaid" ||
          subStatus === "incomplete_expired"
        ) {
          mappedStatus = "cancelled";
        } else {
          mappedStatus = subStatus;
        }

        const expiresAtDate = subscription.current_period_end
          ? new Date(subscription.current_period_end * 1000)
          : null;
        // Reflects a pending "cancel at period end" request (from our own
        // /api/subscription/cancel call, or one made directly in Stripe) —
        // this is the authoritative sync; the cancel route only sets it
        // optimistically ahead of this webhook arriving.
        const cancelAtPeriodEnd = Boolean(subscription.cancel_at_period_end);

        if (hasDb) {
          try {
            await db
              .update(schema.licenses)
              .set({
                status: mappedStatus,
                expiresAt: expiresAtDate,
                updatedAt: new Date(),
              })
              .where(eq(schema.licenses.stripeSubscriptionId, subscriptionId));

            if (
              mappedStatus === "cancelled" ||
              mappedStatus === "unpaid" ||
              mappedStatus === "past_due"
            ) {
              await db
                .update(schema.users)
                .set({
                  plan: "free",
                  expiresAt: expiresAtDate,
                  cancelAtPeriodEnd: false,
                  updatedAt: new Date(),
                })
                .where(eq(schema.users.stripeSubscriptionId, subscriptionId));
            } else {
              await db
                .update(schema.users)
                .set({
                  expiresAt: expiresAtDate,
                  cancelAtPeriodEnd,
                  updatedAt: new Date(),
                })
                .where(eq(schema.users.stripeSubscriptionId, subscriptionId));
            }
          } catch (dbErr) {
            console.error("Database error updating subscription:", dbErr);
          }
        }
        break;
      }

      case "customer.subscription.deleted": {
        const subscription = event.data.object as Stripe.Subscription;
        const subscriptionId = subscription.id;

        if (hasDb) {
          try {
            await db
              .update(schema.licenses)
              .set({
                status: "cancelled",
                updatedAt: new Date(),
              })
              .where(eq(schema.licenses.stripeSubscriptionId, subscriptionId));

            await db
              .update(schema.users)
              .set({
                plan: "free",
                cancelAtPeriodEnd: false,
                updatedAt: new Date(),
              })
              .where(eq(schema.users.stripeSubscriptionId, subscriptionId));
          } catch (dbErr) {
            console.error("Database error deleting subscription:", dbErr);
          }
        }
        break;
      }

      case "invoice.payment_succeeded": {
        const invoice = event.data.object as Stripe.Invoice;
        const subId =
          typeof invoice.subscription === "string"
            ? invoice.subscription
            : invoice.subscription?.id || null;

        if (subId && hasDb) {
          try {
            const periodEnd = invoice.lines?.data[0]?.period?.end;
            const expiresAtDate = periodEnd ? new Date(periodEnd * 1000) : null;

            await db
              .update(schema.licenses)
              .set({
                status: "active",
                ...(expiresAtDate ? { expiresAt: expiresAtDate } : {}),
                updatedAt: new Date(),
              })
              .where(eq(schema.licenses.stripeSubscriptionId, subId));
          } catch (dbErr) {
            console.error("Database error on invoice.payment_succeeded:", dbErr);
          }
        }
        break;
      }

      case "invoice.payment_failed": {
        // Only flag the license — don't touch the user's plan here. Stripe
        // retries a failed card automatically (Smart Retries) and the
        // subscription commonly stays "active" through those retries, so
        // downgrading on the very first failed attempt would strand a paying
        // customer mid-retry over what's often a transient decline. The
        // actual downgrade happens in customer.subscription.updated once
        // Stripe itself moves the subscription to past_due/unpaid/canceled
        // (retries exhausted) — invoice.payment_succeeded restores "active"
        // here if a later retry succeeds.
        const invoice = event.data.object as Stripe.Invoice;
        const subId =
          typeof invoice.subscription === "string"
            ? invoice.subscription
            : invoice.subscription?.id || null;

        if (subId && hasDb) {
          try {
            await db
              .update(schema.licenses)
              .set({
                status: "past_due",
                updatedAt: new Date(),
              })
              .where(eq(schema.licenses.stripeSubscriptionId, subId));
          } catch (dbErr) {
            console.error("Database error on invoice.payment_failed:", dbErr);
          }
        }
        break;
      }

      default:
        // Whitelisted events without side effects (e.g. checkout.session.expired)
        break;
    }

    return NextResponse.json({ received: true });
  } catch (error) {
    // Internal details go to the logs only — never back to the client.
    console.error("Stripe Webhook Error:", error);
    return NextResponse.json(
      { error: GENERIC_WEBHOOK_ERROR },
      { status: 500 }
    );
  }
}
