import {
  useCallback,
  useEffect,
  useLayoutEffect,
  useRef,
  useState,
  type CSSProperties,
} from "react";
import { createPortal } from "react-dom";
import { useEditor, EditorContent, type Editor, Extension } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { Underline } from "@tiptap/extension-underline";
import { TextStyle } from "@tiptap/extension-text-style";
import { Color } from "@tiptap/extension-color";
import { FontFamily } from "@tiptap/extension-font-family";
import { TextAlign } from "@tiptap/extension-text-align";
import { safeHtml } from "@/lib/safe-html";
import {
  Bold as BoldIcon,
  Italic as ItalicIcon,
  Underline as UnderlineIcon,
  AlignLeft as AlignLeftIcon,
  AlignCenter as AlignCenterIcon,
  AlignRight as AlignRightIcon,
  Check,
  X as XIcon,
} from "lucide-react";

const COLOR_SWATCHES = [
  { value: "", label: "default" },
  { value: "#0B1E3F", label: "navy" },
  { value: "#dc2626", label: "red" },
  { value: "#ea580c", label: "orange" },
  { value: "#ca8a04", label: "amber" },
  { value: "#16a34a", label: "green" },
  { value: "#2563eb", label: "blue" },
  { value: "#7c3aed", label: "violet" },
  { value: "#6b7280", label: "gray" },
];

const FONT_OPTIONS = [
  { value: "", label: "default" },
  { value: "system-ui", label: "system-ui" },
  { value: "Cairo", label: "Cairo" },
  { value: "Tajawal", label: "Tajawal" },
  { value: "Amiri", label: "Amiri" },
  { value: "Noto Naskh Arabic", label: "Noto" },
  { value: "IBM Plex Sans Arabic", label: "IBM Plex" },
];

const FONT_SIZE_OPTIONS = [
  { value: "", label: "default" },
  { value: "10px", label: "10" },
  { value: "12px", label: "12" },
  { value: "14px", label: "14" },
  { value: "16px", label: "16" },
  { value: "18px", label: "18" },
  { value: "20px", label: "20" },
  { value: "24px", label: "24" },
  { value: "28px", label: "28" },
];

declare module "@tiptap/core" {
  interface Commands<ReturnType> {
    fontSize: {
      setFontSize: (size: string) => ReturnType;
      unsetFontSize: () => ReturnType;
    };
  }
}

