"use client";

import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useCart, clearCart, mergeServerCartIntoLocal } from "../../lib/cart";
import { useProducts } from "../../lib/products";
import { useDiscount, clearDiscount } from "../../lib/discount";
import { computeSubtotal, computeCartTotals, effectivePrice, effectiveTaxRate, cartHasShippableItem } from "../../lib/cartTotals";
import { computeTaxBreakdown } from "@einfach-produktiv/invoicing";
import { formatPrice } from "../../lib/format";
import { Reveal } from "../../components/Reveal";
import { CustomSelect } from "../../components/CustomSelect";
import { VersandModal } from "../../components/VersandModal";
import { AddressAutocomplete } from "./AddressAutocomplete";
import { VatBreakdown } from "../../components/VatBreakdown";
import { CheckoutSteps } from "../../components/CheckoutSteps";
import { ORDER_KEY, PENDING_ORDER_KEY, type OrderSnapshot } from "../../lib/order";
import { PaymentStep } from "./PaymentStep";
import { dispatchAuthChanged } from "../../lib/auth";
import { readCheckoutDraft, writeCheckoutDraft, clearCheckoutDraft } from "../../lib/checkoutDraft";
import { normalizeVatId, isValidVatId, isValidPlz, plzInputPattern } from "@einfach-produktiv/invoicing";
import { computeExemptTotals, destinationCountry, isExemptionEligibleCountry } from "../../lib/vatExemption";
import { validateEmailFormat } from "../../lib/email";
import type { ShippingMethod, ShippingCountry, PaymentMethod, TrustBadge, ShippingSettings } from "../../lib/payload";
import { groupPaymentMethodsForCheckout } from "../../lib/payload";
import type { CustomerProfile } from "../../lib/customerAuth";

// Native HTML5 pattern validation (instant, no round-trip) mirroring the
// same rules Orders.ts/Customers.ts enforce server-side — a plausibility
// check, not the source of truth (the backend re-validates regardless of
// what a customer's browser did or didn't catch). `plzDigitsMap` comes
// from Payload's shipping-countries collection (see lib/payload.ts's
// getShippingCountries()) — which countries are offered, and how many PLZ
// digits each expects, is admin-configurable now, not a hardcoded array
// here. `?? 4` only matters if a country somehow isn't in the map at all
// (shouldn't happen — the <select> options are built from the same list).
function plzPattern(country: string, plzDigitsMap: Record<string, number>): string {
  return plzInputPattern(plzDigitsMap[country] ?? 4);
}

// Same rules as the pattern/required attributes each field already
// carries (and what Orders.ts/Customers.ts re-enforce server-side) — this
// is the plausibility check surfaced immediately on blur, not a second
// source of truth. Returns "" for valid, an error message otherwise.
function validateRequired(label: string, value: string): string {
  return value.trim() ? "" : `${label} ist erforderlich.`;
}

function validateZip(value: string, country: string, plzDigitsMap: Record<string, number>): string {
  if (!value.trim()) return "PLZ ist erforderlich.";
  const digits = plzDigitsMap[country] ?? 4;
  return isValidPlz(value, digits) ? "" : `PLZ muss aus ${digits} Ziffern bestehen.`;
}

function validatePackstationNumber(value: string): string {
  if (!value.trim()) return "Packstationsnummer ist erforderlich.";
  return /^\d{1,3}$/.test(value) ? "" : "Packstationsnummer muss aus 1 bis 3 Ziffern bestehen.";
}

function validatePostNumber(value: string): string {
  if (!value.trim()) return "Postnummer ist erforderlich.";
  return /^\d{6,10}$/.test(value) ? "" : "Postnummer muss aus 6 bis 10 Ziffern bestehen.";
}

// Optional field — "" (valid) whenever empty, only format-checked once
// something's actually been typed, same "never required by the other"
// reasoning as the checkout body's own companyName/vatId handling.
function validateVatIdFormat(value: string): string {
  if (!value.trim()) return "";
  return isValidVatId(normalizeVatId(value)) ? "" : "Ungültiges USt-IdNr.-Format (z. B. DE123456789).";
}

// forwardRef so callers can imperatively .focus() a specific field (see
// the "Andere E-Mail-Adresse verwenden" handler below) — plain props
// couldn't do that, and every other FormField usage is unaffected since
// ref is optional.
const FormField = forwardRef<
  HTMLInputElement,
  { label: string; wrapperClassName?: string; error?: string } & React.InputHTMLAttributes<HTMLInputElement>
>(function FormField({ label, wrapperClassName = "flex-1 min-w-0", error, ...props }, ref) {
  return (
    <label className={`flex flex-col gap-2 items-start ${wrapperClassName}`}>
      <span className="text-label text-text-muted">{label}</span>
      <input
        {...props}
        ref={ref}
        aria-invalid={error ? true : undefined}
        className={`w-full border rounded-sm px-4 py-3 text-body-sm text-text-primary outline-none focus:border-brand transition-colors ${
          error ? "border-red-600" : "border-border"
        }`}
      />
      {error && <span className="text-label text-red-600">{error}</span>}
    </label>
  );
});

