import { formatPrice } from "./format";

const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://payload.mk360.de";
const TENANT_SLUG = "einfach-produktiv";

// Used only by the fetchers backing Live Preview (posts, legal pages,
// testimonials) — callers pass `draft: true` only while Draft Mode is
// enabled (a document open in the Payload admin's Live Preview iframe),
// bypassing the normal 60s ISR cache there without affecting ordinary
// visitors. Deliberately does NOT call next/headers' draftMode() itself
// here — this module's mapping functions/types (mapPayloadTestimonial,
// mapPayloadPost, etc.) are also imported by "use client" components
// (LiveTestimonialsGrid.tsx, LivePostContent.tsx), and next/headers is a
// server-only import that breaks the client bundle the moment anything
// in this file touches it, even indirectly.
function livePreviewCacheOption(draft: boolean): { cache: "no-store" } | { next: { revalidate: 60 } } {
  return draft ? { cache: "no-store" } : { next: { revalidate: 60 } };
}

export type BlogPost = {
  id: number;
  title: string;
  slug: string;
  category: string;
  readTime: number;
  excerpt: string;
  thumbnail: string | null;
  publishedAt: string;
  featured: boolean;
};

type PayloadPost = {
  id: number;
  title: string;
  slug: string;
  category: { name: string } | number | null;
  readTime: number;
  excerpt: string;
  thumbnail: { url: string } | number | null;
  publishedAt: string;
  featured: boolean;
};

// sort: "-featured,-publishedAt" — Payload's multi-field sort puts any
// featured post(s) first, most-recently-published first among those, then
// the rest by date. Callers that just want posts[0] as "the" featured post
// (e.g. /blog) get a deterministic pick even if more than one post was
// accidentally marked featured — no special-casing needed here.
export async function getBlogPosts(limit = 3): Promise<BlogPost[]> {
  const params = new URLSearchParams({
    "where[tenant.slug][equals]": TENANT_SLUG,
    sort: "-featured,-publishedAt",
    depth: "2",
    limit: String(limit),
  });

  const res = await fetch(`${PAYLOAD_URL}/api/posts?${params}`, {
    next: { revalidate: 60 },
  });
  if (!res.ok) {
    console.error(`getBlogPosts: Payload returned ${res.status} ${res.statusText}`);
    return [];
  }

  const data: { docs?: PayloadPost[] } = await res.json();
  const docs = Array.isArray(data.docs) ? data.docs : [];
  return docs.map((post) => ({
    id: post.id,
    title: post.title,
    slug: post.slug,
    category:
      typeof post.category === "object" && post.category
        ? post.category.name
        : "",
    readTime: post.readTime,
    excerpt: post.excerpt,
    thumbnail:
      typeof post.thumbnail === "object" && post.thumbnail
        ? post.thumbnail.url
        : null,
    publishedAt: post.publishedAt,
    featured: post.featured,
  }));
}

export type PostDetail = BlogPost & {
  content: unknown;
  /** Label for every blockquote callout in this post's content (icon +
   * underline included) — empty string hides that left side entirely,
   * the blockquote itself still renders. See RichText.tsx's "quote" case. */
  quoteLabel: string;
  /** Powers the "Passend dazu" card at the end of the post — null hides
   * the card entirely, per-post choice (unlike Products.spotlight, which
   * is a single site-wide flag). */
  relatedProduct: Product | null;
};

export type PayloadPostDetail = PayloadPost & {
  content: unknown;
  quoteLabel: string | null;
  relatedProduct: PayloadProduct | null;
};

// Shared by getPostBySlug() and LivePostContent.tsx (which re-maps the raw
// document useLivePreview receives via postMessage using this same logic,
// instead of duplicating the field mapping a second time).
export function mapPayloadPost(doc: PayloadPostDetail): PostDetail {
  return {
    id: doc.id,
    title: doc.title,
    slug: doc.slug,
    category:
      typeof doc.category === "object" && doc.category ? doc.category.name : "",
    readTime: doc.readTime,
    excerpt: doc.excerpt,
    thumbnail:
      typeof doc.thumbnail === "object" && doc.thumbnail ? doc.thumbnail.url : null,
    content: doc.content,
    publishedAt: doc.publishedAt,
    featured: doc.featured,
    quoteLabel: doc.quoteLabel ?? "",
    relatedProduct: doc.relatedProduct ? mapPayloadProduct(doc.relatedProduct) : null,
  };
}