const FontSize = Extension.create({
  name: "fontSize",
  addOptions() {
    return { types: ["textStyle"] };
  },
  addGlobalAttributes() {
    return [
      {
        types: this.options.types,
        attributes: {
          fontSize: {
            default: null,
            parseHTML: (element: HTMLElement) =>
              element.style.fontSize?.replace(/['"]+/g, "") || null,
            renderHTML: (attrs: { fontSize?: string | null }) => {
              if (!attrs.fontSize) return {};
              return { style: `font-size: ${attrs.fontSize}` };
            },
          },
        },
      },
    ];
  },
  addCommands() {
    return {
      setFontSize:
        (size: string) =>
        ({ chain }) =>
          chain().setMark("textStyle", { fontSize: size }).run(),
      unsetFontSize:
        () =>
        ({ chain }) =>
          chain()
            .setMark("textStyle", { fontSize: null })
            .removeEmptyTextStyle()
            .run(),
    };
  },
});

/**
 * In-place rich text editor used in the executive-meetings schedule for
 * Word-like editing of meeting names + attendee names.
 *
 * - When not editing, renders the saved sanitized HTML via dangerouslySetInnerHTML.
 * - Click (or focus + Enter) enters edit mode with a floating toolbar.
 * - Saves on blur outside the editor+toolbar or Ctrl/Cmd+Enter.
 * - Esc cancels.
 *
 * The server is the source of sanitization. This component only sends what
 * Tiptap produces; the server strips anything outside the allowlist.
 */
export function EditableCell({
  value,
  onSave,
  placeholder,
  ariaLabel,
  dir,
  className,
  multiline = false,
  disabled = false,
  fontStyle,
  testId,
  startInEditMode = false,
  onStartedEditing,
  forceSaveOnBlur = false,
  onCancel,
}: {
  value: string;
  onSave: (html: string) => Promise<void> | void;
  placeholder?: string;
  ariaLabel?: string;
  dir?: "rtl" | "ltr" | "auto";
  className?: string;
  multiline?: boolean;
  disabled?: boolean;
  fontStyle?: CSSProperties;
  testId?: string;
  startInEditMode?: boolean;
  onStartedEditing?: () => void;
  /**
   * When true, `onSave` is invoked on blur even if the content is unchanged.
   * Used by the schedule's "+ Add attendee" ghost row so the caller can
   * commit a new attendee or discard an empty add when the user blurs out
   * of a freshly-opened cell.
   */
  forceSaveOnBlur?: boolean;
  /**
   * Called when the user explicitly cancels (Escape). Used by the ghost
   * "+ Add attendee" row so the caller can clear pending state and remove
   * the synthetic row from the cell.
   */
  onCancel?: () => void;
}) {
  const [editing, setEditing] = useState(false);
  const wrapperRef = useRef<HTMLDivElement | null>(null);
  const toolbarRef = useRef<HTMLDivElement | null>(null);
  const blurTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  useEffect(() => {
    if (startInEditMode && !editing && !disabled) {
      setEditing(true);
      onStartedEditing?.();
    }
  }, [startInEditMode, editing, disabled, onStartedEditing]);

  const editor = useEditor(
    {
      extensions: [
        StarterKit.configure({
          heading: false,
          codeBlock: false,
          blockquote: false,
          horizontalRule: false,
          bulletList: false,
          orderedList: false,
          listItem: false,
        }),
        Underline,
        TextStyle,
        Color,
        FontFamily.configure({ types: ["textStyle"] }),
        FontSize,
        TextAlign.configure({
          types: ["paragraph", "heading"],
          alignments: ["left", "center", "right"],
        }),
      ],
      content: value || "",
      editable: editing && !disabled,
      editorProps: {
        attributes: {
          class:
            "outline-none focus:outline-none min-h-[1.5em] " +
            (multiline ? "" : "whitespace-nowrap overflow-hidden"),
          dir: dir ?? "auto",
        },
        handleKeyDown: (_view, event) => {
          if (event.key === "Escape") {
            cancelEdit();
            return true;
          }
          if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
            saveEdit();
            return true;
          }
          if (event.key === "Enter" && !multiline && !event.shiftKey) {
            event.preventDefault();
            saveEdit();
            return true;
          }
          return false;
        },
      },
    },
    [],
  );

  useEffect(() => {
    if (!editor) return;
    if (!editing && editor.getHTML() !== (value || "<p></p>")) {
      editor.commands.setContent(value || "", { emitUpdate: false });
    }
  }, [editor, value, editing]);

  useEffect(() => {
    if (!editor) return;
    editor.setEditable(editing && !disabled);
    if (editing) {
      const id = setTimeout(() => editor.commands.focus("end"), 0);
      return () => clearTimeout(id);
    }
    return undefined;
  }, [editor, editing, disabled]);

  // If the host disables this cell while we're mid-edit (e.g. the
  // schedule's global Edit toggle is flipped back to view mode), drop
  // any unsaved draft and exit edit mode immediately. Without this the
  // cell stays in editing state with a frozen toolbar even though the
  // editor itself has gone read-only via setEditable above.
  useEffect(() => {
    if (disabled && editing && editor) {
      editor.commands.setContent(value || "", { emitUpdate: false });
      setEditing(false);
      if (blurTimerRef.current) {
        clearTimeout(blurTimerRef.current);
        blurTimerRef.current = null;
      }
    }
  }, [disabled, editing, editor, value]);

  const enterEdit = () => {
    if (disabled) return;
    setEditing(true);
  };

  const cancelEdit = useCallback(() => {
    if (!editor) return;
    editor.commands.setContent(value || "", { emitUpdate: false });
    setEditing(false);
    onCancel?.();
  }, [editor, value, onCancel]);

  const saveEdit = useCallback(async () => {
    if (!editor) return;
    const html = editor.getHTML();
    const stripped = html === "<p></p>" ? "" : html;
    setEditing(false);
    if (stripped !== value || forceSaveOnBlur) {
      try {
        await onSave(stripped);
      } catch {
        editor.commands.setContent(value || "", { emitUpdate: false });
      }
    }
  }, [editor, onSave, value, forceSaveOnBlur]);

  const onFocusOut = (e: React.FocusEvent<HTMLDivElement>) => {
    if (!editing) return;
    const next = e.relatedTarget as Node | null;
    if (
      next &&
      (wrapperRef.current?.contains(next) ||
        toolbarRef.current?.contains(next))
    )
      return;
    if (blurTimerRef.current) clearTimeout(blurTimerRef.current);
    blurTimerRef.current = setTimeout(() => {
      saveEdit();
    }, 100);
  };

  // When the toolbar is portalled outside wrapperRef, native pointerdown on
  // the toolbar would still trigger our onFocusOut path on some browsers.
  // We watch document-level pointerdown and ignore taps inside the toolbar.
  useEffect(() => {
    if (!editing) return;
    const onPointerDownDoc = (e: PointerEvent) => {
      const target = e.target as Node | null;
      if (!target) return;
      if (
        wrapperRef.current?.contains(target) ||
        toolbarRef.current?.contains(target)
      ) {
        if (blurTimerRef.current) {
          clearTimeout(blurTimerRef.current);
          blurTimerRef.current = null;
        }
      }
    };
    document.addEventListener("pointerdown", onPointerDownDoc, true);
    return () => {
      document.removeEventListener("pointerdown", onPointerDownDoc, true);
    };
  }, [editing]);

  const onFocusIn = () => {
    if (blurTimerRef.current) {
      clearTimeout(blurTimerRef.current);
      blurTimerRef.current = null;
    }
  };

  if (!editor) {
    return (
      <div
        className={className}
        style={fontStyle}
        dangerouslySetInnerHTML={{
          __html: value ? safeHtml(value) : (placeholder ?? ""),
        }}
      />
    );
  }

  if (!editing) {
    return (
      <div
        role={disabled ? undefined : "button"}
        tabIndex={disabled ? -1 : 0}
        aria-label={ariaLabel}
        className={
          (className ?? "") +
          (disabled
            ? ""
            : " cursor-text hover:bg-blue-50/50 focus:bg-blue-50/50 focus:outline-none rounded transition-colors")
        }
        style={fontStyle}
        data-testid={testId}
        onClick={enterEdit}
        onKeyDown={(e) => {
          if (e.key === "Enter" || e.key === " ") {
            e.preventDefault();
            enterEdit();
          }
        }}
      >
        {value ? (
          <span dangerouslySetInnerHTML={{ __html: safeHtml(value) }} />
        ) : (
          <span className="text-muted-foreground italic">
            {placeholder ?? ""}
          </span>
        )}
      </div>
    );
  }

  return (
    <div
      ref={wrapperRef}
      className={"relative " + (className ?? "")}
      onFocus={onFocusIn}
      onBlur={onFocusOut}
      data-testid={testId}
      data-editing="true"
      style={fontStyle}
    >
      <FormattingToolbar
        editor={editor}
        anchorRef={wrapperRef}
        toolbarRef={toolbarRef}
        onSave={saveEdit}
        onCancel={cancelEdit}
      />
      <div
        className="border border-blue-400 rounded bg-white px-1.5 py-1 shadow-sm focus-within:border-blue-600"
        style={fontStyle}
      >
        <EditorContent editor={editor} />
      </div>
    </div>
  );
}

function FormattingToolbar({
  editor,
  anchorRef,
  toolbarRef,
  onSave,
  onCancel,
}: {
  editor: Editor;
  anchorRef: React.RefObject<HTMLElement | null>;
  toolbarRef: React.MutableRefObject<HTMLDivElement | null>;
  onSave: () => void;
  onCancel: () => void;
}) {
  const [, setTick] = useState(0);
  useEffect(() => {
    const update = () => setTick((n) => n + 1);
    editor.on("selectionUpdate", update);
    editor.on("transaction", update);
    return () => {
      editor.off("selectionUpdate", update);
      editor.off("transaction", update);
    };
  }, [editor]);

  const isActive = (mark: string, attrs?: Record<string, unknown>) =>
    editor.isActive(mark, attrs);

  const btn = (active: boolean) =>
    "p-1 rounded text-xs " +
    (active
      ? "bg-[#0B1E3F] text-white"
      : "bg-white text-[#0B1E3F] hover:bg-gray-100 border border-gray-200");

  // Position the toolbar via a portal so it escapes table cells with
  // overflow:hidden. We try to place it above the editor, but flip below
  // when there isn't enough room (e.g. first row of the table). We also
  // clamp horizontally to keep it inside the viewport.
  const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
  const [toolbarEl, setToolbarEl] = useState<HTMLDivElement | null>(null);
  const setToolbarNode = useCallback(
    (node: HTMLDivElement | null) => {
      toolbarRef.current = node;
      setToolbarEl(node);
    },
    [toolbarRef],
  );

  useLayoutEffect(() => {
    // Walk up to find the nearest horizontally-scrolling ancestor
    // (the table's `overflow-x-auto` wrapper). The toolbar must stay
    // inside that container's bounds so it doesn't visually escape the
    // table.
    const findScrollContainer = (el: HTMLElement | null): HTMLElement | null => {
      let cur: HTMLElement | null = el?.parentElement ?? null;
      while (cur && cur !== document.body) {
        const style = window.getComputedStyle(cur);
        const ox = style.overflowX;
        if (ox === "auto" || ox === "scroll") return cur;
        cur = cur.parentElement;
      }
      return null;
    };
    const update = () => {
      const anchor = anchorRef.current;
      if (!anchor) return;
      const rect = anchor.getBoundingClientRect();
      const tbHeight = toolbarEl?.offsetHeight ?? 36;
      const tbWidth = toolbarEl?.offsetWidth ?? 320;
      // Always place BELOW the editing cell. The task spec requires the
      // toolbar to never sit on top of the row above the active cell, so
      // we deliberately do NOT flip above even when there isn't room
      // below — the page can scroll instead.
      const top = rect.bottom + 4;
      // Clamp horizontally to the table's scroll container if we can
      // find one; fall back to the viewport otherwise. This keeps the
      // toolbar visually anchored inside the table.
      const container = findScrollContainer(anchor);
      const containerRect = container?.getBoundingClientRect();
      const minLeft = containerRect ? containerRect.left + 4 : 8;
      const maxLeft = containerRect
        ? containerRect.right - tbWidth - 4
        : window.innerWidth - tbWidth - 8;
      let left = rect.left;
      if (left > maxLeft) left = maxLeft;
      if (left < minLeft) left = minLeft;
      setPos({ top, left });
    };
    update();
    const raf = requestAnimationFrame(update);
    let ro: ResizeObserver | null = null;
    if (typeof ResizeObserver !== "undefined") {
      ro = new ResizeObserver(update);
      if (anchorRef.current) ro.observe(anchorRef.current);
      if (toolbarEl) ro.observe(toolbarEl);
    }
    window.addEventListener("scroll", update, true);
    window.addEventListener("resize", update);
    return () => {
      cancelAnimationFrame(raf);
      ro?.disconnect();
      window.removeEventListener("scroll", update, true);
      window.removeEventListener("resize", update);
    };
  }, [anchorRef, toolbarEl]);

  const node = (
    <div
      ref={setToolbarNode}
      style={{
        position: "fixed",
        top: pos?.top ?? -9999,
        left: pos?.left ?? -9999,
        zIndex: 1000,
        visibility: pos ? "visible" : "hidden",
      }}
      className="flex items-center gap-1 bg-white border border-gray-200 rounded shadow-md px-1 py-0.5"
      onMouseDown={(e) => e.preventDefault()}
      data-testid="em-edit-toolbar"
    >
      <button
        type="button"
        className={btn(isActive("bold"))}
        onClick={() => editor.chain().focus().toggleBold().run()}
        aria-label="Bold"
        data-testid="em-edit-bold"
      >
        <BoldIcon className="w-3.5 h-3.5" />
      </button>
      <button
        type="button"
        className={btn(isActive("italic"))}
        onClick={() => editor.chain().focus().toggleItalic().run()}
        aria-label="Italic"
        data-testid="em-edit-italic"
      >
        <ItalicIcon className="w-3.5 h-3.5" />
      </button>
      <button
        type="button"
        className={btn(isActive("underline"))}
        onClick={() => editor.chain().focus().toggleUnderline().run()}
        aria-label="Underline"
        data-testid="em-edit-underline"
      >
        <UnderlineIcon className="w-3.5 h-3.5" />
      </button>
      <span className="w-px h-4 bg-gray-200 mx-0.5" />
      {COLOR_SWATCHES.map((c) => (
        <button
          key={c.value || "default"}
          type="button"
          className={
            "w-4 h-4 rounded-sm border " +
            (c.value
              ? "border-gray-300 hover:scale-110"
              : "border-gray-400 bg-[repeating-linear-gradient(45deg,#fff,#fff_3px,#e5e7eb_3px,#e5e7eb_6px)]")
          }
          style={c.value ? { backgroundColor: c.value } : undefined}
          onClick={() => {
            if (c.value) {
              editor.chain().focus().setColor(c.value).run();
            } else {
              editor.chain().focus().unsetColor().run();
            }
          }}
          aria-label={`color ${c.label}`}
          data-testid={`em-edit-color-${c.label}`}
        />
      ))}
      <span className="w-px h-4 bg-gray-200 mx-0.5" />
      <button
        type="button"
        className={btn(isActive("paragraph", { textAlign: "right" }))}
        onClick={() => editor.chain().focus().setTextAlign("right").run()}
        aria-label="Align right"
        data-testid="em-edit-align-right"
      >
        <AlignRightIcon className="w-3.5 h-3.5" />
      </button>
      <button
        type="button"
        className={btn(isActive("paragraph", { textAlign: "center" }))}
        onClick={() => editor.chain().focus().setTextAlign("center").run()}
        aria-label="Align center"
        data-testid="em-edit-align-center"
      >
        <AlignCenterIcon className="w-3.5 h-3.5" />
      </button>
      <button
        type="button"
        className={btn(isActive("paragraph", { textAlign: "left" }))}
        onClick={() => editor.chain().focus().setTextAlign("left").run()}
        aria-label="Align left"
        data-testid="em-edit-align-left"
      >
        <AlignLeftIcon className="w-3.5 h-3.5" />
      </button>
      <span className="w-px h-4 bg-gray-200 mx-0.5" />
      <select
        className="text-xs border border-gray-200 rounded bg-white text-[#0B1E3F] px-1 py-0.5"
        value={(editor.getAttributes("textStyle").fontFamily as string) || ""}
        onChange={(e) => {
          const v = e.target.value;
          if (v) editor.chain().focus().setFontFamily(v).run();
          else editor.chain().focus().unsetFontFamily().run();
        }}
        aria-label="Font"
        data-testid="em-edit-font"
      >
        {FONT_OPTIONS.map((f) => (
          <option key={f.value || "default"} value={f.value}>
            {f.label}
          </option>
        ))}
      </select>
      <select
        className="text-xs border border-gray-200 rounded bg-white text-[#0B1E3F] px-1 py-0.5"
        value={(editor.getAttributes("textStyle").fontSize as string) || ""}
        onChange={(e) => {
          const v = e.target.value;
          if (v) editor.chain().focus().setFontSize(v).run();
          else editor.chain().focus().unsetFontSize().run();
        }}
        aria-label="Font size"
        data-testid="em-edit-font-size"
      >
        {FONT_SIZE_OPTIONS.map((f) => (
          <option key={f.value || "default"} value={f.value}>
            {f.label}
          </option>
        ))}
      </select>
      <span className="w-px h-4 bg-gray-200 mx-0.5" />
      <button
        type="button"
        className="p-1 rounded bg-green-600 text-white hover:bg-green-700"
        onClick={onSave}
        aria-label="Save"
        data-testid="em-edit-save"
      >
        <Check className="w-3.5 h-3.5" />
      </button>
      <button
        type="button"
        className="p-1 rounded bg-gray-200 text-[#0B1E3F] hover:bg-gray-300"
        onClick={onCancel}
        aria-label="Cancel"
        data-testid="em-edit-cancel"
      >
        <XIcon className="w-3.5 h-3.5" />
      </button>
    </div>
  );

  if (typeof document === "undefined") return node;
  return createPortal(node, document.body);
}