export function CheckoutContent({
  shippingMethods,
  shippingCountries,
  paymentMethods,
  trustBadges,
  shippingSettings,
  defaultTaxRate,
  kleinunternehmer,
  customerEmail,
  savedProfile,
}: {
  shippingMethods: ShippingMethod[];
  /** Which countries the "Land" <select>s offer, and each one's PLZ digit
   * count — admin-configurable (Payload's shipping-countries collection),
   * not a hardcoded array here anymore. */
  shippingCountries: ShippingCountry[];
  paymentMethods: PaymentMethod[];
  trustBadges: TrustBadge[];
  /** Delivery-time disclosure (Payload's Shipping Settings) — named
   * "shippingSettings", not "shipping", since that name is already the
   * local computed shipping-cost value below. */
  shippingSettings: ShippingSettings;
  /** Tenant's default VAT rate, same role as CartContent's own prop. */
  defaultTaxRate: number;
  /** §19 UStG — same role as CartContent's own prop. Also forces the VIES
   * exemption preview off below: a Kleinunternehmer never charges VAT on
   * anything, domestic or cross-border, so there's nothing left for the
   * intra-community exemption to zero-rate. */
  kleinunternehmer: boolean;
  /** From the checkout page's own session read (app/lib/customerAuth.ts) —
   * null means no account is logged in yet, which flips "1. Rechnungsadresse"
   * into inline-registration mode (password field shown, account created on
   * submit) since an account is required to buy. */
  customerEmail: string | null;
  /** Full profile (name + saved address) — null when logged out, pre-fills
   * Card 1's fields for a returning customer instead of leaving them blank. */
  savedProfile: CustomerProfile | null;
}) {
  const router = useRouter();
  const cart = useCart();
  const products = useProducts();
  const discount = useDiscount();
  // { "Deutschland": 5, "Österreich": 4, ... } — built once from the
  // fetched list rather than re-deriving it inline at every plzPattern()/
  // validateZip() call site.
  const plzDigitsMap: Record<string, number> = Object.fromEntries(shippingCountries.map((c) => [c.name, c.plzDigits]));
  const [shippingMethodId, setShippingMethodId] = useState<number | null>(shippingMethods[0]?.id ?? null);
  // Kreditkarte/PayPal collapse into one "Online-Zahlung" option here —
  // see groupPaymentMethodsForCheckout's own comment for why. The
  // resulting id is still a real payment-methods row id, so everything
  // downstream (submission, sessionStorage draft restore) is unaffected.
  const paymentOptions = useMemo(() => groupPaymentMethodsForCheckout(paymentMethods), [paymentMethods]);
  const [paymentMethodId, setPaymentMethodId] = useState<number | null>(paymentOptions[0]?.id ?? null);
  const [versandOpen, setVersandOpen] = useState(false);
  const [purchaseError, setPurchaseError] = useState<string | null>(null);
  const [purchasing, setPurchasing] = useState(false);
  // Set once /api/checkout returns `requiresPayment: true` (Kreditkarte/
  // PayPal) — see spicy-leaping-pizza.md §3. Replaces the form with
  // PaymentStep instead of navigating away immediately, since the order
  // isn't actually confirmed yet at this point.
  const [paymentStep, setPaymentStep] = useState<{
    clientSecret: string;
    orderNumber: string;
    orderId: number;
    testMode: boolean;
    providerReference?: string;
  } | null>(null);
  const [showLogin, setShowLogin] = useState(false);
  // Scroll target for the submit-time emailExists fallback below — the
  // common case (blur-triggered, see handleEmailBlur) needs no scroll at
  // all, since the inline login prompt already renders right where the
  // shopper is looking (under Card 1's own email field); this ref only
  // matters if a shopper filled the whole form and hit submit from further
  // down the page without ever blurring the email field first (e.g.
  // browser autofill).
  const loginGateRef = useRef<HTMLDivElement>(null);
  // Focused after "Andere E-Mail-Adresse verwenden" clears the field below
  // — the shopper explicitly asked to type a different one, so the cursor
  // should already be waiting there instead of making them click in.
  const emailInputRef = useRef<HTMLInputElement>(null);
  const [loginPassword, setLoginPassword] = useState("");
  const [loginError, setLoginError] = useState<string | null>(null);
  const [loggingIn, setLoggingIn] = useState(false);

  // Scrolls the inline login prompt into view once it actually exists in
  // the DOM — can't do this synchronously right where setShowLogin(true)
  // is called (handleEmailBlur/handleSubmit below): the prompt is
  // conditionally rendered on `showLogin`, so loginGateRef.current is
  // still null until after the next render flushes. Effect fires post-
  // render instead, once the ref is actually attached. A no-op on the
  // common blur-triggered path in practice — the prompt renders right
  // under the email field the shopper is already looking at, already in
  // view — but still correct/harmless there too.
  useEffect(() => {
    if (showLogin) loginGateRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
  }, [showLogin]);
  // Per-field inline validation, populated on blur (see each field's own
  // onBlur below) — surfaces the same plausibility checks the pattern/
  // required attributes already declare, immediately instead of only at
  // submit time (native browser validation still applies too, as a
  // fallback for fields somehow never blurred, e.g. autofill).
  const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});

  // `refocusEl`, when given, gets focus put right back on it whenever
  // `message` is non-empty — a field that just failed its own blur
  // validation keeps the cursor instead of letting focus move on to
  // wherever the customer tabbed/clicked next, so the correction happens
  // immediately instead of being left for a submit attempt to catch later.
  function setFieldError(name: string, message: string, refocusEl?: HTMLInputElement) {
    setFieldErrors((prev) => {
      if (!message) {
        if (!(name in prev)) return prev;
        const next = { ...prev };
        delete next[name];
        return next;
      }
      if (prev[name] === message) return prev;
      return { ...prev, [name]: message };
    });
    if (message && refocusEl) {
      refocusEl.focus();
    }
  }

  // Address-card fields — controlled (unlike before) so they can be
  // persisted via lib/checkoutDraft.ts and restored after navigating away
  // from /checkout and back. Initial values still come from savedProfile
  // only (server-safe); a saved draft, if any, overwrites them in the
  // hydration effect below rather than here, to avoid an SSR/hydration
  // mismatch the same way BestellbestaetigungContent's own browser-only
  // read does.
  const [firstName, setFirstName] = useState(savedProfile?.firstName ?? "");
  const [lastName, setLastName] = useState(savedProfile?.lastName ?? "");
  const [email, setEmail] = useState(savedProfile?.email ?? customerEmail ?? "");
  // Optional B2B fields — sit right next to Rechnungsadresse (not a
  // separately gated "order as a business" toggle) since each is
  // independently optional (see Customers.ts/Orders.ts's own comment on
  // why neither implies the other). Prefilled from the saved profile, same
  // as every other Card 1 field.
  const [companyName, setCompanyName] = useState(savedProfile?.companyName ?? "");
  const [vatId, setVatId] = useState(savedProfile?.vatId ?? "");
  // Always a plain street address — a Packstation isn't a valid Rechnungs-
  // adresse (an invoice needs a real postal address). Packstation is only
  // ever offered on the separate, optional shipping-address override below.
  const [street, setStreet] = useState(savedProfile?.street ?? "");
  const [zip, setZip] = useState(savedProfile?.zip ?? "");
  const [city, setCity] = useState(savedProfile?.city ?? "");
  const [country, setCountry] = useState(savedProfile?.country ?? "Deutschland");
  // Optional package destination distinct from the billing address above.
  // Seeded from savedProfile's own "Lieferadresse" tab (Customers.ts),
  // same precedence as Card 1's billing fields above: draft (if any)
  // overwrites this in the hydration effect below, savedProfile is only
  // the pre-hydration/SSR-safe fallback. Unlike Card 1, savedProfile *can*
  // supply these now (see Customers.ts's "Lieferadresse" tab) — this used
  // to always start empty since the saved profile had only one address.
  const [hasDifferentShippingAddress, setHasDifferentShippingAddress] = useState(savedProfile?.hasDifferentShippingAddress ?? false);
  const [shippingFirstName, setShippingFirstName] = useState(savedProfile?.shippingFirstName ?? "");
  const [shippingLastName, setShippingLastName] = useState(savedProfile?.shippingLastName ?? "");
  const [shippingDeliveryMethod, setShippingDeliveryMethod] = useState<"address" | "packstation">(
    savedProfile?.shippingDeliveryMethod ?? "address",
  );
  const [shippingStreet, setShippingStreet] = useState(savedProfile?.shippingStreet ?? "");
  const [shippingPackstationNumber, setShippingPackstationNumber] = useState(savedProfile?.shippingPackstationNumber ?? "");
  const [shippingPostNumber, setShippingPostNumber] = useState(savedProfile?.shippingPostNumber ?? "");
  const [shippingZip, setShippingZip] = useState(savedProfile?.shippingZip ?? "");
  const [shippingCity, setShippingCity] = useState(savedProfile?.shippingCity ?? "");
  const [shippingCountry, setShippingCountry] = useState(savedProfile?.shippingCountry ?? "Deutschland");
  // Optional, mirrors companyName above — no shipping-side vatId though,
  // a VAT ID is a billing/invoice concept, not a shipping one. Contact
  // email/phone have no billing-side equivalent at all: they're handed to
  // the shipping carrier, not used for any customer communication.
  const [shippingCompanyName, setShippingCompanyName] = useState(savedProfile?.shippingCompanyName ?? "");
  const [shippingContactEmail, setShippingContactEmail] = useState(savedProfile?.shippingContactEmail ?? "");
  const [shippingContactPhone, setShippingContactPhone] = useState(savedProfile?.shippingContactPhone ?? "");
  const [newsletterOptIn, setNewsletterOptIn] = useState(false);
  // Live VIES status for the USt-IdNr. field — only meaningful once the
  // goods' destination (shipping override country when set, billing
  // country otherwise) is Österreich, the one EU-cross-border option this
  // checkout offers (see lib/vatExemption.ts). "valid" is what actually
  // drives the exempt-totals preview below; api/checkout/route.ts re-runs
  // this exact same VIES check server-side at submit time regardless —
  // this state is a preview, never the source of truth.
  const [vatIdViesStatus, setVatIdViesStatus] = useState<"idle" | "checking" | "valid" | "invalid" | "unavailable">("idle");
  const [dhlPostNumberStatus, setDhlPostNumberStatus] = useState<"idle" | "checking" | "valid" | "invalid" | "unavailable">("idle");
  // Flips true only after the hydration effect's setState calls have
  // actually landed in a render — gates the write-back effect below so it
  // never fires with the pre-hydration defaults first and briefly
  // clobbers a real saved draft with them (both effects otherwise run in
  // the same post-mount flush, before hydration's setState is reflected
  // in either effect's closure).
  const [draftHydrated, setDraftHydrated] = useState(false);

  useEffect(() => {
    // One-time sync from browser-only localStorage to app state on mount —
    // same reasoning as CartContent.tsx's URL-code auto-apply and
    // BestellbestaetigungContent's own sessionStorage read, not a
    // render-cascade: this can't run any earlier (no localStorage on the
    // server) and never re-runs after mount ([] deps).
    /* eslint-disable react-hooks/set-state-in-effect */
    const draft = readCheckoutDraft();
    if (draft) {
      if (draft.firstName) setFirstName(draft.firstName);
      if (draft.lastName) setLastName(draft.lastName);
      if (draft.email) setEmail(draft.email);
      if (draft.companyName) setCompanyName(draft.companyName);
      if (draft.vatId) setVatId(draft.vatId);
      if (draft.street) setStreet(draft.street);
      if (draft.zip) setZip(draft.zip);
      if (draft.city) setCity(draft.city);
      if (draft.country) setCountry(draft.country);
      if (typeof draft.hasDifferentShippingAddress === "boolean") setHasDifferentShippingAddress(draft.hasDifferentShippingAddress);
      if (draft.shippingFirstName) setShippingFirstName(draft.shippingFirstName);
      if (draft.shippingLastName) setShippingLastName(draft.shippingLastName);
      if (draft.shippingDeliveryMethod) setShippingDeliveryMethod(draft.shippingDeliveryMethod);
      if (draft.shippingStreet) setShippingStreet(draft.shippingStreet);
      if (draft.shippingPackstationNumber) setShippingPackstationNumber(draft.shippingPackstationNumber);
      if (draft.shippingPostNumber) setShippingPostNumber(draft.shippingPostNumber);
      if (draft.shippingZip) setShippingZip(draft.shippingZip);
      if (draft.shippingCity) setShippingCity(draft.shippingCity);
      if (draft.shippingCountry) setShippingCountry(draft.shippingCountry);
      if (draft.shippingCompanyName) setShippingCompanyName(draft.shippingCompanyName);
      if (draft.shippingContactEmail) setShippingContactEmail(draft.shippingContactEmail);
      if (draft.shippingContactPhone) setShippingContactPhone(draft.shippingContactPhone);
      if (typeof draft.newsletterOptIn === "boolean") setNewsletterOptIn(draft.newsletterOptIn);
      if (draft.shippingMethodId != null) setShippingMethodId(draft.shippingMethodId);
      if (draft.paymentMethodId != null) setPaymentMethodId(draft.paymentMethodId);
    }
    setDraftHydrated(true);
    /* eslint-enable react-hooks/set-state-in-effect */
  }, []);

  useEffect(() => {
    if (!draftHydrated) return;
    writeCheckoutDraft({
      firstName,
      lastName,
      email,
      companyName,
      vatId,
      street,
      zip,
      city,
      country,
      hasDifferentShippingAddress,
      shippingFirstName,
      shippingLastName,
      shippingDeliveryMethod,
      shippingStreet,
      shippingPackstationNumber,
      shippingPostNumber,
      shippingZip,
      shippingCity,
      shippingCountry,
      shippingCompanyName,
      shippingContactEmail,
      shippingContactPhone,
      newsletterOptIn,
      shippingMethodId,
      paymentMethodId,
    });
  }, [
    draftHydrated,
    firstName,
    lastName,
    email,
    companyName,
    vatId,
    street,
    zip,
    city,
    country,
    hasDifferentShippingAddress,
    shippingFirstName,
    shippingLastName,
    shippingDeliveryMethod,
    shippingStreet,
    shippingPackstationNumber,
    shippingPostNumber,
    shippingZip,
    shippingCity,
    shippingCountry,
    shippingCompanyName,
    shippingContactEmail,
    shippingContactPhone,
    newsletterOptIn,
    shippingMethodId,
    paymentMethodId,
  ]);

  const productsLoading = products.length === 0 && cart.length > 0;
  const items = cart
    .map((entry) => ({ entry, product: products.find((p) => p.id === entry.id) }))
    .filter((row): row is { entry: typeof cart[number]; product: NonNullable<(typeof row)["product"]> } => Boolean(row.product));

  const subtotal = computeSubtotal(items);
  const selectedShipping = shippingMethods.find((m) => m.id === shippingMethodId) ?? null;
  const freeShipping =
    selectedShipping?.freeShippingThreshold !== null &&
    selectedShipping?.freeShippingThreshold !== undefined &&
    subtotal >= selectedShipping.freeShippingThreshold;
  const shipping = items.length === 0 || !cartHasShippableItem(items) || freeShipping ? 0 : selectedShipping?.price ?? 0;
  // For VersandModal — same "lowest freeShippingThreshold among active
  // methods" rule as CartContent.tsx/getTrustBadges(), independent of
  // whichever method is currently selected in the radio list above.
  const shippingMethodThresholds = shippingMethods.map((m) => m.freeShippingThreshold).filter((t): t is number => t !== null);
  const lowestFreeShippingThreshold = shippingMethodThresholds.length > 0 ? Math.min(...shippingMethodThresholds) : null;
  const { totalSavings, discountAmount, total } = computeCartTotals(items, shipping, discount);
  const taxBreakdown = computeTaxBreakdown(
    items.map(({ entry, product }) => ({
      quantity: entry.qty,
      unitPrice: effectivePrice(entry, product),
      taxRatePercent: effectiveTaxRate(product, defaultTaxRate),
    })),
    subtotal,
    discountAmount,
    shipping,
  );

  // Live preview only — api/checkout/route.ts re-runs the same VIES check
  // server-side at submit time and is the actual source of truth (see
  // lib/vatExemption.ts). Destination is the shipping override's country
  // when set, the billing country otherwise — the exemption depends on
  // where the goods actually move to, not necessarily the invoice address.
  const buyerDestinationCountry = destinationCountry(country, hasDifferentShippingAddress, shippingCountry);
  const vatExemptPreview = !kleinunternehmer && vatIdViesStatus === "valid" && isExemptionEligibleCountry(buyerDestinationCountry);
  const exemptTotalsPreview = vatExemptPreview
    ? computeExemptTotals(
        items.map(({ entry, product }) => ({
          quantity: entry.qty,
          grossUnitPrice: effectivePrice(entry, product),
          taxRatePercent: effectiveTaxRate(product, defaultTaxRate),
        })),
        shipping,
        defaultTaxRate,
        discountAmount,
      )
    : null;
  const displaySubtotal = exemptTotalsPreview?.subtotal ?? subtotal;
  const displayShipping = exemptTotalsPreview?.shippingCost ?? shipping;
  const displayTotal = exemptTotalsPreview?.total ?? total;

  // USt-IdNr. blur — format-checks first (always), then a live VIES lookup
  // for ANY country, not just Österreich. Two genuinely separate concerns:
  // whether this is a real, currently-registered VAT ID at all (data
  // quality — worth knowing regardless of destination, same reasoning as
  // company-settings.vatId's own VIES check on the backend) vs. whether
  // *this transaction* qualifies for the cross-border exemption (a
  // narrower legal question, still gated on isExemptionEligibleCountry()
  // wherever vatExemptPreview/exemptTotalsPreview are computed below — a
  // validated German VAT ID never zero-rates a domestic sale).
  async function handleVatIdBlur(e: React.FocusEvent<HTMLInputElement>) {
    const input = e.target;
    const value = input.value;
    const formatError = validateVatIdFormat(value);
    setFieldError("vatId", formatError, input);
    if (!value.trim() || formatError) {
      setVatIdViesStatus("idle");
      return;
    }
    setVatIdViesStatus("checking");
    try {
      const res = await fetch("/api/checkout/validate-vat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ vatId: value }),
      });
      const data = await res.json();
      if (!data.ok) {
        setVatIdViesStatus("unavailable");
        return;
      }
      setVatIdViesStatus(data.valid ? "valid" : "invalid");
      // Re-focus (no select-all) on an unconfirmed VAT ID so the customer's
      // attention returns to the field without wiping what they typed.
      if (!data.valid) {
        input.focus();
      }
    } catch {
      setVatIdViesStatus("unavailable");
    }
  }

  // Live-checks the Packstation Postnummer against DHL, mirroring
  // handleVatIdBlur's shape. `ok: false` from the proxy route covers both
  // "DHL unreachable" and "this tenant doesn't have Postnummer-validation
  // activated" (dhl-settings.postnummerEnabled off) — the latter is the
  // common case for shops without a DHL integration, so it silently falls
  // back to idle (format-only, already checked above) rather than showing
  // an alarming "currently unavailable" message for a feature that was
  // simply never turned on.
  async function handlePostNumberBlur(e: React.FocusEvent<HTMLInputElement>) {
    const input = e.target;
    const value = input.value;
    const formatError = validatePostNumber(value);
    setFieldError("shippingPostNumber", formatError, input);
    if (!value.trim() || formatError || !shippingFirstName.trim() || !shippingLastName.trim()) {
      setDhlPostNumberStatus("idle");
      return;
    }
    setDhlPostNumberStatus("checking");
    try {
      const res = await fetch("/api/checkout/validate-dhl-postnumber", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ postNumber: value, firstName: shippingFirstName, lastName: shippingLastName }),
      });
      const data = await res.json();
      if (!data.ok) {
        setDhlPostNumberStatus("idle");
        return;
      }
      setDhlPostNumberStatus(data.valid ? "valid" : "invalid");
      if (!data.valid) input.focus();
    } catch {
      setDhlPostNumberStatus("idle");
    }
  }

  // Logs into an existing account inline, without leaving /checkout —
  // router.refresh() re-runs the page's Server Component, which re-reads
  // the now-set session cookie and passes the resolved customerEmail back
  // down, flipping this form out of registration mode.
  async function handleLogin() {
    setLoggingIn(true);
    setLoginError(null);
    try {
      const res = await fetch("/api/account/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        // Card 1's own `email` state, not a separate re-typed value — the
        // login prompt now renders inline right under that same field (see
        // "1. Rechnungsadresse" below), so asking for the email a second
        // time would just be redundant. Whatever's currently in the field
        // is authoritative; the server rejects it the normal way if it
        // doesn't match an account.
        body: JSON.stringify({ email, password: loginPassword }),
      });
      const data = await res.json();
      if (!data.ok) {
        setLoginError(data.reason || "Login fehlgeschlagen.");
        setLoggingIn(false);
        return;
      }
      await mergeServerCartIntoLocal();
      dispatchAuthChanged();
      router.refresh();
    } catch {
      setLoginError("Login ist gerade nicht möglich.");
      setLoggingIn(false);
    }
  }

  async function handleLogout() {
    await fetch("/api/account/logout", { method: "POST" });
    dispatchAuthChanged();
    router.refresh();
  }

  // Checks whether the email just typed into Card 1 already has an
  // account — proactively, on blur, rather than only finding out after a
  // registration attempt fails (see handleSubmit's own emailExists
  // handling, which still applies as a fallback if this check is skipped
  // or the account was created in the meantime). Silent when there's no
  // match — the account gate simply stays empty; nothing is shown "just
  // in case", only once actually relevant.
  async function handleEmailBlur(e: React.FocusEvent<HTMLInputElement>) {
    const email = e.target.value.trim();
    setFieldError("email", validateEmailFormat(email), e.target);
    if (!email || customerEmail || showLogin) return;
    try {
      const res = await fetch("/api/account/check-email", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email }),
      });
      const data = await res.json();
      if (data.exists) {
        setShowLogin(true);
      }
    } catch {
      // Best-effort — worst case, the registration attempt itself catches
      // the collision at submit time instead (handleSubmit).
    }
  }

  // Always goes through /api/checkout now — that route re-prices
  // everything server-side, re-validates+redeems a discount code exactly
  // once (see its own comment), registers a new account inline when no
  // session exists yet ("Konto Pflicht"), and only then persists the order
  // in Payload. Replaces the old client-only sessionStorage snapshot.
  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setPurchasing(true);
    setPurchaseError(null);

    const form = new FormData(e.currentTarget);
    const body = {
      cart,
      shippingMethodId,
      paymentMethodId,
      discountCode: discount?.code ?? null,
      firstName,
      lastName,
      email,
      companyName: companyName || undefined,
      vatId: vatId || undefined,
      // Deliberately still read from FormData, not state — password is the
      // one address-card field that stays uncontrolled/unpersisted (see
      // lib/checkoutDraft.ts's own comment on why).
      password: customerEmail ? undefined : String(form.get("password") ?? ""),
      // Rechnungsadresse is always a plain street address — see the
      // street state's own comment on why Packstation isn't offered here.
      deliveryMethod: "address" as const,
      street: street || undefined,
      zip,
      city,
      country,
      hasDifferentShippingAddress,
      shippingFirstName: hasDifferentShippingAddress ? shippingFirstName : undefined,
      shippingLastName: hasDifferentShippingAddress ? shippingLastName : undefined,
      shippingDeliveryMethod: hasDifferentShippingAddress ? shippingDeliveryMethod : undefined,
      shippingStreet: hasDifferentShippingAddress ? shippingStreet || undefined : undefined,
      shippingPackstationNumber: hasDifferentShippingAddress ? shippingPackstationNumber || undefined : undefined,
      shippingPostNumber: hasDifferentShippingAddress ? shippingPostNumber || undefined : undefined,
      shippingZip: hasDifferentShippingAddress ? shippingZip : undefined,
      shippingCity: hasDifferentShippingAddress ? shippingCity : undefined,
      shippingCountry: hasDifferentShippingAddress ? shippingCountry : undefined,
      shippingCompanyName: hasDifferentShippingAddress ? shippingCompanyName || undefined : undefined,
      shippingContactEmail: hasDifferentShippingAddress ? shippingContactEmail || undefined : undefined,
      shippingContactPhone: hasDifferentShippingAddress ? shippingContactPhone || undefined : undefined,
      newsletterOptIn,
    };

    try {
      const res = await fetch("/api/checkout", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      const data = await res.json();
      if (!data.ok) {
        // The email typed into Card 1 already belongs to an existing
        // account — registering was never going to work here. Switch
        // straight to the login prompt instead of just showing an error
        // with no clear next step; the customer only needs to add their
        // password and resubmit. The scroll-into-view (for a shopper who
        // filled the whole form and hit submit without ever blurring the
        // email field, e.g. autofill) happens in the loginGateRef effect
        // below, not here — this div doesn't exist in the DOM yet at this
        // exact point, `showLogin` only flips it in after the next render.
        if (data.emailExists) {
          setShowLogin(true);
        }
        setPurchaseError(data.reason || "Die Bestellung konnte nicht abgeschlossen werden.");
        setPurchasing(false);
        return;
      }

      const snapshot: OrderSnapshot = {
        items: cart,
        orderNumber: data.orderNumber,
        orderDateIso: data.orderDateIso,
        shippingCost: data.shippingCost,
        paymentMethodTitle: data.paymentMethodTitle,
        discountCode: data.discountCode,
        discountAmount: data.discountAmount,
        vatExempt: Boolean(data.vatExempt),
        kleinunternehmer: Boolean(data.kleinunternehmer),
      };

      if (data.requiresPayment) {
        // Order exists in Payload now (status 'pending_payment'), but
        // nothing is confirmed yet — the sessionStorage snapshot, cart
        // clear, and navigation to /bestellbestaetigung all wait for
        // /checkout/verarbeitung to see a confirmed payment (see that
        // page's own comment). A cancelled/failed payment must leave the
        // cart intact so the customer can just retry.
        try {
          window.sessionStorage.setItem(PENDING_ORDER_KEY, JSON.stringify(snapshot));
        } catch {
          // Same private-browsing fallback as the confirmed-order path
          // below — /checkout/verarbeitung falls back to its own empty
          // state if this didn't persist.
        }
        setPaymentStep({
          clientSecret: data.clientSecret,
          orderNumber: data.orderNumber,
          orderId: data.orderId,
          testMode: Boolean(data.testMode),
          providerReference: data.providerReference,
        });
        setPurchasing(false);
        return;
      }

      try {
        window.sessionStorage.setItem(ORDER_KEY, JSON.stringify(snapshot));
      } catch {
        // sessionStorage unavailable (private browsing etc.) — the
        // confirmation page falls back to its own empty state.
      }
      clearCart();
      clearDiscount();
      clearCheckoutDraft();
      // Guest checkout with a password creates+logs into a new account
      // server-side — Navbar needs to know even though it isn't remounting.
      dispatchAuthChanged();
      router.push("/bestellbestaetigung");
    } catch {
      setPurchaseError("Die Bestellung konnte gerade nicht abgeschlossen werden.");
      setPurchasing(false);
    }
  }

  if (!productsLoading && items.length === 0) {
    return (
      <Reveal className="flex flex-col gap-6 items-start pt-10 pb-16 px-[var(--layout-padding-x)] w-full">
        <p
          className="font-semibold text-h-feature text-text-primary"
          style={{ fontFamily: "var(--font-lora)" }}
        >
          Checkout
        </p>
        <p className="text-body text-text-muted">
          Dein Warenkorb ist leer — es gibt noch nichts zum Abschließen.
        </p>
        <Link
          href="/shop"
          className="flex items-center gap-2 px-7 py-4 rounded-sm bg-brand text-h4 font-bold text-text-primary hover:bg-brand-hover active:scale-[0.97] transition-all"
        >
          Zum Shop
        </Link>
      </Reveal>
    );
  }

  // Order already exists in Payload (status 'pending_payment') — this
  // replaces the address/cart form with the actual payment UI rather than
  // navigating away, since nothing is confirmed yet. See PaymentStep's own
  // comment and spicy-leaping-pizza.md §3.
  if (paymentStep) {
    return (
      <Reveal className="flex flex-col gap-8 items-start pt-8 pb-16 px-[var(--layout-padding-x)] w-full max-w-xl mx-auto">
        <p
          className="font-semibold text-h-feature text-text-primary"
          style={{ fontFamily: "var(--font-lora)" }}
        >
          Zahlung
        </p>
        <p className="text-body-sm text-text-muted">
          Bestellung {paymentStep.orderNumber} wurde angelegt — schließe jetzt die Zahlung ab.
        </p>
        <PaymentStep
          clientSecret={paymentStep.clientSecret}
          orderNumber={paymentStep.orderNumber}
          orderId={paymentStep.orderId}
          testMode={paymentStep.testMode}
          providerReference={paymentStep.providerReference}
        />
      </Reveal>
    );
  }

  return (
    <>
      {/* Header — breadcrumb, stepper, title */}
      <Reveal className="flex flex-col gap-8 items-start pt-8 pb-6 px-[var(--layout-padding-x)] w-full">
        <p className="flex items-center gap-2 text-body-sm text-text-muted">
          <Link href="/" className="hover:text-brand transition-colors">Startseite</Link>
          <span>›</span>
          <Link href="/cart" className="hover:text-brand transition-colors">Warenkorb</Link>
          <span>›</span>
          <span className="text-text-primary">Checkout</span>
        </p>

        <CheckoutSteps current={2} />

        <div className="flex flex-col gap-2">
          <p
            className="font-semibold text-h-feature text-text-primary"
            style={{ fontFamily: "var(--font-lora)" }}
          >
            Checkout
          </p>
          <p className="text-body text-text-muted">Fast geschafft! Nur noch ein paar Angaben.</p>
        </div>

        {/* Only the passive "already logged in" state stays up here — the
            reactive "this email already has an account, please log in"
            prompt used to render in this same spot too, which meant it
            could pop in well above wherever the shopper had scrolled to
            fill in Card 1's email field, mobile especially (no scroll-to
            was even wired up for the common case — only the submit-time
            fallback further down had one). Moved inline right under Card
            1's own email field instead (see "1. Rechnungsadresse" below) —
            it now appears exactly where the shopper's attention already
            is, no scrolling needed either way. */}
        {customerEmail && (
          <p className="text-body-sm text-text-primary">
            Eingeloggt als <span className="font-bold">{customerEmail}</span>{" "}
            <button type="button" onClick={handleLogout} className="underline hover:text-brand transition-colors">
              Abmelden
            </button>
          </p>
        )}
      </Reveal>

      <form onSubmit={handleSubmit} className="flex flex-col lg:flex-row gap-8 lg:gap-10 items-start pb-10 pt-2 px-[var(--layout-padding-x)] w-full">
        {/* Form column */}
        <div className="w-full lg:flex-1 flex flex-col gap-6 items-start min-w-0">
          {/* 1. Rechnungsadresse */}
          {/* border-[#c4b8a0], not the shared border-border (#e5e0d8) — that
              token is nearly the same luminance as this card's own bg-bg-base
              background (#f8f5f1), so the card's outline barely read at all
              (fixed 2026-07-24, feedback: "die Striche bei den Steps sind zu
              hell"). Darker but still on the warm-cream palette, scoped to
              just these 4 step cards rather than the shared token, since
              border-border reads fine everywhere else it's paired with a
              genuinely different background. */}
          <Reveal className="w-full bg-bg-base border border-[#c4b8a0] rounded-md p-7 flex flex-col gap-5 items-start">
            <p
              className="font-semibold text-h-small text-text-primary"
              style={{ fontFamily: "var(--font-lora)" }}
            >
              1. Rechnungsadresse
            </p>
            <div className="flex flex-col sm:flex-row gap-4 w-full">
              <FormField
                label="Vorname"
                name="firstName"
                type="text"
                value={firstName}
                onChange={(e) => setFirstName(e.target.value)}
                onBlur={(e) => setFieldError("firstName", validateRequired("Vorname", e.target.value), e.target)}
                error={fieldErrors.firstName}
                placeholder="Max"
                autoComplete="given-name"
                required
              />
              <FormField
                label="Nachname"
                name="lastName"
                type="text"
                value={lastName}
                onChange={(e) => setLastName(e.target.value)}
                onBlur={(e) => setFieldError("lastName", validateRequired("Nachname", e.target.value), e.target)}
                error={fieldErrors.lastName}
                placeholder="Mustermann"
                autoComplete="family-name"
                required
              />
            </div>
            {/* Optional B2B fields — both independently optional (see
                Orders.ts's own comment: a sole proprietor might give a VAT
                ID with no separate "company name", and vice versa), so
                neither is required just because the other is filled in. */}
            <div className="flex flex-col sm:flex-row gap-4 w-full">
              <FormField
                label="Firma (optional)"
                name="companyName"
                type="text"
                value={companyName}
                onChange={(e) => setCompanyName(e.target.value)}
                placeholder="Muster GmbH"
                autoComplete="organization"
              />
              <div className="flex-1 min-w-0 flex flex-col gap-1">
                <FormField
                  label="USt-IdNr. (optional)"
                  name="vatId"
                  type="text"
                  value={vatId}
                  onChange={(e) => {
                    setVatId(e.target.value);
                    setVatIdViesStatus("idle");
                  }}
                  onBlur={handleVatIdBlur}
                  error={fieldErrors.vatId}
                  placeholder="DE123456789"
                  autoComplete="off"
                  pattern="[A-Za-z]{2}[A-Za-z0-9]{2,12}"
                  maxLength={14}
                  title="EU-Format: 2 Buchstaben Länderpräfix + bis zu 12 alphanumerische Zeichen, z. B. DE123456789."
                  wrapperClassName="w-full"
                />
                {/* Shown for any country — validity is worth confirming
                    regardless of destination (data quality: is this VAT ID
                    even real). Only the "valid" message's wording differs
                    by destination: Österreich additionally gets the
                    exemption note, Deutschland/Schweiz just get a plain
                    confirmation, since the exemption never applies there
                    even for a genuinely valid VAT ID. */}
                <p className="text-label text-text-muted min-h-[1.05rem]">
                  {vatIdViesStatus === "checking" && "USt-IdNr. wird geprüft…"}
                  {vatIdViesStatus === "valid" && (
                    <span className="text-success">
                      ✓ USt-IdNr. bestätigt
                      {isExemptionEligibleCountry(buyerDestinationCountry) ? " — Lieferung wird steuerfrei berechnet." : "."}
                    </span>
                  )}
                  {vatIdViesStatus === "invalid" && (
                    <span className="text-red-600">USt-IdNr. konnte nicht bestätigt werden.</span>
                  )}
                  {vatIdViesStatus === "unavailable" && "USt-IdNr.-Prüfung derzeit nicht möglich."}
                </p>
              </div>
            </div>
            {/* w-[calc(50%-0.5rem)] at sm: — exactly matches Vorname's
                actual rendered width in the 2-col row above (each half of
                a gap-4 flex row), instead of stretching full-width. */}
            <FormField
              ref={emailInputRef}
              label="E-Mail-Adresse"
              name="email"
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="max@beispiel.de"
              autoComplete="email"
              required
              onBlur={handleEmailBlur}
              error={fieldErrors.email}
              wrapperClassName="w-full sm:w-[calc(50%-0.5rem)] sm:flex-none min-w-0"
            />
            {/* Not needed once already logged in (existing session, no
                password to collect) — and swapped for the inline login
                prompt below instead of the "create a new account" password
                field the moment handleEmailBlur/handleSubmit's own
                emailExists fallback detects the typed email already
                belongs to an account. Rendered right under the email field
                itself (previously a separate block all the way at the top
                of the page, above the form — easy to lose track of once
                scrolled down to fill in Card 1, especially on mobile,
                since nothing auto-scrolled to it on the common
                blur-triggered path either). */}
            {!customerEmail && (showLogin ? (
              <div ref={loginGateRef} className="flex flex-col gap-2 w-full sm:w-[calc(50%-0.5rem)] sm:flex-none min-w-0">
                <p className="text-label text-text-muted">
                  Für diese E-Mail-Adresse existiert bereits ein Konto.
                </p>
                <FormField
                  label="Passwort"
                  type="password"
                  value={loginPassword}
                  onChange={(e) => setLoginPassword(e.target.value)}
                  autoComplete="current-password"
                  wrapperClassName="w-full"
                />
                <div className="flex items-center gap-3">
                  <button
                    type="button"
                    onClick={handleLogin}
                    disabled={loggingIn}
                    className={`px-5 py-2.5 rounded-sm bg-brand hover:bg-brand-hover font-bold text-body-sm text-text-primary transition-colors ${loggingIn ? "opacity-70 pointer-events-none" : ""}`}
                  >
                    {loggingIn ? "…" : "Einloggen"}
                  </button>
                  <button
                    type="button"
                    onClick={() => {
                      setShowLogin(false);
                      setLoginError(null);
                      // Clears Card 1's own email field too, not just the
                      // login prompt — leaving the already-registered
                      // address sitting there meant the shopper had to
                      // manually select/delete it before typing a new one.
                      // Password cleared alongside it since it was only
                      // ever meaningful for the email that's now gone.
                      setEmail("");
                      setLoginPassword("");
                      setFieldError("email", "");
                      emailInputRef.current?.focus();
                    }}
                    className="text-label text-text-muted underline hover:text-text-primary transition-colors"
                  >
                    Andere E-Mail-Adresse verwenden
                  </button>
                </div>
                {loginError && <p className="text-label text-red-600">{loginError}</p>}
              </div>
            ) : (
              <div className="flex flex-col gap-2 w-full sm:w-[calc(50%-0.5rem)] sm:flex-none min-w-0">
                <FormField
                  label="Passwort (für dein Konto)"
                  name="password"
                  type="password"
                  placeholder="Mind. 8 Zeichen"
                  autoComplete="new-password"
                  required
                  minLength={8}
                  // No refocusEl (3rd arg) here, unlike this form's other
                  // blur-validated fields — this field is at the end of the
                  // "2. Kontodaten" card, right before Card 3+ and the
                  // submit button further down the page; forcing focus back
                  // into it on every blur while <8 chars meant a shopper
                  // could never Tab/click past it (or reach the submit
                  // button at all) until the password was already valid —
                  // effectively trapped. The inline red error text below
                  // still appears immediately either way; only the forced
                  // refocus is dropped.
                  onBlur={(e) =>
                    setFieldError(
                      "password",
                      !e.target.value
                        ? "Passwort ist erforderlich."
                        : e.target.value.length < 8
                          ? "Passwort muss mindestens 8 Zeichen lang sein."
                          : "",
                    )
                  }
                  error={fieldErrors.password}
                  wrapperClassName="w-full"
                />
                <p className="text-label text-text-muted">
                  Mit dem Kauf legen wir automatisch ein Konto für dich an — damit du deine Bestellungen später
                  einsehen und bei Bedarf stornieren oder zurücksenden kannst.
                </p>
              </div>
            ))}
            {/* Always a plain street address — Packstation isn't a valid
                Rechnungsadresse (an invoice needs a real postal address).
                Packstation is only ever offered below, in the optional
                "Abweichende Lieferadresse" section's own Lieferart toggle. */}
            <AddressAutocomplete
              label="Straße und Hausnummer"
              name="street"
              value={street}
              onChange={setStreet}
              onBlur={(e) => setFieldError("street", validateRequired("Straße und Hausnummer", e.target.value), e.target)}
              onSelectSuggestion={(s) => {
                setZip(s.zip);
                setCity(s.city);
              }}
              error={fieldErrors.street}
              placeholder="Musterstraße 1"
              autoComplete="street-address"
              required
              wrapperClassName="w-full sm:w-[calc(50%-0.5rem)] sm:flex-none min-w-0"
            />
            <div className="flex flex-col sm:flex-row gap-4 w-full">
              <FormField
                label="PLZ"
                name="zip"
                type="text"
                value={zip}
                onChange={(e) => setZip(e.target.value)}
                onBlur={(e) => setFieldError("zip", validateZip(e.target.value, country, plzDigitsMap), e.target)}
                error={fieldErrors.zip}
                placeholder="10115"
                autoComplete="postal-code"
                inputMode="numeric"
                pattern={plzPattern(country, plzDigitsMap)}
                maxLength={plzDigitsMap[country] ?? 4}
                title={`PLZ muss aus ${plzDigitsMap[country] ?? 4} Ziffern bestehen.`}
                required
              />
              <FormField
                label="Ort"
                name="city"
                type="text"
                value={city}
                onChange={(e) => setCity(e.target.value)}
                onBlur={(e) => setFieldError("city", validateRequired("Ort", e.target.value), e.target)}
                error={fieldErrors.city}
                placeholder="Berlin"
                autoComplete="address-level2"
                required
              />
            </div>
            <label className="flex flex-col gap-2 items-start w-full">
              <span className="text-label text-text-muted">Land</span>
              {/* CustomSelect, not a native <select> — consistent with the
                  filter dropdowns elsewhere (native options popups can't be
                  styled at all). includeAllOption={false}: every country is
                  a real, selectable choice, there's no "clear" concept
                  here — "country" is always genuinely set to something. */}
              <CustomSelect
                label="Land wählen"
                includeAllOption={false}
                fullWidth
                options={shippingCountries.map((c) => ({ value: c.name, label: c.name }))}
                value={country}
                onChange={setCountry}
              />
            </label>

            <div className="h-px bg-border w-full" />

            <label className="flex gap-3 items-start w-full cursor-pointer">
              <input
                type="checkbox"
                checked={hasDifferentShippingAddress}
                onChange={(e) => setHasDifferentShippingAddress(e.target.checked)}
                className="size-5 shrink-0 mt-0.5 rounded-xs border border-border accent-brand"
              />
              <span className="text-body-sm text-text-primary">Abweichende Lieferadresse verwenden</span>
            </label>

            {hasDifferentShippingAddress && (
              <div className="flex flex-col gap-4 items-start w-full">
                <p className="font-semibold text-body-sm text-text-primary">Lieferadresse</p>
                <div className="flex flex-col sm:flex-row gap-4 w-full">
                  <FormField
                    label="Vorname"
                    type="text"
                    value={shippingFirstName}
                    onChange={(e) => setShippingFirstName(e.target.value)}
                    onBlur={(e) => setFieldError("shippingFirstName", validateRequired("Vorname", e.target.value), e.target)}
                    error={fieldErrors.shippingFirstName}
                    placeholder="Max"
                    autoComplete="off"
                    required
                  />
                  <FormField
                    label="Nachname"
                    type="text"
                    value={shippingLastName}
                    onChange={(e) => setShippingLastName(e.target.value)}
                    onBlur={(e) => setFieldError("shippingLastName", validateRequired("Nachname", e.target.value), e.target)}
                    error={fieldErrors.shippingLastName}
                    placeholder="Mustermann"
                    autoComplete="off"
                    required
                  />
                </div>
                <FormField
                  label="Firma (optional)"
                  type="text"
                  value={shippingCompanyName}
                  onChange={(e) => setShippingCompanyName(e.target.value)}
                  placeholder="Muster GmbH"
                  autoComplete="off"
                  wrapperClassName="w-full"
                />
                <div className="w-full sm:w-[calc(50%-0.5rem)] flex flex-col gap-2 items-start">
                  <span className="text-label text-text-muted">Lieferart</span>
                  <div className="flex w-full rounded-sm border border-border overflow-hidden">
                    <button
                      type="button"
                      onClick={() => setShippingDeliveryMethod("address")}
                      aria-pressed={shippingDeliveryMethod === "address"}
                      className={`flex-1 py-3 text-body-sm font-bold transition-colors ${
                        shippingDeliveryMethod === "address"
                          ? "bg-brand text-text-primary"
                          : "text-text-muted hover:text-text-primary"
                      }`}
                    >
                      Lieferadresse
                    </button>
                    <button
                      type="button"
                      onClick={() => setShippingDeliveryMethod("packstation")}
                      aria-pressed={shippingDeliveryMethod === "packstation"}
                      className={`flex-1 py-3 text-body-sm font-bold border-l border-border transition-colors ${
                        shippingDeliveryMethod === "packstation"
                          ? "bg-brand text-text-primary"
                          : "text-text-muted hover:text-text-primary"
                      }`}
                    >
                      Packstation
                    </button>
                  </div>
                </div>
                {shippingDeliveryMethod === "address" ? (
                  <AddressAutocomplete
                    label="Straße und Hausnummer"
                    value={shippingStreet}
                    onChange={setShippingStreet}
                    onBlur={(e) => setFieldError("shippingStreet", validateRequired("Straße und Hausnummer", e.target.value), e.target)}
                    onSelectSuggestion={(s) => {
                      setShippingZip(s.zip);
                      setShippingCity(s.city);
                    }}
                    error={fieldErrors.shippingStreet}
                    placeholder="Musterstraße 1"
                    autoComplete="off"
                    required
                    wrapperClassName="w-full sm:w-[calc(50%-0.5rem)] sm:flex-none min-w-0"
                  />
                ) : (
                  <div className="flex flex-col sm:flex-row gap-4 w-full">
                    <FormField
                      label="Packstationnummer"
                      type="text"
                      value={shippingPackstationNumber}
                      onChange={(e) => setShippingPackstationNumber(e.target.value)}
                      onBlur={(e) => setFieldError("shippingPackstationNumber", validatePackstationNumber(e.target.value), e.target)}
                      error={fieldErrors.shippingPackstationNumber}
                      inputMode="numeric"
                      placeholder="123"
                      autoComplete="off"
                      pattern="\d{1,3}"
                      maxLength={3}
                      title="Packstationsnummer muss aus 1 bis 3 Ziffern bestehen (1–999)."
                      required
                    />
                    <div className="flex-1 min-w-0 flex flex-col gap-1">
                      <FormField
                        label="Postnummer"
                        type="text"
                        value={shippingPostNumber}
                        onChange={(e) => setShippingPostNumber(e.target.value)}
                        onBlur={handlePostNumberBlur}
                        error={fieldErrors.shippingPostNumber}
                        inputMode="numeric"
                        placeholder="1234567890"
                        autoComplete="off"
                        pattern="\d{6,10}"
                        maxLength={10}
                        title="Postnummer muss aus 6 bis 10 Ziffern bestehen."
                        required
                        wrapperClassName="w-full"
                      />
                      {/* Silent when idle — covers both "not yet checked" and
                          "this shop has no DHL Postnummer-validation active",
                          same reasoning as handlePostNumberBlur's own comment. */}
                      <p className="text-label text-text-muted min-h-[1.05rem]">
                        {dhlPostNumberStatus === "checking" && "Postnummer wird bei DHL geprüft…"}
                        {dhlPostNumberStatus === "valid" && <span className="text-success">✓ Postnummer bestätigt</span>}
                        {dhlPostNumberStatus === "invalid" && (
                          <span className="text-red-600">DHL konnte Postnummer/Name-Kombination nicht bestätigen.</span>
                        )}
                      </p>
                    </div>
                  </div>
                )}
                <div className="flex flex-col sm:flex-row gap-4 w-full">
                  <FormField
                    label="PLZ"
                    type="text"
                    value={shippingZip}
                    onChange={(e) => setShippingZip(e.target.value)}
                    onBlur={(e) => setFieldError("shippingZip", validateZip(e.target.value, shippingCountry, plzDigitsMap), e.target)}
                    error={fieldErrors.shippingZip}
                    placeholder="10115"
                    autoComplete="off"
                    inputMode="numeric"
                    pattern={plzPattern(shippingCountry, plzDigitsMap)}
                    maxLength={plzDigitsMap[shippingCountry] ?? 4}
                    title={`PLZ muss aus ${plzDigitsMap[shippingCountry] ?? 4} Ziffern bestehen.`}
                    required
                  />
                  <FormField
                    label="Ort"
                    type="text"
                    value={shippingCity}
                    onChange={(e) => setShippingCity(e.target.value)}
                    onBlur={(e) => setFieldError("shippingCity", validateRequired("Ort", e.target.value), e.target)}
                    error={fieldErrors.shippingCity}
                    placeholder="Berlin"
                    autoComplete="off"
                    required
                  />
                </div>
                <label className="flex flex-col gap-2 items-start w-full">
                  <span className="text-label text-text-muted">Land</span>
                  <CustomSelect
                    label="Land wählen"
                    includeAllOption={false}
                    fullWidth
                    options={shippingCountries.map((c) => ({ value: c.name, label: c.name }))}
                    value={shippingCountry}
                    onChange={setShippingCountry}
                  />
                </label>
                {/* Both optional and independent — handed to the shipping
                    carrier, not used for any customer communication (that
                    stays the account email above). Useful when the
                    recipient at this address isn't reachable via the
                    account holder's own email/phone. */}
                <div className="flex flex-col sm:flex-row gap-4 w-full">
                  <FormField
                    label="Kontakt-E-Mail (optional)"
                    type="email"
                    value={shippingContactEmail}
                    onChange={(e) => setShippingContactEmail(e.target.value)}
                    placeholder="empfang@beispiel.de"
                    autoComplete="off"
                  />
                  <FormField
                    label="Telefonnummer (optional)"
                    type="tel"
                    value={shippingContactPhone}
                    onChange={(e) => setShippingContactPhone(e.target.value)}
                    placeholder="+49 30 123456"
                    autoComplete="off"
                  />
                </div>
                <p className="text-label text-text-muted">Werden nur dem Versanddienstleister übergeben, z. B. für Lieferbenachrichtigungen.</p>
              </div>
            )}

            <div className="h-px bg-border w-full" />

            <label className="flex gap-3 items-start w-full cursor-pointer">
              <input
                type="checkbox"
                name="newsletterOptIn"
                checked={newsletterOptIn}
                onChange={(e) => setNewsletterOptIn(e.target.checked)}
                className="size-5 shrink-0 mt-0.5 rounded-xs border border-border accent-brand"
              />
              <span className="flex flex-col gap-1 text-body-sm text-text-primary">
                Ich möchte regelmäßig Impulse &amp; Tipps per E-Mail erhalten.
                <span className="text-label text-text-muted">Du kannst dich jederzeit mit einem Klick abmelden.</span>
              </span>
            </label>
          </Reveal>

          {/* 2. Versandart */}
          <Reveal delay={0.05} className="w-full bg-bg-base border border-[#c4b8a0] rounded-md p-7 flex flex-col gap-5 items-start">
            <p
              className="font-semibold text-h-small text-text-primary"
              style={{ fontFamily: "var(--font-lora)" }}
            >
              2. Versandart
            </p>
            {shippingMethods.map((method) => {
              const methodFree = method.freeShippingThreshold !== null && subtotal >= method.freeShippingThreshold;
              return (
                <label key={method.id} className="flex items-center gap-3 w-full cursor-pointer">
                  <input
                    type="radio"
                    name="shipping"
                    checked={shippingMethodId === method.id}
                    onChange={() => setShippingMethodId(method.id)}
                    className="size-5 shrink-0 accent-brand"
                  />
                  <span className="flex-1 flex flex-col gap-0.5">
                    <span className="text-body-sm text-text-primary">{method.title}</span>
                    <span className="text-label text-text-muted">{method.description}</span>
                  </span>
                  <span className="text-body-sm text-text-primary whitespace-nowrap">
                    {methodFree ? "Kostenlos" : formatPrice(method.price)}
                  </span>
                </label>
              );
            })}
          </Reveal>

          {/* 3. Zahlungsart */}
          <Reveal delay={0.1} className="w-full bg-bg-base border border-[#c4b8a0] rounded-md p-7 flex flex-col gap-5 items-start">
            <p
              className="font-semibold text-h-small text-text-primary"
              style={{ fontFamily: "var(--font-lora)" }}
            >
              3. Zahlungsart
            </p>

            {paymentOptions.map((method) => (
              <label key={method.id} className="flex flex-col gap-1 w-full cursor-pointer">
                <span className="flex items-center gap-3 w-full">
                  <input
                    type="radio"
                    name="payment"
                    checked={paymentMethodId === method.id}
                    onChange={() => setPaymentMethodId(method.id)}
                    className="size-5 shrink-0 accent-brand"
                  />
                  <span className="flex-1 text-body-sm text-text-primary">{method.title}</span>
                  <span className="flex items-center gap-2 shrink-0">
                    {method.icons.map((icon, i) => (
                      <div key={i} className="relative h-5 w-8 shrink-0">
                        <Image src={icon} alt="" fill sizes="32px" className="object-contain" />
                      </div>
                    ))}
                  </span>
                </span>
                {method.hint && <span className="pl-8 text-label text-text-muted">{method.hint}</span>}
              </label>
            ))}

            <button
              type="submit"
              disabled={purchasing}
              className={`w-full flex items-center justify-center py-4 rounded-sm bg-brand hover:bg-brand-hover active:scale-[0.97] transition-all font-bold text-body text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base ${purchasing ? "pointer-events-none opacity-70" : ""}`}
            >
              {purchasing ? "Einen Moment…" : "Jetzt kaufen (zahlungspflichtig)"}
            </button>

            {purchaseError && (
              <p className="text-label text-red-600 text-center w-full">{purchaseError}</p>
            )}

            <p className="text-label text-text-muted text-center w-full">
              Mit dem Kauf akzeptierst du unsere{" "}
              <Link href="/agb" target="_blank" rel="noopener noreferrer" className="underline hover:text-brand">
                AGB
              </Link>{" "}
              und die{" "}
              <Link href="/datenschutz" target="_blank" rel="noopener noreferrer" className="underline hover:text-brand">
                Datenschutzerklärung
              </Link>.
            </p>

            <div className="flex gap-2 items-center justify-center w-full">
              <Image alt="" src="/icon-lock.svg" width={14} height={16} className="w-3.5 h-4" />
              <span className="text-body-sm text-text-muted">Sichere SSL-Verschlüsselung</span>
            </div>
          </Reveal>
        </div>

        {/* Sidebar */}
        <Reveal delay={0.15} className="w-full lg:w-[24.375rem] lg:shrink-0 flex flex-col gap-6 items-start">
          <div className="bg-bg-base border border-[#c4b8a0] rounded-md p-7 flex flex-col gap-5 items-start w-full">
            <p
              className="font-semibold text-h-small text-text-primary"
              style={{ fontFamily: "var(--font-lora)" }}
            >
              Deine Bestellung
            </p>

            {items.map(({ entry, product }) => {
              const unitPrice = effectivePrice(entry, product);
              const taxRate = effectiveTaxRate(product, defaultTaxRate);
              const lineKey = entry.variant ? `${product.id}::${entry.variant}` : product.id;
              return (
              <div key={lineKey} className="flex gap-4 items-center w-full">
                <div className="relative size-16 shrink-0 rounded-sm overflow-hidden">
                  <Image src={product.image} alt={product.name} fill sizes="64px" className="object-cover" />
                </div>
                <div className="flex-1 min-w-0 flex flex-col gap-0.5">
                  <p className="text-body-sm text-text-primary">
                    {product.name}
                    {entry.variant ? ` (${entry.variant})` : ""}
                  </p>
                  <p className="text-label text-text-muted">
                    {entry.qty} × {formatPrice(unitPrice)} {!kleinunternehmer && <span>inkl. {taxRate}% MwSt.</span>}
                  </p>
                </div>
                <p className="text-body-sm text-text-primary whitespace-nowrap">{formatPrice(entry.qty * unitPrice)}</p>
              </div>
              );
            })}

            <div className="h-px bg-border w-full" />

            <div className="flex items-center w-full">
              <span className="text-body-sm text-text-primary">Zwischensumme</span>
              <span className="flex-1" />
              <span className="text-body-sm text-text-primary">{formatPrice(displaySubtotal)}</span>
            </div>

            {totalSavings > 0 && (
              <div className="flex items-center w-full">
                <span className="text-body-sm text-success">Du sparst</span>
                <span className="flex-1" />
                <span className="font-bold text-body-sm text-success">-{formatPrice(totalSavings)}</span>
              </div>
            )}

            {/* Display-only — applied in /cart, this page has no input of
                its own (see lib/discount.ts). */}
            {discount && (
              <div className="flex items-center w-full">
                <span className="text-body-sm text-success">Rabattcode ({discount.code})</span>
                <span className="flex-1" />
                <span className="font-bold text-body-sm text-success">-{formatPrice(discountAmount)}</span>
              </div>
            )}

            {/* Hidden entirely (not just "Kostenlos") when nothing in the
                cart actually triggers shipping at all — a different state
                from hitting the free-shipping threshold, which is still a
                real promotional message worth showing. */}
            {cartHasShippableItem(items) && (
              <div className="flex flex-col gap-0.5 w-full">
                <div className="flex items-center w-full">
                  <span className="flex items-center gap-1.5 text-body-sm text-text-primary">
                    Versand
                    <button
                      type="button"
                      onClick={() => setVersandOpen(true)}
                      className="text-text-muted hover:text-brand transition-colors"
                      aria-label="Alle Informationen zu Versandkosten und Lieferzeiten"
                    >
                      <span aria-hidden>ⓘ</span>
                    </button>
                  </span>
                  <span className="flex-1" />
                  <span className="text-body-sm text-text-primary">
                    {displayShipping === 0 ? "Kostenlos" : formatPrice(displayShipping)}
                  </span>
                </div>
                <p className="text-label text-text-muted">
                  {shipping === 0 && selectedShipping?.freeShippingThreshold != null
                    ? `ab ${formatPrice(selectedShipping.freeShippingThreshold)} innerhalb Deutschlands`
                    : (selectedShipping?.description ?? "innerhalb Deutschlands")}
                </p>
                <p className="text-label text-text-muted">
                  Lieferzeit {shippingSettings.totalDays.min}–{shippingSettings.totalDays.max} Werktage
                </p>
              </div>
            )}

            <div className="h-px bg-border w-full" />

            <div className="flex flex-col gap-0.5 w-full">
              <div className="flex items-center w-full">
                <span
                  className="font-semibold text-h4 text-text-primary"
                  style={{ fontFamily: "var(--font-lora)" }}
                >
                  Gesamtsumme
                </span>
                <span className="flex-1" />
                <span className="font-bold text-h-small text-text-primary">{formatPrice(displayTotal)}</span>
              </div>
              {kleinunternehmer ? (
                <p className="text-label text-text-muted">Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.</p>
              ) : vatExemptPreview ? (
                <p className="text-label text-text-muted">Steuerfreie innergemeinschaftliche Lieferung (§4 Nr. 1b UStG)</p>
              ) : (
                <VatBreakdown groups={taxBreakdown} />
              )}
            </div>
          </div>

          {/* title + description here — /cart's sidebar renders the same
              CartTrustBadges docs with title only, see CartContent.tsx. */}
          <div className="flex flex-col gap-4 items-start w-full">
            {trustBadges.map((b) => (
              <div key={b.id} className="flex gap-3 items-start w-full">
                <Image alt="" src={b.icon} width={20} height={20} className="size-5 shrink-0 object-contain mt-0.5" />
                <div className="flex-1 flex flex-col gap-0.5">
                  <p className="text-body-sm font-semibold text-text-primary">{b.title}</p>
                  <p className="text-label text-text-muted">{b.description}</p>
                </div>
              </div>
            ))}
          </div>

          {/* Newsletter hint — divider under the TEXT, not the icon, per
              this page's own Figma spec (different from /cart's version,
              which puts the divider under the icon — the two aren't meant
              to be identical, see project notes on this pattern). */}
          <div className="bg-bg-muted flex gap-5 items-start p-6 rounded-md w-full">
            <Image alt="" src="/icon-envelope-hint.png" width={40} height={32} className="h-8 w-10 shrink-0 object-contain" />
            <div className="flex flex-col gap-2 items-start flex-1 min-w-0 text-text-primary">
              {/* Conditional on the (unchecked-by-default) consent
                  checkbox above, not stated as a given — a pre-ticked
                  opt-in would be unlawful (UWG/DSGVO, see the ECJ's
                  Planet49 ruling on pre-checked consent), and this copy
                  shouldn't promise emails as if opting in were already
                  decided either. */}
              <p className="text-body-sm leading-[1.45]">
                Wenn du zustimmst, bekommst du nach deiner Bestellung regelmäßig Impulse &amp; Tipps per E-Mail.
              </p>
              <p className="text-body-sm leading-[1.45]">
                Für mehr Klarheit, Fokus und Struktur – jede Woche.
              </p>
              <div className="flex gap-1.5 items-center">
                <div className="h-[0.1875rem] w-5 bg-brand" />
                <div className="h-[0.1875rem] w-2.5 bg-brand" />
              </div>
            </div>
          </div>
        </Reveal>
      </form>

      <VersandModal
        open={versandOpen}
        onClose={() => setVersandOpen(false)}
        shipping={shippingSettings}
        shippingCost={shippingMethods[0]?.price ?? 0}
        freeShippingThreshold={lowestFreeShippingThreshold}
      />
    </>
  );
}
