import { NextResponse } from "next/server";
import type { CartItem } from "../../lib/cart";
import { getShippingMethods, getPaymentMethods, getCompanySettings, groupPaymentMethodsForCheckout } from "../../lib/payload";
import { validateDiscountCode, redeemDiscountCode } from "../../lib/discountServer";
import { createOrder } from "../../lib/orderServer";
import { getSessionCustomer, registerCustomer, setSessionCookie, type CustomerSummary } from "../../lib/customerAuth";
import { fetchProductsBySlug } from "../../lib/productsServer";
import { describeBundleContents } from "../../lib/bundleContents";
import { sendCriticalAlert } from "../../lib/alertAdmin";
import { sendOrderConfirmationEmail } from "../../lib/orderEmail";
import { normalizeVatId, isValidVatId } from "@einfach-produktiv/invoicing";
import { checkVatIdViaVies } from "@einfach-produktiv/invoicing/vies";
import { computeExemptTotals, destinationCountry, isExemptionEligibleCountry } from "../../lib/vatExemption";
import { upsertNewsletterContact } from "../../lib/brevo";
import { paymentProvider, isPaymentTestMode } from "../../lib/payments";

// Plain float arithmetic on money (quantity × unitPrice summed across
// lines, a percent discount, subtracting/adding those together) drifts
// into results like 84.30000000000001 — cosmetically invisible wherever
// formatPrice()'s toFixed(2) already rounds for display, but stored as-is
// on the order otherwise, which is where it actually showed up (Payload's
// admin list/edit view for a plain number field has no such formatting).
// Rounded once here, right before persisting, rather than chasing it down
// at every downstream display site.
function roundMoney(amount: number): number {
  return Math.round(amount * 100) / 100;
}

type CheckoutBody = {
  cart: CartItem[];
  shippingMethodId: number;
  paymentMethodId: number;
  discountCode: string | null;
  firstName: string;
  lastName: string;
  email: string;
  password?: string;
  companyName?: string;
  vatId?: string;
  deliveryMethod: "address" | "packstation";
  street?: string;
  packstationNumber?: string;
  postNumber?: string;
  zip: string;
  city: string;
  country: string;
  hasDifferentShippingAddress?: boolean;
  shippingFirstName?: string;
  shippingLastName?: string;
  shippingDeliveryMethod?: "address" | "packstation";
  shippingStreet?: string;
  shippingPackstationNumber?: string;
  shippingPostNumber?: string;
  shippingZip?: string;
  shippingCity?: string;
  shippingCountry?: string;
  shippingCompanyName?: string;
  shippingContactEmail?: string;
  shippingContactPhone?: string;
  newsletterOptIn: boolean;
};

function isValidBody(body: unknown): body is CheckoutBody {
  const b = body as Partial<CheckoutBody> | null;
  return Boolean(
    b &&
      Array.isArray(b.cart) &&
      b.cart.length > 0 &&
      typeof b.shippingMethodId === "number" &&
      typeof b.paymentMethodId === "number" &&
      typeof b.firstName === "string" &&
      b.firstName &&
      typeof b.lastName === "string" &&
      b.lastName &&
      typeof b.email === "string" &&
      b.email &&
      (b.deliveryMethod === "address" || b.deliveryMethod === "packstation") &&
      typeof b.zip === "string" &&
      b.zip &&
      typeof b.city === "string" &&
      b.city &&
      typeof b.country === "string" &&
      b.country,
  );
}

