import type { ReactNode } from "react";
import Image from "next/image";
import type { TOCSection } from "./SectionTOC";

// Minimal Lexical JSON → JSX renderer for Payload's richText fields.
// Deliberately small and dependency-free (matches the project's existing
// style — see Posts.ts's own hand-rolled extractPlainText on the Payload
// side) rather than pulling in @payloadcms/richtext-lexical's full React
// renderer just to walk a legal page's headings/paragraphs/lists. Covers
// the node types real content actually uses; add more only when a page
// genuinely needs them.

type LexicalNode = {
  type: string;
  children?: LexicalNode[];
  text?: string;
  format?: number;
  tag?: string;
  listType?: "bullet" | "number";
  fields?: { url?: string };
};

// Lexical's text format is a bitmask — see TextFormatType in the Lexical
// source (IS_BOLD = 1, IS_ITALIC = 2, IS_UNDERLINE = 8).
const BOLD = 1;
const ITALIC = 2;
const UNDERLINE = 8;

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.
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;
}

function renderChildren(nodes: LexicalNode[] | undefined, keyPrefix: string, quoteLabel: string): ReactNode {
  if (!nodes) return null;
  return nodes.map((node, i) => renderNode(node, `${keyPrefix}-${i}`, quoteLabel));
}

function renderNode(node: LexicalNode, key: string, quoteLabel: string): ReactNode {
  switch (node.type) {
    case "linebreak":
      return <br key={key} />;
    case "text": {
      let el: ReactNode = node.text;
      const format = node.format ?? 0;
      if (format & BOLD) el = <strong key={key}>{el}</strong>;
      if (format & ITALIC) el = <em key={key}>{el}</em>;
      if (format & UNDERLINE) el = <u key={key}>{el}</u>;
      return <span key={key}>{el}</span>;
    }
    case "link":
      return (
        <a
          key={key}
          href={node.fields?.url ?? "#"}
          className="text-brand hover:underline"
        >
          {renderChildren(node.children, key, quoteLabel)}
        </a>
      );
    case "heading": {
      const Tag = (node.tag ?? "h2") as "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
      const text = plainText(node);
      return (
        <Tag
          key={key}
          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)" }}
        >
          {renderChildren(node.children, key, quoteLabel)}
          <span className="block h-[0.125rem] w-8 bg-brand mt-2" aria-hidden />
        </Tag>
      );
    }
    case "list": {
      const ListTag = node.listType === "number" ? "ol" : "ul";
      return (
        <ListTag
          key={key}
          className={
            "flex flex-col gap-2 text-body text-text-body " +
            (node.listType === "number" ? "list-decimal pl-5" : "list-disc pl-5")
          }
        >
          {renderChildren(node.children, key, quoteLabel)}
        </ListTag>
      );
    }
    case "listitem":
      return (
        <li key={key}>{renderChildren(node.children, key, quoteLabel)}</li>
      );
    case "paragraph":
      return (
        <p key={key} className="text-body text-text-body">
          {renderChildren(node.children, key, quoteLabel)}
        </p>
      );
    // Lexical's default blockquote feature — used sitewide as a "Merke
    // dir:" pull-quote callout, per page-blog-detail's actual built Figma
    // frame (node 4676:341, file jCCZyh1DGwdjpv1wGge9To) — NOT a bordered/
    // background card (an earlier version of this guessed one; the real
    // design has no background or padding at all, just a plain 3-column
    // row: label+underline, a full-height divider rule, then the quote
    // lines). Icon is the actual exported sparkle asset from that node
    // (icon-sparkle-merke-dir.png), not a hand-drawn approximation. The
    // "Merke dir:" label itself is generic/hardcoded here rather than
    // content-authored, since a blog post's own body text drives which
    // lines get quoted, not the label framing them — legal pages never
    // use blockquotes, so this styling is effectively blog-only in
    // practice despite living in the shared renderer.
    case "quote":
      return (
        <div key={key} className="relative flex items-start gap-6 w-full my-6">
          {/* Label/icon/underline are optional (Posts.quoteLabel) — if
              empty, only the divider + quote text render. The blockquote
              itself is never optional, just this framing around it. */}
          {quoteLabel && (
            <>
              <div className="flex items-center gap-2 shrink-0">
                <span className="relative flex h-7 w-6 items-center justify-center shrink-0">
                  <Image alt="" src="/icon-sparkle-merke-dir.png" fill sizes="24px" className="object-contain" />
                </span>
                <span
                  className="font-bold text-text-primary text-[1.625rem] whitespace-nowrap"
                  style={{ fontFamily: "var(--font-caveat)" }}
                >
                  {quoteLabel}
                </span>
              </div>
              {/* Hand-drawn underline image, not a plain bar — exported
                  straight from the Figma node (label-underline). */}
              <Image
                alt=""
                src="/icon-merke-dir-underline.png"
                width={136}
                height={23}
                className="absolute left-8 top-[2.1875rem] w-[8.5rem] h-[1.4375rem] object-cover pointer-events-none"
              />
            </>
          )}
          <div className="w-px self-stretch bg-brand shrink-0" />
          {/* Lexical's real QuoteNode holds flat text/linebreak children
              directly, NOT nested paragraphs — pressing Enter inside a
              blockquote in the editor exits it into a new paragraph
              rather than adding a line within it (confirmed by reading
              @lexical/rich-text's QuoteNode.insertNewAfter). An earlier
              version of this case assumed nested-paragraph children,
              which only happened to work for this session's own
              hand-authored seed JSON — any blockquote actually typed in
              the CMS (Shift+Enter for a soft line break) rendered blank,
              since child.children was undefined on a plain text node. */}
          <p
            className="text-text-primary text-[1.75rem] leading-[1.1] flex-1"
            style={{ fontFamily: "var(--font-caveat)" }}
          >
            {renderChildren(node.children, key, quoteLabel)}
          </p>
        </div>
      );
    default:
      return renderChildren(node.children, key, quoteLabel);
  }
}

export function RichText({
  content,
  quoteLabel = "Merke dir:",
}: {
  content: unknown;
  /** Label for any blockquote's callout (see the "quote" case above) —
   * 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 blockquote here. */
  quoteLabel?: string;
}) {
  const root = (content as { root?: LexicalNode })?.root;
  if (!root?.children) return null;

  return (
    <div className="flex flex-col gap-4 w-full">
      {renderChildren(root.children, "root", quoteLabel)}
    </div>
  );
}
