"use client";

import { useSyncExternalStore } from "react";

const CART_KEY = "ep_cart";
const CART_EVENT = "ep-cart-updated";
const EMPTY_CART: CartItem[] = [];

// `variant` is the selected variant's name (products.variants[].name),
// undefined for a plain product with no variants. Two lines with the same
// `id` but different `variant` are separate cart entries, never merged —
// same "distinguish by the full key, not just id" reasoning as the
// server-side cart mirror (Customers.ts's cart array, which stores this
// same field as `variantName`).
export type CartItem = { id: string; qty: number; variant?: string };

function readCart(): CartItem[] {
  if (typeof window === "undefined") return [];
  try {
    const raw = window.localStorage.getItem(CART_KEY);
    const parsed = raw ? JSON.parse(raw) : [];
    return Array.isArray(parsed) ? parsed : [];
  } catch {
    return [];
  }
}

function writeCart(items: CartItem[]) {
  window.localStorage.setItem(CART_KEY, JSON.stringify(items));
  window.dispatchEvent(new Event(CART_EVENT));
}

export function getCartCount(): number {
  return readCart().reduce((sum, item) => sum + item.qty, 0);
}

// A line is identified by (id, variant) together, not id alone — the same
// product with two different variants selected are separate cart entries.
// `variant` undefined on both sides (the common, no-variants case) still
// matches by simple equality, so every existing call site that never
// passes a variant keeps working unchanged.
function sameLine(item: CartItem, id: string, variant: string | undefined): boolean {
  return item.id === id && item.variant === variant;
}

export function addToCart(id: string, qty = 1, variant?: string) {
  const items = readCart();
  const existing = items.find((i) => sameLine(i, id, variant));
  if (existing) existing.qty += qty;
  else items.push(variant ? { id, qty, variant } : { id, qty });
  writeCart(items);
}

export function removeFromCart(id: string, variant?: string) {
  writeCart(readCart().filter((i) => !sameLine(i, id, variant)));
}

// Called by /bestellbestaetigung once it has captured a snapshot of the
// cart to display — there's no real order backend here, so "placing an
// order" just means the local cart empties out the same way it would
// after a real purchase completes.
export function clearCart() {
  writeCart([]);
}

// qty <= 0 removes the item outright — the cart page's quantity stepper
// never lets the visible count go below 1, but this keeps the function
// itself safe to call with any integer without a separate remove path.
export function setQuantity(id: string, qty: number, variant?: string) {
  if (qty <= 0) {
    removeFromCart(id, variant);
    return;
  }
  const items = readCart();
  const existing = items.find((i) => sameLine(i, id, variant));
  if (existing) existing.qty = qty;
  writeCart(items);
}

// Cached-by-raw-string snapshot, not a fresh JSON.parse() every call —
// useSyncExternalStore (below) requires getSnapshot to return the SAME
// reference when the underlying data hasn't actually changed, or React
// treats every render as a change. readCart()'s plain JSON.parse would
// allocate a new array each call and break that.
let cachedRaw: string | null | undefined;
let cachedItems: CartItem[] = EMPTY_CART;

export function getCart(): CartItem[] {
  if (typeof window === "undefined") return EMPTY_CART;
  const raw = window.localStorage.getItem(CART_KEY);
  if (raw === cachedRaw) return cachedItems;
  cachedRaw = raw;
  try {
    const parsed = raw ? JSON.parse(raw) : EMPTY_CART;
    cachedItems = Array.isArray(parsed) ? parsed : EMPTY_CART;
  } catch {
    cachedItems = EMPTY_CART;
  }
  return cachedItems;
}

function subscribe(onStoreChange: () => void) {
  window.addEventListener(CART_EVENT, onStoreChange);
  window.addEventListener("storage", onStoreChange);
  return () => {
    window.removeEventListener(CART_EVENT, onStoreChange);
    window.removeEventListener("storage", onStoreChange);
  };
}