export async function POST(request: Request) {
  const body = await request.json().catch(() => null);
  if (!isValidBody(body)) {
    return NextResponse.json({ ok: false, reason: "Bitte alle Pflichtfelder ausfüllen." }, { status: 400 });
  }
  if (body.deliveryMethod === "address" && !body.street) {
    return NextResponse.json({ ok: false, reason: "Bitte Straße und Hausnummer angeben." }, { status: 400 });
  }
  if (body.deliveryMethod === "packstation" && (!body.packstationNumber || !body.postNumber)) {
    return NextResponse.json({ ok: false, reason: "Bitte Packstation- und Postnummer angeben." }, { status: 400 });
  }
  // Optional — only format-checked when actually provided, same "never
  // trust the client" reasoning as every other checkout field re-validated
  // here. Normalized the same way Orders.ts's own field does (uppercase +
  // trim), so the snapshot on the order matches what would've been
  // accepted directly through the Payload admin.
  const normalizedVatId = body.vatId ? normalizeVatId(body.vatId) : undefined;
  if (normalizedVatId && !isValidVatId(normalizedVatId)) {
    return NextResponse.json({ ok: false, reason: "Ungültiges USt-IdNr.-Format (z. B. DE123456789)." }, { status: 400 });
  }
  if (body.hasDifferentShippingAddress) {
    if (!body.shippingFirstName || !body.shippingLastName || !body.shippingZip || !body.shippingCity || !body.shippingCountry) {
      return NextResponse.json({ ok: false, reason: "Bitte alle Felder der Lieferadresse ausfüllen." }, { status: 400 });
    }
    if (body.shippingDeliveryMethod === "address" && !body.shippingStreet) {
      return NextResponse.json({ ok: false, reason: "Bitte Straße und Hausnummer der Lieferadresse angeben." }, { status: 400 });
    }
    if (body.shippingDeliveryMethod === "packstation" && (!body.shippingPackstationNumber || !body.shippingPostNumber)) {
      return NextResponse.json({ ok: false, reason: "Bitte Packstation- und Postnummer der Lieferadresse angeben." }, { status: 400 });
    }
    if (body.shippingDeliveryMethod !== "address" && body.shippingDeliveryMethod !== "packstation") {
      return NextResponse.json({ ok: false, reason: "Lieferart der Lieferadresse ist ungültig." }, { status: 400 });
    }
  }

  // Auth: an existing session wins; otherwise this checkout submit doubles
  // as inline registration ("Konto Pflicht, Registrierung direkt im
  // Checkout") — logging in with an *existing* account happens separately
  // beforehand via /api/account/login from the checkout page's own toggle.
  let customer: CustomerSummary;
  const session = await getSessionCustomer();
  if (session) {
    customer = session.customer;
  } else {
    if (!body.password) {
      return NextResponse.json({ ok: false, reason: "Bitte ein Passwort für dein Konto vergeben." }, { status: 400 });
    }
    const result = await registerCustomer({
      firstName: body.firstName,
      lastName: body.lastName,
      email: body.email,
      password: body.password,
    });
    if (!result.ok) return NextResponse.json(result, { status: 400 });
    await setSessionCookie(result.token);
    customer = result.customer;
  }

  // Re-price everything server-side — never trust client-submitted prices.
  const [productsBySlug, companySettings] = await Promise.all([fetchProductsBySlug(), getCompanySettings()]);
  const defaultTaxRate = companySettings?.taxRatePercent ?? 19;
  // §19 UStG — a Kleinunternehmer tenant never charges VAT on anything,
  // full stop, so every item's tax rate is forced to 0% here regardless of
  // its own catalog/company-settings default rate. Unlike the
  // intra-community exemption below, prices are NOT de-grossed — see
  // Orders.ts's own kleinunternehmer field comment and this shop's
  // Kleinunternehmer decision: catalog gross prices stay exactly what they
  // are, they simply never had a VAT component charged on top in the
  // first place.
  const kleinunternehmer = Boolean(companySettings?.kleinunternehmer);
  const items: {
    productId: number;
    productName: string;
    quantity: number;
    unitPrice: number;
    imageUrl: string | null;
    taxRatePercent: number;
    bundleContents: string | null;
    variantName: string | null;
    sku: string | null;
  }[] = [];
  for (const line of body.cart) {
    const product = productsBySlug.get(line.id);
    if (!product) return NextResponse.json({ ok: false, reason: "Ein Artikel im Warenkorb ist nicht mehr verfügbar." }, { status: 400 });
    // Same "never trust the client" reasoning as unitPrice below — a
    // requested variant that no longer exists on this product (removed,
    // or never existed — a tampered request) fails the whole checkout
    // rather than silently falling back to the base product/price.
    let variant: { name: string; priceOverride: number | null; sku: string | null } | null = null;
    if (line.variant) {
      variant = product.variants?.find((v) => v.name === line.variant) ?? null;
      if (!variant) return NextResponse.json({ ok: false, reason: "Eine gewählte Variante ist nicht mehr verfügbar." }, { status: 400 });
    }
    // Same depth-in-defense reasoning as the price re-check above — the
    // storefront already disables "add to cart" for sold-out items, but a
    // tampered/stale request could still submit one, so stock is
    // re-validated here as the actual source of truth. Falls through
    // (buyable) whenever trackInventory is off or backorders are allowed.
    const stockSource = line.variant ? product.variants?.find((v) => v.name === line.variant) : product;
    if (stockSource?.trackInventory && !stockSource.allowBackorder && (stockSource.stock ?? 0) < line.qty) {
      return NextResponse.json(
        { ok: false, reason: `"${product.name}"${line.variant ? ` (${line.variant})` : ""} ist nicht mehr in ausreichender Menge verfügbar.` },
        { status: 400 },
      );
    }
    const imageUrl = typeof product.image === "object" && product.image ? product.image.url : null;
    items.push({
      productId: product.id,
      productName: product.name,
      quantity: line.qty,
      unitPrice: variant?.priceOverride ?? product.price,
      imageUrl,
      taxRatePercent: kleinunternehmer ? 0 : (product.taxRatePercent ?? defaultTaxRate),
      bundleContents: describeBundleContents(product),
      variantName: variant?.name ?? null,
      // Variant sku takes precedence over the product's own — same
      // "variant overrides product" precedence unitPrice already uses.
      sku: variant?.sku ?? product.sku ?? null,
    });
  }
  const subtotal = roundMoney(items.reduce((sum, i) => sum + i.quantity * i.unitPrice, 0));
  // Products.noShippingCost — a cart made up entirely of items that opt
  // out of shipping costs (e.g. purely digital downloads) never gets
  // charged shipping at all, regardless of the free-shipping threshold.
  // A single item WITHOUT the flag still triggers normal shipping for the
  // whole order — this only exempts a product, never the whole cart just
  // because it contains an exempt item.
  const hasShippableItem = body.cart.some((line) => !productsBySlug.get(line.id)?.noShippingCost);

  const shippingMethods = await getShippingMethods();
  const shippingMethod = shippingMethods.find((m) => m.id === body.shippingMethodId);
  if (!shippingMethod) return NextResponse.json({ ok: false, reason: "Versandart ist ungültig." }, { status: 400 });
  const freeShipping = shippingMethod.freeShippingThreshold != null && subtotal >= shippingMethod.freeShippingThreshold;
  const shippingCost = !hasShippableItem || freeShipping ? 0 : shippingMethod.price;

  const paymentMethods = await getPaymentMethods();
  const paymentMethod = paymentMethods.find((m) => m.id === body.paymentMethodId);
  if (!paymentMethod) return NextResponse.json({ ok: false, reason: "Zahlungsart ist ungültig." }, { status: 400 });

  let discountAmount = 0;
  if (body.discountCode) {
    const validation = await validateDiscountCode(body.discountCode, subtotal);
    if (!validation.valid) return NextResponse.json({ ok: false, reason: validation.reason }, { status: 400 });
    const redeemed = await redeemDiscountCode(validation.doc);
    if (!redeemed) return NextResponse.json({ ok: false, reason: "Rabattcode konnte nicht eingelöst werden." }, { status: 400 });
    discountAmount = roundMoney(
      validation.doc.type === "percent" ? (subtotal * validation.doc.value) / 100 : Math.min(validation.doc.value, subtotal),
    );
  }

  // VAT-ID validity and the exemption decision are two separate questions.
  // Validity (is this actually a currently-registered VAT ID at all) is
  // checked via VIES for ANY country whenever one is given — worth
  // recording regardless of destination, same "data quality" reasoning as
  // company-settings.vatId's own VIES check on the backend; a merely
  // format-valid id (e.g. "ED123456789" — "ED" isn't even a real country
  // code) is never enough on its own. The exemption itself
  // (innergemeinschaftliche Lieferung, §4 Nr. 1b UStG) additionally
  // requires the goods' actual destination (the shipping override's
  // country when set, the billing country otherwise) to be Österreich,
  // the one EU-cross-border option this checkout offers — a validated
  // *German* VAT ID never zero-rates a domestic sale, no matter how real
  // it is. VIES being unreachable fails closed on the exemption: normal
  // VAT applies, never a guessed exemption (vatIdValidatedAt just stays
  // unset in that case too).
  let vatExempt = false;
  let vatIdValidatedAt: string | null = null;
  // A Kleinunternehmer never charges VAT on any sale, domestic or
  // cross-border — the intra-community exemption exists to zero-rate what
  // would otherwise be a positive-rate charge, which never applies here in
  // the first place, so the VIES lookup is skipped entirely (also saves an
  // unneeded network round-trip).
  const buyerDestinationCountry = destinationCountry(body.country, Boolean(body.hasDifferentShippingAddress), body.shippingCountry);
  if (!kleinunternehmer && normalizedVatId) {
    const viesResult = await checkVatIdViaVies(normalizedVatId);
    if (viesResult.ok && viesResult.valid) {
      vatIdValidatedAt = new Date().toISOString();
      if (isExemptionEligibleCountry(buyerDestinationCountry)) {
        vatExempt = true;
      }
    }
  }

  if (vatExempt) {
    // Re-price every line net of VAT (0% now applies) instead of the
    // catalog's normal VAT-inclusive price — the whole point of the
    // exemption is that the buyer pays less, not that this shop quietly
    // keeps the VAT portion as extra margin. items/subtotal/shippingCost
    // below are overwritten with the de-grossed figures actually charged
    // and actually persisted on the order/invoice.
    for (const item of items) {
      item.unitPrice = roundMoney(item.unitPrice / (1 + item.taxRatePercent / 100));
      item.taxRatePercent = 0;
    }
  }
  const exemptTotals = vatExempt
    ? computeExemptTotals(
        items.map((i) => ({ quantity: i.quantity, grossUnitPrice: i.unitPrice, taxRatePercent: 0 })),
        shippingCost,
        defaultTaxRate,
        discountAmount,
      )
    : null;
  // Note: exemptTotals recomputes `subtotal` from the already-degrossed
  // `items` above (taxRatePercent 0 there means computeExemptTotals's own
  // degross() step is a no-op on them) — it exists mainly to degross
  // `shippingCost` the same way, and to keep both figures derived through
  // one shared function rather than duplicating the arithmetic here.
  const finalSubtotal = exemptTotals?.subtotal ?? subtotal;
  const finalShippingCost = exemptTotals?.shippingCost ?? shippingCost;
  const total = roundMoney(Math.max(0, finalSubtotal - discountAmount) + finalShippingCost);

  // Gated-payment branch (Kreditkarte/PayPal today) — see
  // spicy-leaping-pizza.md §3. The PaymentIntent is created BEFORE the
  // order so its id can be persisted onto the order at creation time
  // (providerReference), rather than needing a second authenticated
  // update call that doesn't otherwise exist from this service. Stripe
  // generates a PaymentIntent id independent of any order existing yet.
  const requiresPayment = paymentMethod.provider === "stripe";
  let providerReference: string | undefined;
  let clientSecret: string | undefined;
  if (requiresPayment) {
    try {
      const intent = await paymentProvider.createPaymentIntent({
        amountCents: Math.round(total * 100),
        currency: "eur",
        customerEmail: body.email,
        description: `einfach produktiv Bestellung — ${body.firstName} ${body.lastName}`,
      });
      providerReference = intent.providerReference;
      clientSecret = intent.clientSecret;
    } catch (err) {
      sendCriticalAlert("Zahlung konnte nicht vorbereitet werden", {
        customerEmail: body.email,
        total,
        error: String(err),
      });
      return NextResponse.json({ ok: false, reason: "Die Zahlung konnte gerade nicht vorbereitet werden." }, { status: 500 });
    }
  }

  const order = await createOrder({
    customerId: customer.id,
    customerFirstName: body.firstName,
    customerLastName: body.lastName,
    customerEmail: body.email,
    companyName: body.companyName || undefined,
    vatId: normalizedVatId,
    vatExempt,
    kleinunternehmer,
    vatIdValidatedAt,
    deliveryMethod: body.deliveryMethod,
    street: body.street,
    packstationNumber: body.packstationNumber,
    postNumber: body.postNumber,
    zip: body.zip,
    city: body.city,
    country: body.country,
    hasDifferentShippingAddress: Boolean(body.hasDifferentShippingAddress),
    shippingFirstName: body.shippingFirstName,
    shippingLastName: body.shippingLastName,
    shippingDeliveryMethod: body.shippingDeliveryMethod,
    shippingStreet: body.shippingStreet,
    shippingPackstationNumber: body.shippingPackstationNumber,
    shippingPostNumber: body.shippingPostNumber,
    shippingZip: body.shippingZip,
    shippingCity: body.shippingCity,
    shippingCountry: body.shippingCountry,
    shippingCompanyName: Boolean(body.hasDifferentShippingAddress) ? body.shippingCompanyName || undefined : undefined,
    shippingContactEmail: Boolean(body.hasDifferentShippingAddress) ? body.shippingContactEmail || undefined : undefined,
    shippingContactPhone: Boolean(body.hasDifferentShippingAddress) ? body.shippingContactPhone || undefined : undefined,
    newsletterOptIn: Boolean(body.newsletterOptIn),
    items,
    subtotal: finalSubtotal,
    shippingCost: finalShippingCost,
    shippingMethodTitle: shippingMethod.title,
    shippingMethod: shippingMethod.id,
    // The checkout UI collapses Kreditkarte/PayPal into one "Online-
    // Zahlung" pre-selection (see groupPaymentMethodsForCheckout) — the
    // customer hasn't actually chosen an instrument yet at this point,
    // Stripe's Payment Element does that next. Snapshotting the specific
    // resolved row's title here would just record whichever row happened
    // to be the group's representative id, not what was really picked.
    // The webhook route refines this to the real instrument
    // ("Kreditkarte"/"PayPal") once Stripe reports it, via confirm-payment.
    paymentMethodTitle: requiresPayment ? "Online-Zahlung" : paymentMethod.title,
    discountCode: body.discountCode || null,
    discountAmount,
    total,
    ...(requiresPayment
      ? { status: "pending_payment" as const, paymentProvider: "stripe" as const, paymentStatus: "pending" as const, providerReference }
      : {}),
  });
  if (!order) {
    // The worst-case failure in this whole flow: the customer went
    // through checkout believing they bought something, and nothing was
    // persisted. Kuma's uptime checks can't see this (the site is up,
    // this route just returned a 500) — this is the one alert path that
    // can.
    sendCriticalAlert("Bestellung konnte nicht gespeichert werden", {
      customerId: customer.id,
      customerEmail: body.email,
      cart: body.cart,
      total,
      timestamp: new Date().toISOString(),
    });
    return NextResponse.json({ ok: false, reason: "Bestellung konnte nicht gespeichert werden." }, { status: 500 });
  }

  if (requiresPayment && providerReference) {
    // Best-effort — see stripeProvider.attachOrderMetadata's own comment.
    // Not fatal: the order's own `providerReference` field (already
    // persisted above) remains the source of truth for the
    // expirePendingPayments cleanup job either way; this only speeds up
    // the webhook's fast path.
    await paymentProvider.attachOrderMetadata(providerReference, { orderId: String(order.id), orderNumber: order.orderNumber }).catch((err) => {
      sendCriticalAlert("Zahlungsmetadaten konnten nicht verknüpft werden", {
        orderNumber: order.orderNumber,
        providerReference,
        error: String(err),
      });
    });
  }

  // Deferred for gated payment methods (Kreditkarte/PayPal) until the
  // webhook confirms payment — see spicy-leaping-pizza.md §3/§4. Sent
  // from the backend's confirm-payment endpoint instead, at that point.
  // Unchanged for Überweisung: fires immediately, exactly as before.
  if (!requiresPayment) {
    // Fire-and-forget — a failed confirmation email must never undo an
    // already-successful order or block the response the customer is
    // waiting on. Lower severity than the "order lost" alert above (the
    // order itself is safe either way), but still worth knowing about, since
    // it's the one thing that would otherwise fail completely silently.
    sendOrderConfirmationEmail(
      {
        orderNumber: order.orderNumber,
        createdAt: order.createdAt,
        invoiceNumber: order.invoiceNumber as string,
        invoiceIssuedAt: order.invoiceIssuedAt as string,
        customerFirstName: body.firstName,
        customerLastName: body.lastName,
        companyName: body.companyName || undefined,
        vatId: normalizedVatId,
        vatExempt,
        kleinunternehmer,
        deliveryMethod: body.deliveryMethod,
        street: body.street,
        packstationNumber: body.packstationNumber,
        postNumber: body.postNumber,
        zip: body.zip,
        city: body.city,
        country: body.country,
        hasDifferentShippingAddress: Boolean(body.hasDifferentShippingAddress),
        shippingFirstName: body.shippingFirstName,
        shippingLastName: body.shippingLastName,
        shippingCompanyName: body.hasDifferentShippingAddress ? body.shippingCompanyName : undefined,
        shippingDeliveryMethod: body.shippingDeliveryMethod,
        shippingStreet: body.shippingStreet,
        shippingPackstationNumber: body.shippingPackstationNumber,
        shippingPostNumber: body.shippingPostNumber,
        shippingZip: body.shippingZip,
        shippingCity: body.shippingCity,
        shippingCountry: body.shippingCountry,
        shippingContactEmail: body.hasDifferentShippingAddress ? body.shippingContactEmail : undefined,
        shippingContactPhone: body.hasDifferentShippingAddress ? body.shippingContactPhone : undefined,
        paymentMethodTitle: paymentMethod.title,
        items: items.map((i) => ({
          productName: i.productName,
          quantity: i.quantity,
          unitPrice: i.unitPrice,
          imageUrl: i.imageUrl,
          taxRatePercent: i.taxRatePercent,
          bundleContents: i.bundleContents,
          variantName: i.variantName,
          sku: i.sku,
        })),
        subtotal: finalSubtotal,
        shippingCost: finalShippingCost,
        discountAmount,
        discountCode: body.discountCode || null,
        total,
        isManualPayment: true,
        // Only meaningful for the Vorkasse notice above — whether a
        // switch to Kreditkarte/PayPal is even worth mentioning right now.
        hasOnlinePaymentOption: groupPaymentMethodsForCheckout(paymentMethods).some((m) => m.provider === "stripe"),
      },
      body.email,
    ).catch((err) => {
      sendCriticalAlert("Bestätigungs-Mail konnte nicht gesendet werden", {
        orderNumber: order.orderNumber,
        customerEmail: body.email,
        error: String(err),
      });
    });
  }

  // Fire-and-forget, same reasoning as the confirmation email above — a
  // failed marketing sync is not worth failing checkout over, and doesn't
  // even need a critical alert (nothing customer-facing depends on it).
  // Not gated on payment confirmation — a newsletter signup intent isn't
  // an order-fulfillment concern, unlike the confirmation email/invoice.
  if (body.newsletterOptIn) {
    upsertNewsletterContact(body.email, "checkout").catch(() => {});
  }

  return NextResponse.json({
    ok: true,
    orderNumber: order.orderNumber,
    orderId: order.id,
    orderDateIso: order.createdAt,
    ...(requiresPayment
      ? {
          requiresPayment: true as const,
          clientSecret,
          testMode: isPaymentTestMode,
          // Only surfaced in test mode — PaymentStep's "Testzahlung"
          // buttons need it to call the test-confirm route directly,
          // since there's no real Stripe redirect to carry it back
          // through. A real PaymentIntent id isn't secret (only its
          // client_secret is), but there's no reason to expose it to the
          // client outside test mode either.
          ...(isPaymentTestMode ? { providerReference } : {}),
        }
      : {}),
    shippingCost: finalShippingCost,
    paymentMethodTitle: requiresPayment ? "Online-Zahlung" : paymentMethod.title,
    discountCode: body.discountCode || null,
    discountAmount,
    vatExempt,
    kleinunternehmer,
  });
}