export async function getPostBySlug(slug: string, options?: { draft?: boolean }): Promise<PostDetail | null> {
  const params = new URLSearchParams({
    "where[tenant.slug][equals]": TENANT_SLUG,
    "where[slug][equals]": slug,
    depth: "2",
    limit: "1",
  });

  const res = await fetch(`${PAYLOAD_URL}/api/posts?${params}`, livePreviewCacheOption(Boolean(options?.draft)));
  if (!res.ok) {
    console.error(`getPostBySlug: Payload returned ${res.status} ${res.statusText}`);
    return null;
  }

  const data: { docs?: PayloadPostDetail[] } = await res.json();
  const doc = data.docs?.[0];
  if (!doc) return null;
  return mapPayloadPost(doc);
}

// Frontend-facing shape — `id` is Payload's `slug` field, not its numeric
// row id. Cart items are stored in localStorage keyed by this string (see
// lib/cart.ts), so slugs were chosen in the Products collection to match
// the ids the old hardcoded catalog used ("todo-karten" etc.) — switching
// to numeric ids here would silently orphan every existing shopper's cart.
export type Product = {
  id: string;
  name: string;
  description: string;
  price: number;
  compareAtPrice: number | null;
  image: string;
  href: string | null;
  // `active` is opt-in for callers to filter by, not applied inside
  // getProducts()/getProductBySlug() themselves — cart, checkout, order
  // confirmation, and already-linked product detail pages (e.g.
  // TodoKartenHero/Pricing calling getProductBySlug directly) all need to
  // keep resolving a product regardless of its active status, unlike the
  // shop grid / spotlight / related-products discovery surfaces, which
  // filter `.filter(p => p.active)` themselves.
  active: boolean;
  updatedAt: string;
  spotlight: boolean;
  spotlightEyebrow: string | null;
  spotlightHeadline: string | null;
  spotlightText: string | null;
  spotlightImage: string | null;
};

type PayloadProduct = {
  id: number;
  name: string;
  slug: string;
  description: string | null;
  price: number;
  compareAtPrice: number | null;
  image: { url: string } | number | null;
  detailHref: string | null;
  active: boolean;
  updatedAt: string;
  spotlight: boolean;
  spotlightEyebrow: string | null;
  spotlightHeadline: string | null;
  spotlightText: string | null;
  spotlightImage: { url: string } | number | null;
};

// Shared by getProducts() and getPostBySlug()'s relatedProduct — kept in
// one place instead of duplicating the same field mapping, which is
// exactly the kind of drift this session's Shipping Settings work was
// about eliminating elsewhere.
export function mapPayloadProduct(product: PayloadProduct): Product {
  return {
    id: product.slug,
    name: product.name,
    description: product.description ?? "",
    price: product.price,
    compareAtPrice: product.compareAtPrice ?? null,
    image: typeof product.image === "object" && product.image ? product.image.url : "",
    href: product.detailHref || null,
    active: product.active,
    updatedAt: product.updatedAt,
    spotlight: product.spotlight,
    spotlightEyebrow: product.spotlightEyebrow || null,
    spotlightHeadline: product.spotlightHeadline || null,
    spotlightText: product.spotlightText || null,
    spotlightImage:
      typeof product.spotlightImage === "object" && product.spotlightImage ? product.spotlightImage.url : null,
  };
}

export async function getProducts(): Promise<Product[]> {
  const params = new URLSearchParams({
    "where[tenant.slug][equals]": TENANT_SLUG,
    sort: "sortOrder",
    depth: "2",
    limit: "100",
  });

  const res = await fetch(`${PAYLOAD_URL}/api/products?${params}`, {
    next: { revalidate: 60 },
  });
  if (!res.ok) {
    console.error(`getProducts: Payload returned ${res.status} ${res.statusText}`);
    return [];
  }

  const data: { docs?: PayloadProduct[] } = await res.json();
  const docs = Array.isArray(data.docs) ? data.docs : [];
  return docs.map(mapPayloadProduct);
}

