import Image from "next/image";
import Link from "next/link";
import { RichText as LexicalRichText, type JSXConvertersFunction } from "@payloadcms/richtext-lexical/react";
import type { TOCSection } from "./SectionTOC";
import { QuoteLabel } from "./QuoteLabel";
import { StepArrow } from "./StepArrow";
import { STEP_ICONS, SYMBOLIC_ICONS } from "./icons/StepIcons";

// Switched 2026-07-24 from a small hand-rolled Lexical JSON->JSX walker to
// Payload's own official React renderer + custom JSXConverters — needed
// once Posts.content gained custom Lexical Blocks (Bild/Bildergalerie/
// Video/Zitat, see payload/src/collections/Posts.ts), which the old
// hand-rolled switch had no case for at all. extractHeadings()/headingId()
// below are kept as an independent, minimal walk over the raw JSON (same
// as before) — they only ever need to find h2 headings for SectionTOC and
// never touch Blocks, no reason to route that through the new renderer too.

type LexicalNode = {
  type: string;
  children?: LexicalNode[];
  text?: string;
  tag?: string;
};

function plainText(node: LexicalNode): string {
  if (node.type === "text") return node.text ?? "";
  return (node.children ?? []).map(plainText).join("");
}

// Numbered legal-page headings ("1. Verantwortlicher") get a stable
// "section-1" id from the leading number — immune to copy edits changing
// the heading text later, unlike a text-derived slug. Anything else
// (headings with no leading number) falls back to a plain slugify.
// Exported — the Impressum page renders some of its own headings outside
// this CMS-driven richText (the "Angaben zum Anbieter"/"Umsatzsteuer"/
// "Verantwortlich für den Inhalt" sections come straight from
// company-settings, not the richText field, see app/impressum/page.tsx)
// and needs the exact same id-assignment logic so its SectionTOC entries
// actually match the ids those headings render with.
export function headingId(text: string): string {
  const numbered = text.match(/^(\d+)\./);
  if (numbered) return `section-${numbered[1]}`;
  return text
    .toLowerCase()
    .replace(/[^a-z0-9äöüß]+/g, "-")
    .replace(/^-+|-+$/g, "");
}

// Walks the same tree the renderer does, collecting h2 headings for a
// SectionTOC sidebar — kept in this file (not duplicated) so the ids it
// produces can never drift from the ones the renderer actually assigns.
export function extractHeadings(content: unknown): TOCSection[] {
  const root = (content as { root?: LexicalNode })?.root;
  if (!root?.children) return [];
  const headings: TOCSection[] = [];
  const walk = (nodes: LexicalNode[]) => {
    for (const node of nodes) {
      if (node.type === "heading" && (node.tag ?? "h2") === "h2") {
        const text = plainText(node);
        headings.push({ id: headingId(text), title: text });
      }
      if (node.children) walk(node.children);
    }
  };
  walk(root.children);
  return headings;
}

// Payload upload relations resolve to the full media doc when fetched at
// sufficient depth (every richText-consuming fetch in app/lib/payload.ts
// already uses depth >= 2), or fall back to a bare id if not — only
// render when actually populated.
type MediaRef = { url?: string | null } | number | null | undefined;
function mediaUrl(ref: MediaRef): string | null {
  if (ref && typeof ref === "object" && typeof ref.url === "string") return ref.url;
  return null;
}

// Naive YouTube/Vimeo URL -> embed URL. Not exhaustive (no playlist/short-
// link edge cases) — good enough for a "paste a link" editor field; a
// URL that doesn't match either pattern just doesn't render rather than
// guessing wrong.
function toEmbedUrl(url: string): string | null {
  const youtube = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([\w-]{6,})/);
  if (youtube) return `https://www.youtube.com/embed/${youtube[1]}`;
  const vimeo = url.match(/vimeo\.com\/(\d+)/);
  if (vimeo) return `https://player.vimeo.com/video/${vimeo[1]}`;
  return null;
}

type ImageBlockFields = { image: MediaRef; caption?: string | null };
type ImageGalleryBlockFields = { images: { image: MediaRef; caption?: string | null }[] };
type VideoEmbedBlockFields = { url: string; caption?: string | null };
type QuoteBlockFields = { text: string; label?: string | null };
type IconBlockFields = { icon: string };
type PillListBlockFields = { items: { id?: string | null; label: string }[] };
type ChecklistImageBlockFields = { image: MediaRef; items: { id?: string | null; text: string }[] };
type TableBlockFields = { labelHeader: string; valueHeader: string; rows: { id?: string | null; label: string; value: string }[] };
type StepRowBlockFields = {
  items: { id?: string | null; icon: string; title: string; subtitle?: string | null; description: string }[];
};
type CtaCardBlockFields = { eyebrow: string; title: string; description?: string | null; href: string };