// useSyncExternalStore, not useState+useEffect — localStorage is an
// external store outside React, and the previous approach (setState
// synchronously inside an effect body) causes an extra render and trips
// the react-hooks/set-state-in-effect lint rule. This is React's own
// recommended pattern for subscribing to exactly this kind of external
// store, and is correctly SSR-safe via the third (server snapshot)
// argument — 0 / EMPTY_CART until the client hydrates and reads
// localStorage for real.
export function useCartCount(): number {
  return useSyncExternalStore(subscribe, getCartCount, () => 0);
}

export function useCart(): CartItem[] {
  return useSyncExternalStore(subscribe, getCart, () => EMPTY_CART);
}

// Called right after a successful login (LoginForm.tsx, CheckoutContent.tsx's
// inline login toggle) — folds whatever was saved server-side into the
// local cart by quantity (addToCart adds to an existing line rather than
// overwriting it), so items added as a guest before logging in aren't lost.
// This only stays correct because LogoutButton.tsx clears the local cart on
// logout — the local cart is always either empty (no guest additions since
// the last logout) or holds only genuinely new guest-session items, never a
// stale copy of what's already in the server cart, so this add never
// double-counts.
//
// Always pushes the resulting cart to the server itself at the end,
// explicitly — does NOT rely on CartSync's own change-triggered push.
// That was the bug (confirmed live 2026-08-01): logging in with an empty
// server cart but a non-empty guest cart means the merge loop below never
// actually runs (nothing in `data.cart` to add), so `writeCart` is never
// called, no change event fires, and CartSync's effect — which only
// re-fires when the cart *reference* actually changes — never pushes the
// guest cart to the server at all. The cart looked fine on the device
// that was already logged in, but never reached any other device. An
// explicit push here doesn't depend on anything having changed.
export async function mergeServerCartIntoLocal(): Promise<void> {
  try {
    const res = await fetch("/api/account/cart");
    if (res.ok) {
      const data: { cart?: CartItem[] } = await res.json();
      for (const item of data.cart ?? []) addToCart(item.id, item.qty, item.variant);
    }
  } catch {
    // Best-effort — a failed merge just means the server-side cart stays
    // as it was; nothing local is lost either way. Still fall through to
    // push below — the local (guest) cart is genuine either way.
  }
  try {
    await fetch("/api/account/cart", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ cart: readCart() }),
    });
  } catch {
    // Best-effort — CartSync's own push will retry on the next cart
    // change, or the next login-time merge tries again.
  }
}

// Cross-device sync for an *already* logged-in session — CartSync.tsx
// only ever pushes local→server, and mergeServerCartIntoLocal() above only
// ever runs at the moment of a fresh login/register, so a customer already
// signed in on device B never picked up a change made on device A. This
// covers that gap: called on mount and on window focus (CartSync.tsx), not
// just at login.
//
// Replaces the local cart outright with the server's — NOT an additive
// merge like mergeServerCartIntoLocal() above (that one is only safe
// right after login, when the local cart can't yet overlap the server
// one, see its own comment). An earlier version of this function only
// added/updated matching lines and never removed a local line the server
// no longer had, which meant a *removal* on device A never reached device
// B (confirmed live 2026-08-01 — adding synced, removing didn't). Server
// wins outright, including "server cart is now empty."
//
// Trade-off: a local addition made in the last few hundred ms — after
// this fetch went out but before CartSync's own 800ms debounce pushed it
// — could theoretically get overwritten by a pull that lands in between.
// Narrow enough (and self-healing on the next change/focus) to accept,
// consistent with every other "best-effort" sync path in this file.
export async function pullServerCart(): Promise<void> {
  try {
    const res = await fetch("/api/account/cart");
    if (!res.ok) return;
    const data: { cart?: CartItem[] } = await res.json();
    writeCart(data.cart ?? []);
  } catch {
    // Best-effort — a failed pull just means this device doesn't see
    // another device's changes yet; nothing local is lost either way.
  }
}