export async function getProductBySlug(slug: string): Promise<Product | null> {
  const products = await getProducts();
  return products.find((p) => p.id === slug) ?? null;
}

// Derived from getProducts() (same 60s-ISR-cached fetch every other
// discovery surface already uses) instead of its own separate Payload
// query — also what lets the auto-spotlight rule below just be a plain
// array check instead of a second round-trip.
//
// Auto-spotlight: with exactly 1 active product, that product IS the
// spotlight, full stop — overriding any `spotlight` flag set on some
// other (inactive) product. Confirmed product decision, not just a
// no-manual-flag fallback. Otherwise, same deterministic tie-break as
// before (most-recently-updated wins) among active products actually
// flagged `spotlight`.
export async function getSpotlightProduct(): Promise<Product | null> {
  const products = await getProducts();
  const active = products.filter((p) => p.active);

  if (active.length === 1) return active[0];

  const flagged = active.filter((p) => p.spotlight);
  if (flagged.length === 0) return null;
  return flagged.reduce((latest, p) => (p.updatedAt > latest.updatedAt ? p : latest));
}

export type TrustBadge = { id: number; title: string; description: string; icon: string };

type PayloadTrustBadge = { id: number; title: string; description: string; icon: { url: string } | number | null };

async function fetchTrustBadgeList(collection: "trust-badges" | "cart-trust-badges"): Promise<TrustBadge[]> {
  const params = new URLSearchParams({
    "where[tenant.slug][equals]": TENANT_SLUG,
    sort: "sortOrder",
    depth: "1",
    limit: "50",
  });

  const res = await fetch(`${PAYLOAD_URL}/api/${collection}?${params}`, {
    next: { revalidate: 60 },
  });
  if (!res.ok) {
    console.error(`fetchTrustBadgeList(${collection}): Payload returned ${res.status} ${res.statusText}`);
    return [];
  }

  const data: { docs?: PayloadTrustBadge[] } = await res.json();
  const docs = Array.isArray(data.docs) ? data.docs : [];
  return docs.map((doc) => ({
    id: doc.id,
    title: doc.title,
    description: doc.description,
    icon: typeof doc.icon === "object" && doc.icon ? doc.icon.url : "",
  }));
}

// Substitutes literal "{{lieferzeit}}" / "{{kostenfreiab}}" tokens (e.g. in
// a badge's "In {{lieferzeit}} bei dir." or "Ab {{kostenfreiab}}
// Bestellwert innerhalb DE." copy) with the live delivery-time range /
// free-shipping threshold — lets an editor reference these numbers from
// free text without duplicating and hand-maintaining them, which is
// exactly how the "Schneller Versand" and "Versandkostenfrei" badges used
// to drift from the real numbers whenever those changed elsewhere but not
// here too. The threshold comes from the lowest freeShippingThreshold
// among active ShippingMethods (same rule /cart's own banner uses), not
// lib/shipping.ts's separate FREE_SHIPPING_THRESHOLD constant — that
// constant is a third, independent copy of the same fact and not
// necessarily what actually governs checkout.
function resolveShippingTokens(text: string, shipping: ShippingSettings, freeShippingThreshold: number | null): string {
  let result = text.replaceAll("{{lieferzeit}}", `${shipping.totalDays.min}–${shipping.totalDays.max} Werktagen`);
  if (freeShippingThreshold !== null) {
    result = result.replaceAll("{{kostenfreiab}}", formatPrice(freeShippingThreshold));
  }
  return result;
}

// Powers TrustRow.tsx — the horizontal "Schneller Versand /
// Versandkostenfrei / Mit Liebe verpackt" row.
export async function getTrustBadges(): Promise<TrustBadge[]> {
  const [badges, shipping, shippingMethods] = await Promise.all([
    fetchTrustBadgeList("trust-badges"),
    getShippingSettings(),
    getShippingMethods(),
  ]);
  const thresholds = shippingMethods
    .map((m) => m.freeShippingThreshold)
    .filter((t): t is number => t !== null);
  const freeShippingThreshold = thresholds.length > 0 ? Math.min(...thresholds) : null;

  return badges.map((b) => ({
    ...b,
    title: resolveShippingTokens(b.title, shipping, freeShippingThreshold),
    description: resolveShippingTokens(b.description, shipping, freeShippingThreshold),
  }));
}