function BlockCaption({ caption }: { caption?: string | null }) {
  if (!caption) return null;
  return <p className="text-body-sm text-text-muted text-center">{caption}</p>;
}

// Same checkmark as PageBlocks.tsx's own — see that file's comment.
function CheckIcon() {
  return (
    <svg width="18" height="18" viewBox="0 0 18 18" fill="none" className="shrink-0 mt-1">
      <path d="M3 9.5l4 4L15 4" stroke="#f6a701" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

// Same visual treatment as the QuoteBlock converter below (and the native
// blockquote case it replaces going forward) — see that converter's own
// comment for why both still exist.
function Quote({ label, children }: { label?: string; children: React.ReactNode }) {
  return (
    <div className="relative flex items-start gap-6 w-full my-6">
      {/* Label/icon/underline are optional — if empty, only the divider +
          quote text render. The quote itself is never optional, just this
          framing around it. */}
      {label && <QuoteLabel label={label} />}
      <div className="w-px self-stretch bg-brand shrink-0" />
      <p
        className="text-text-primary text-[1.75rem] leading-[1.1] flex-1"
        style={{ fontFamily: "var(--font-caveat)" }}
      >
        {children}
      </p>
    </div>
  );
}

// A factory, not a module-level constant — needs to close over each
// call's own `quoteLabel` (the native "quote" converter reads it). Server
// Components can render multiple posts concurrently in the same process,
// so a shared module-level variable set right before rendering would be
// a real race condition, not just a style choice.
function buildConverters(quoteLabel: string): JSXConvertersFunction {
  return ({ defaultConverters }) => ({
    ...defaultConverters,
    paragraph: ({ node, nodesToJSX }) => (
      <p className="text-body text-text-body">{nodesToJSX({ nodes: node.children })}</p>
    ),
    heading: ({ node, nodesToJSX }) => {
      const Tag = node.tag as "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
      const text = plainText(node as unknown as LexicalNode);
      return (
        <Tag
          id={Tag === "h2" ? headingId(text) : undefined}
          className="font-semibold text-h-small text-text-primary mt-2 scroll-mt-32 first:mt-0"
          style={{ fontFamily: "var(--font-lora)" }}
        >
          {nodesToJSX({ nodes: node.children })}
          <span className="block h-[0.125rem] w-8 bg-brand mt-2" aria-hidden />
        </Tag>
      );
    },
    list: ({ node, nodesToJSX }) => {
      const ListTag = node.listType === "number" ? "ol" : "ul";
      return (
        <ListTag
          className={
            "flex flex-col gap-2 text-body text-text-body " +
            (node.listType === "number" ? "list-decimal pl-5" : "list-disc pl-5")
          }
        >
          {nodesToJSX({ nodes: node.children })}
        </ListTag>
      );
    },
    listitem: ({ node, nodesToJSX }) => <li>{nodesToJSX({ nodes: node.children })}</li>,
    link: ({ node, nodesToJSX }) => (
      <a
        href={node.fields?.url ?? "#"}
        className="text-brand hover:underline"
        {...(node.fields?.newTab ? { target: "_blank", rel: "noopener noreferrer" } : {})}
      >
        {nodesToJSX({ nodes: node.children })}
      </a>
    ),
    // Lexical auto-detects a typed-out URL/email as its own "autolink" node,
    // distinct from an editor-inserted "link" node — falls through to
    // Payload's unstyled default converter without this, which is why the
    // Impressum/AGB's typed-in-place mailto addresses rendered as plain
    // black text instead of matching every editor-inserted link.
    autolink: ({ node, nodesToJSX }) => (
      <a href={node.fields?.url ?? "#"} className="text-brand hover:underline">
        {nodesToJSX({ nodes: node.children })}
      </a>
    ),
    // Lexical's native blockquote feature — used by every post written
    // before Blocks existed. Kept working exactly as before (own comment on
    // Posts.ts's `content` field editor config on why this stays enabled
    // alongside the new QuoteBlock) rather than migrating old content.
    quote: ({ node, nodesToJSX }) => (
      <Quote label={quoteLabel}>{nodesToJSX({ nodes: node.children })}</Quote>
    ),
    blocks: {
      image: ({ node }: { node: { fields: unknown } }) => {
        const fields = node.fields as ImageBlockFields;
        const url = mediaUrl(fields.image);
        if (!url) return null;
        return (
          <div className="flex flex-col gap-2 w-full">
            <div className="relative w-full aspect-[3/2] rounded-md overflow-hidden bg-bg-muted">
              <Image alt="" src={url} fill sizes="(min-width: 768px) 48rem, 100vw" className="object-cover" />
            </div>
            <BlockCaption caption={fields.caption} />
          </div>
        );
      },
      imageGallery: ({ node }: { node: { fields: unknown } }) => {
        const fields = node.fields as ImageGalleryBlockFields;
        const images = (fields.images ?? []).filter((row) => mediaUrl(row.image));
        if (images.length === 0) return null;
        return (
          <div className="grid grid-cols-2 gap-4 w-full">
            {images.map((row, i) => (
              <div key={i} className="flex flex-col gap-2">
                <div className="relative aspect-[4/3] rounded-md overflow-hidden bg-bg-muted">
                  <Image
                    alt=""
                    src={mediaUrl(row.image)!}
                    fill
                    sizes="(min-width: 768px) 24rem, 50vw"
                    className="object-cover"
                  />
                </div>
                <BlockCaption caption={row.caption} />
              </div>
            ))}
          </div>
        );
      },
      videoEmbed: ({ node }: { node: { fields: unknown } }) => {
        const fields = node.fields as VideoEmbedBlockFields;
        const embedUrl = toEmbedUrl(fields.url);
        if (!embedUrl) return null;
        return (
          <div className="flex flex-col gap-2 w-full">
            <div className="relative w-full aspect-video rounded-md overflow-hidden bg-bg-muted">
              <iframe
                src={embedUrl}
                title={fields.caption ?? "Video"}
                className="absolute inset-0 h-full w-full"
                allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
                allowFullScreen
              />
            </div>
            <BlockCaption caption={fields.caption} />
          </div>
        );
      },
      // Per-quote label, unlike Posts.quoteLabel above (one label shared by
      // every native blockquote in the post) — new quotes going forward use
      // this instead of the native blockquote feature.
      quote: ({ node }: { node: { fields: unknown } }) => {
        const fields = node.fields as QuoteBlockFields;
        const lines = fields.text.split("\n");
        return (
          <Quote label={fields.label ?? undefined}>
            {lines.map((line, i) => (
              <span key={i}>
                {line}
                {i < lines.length - 1 && <br />}
              </span>
            ))}
          </Quote>
        );
      },
      // Same page-builder blocks as PageBlocks.tsx (Pages.layout) — see
      // that file's own comment on each block's visual treatment, mirrored
      // here field-for-field since it's the exact same Block config
      // registered a second time (Posts.content's BlocksFeature) so blog
      // posts can use them mid-article too. `testimonialsRef` is the one
      // page-builder block deliberately NOT included: it needs an async
      // data fetch, and this converter set is shared with LiveRichText's
      // client-side live-preview rendering (see blog's LivePostContent.tsx),
      // where an async component would break — PageBlocks.tsx only gets
      // away with it by awaiting outside the sync per-block switch, in its
      // own async server component.
      icon: ({ node }: { node: { fields: unknown } }) => {
        const fields = node.fields as IconBlockFields;
        const SymbolIcon = SYMBOLIC_ICONS[fields.icon];
        return SymbolIcon ? <div>{SymbolIcon()}</div> : null;
      },
      pillList: ({ node }: { node: { fields: unknown } }) => {
        const fields = node.fields as PillListBlockFields;
        return (
          <div className="flex flex-wrap gap-2">
            {fields.items.map((item, i) => (
              <span key={item.id ?? i} className="text-body-sm text-text-muted bg-bg-muted rounded-full px-3 py-1">
                {item.label}
              </span>
            ))}
          </div>
        );
      },
      checklistImage: ({ node }: { node: { fields: unknown } }) => {
        const fields = node.fields as ChecklistImageBlockFields;
        const url = mediaUrl(fields.image);
        return (
          <div className="flex flex-col lg:flex-row gap-8 lg:gap-10 items-center w-full">
            {url && (
              <div className="relative w-full lg:w-[44%] lg:shrink-0 rounded-xl overflow-hidden bg-bg-muted" style={{ minHeight: "16rem" }}>
                <Image alt="" src={url} fill sizes="(min-width: 1024px) 44vw, 100vw" className="object-cover" />
              </div>
            )}
            <ul className="flex-1 w-full flex flex-col gap-4">
              {fields.items.map((item, i) => (
                <li key={item.id ?? i} className="flex items-start gap-3">
                  <CheckIcon />
                  <p className="text-body text-text-body">{item.text}</p>
                </li>
              ))}
            </ul>
          </div>
        );
      },
      table: ({ node }: { node: { fields: unknown } }) => {
        const fields = node.fields as TableBlockFields;
        return (
          <div className="bg-bg-muted rounded-xl overflow-hidden w-full">
            <table className="w-full text-left border-collapse">
              <thead>
                <tr className="border-b-2 border-brand">
                  <th className="py-3 pl-6 pr-4 font-semibold text-body-sm text-text-primary uppercase tracking-wide">{fields.labelHeader}</th>
                  <th className="py-3 pr-6 font-semibold text-body-sm text-text-primary uppercase tracking-wide">{fields.valueHeader}</th>
                </tr>
              </thead>
              <tbody>
                {fields.rows.map((row, i) => (
                  <tr key={row.id ?? i} className={i % 2 === 1 ? "bg-bg-base/60" : undefined}>
                    <td className="py-2.5 pl-6 pr-4 text-body text-text-body">{row.label}</td>
                    <td className="py-2.5 pr-6 font-semibold text-body text-text-primary">{row.value}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        );
      },
      stepRow: ({ node }: { node: { fields: unknown } }) => {
        const fields = node.fields as StepRowBlockFields;
        const icons = fields.items.map((item) => STEP_ICONS[item.icon]);
        return (
          <div className="flex flex-col lg:flex-row items-center lg:items-start gap-8 lg:gap-2 w-full">
            {fields.items.flatMap((item, i) => [
              <div key={item.id ?? i} className="group flex flex-col items-center gap-4 lg:gap-5 flex-1 min-w-0">
                <div className="flex items-center justify-center w-16 h-14 shrink-0 transition-transform duration-300 group-hover:scale-110">
                  {icons[i]?.()}
                </div>
                <div className="flex flex-col gap-1 text-center">
                  <p className="font-semibold text-text-primary text-[1rem]">{item.title}</p>
                  {item.subtitle && <p className="font-semibold text-[0.75rem] text-brand">{item.subtitle}</p>}
                  <p className="text-[0.875rem] text-text-muted leading-[1.5]">{item.description}</p>
                </div>
              </div>,
              i < fields.items.length - 1 ? (
                <div key={`arrow-${item.id ?? i}`} className="flex items-center justify-center shrink-0 lg:mt-5">
                  <StepArrow className="w-8 h-8 rotate-90 lg:w-10 lg:h-4 lg:rotate-0" />
                </div>
              ) : null,
            ])}
          </div>
        );
      },
      ctaCard: ({ node }: { node: { fields: unknown } }) => {
        const fields = node.fields as CtaCardBlockFields;
        return (
          <Link
            href={fields.href}
            className="group flex items-center justify-between gap-4 border border-border rounded-md px-6 py-5 hover:border-brand transition-colors"
          >
            <div className="flex flex-col gap-1">
              <p className="font-bold text-[0.8125rem] text-brand">{fields.eyebrow}</p>
              <p className="font-semibold text-body text-text-primary" style={{ fontFamily: "var(--font-lora)" }}>{fields.title}</p>
              {fields.description && <p className="text-body-sm text-text-muted">{fields.description}</p>}
            </div>
            <svg viewBox="0 0 20 20" className="size-4 shrink-0 text-text-primary transition-transform duration-200 group-hover:translate-x-1" fill="none" aria-hidden="true">
              <path d="M4 10h12m0 0-5-5m5 5-5 5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
          </Link>
        );
      },
    },
  });
}

export function RichText({
  content,
  quoteLabel = "Merke dir:",
}: {
  content: unknown;
  /** Label for any native blockquote's callout — defaults to "Merke dir:"
   * for callers that don't pass one (legal pages never use blockquotes,
   * so this only actually matters for blog posts). Pass "" to hide the
   * label/icon/underline for every native blockquote here. New content
   * should use the Zitat block instead, which carries its own label. */
  quoteLabel?: string;
}) {
  const root = (content as { root?: { children?: unknown[] } })?.root;
  if (!root?.children) return null;

  return (
    <div className="flex flex-col gap-4 w-full">
      <LexicalRichText
        data={content as Parameters<typeof LexicalRichText>[0]["data"]}
        converters={buildConverters(quoteLabel)}
        disableContainer
      />
    </div>
  );
}