// Powers the "Sichere Zahlung / 14 Tage Rückgaberecht / Nachhaltig
// verpackt" sidebar bullets on /cart (title only) and /checkout
// (title + description) — a different list from TrustBadges.
export async function getCartTrustBadges(): Promise<TrustBadge[]> {
  return fetchTrustBadgeList("cart-trust-badges");
}

export type ShippingMethod = {
  id: number;
  title: string;
  description: string;
  price: number;
  freeShippingThreshold: number | null;
};

type PayloadShippingMethod = ShippingMethod & { active: boolean };

export async function getShippingMethods(): Promise<ShippingMethod[]> {
  const params = new URLSearchParams({
    "where[tenant.slug][equals]": TENANT_SLUG,
    "where[active][equals]": "true",
    sort: "sortOrder",
    limit: "20",
  });

  const res = await fetch(`${PAYLOAD_URL}/api/shipping-methods?${params}`, {
    next: { revalidate: 60 },
  });
  if (!res.ok) {
    console.error(`getShippingMethods: Payload returned ${res.status} ${res.statusText}`);
    return [];
  }

  const data: { docs?: PayloadShippingMethod[] } = await res.json();
  const docs = Array.isArray(data.docs) ? data.docs : [];
  return docs.map((doc) => ({
    id: doc.id,
    title: doc.title,
    description: doc.description,
    price: doc.price,
    freeShippingThreshold: doc.freeShippingThreshold ?? null,
  }));
}

export type ShippingSettings = {
  handlingDays: { min: number; max: number };
  transitDays: { min: number; max: number };
  /** Derived here, not stored in Payload — a third independently-editable
   * copy of the same fact is exactly the drift this collection replaces. */
  totalDays: { min: number; max: number };
};

type PayloadShippingSettings = {
  handlingDaysMin: number;
  handlingDaysMax: number;
  transitDaysMin: number;
  transitDaysMax: number;
};

// Falls back to the site's long-standing real-world numbers (1–2 handling,
// 2–4 transit) if Payload has no row yet or the fetch fails — same values
// the old hardcoded HANDLING_DAYS/TRANSIT_DAYS_DE constants used, so
// nothing regresses before this collection gets seeded/edited.
const SHIPPING_SETTINGS_FALLBACK: ShippingSettings = {
  handlingDays: { min: 1, max: 2 },
  transitDays: { min: 2, max: 4 },
  totalDays: { min: 3, max: 6 },
};

export async function getShippingSettings(): Promise<ShippingSettings> {
  const params = new URLSearchParams({
    "where[tenant.slug][equals]": TENANT_SLUG,
    limit: "1",
  });

  const res = await fetch(`${PAYLOAD_URL}/api/shipping-settings?${params}`, {
    next: { revalidate: 60 },
  });
  if (!res.ok) {
    console.error(`getShippingSettings: Payload returned ${res.status} ${res.statusText}`);
    return SHIPPING_SETTINGS_FALLBACK;
  }

  const data: { docs?: PayloadShippingSettings[] } = await res.json();
  const doc = Array.isArray(data.docs) ? data.docs[0] : undefined;
  if (!doc) return SHIPPING_SETTINGS_FALLBACK;

  const handlingDays = { min: doc.handlingDaysMin, max: doc.handlingDaysMax };
  const transitDays = { min: doc.transitDaysMin, max: doc.transitDaysMax };
  return {
    handlingDays,
    transitDays,
    totalDays: { min: handlingDays.min + transitDays.min, max: handlingDays.max + transitDays.max },
  };
}

export type PaymentMethod = { id: number; title: string; icons: string[] };

type PayloadPaymentMethod = {
  id: number;
  title: string;
  active: boolean;
  icons: { icon: { url: string } | number | null }[];
};

export async function getPaymentMethods(): Promise<PaymentMethod[]> {
  const params = new URLSearchParams({
    "where[tenant.slug][equals]": TENANT_SLUG,
    "where[active][equals]": "true",
    sort: "sortOrder",
    depth: "1",
    limit: "20",
  });

  const res = await fetch(`${PAYLOAD_URL}/api/payment-methods?${params}`, {
    next: { revalidate: 60 },
  });
  if (!res.ok) {
    console.error(`getPaymentMethods: Payload returned ${res.status} ${res.statusText}`);
    return [];
  }

  const data: { docs?: PayloadPaymentMethod[] } = await res.json();
  const docs = Array.isArray(data.docs) ? data.docs : [];
  return docs.map((doc) => ({
    id: doc.id,
    title: doc.title,
    icons: (doc.icons ?? [])
      .map((row) => (typeof row.icon === "object" && row.icon ? row.icon.url : null))
      .filter((url): url is string => Boolean(url)),
  }));
}

export type WerkzeugeCard = {
  id: number;
  title: string;
  description: string;
  icon: string;
  ctaLabel: string;
  ctaHref: string;
};

type PayloadWerkzeugeCard = {
  id: number;
  title: string;
  description: string;
  icon: { url: string } | number | null;
  ctaLabel: string;
  ctaHref: string;
};

export async function getWerkzeugeCards(): Promise<WerkzeugeCard[]> {
  const params = new URLSearchParams({
    "where[tenant.slug][equals]": TENANT_SLUG,
    sort: "sortOrder",
    depth: "1",
    limit: "20",
  });

  const res = await fetch(`${PAYLOAD_URL}/api/werkzeuge-cards?${params}`, {
    next: { revalidate: 60 },
  });
  if (!res.ok) {
    console.error(`getWerkzeugeCards: Payload returned ${res.status} ${res.statusText}`);
    return [];
  }

  const data: { docs?: PayloadWerkzeugeCard[] } = await res.json();
  const docs = Array.isArray(data.docs) ? data.docs : [];
  return docs.map((doc) => ({
    id: doc.id,
    title: doc.title,
    description: doc.description,
    icon: typeof doc.icon === "object" && doc.icon ? doc.icon.url : "",
    ctaLabel: doc.ctaLabel,
    ctaHref: doc.ctaHref,
  }));
}

export type TestimonialsPage = "todo-cards" | "newsletter" | "challenge";

export type Testimonial = { id: number; quote: string; name: string; role: string; avatar: string };

export type PayloadTestimonial = {
  id: number;
  quote: string;
  name: string;
  role: string | null;
  avatar: { url: string } | number | null;
};

// Shared by getTestimonials() and LiveTestimonialsGrid.tsx (re-maps the raw
// document useLivePreview receives via postMessage using this same logic).
export function mapPayloadTestimonial(doc: PayloadTestimonial): Testimonial {
  return {
    id: doc.id,
    quote: doc.quote,
    name: doc.name,
    role: doc.role ?? "",
    avatar: typeof doc.avatar === "object" && doc.avatar ? doc.avatar.url : "",
  };
}

// Powers the identically-styled customer testimonial grids on /todo-cards,
// /newsletter, and /challenge (see TestimonialsGrid.tsx) — previously 3
// hardcoded arrays kept manually in sync. The single-quote "photo band"
// testimonials on /not-found and /bestellbestaetigung are a different
// shape (no avatar/role) and are not part of this collection.
export async function getTestimonials(page: TestimonialsPage, options?: { draft?: boolean }): Promise<Testimonial[]> {
  const params = new URLSearchParams({
    "where[tenant.slug][equals]": TENANT_SLUG,
    "where[page][equals]": page,
    sort: "sortOrder",
    depth: "1",
    limit: "20",
  });

  const res = await fetch(`${PAYLOAD_URL}/api/testimonials?${params}`, livePreviewCacheOption(Boolean(options?.draft)));
  if (!res.ok) {
    console.error(`getTestimonials: Payload returned ${res.status} ${res.statusText}`);
    return [];
  }

  const data: { docs?: PayloadTestimonial[] } = await res.json();
  const docs = Array.isArray(data.docs) ? data.docs : [];
  return docs.map(mapPayloadTestimonial);
}

export type LegalPageType = "impressum" | "datenschutz" | "agb" | "widerruf";

export type LegalPage = {
  type: LegalPageType;
  title: string;
  content: unknown;
  attachment: { url: string; title: string } | null;
};

type PayloadLegalPage = {
  type: LegalPageType;
  title: string;
  content: unknown;
  attachment: { url: string; title: string } | number | null;
};

export async function getLegalPage(type: LegalPageType, options?: { draft?: boolean }): Promise<LegalPage | null> {
  const params = new URLSearchParams({
    "where[tenant.slug][equals]": TENANT_SLUG,
    "where[type][equals]": type,
    depth: "1",
    limit: "1",
  });

  const res = await fetch(`${PAYLOAD_URL}/api/legal-pages?${params}`, livePreviewCacheOption(Boolean(options?.draft)));
  if (!res.ok) {
    console.error(`getLegalPage: Payload returned ${res.status} ${res.statusText}`);
    return null;
  }

  const data: { docs?: PayloadLegalPage[] } = await res.json();
  const doc = data.docs?.[0];
  if (!doc) return null;
  return {
    type: doc.type,
    title: doc.title,
    content: doc.content,
    attachment:
      typeof doc.attachment === "object" && doc.attachment
        ? { url: doc.attachment.url, title: doc.attachment.title }
        : null,
  };
}

export type EmailTemplateType =
  | "order-confirmation"
  | "password-reset"
  | "order-shipped"
  | "order-cancelled"
  | "order-return-requested"
  | "order-returned";

type PayloadEmailTemplate = {
  type: EmailTemplateType;
  subject: string;
  heading: string;
  bodyText: string;
  footerText: string | null;
};

// draft:true is used by app/email-preview/[type]/page.tsx (Live Preview,
// see EmailTemplates.ts in the Payload repo); the real send (orderEmail.ts,
// Customers.ts's forgotPassword hook) always reads the published version.
export async function getEmailTemplate(
  type: EmailTemplateType,
  options?: { draft?: boolean },
): Promise<PayloadEmailTemplate | null> {
  const params = new URLSearchParams({
    "where[tenant.slug][equals]": TENANT_SLUG,
    "where[type][equals]": type,
    limit: "1",
  });

  const res = await fetch(`${PAYLOAD_URL}/api/email-templates?${params}`, livePreviewCacheOption(Boolean(options?.draft)));
  if (!res.ok) {
    console.error(`getEmailTemplate: Payload returned ${res.status} ${res.statusText}`);
    return null;
  }

  const data: { docs?: PayloadEmailTemplate[] } = await res.json();
  return data.docs?.[0] ?? null;
}

export type CompanySettings = {
  sellerName: string;
  sellerStreet: string;
  sellerZip: string;
  sellerCity: string;
  sellerCountry: string;
  sellerEmail: string;
  vatId: string;
  taxRatePercent: number;
  bankDetails: string | null;
};

// Server-only in practice (only ever called from app/lib/invoiceData.ts),
// but kept in this file rather than a "use server"-only module since every
// other Payload fetcher lives here too — no client component imports it.
// company-settings' read access is admin-only plus this same service
// secret (see CompanySettings.ts) — it holds bank details, not something
// to leave publicly readable like Products/ShippingSettings.
export async function getCompanySettings(): Promise<CompanySettings | null> {
  const params = new URLSearchParams({ "where[tenant.slug][equals]": TENANT_SLUG, limit: "1" });
  const res = await fetch(`${PAYLOAD_URL}/api/company-settings?${params}`, {
    headers: { "x-order-service-secret": process.env.ORDER_SERVICE_SECRET || "" },
    cache: "no-store",
  });
  if (!res.ok) {
    console.error(`getCompanySettings: Payload returned ${res.status} ${res.statusText}`);
    return null;
  }
  const data: { docs?: CompanySettings[] } = await res.json();
  return data.docs?.[0] ?? null;
}
