"use client";

import {
  ActionIcon,
  Badge,
  Box,
  Button,
  Group,
  Image,
  Loader,
  CopyButton,
  FileButton,
  Alert,
  Kbd,
  Menu,
  Modal,
  Select,
  Stack,
  Text,
  TextInput,
  Tooltip,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import {
  IconAlertTriangle,
  IconArrowLeft,
  IconDeviceFloppy,
  IconMail,
  IconPhoto,
  IconSend,
  IconLayoutSidebarRightCollapse,
  IconLayoutSidebarRightExpand,
  IconCheck,
  IconCopy,
  IconFilePlus,
  IconFolderPlus,
  IconPhotoUp,
  IconPlus,
  IconDots,
  IconAlertCircle,
  IconProgressCheck,
  IconSearch,
  IconTrash,
  IconX,
} from "@tabler/icons-react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate, useSearchParams } from "react-router";
import {
  createEDMFolder,
  createEDMTemplate,
  deleteEDMAsset,
  deleteEDMTemplate,
  sendEDMTestEmail,
  updateEDMTemplate,
  uploadEDMAsset,
} from "@/lib/features/edm/action";
import { browseEDMFolder, getEDMTemplate, listEDMFolders, listEDMTemplates } from "@/lib/features/edm/query";
import type { EDMAssetEntry, EDMFolder, EDMFolderAsset, EDMFolderTemplate, EDMTemplate } from "@/lib/features/edm/types";
import type { AccessScope } from "@/lib/features/types";
import { folderSelectOptions } from "@/lib/features/edm/folderTree";
import { EDMHtmlEditor } from "../_components/EDMHtmlEditor";
import { WorkspaceMedia } from "./_components/WorkspaceMedia";
import { WorkspacePreview } from "./_components/WorkspacePreview";
import { WorkspaceTree } from "./_components/WorkspaceTree";
import { WorkflowSidebar } from "@/components/WorkflowSidebar";
import { listOpenChangeRequests } from "@/lib/features/workflow/query";
import { resolveChangeRequest } from "@/lib/features/workflow/action";
import type { WorkflowChangeRequest } from "@/lib/features/workflow/types";

interface Props {
  accessScope: AccessScope;
}

type OpenTab = {
  id: string;
  name: string;
  subject: string;
  html: string;
  /** The html as last loaded or saved — what dirty is measured against. */
  savedHtml: string;
};

const SAMPLE_DATA: Record<string, string> = {
  full_name: "Rohit Zare",
  first_name: "Rohit",
  subject: "Your Karma booking",
  memberName: "Rohit Zare",
  member_name: "Rohit Zare",
  account_id: "KC123456",
  email: "rohit.zare@karmagroup.com",
};

/**
 * Editor workspace: tree, tabbed source editor, live preview.
 *
 * Everything here operates on the console's own API, so nothing needs to be
 * installed and it works for whoever can already reach the console.
 */
export default function EDMWorkspaceClientPage({ accessScope }: Props) {
  const navigate = useNavigate();
  const [params] = useSearchParams();

  /** Scope the explorer to one folder when arrived at via "Open in workspace". */
  const rootFolderId = params.get("folder");
  const [rootLabel, setRootLabel] = useState("All templates");

  useEffect(() => {
    if (!rootFolderId) { setRootLabel("All templates"); return; }
    // browse returns the folder record, so the header can name the scope rather
    // than showing a bare uuid.
    void browseEDMFolder(rootFolderId).then((res) => {
      if (res.success && res.data?.folder) setRootLabel(res.data.folder.name);
    });
  }, [rootFolderId]);

  // Pane sizes, dragged by the splitters between them.
  const [treeWidth, setTreeWidth] = useState(250);
  const [previewWidth, setPreviewWidth] = useState(420);
  const [showPreview, setShowPreview] = useState(true);
  const [previewFull, setPreviewFull] = useState(false);

  const [quickOpen, setQuickOpen] = useState(false);
  const [quickQuery, setQuickQuery] = useState("");
  const [allTemplates, setAllTemplates] = useState<EDMTemplate[]>([]);

  const [tabs, setTabs] = useState<OpenTab[]>([]);
  const [activeId, setActiveId] = useState<string | null>(null);
  const [loadingId, setLoadingId] = useState<string | null>(null);
  const [saving, setSaving] = useState(false);
  const [treeToken, setTreeToken] = useState(0);

  const [asset, setAsset] = useState<EDMFolderAsset | null>(null);

  const [allFolders, setAllFolders] = useState<EDMFolder[]>([]);
  const [newTplOpen, setNewTplOpen] = useState(false);
  const [newTplName, setNewTplName] = useState("");
  const [newTplSubject, setNewTplSubject] = useState("");
  const [newTplFolder, setNewTplFolder] = useState<string | null>(null);

  const [newFolderOpen, setNewFolderOpen] = useState(false);
  const [newFolderName, setNewFolderName] = useState("");
  const [newFolderParent, setNewFolderParent] = useState<string | null>(null);

  const [uploaded, setUploaded] = useState<EDMAssetEntry | null>(null);
  const [uploading, setUploading] = useState(false);
  const [creating, setCreating] = useState(false);
  const [dragOver, setDragOver] = useState(false);

  const [tplDelete, setTplDelete] = useState<{ id: string; name: string } | null>(null);
  const [assetDelete, setAssetDelete] = useState<EDMFolderAsset | null>(null);
  const [deleting, setDeleting] = useState(false);
  const [testOpen, setTestOpen] = useState(false);
  /** Template id whose approval drawer is open, null when closed. */
  const [workflowId, setWorkflowId] = useState<string | null>(null);
  /** Caret line in the open file, so a review comment can point at it. */
  const [cursorLine, setCursorLine] = useState(1);
  /** Line to jump to — bumped by clicking a change request. */
  const [revealLine, setRevealLine] = useState<number | null>(null);
  /** Open change requests for the files currently open, keyed by template id. */
  const [openCrs, setOpenCrs] = useState<WorkflowChangeRequest[]>([]);
  const [testTo, setTestTo] = useState("");
  const [sending, setSending] = useState(false);

  const active = tabs.find((t) => t.id === activeId) ?? null;

  /**
   * Open change requests for whatever is open.
   *
   * Keyed on the tab ids rather than the active tab so switching tabs does not
   * refetch — the request already covered every open file.
   */
  const openTabIds = tabs.map((t) => t.id).join(",");
  const refreshCrs = useCallback(async () => {
    const ids = openTabIds ? openTabIds.split(",") : [];
    if (ids.length === 0) {
      setOpenCrs([]);
      return;
    }
    const res = await listOpenChangeRequests(ids);
    setOpenCrs(res.success && Array.isArray(res.data) ? res.data : []);
  }, [openTabIds]);

  useEffect(() => {
    void refreshCrs();
  }, [refreshCrs]);

  const activeCrs = active
    ? openCrs.filter((cr) => cr.target_ref === active.id)
    : [];
  const activeMarkedLines = activeCrs
    .map((cr) => cr.line_number)
    .filter((n): n is number => typeof n === "number" && n > 0);
  const dirty = useMemo(() => tabs.filter((t) => t.html !== t.savedHtml), [tabs]);

  /**
   * Closing the browser tab with unsaved work loses it — nothing is written
   * until Save. The native prompt is the only reliable guard here.
   */
  useEffect(() => {
    if (dirty.length === 0) return;
    const onBeforeUnload = (e: BeforeUnloadEvent) => {
      e.preventDefault();
      e.returnValue = "";
    };
    window.addEventListener("beforeunload", onBeforeUnload);
    return () => window.removeEventListener("beforeunload", onBeforeUnload);
  }, [dirty.length]);

  const openTemplate = useCallback(
    // Only id and name are used; everything else is refetched. Keeping the
    // parameter narrow means a caller holding just a search hit does not have
    // to invent the fields it lacks.
    async (t: { id: string; name: string }) => {
      const already = tabs.find((tab) => tab.id === t.id);
      if (already) {
        setActiveId(t.id);
        return;
      }

      setLoadingId(t.id);
      try {
        // The tree row carries no html — the browse endpoint deliberately omits
        // it so listing a folder costs no storage reads.
        const res = await getEDMTemplate(t.id);
        if (!res.success || !res.data) {
          notifications.show({ color: "red", message: res.message || "Failed to open template" });
          return;
        }
        const version =
          res.data.versions.find((v) => v.active === 1) ?? res.data.versions[0];
        const html = version?.html_content ?? "";

        setTabs((prev) => [
          ...prev,
          { id: t.id, name: res.data.name, subject: version?.subject ?? "", html, savedHtml: html },
        ]);
        setActiveId(t.id);
      } finally {
        setLoadingId(null);
      }
    },
    [tabs],
  );

  const closeTab = (id: string) => {
    const tab = tabs.find((t) => t.id === id);
    if (tab && tab.html !== tab.savedHtml) {
      // eslint-disable-next-line no-alert
      if (!window.confirm(`"${tab.name}" has unsaved changes. Close anyway?`)) return;
    }
    setTabs((prev) => prev.filter((t) => t.id !== id));
    if (activeId === id) {
      const rest = tabs.filter((t) => t.id !== id);
      setActiveId(rest[rest.length - 1]?.id ?? null);
    }
  };

  const save = async () => {
    if (!active) return;
    setSaving(true);
    try {
      const res = await updateEDMTemplate(active.id, { html_content: active.html });
      if (!res.success) {
        notifications.show({ color: "red", message: res.message || "Save failed" });
        return;
      }
      // Update appends a new version server-side, so the saved baseline moves
      // to what we just sent rather than being refetched.
      setTabs((prev) =>
        prev.map((t) => (t.id === active.id ? { ...t, savedHtml: t.html } : t)),
      );
      setTreeToken((n) => n + 1);
      notifications.show({ color: "green", message: `Saved ${active.name}` });
    } finally {
      setSaving(false);
    }
  };

  const sendTest = async () => {
    if (!active || !testTo.trim()) return;
    setSending(true);
    try {
      const res = await sendEDMTestEmail(active.id, testTo.trim());
      notifications.show({
        color: res.success ? "green" : "red",
        message: res.success ? `Test sent to ${testTo.trim()}` : res.message || "Send failed",
      });
      if (res.success) setTestOpen(false);
    } finally {
      setSending(false);
    }
  };

  /**
   * Upload an image and drop its tag straight into the open template.
   *
   * Shared by clipboard paste and drag-and-drop. Both skip the modal on purpose:
   * when someone pastes a screenshot they want it in the email, not a dialog
   * asking what to do with it.
   */
  const uploadAndInsert = useCallback(
    async (file: File) => {
      if (!activeIdRef.current) {
        notifications.show({ color: "orange", message: "Open a template first." });
        return;
      }
      setUploading(true);
      try {
        const res = await uploadEDMAsset(file, rootFolderId);
        if (!res.success || !res.data) {
          notifications.show({ color: "red", message: res.message || "Upload failed" });
          return;
        }
        const url = res.data.url;
        const tag = `<img src="${url}" alt="" style="display:block;max-width:100%;" />`;
        setTabs((prev) =>
          prev.map((t) => {
            if (t.id !== activeIdRef.current) return t;
            // Before </body> — appending lands after </html>, outside the
            // document, where mail clients discard it.
            const body = t.html.search(/<\/body\s*>/i);
            const at = body !== -1 ? body : t.html.search(/<\/html\s*>/i);
            return {
              ...t,
              html:
                at !== -1
                  ? `${t.html.slice(0, at)}${tag}\n${t.html.slice(at)}`
                  : `${t.html}\n${tag}`,
            };
          }),
        );
        notifications.show({ color: "green", message: `${file.name} inserted — save to keep it` });
      } finally {
        setUploading(false);
      }
    },
    [rootFolderId],
  );

  // The paste/drop listeners are registered once; a ref keeps them reading the
  // current tab instead of the one that was open when they were attached.
  const activeIdRef = React.useRef<string | null>(null);
  useEffect(() => { activeIdRef.current = activeId; }, [activeId]);

  /** Paste an image from the clipboard straight into the template. */
  useEffect(() => {
    if (!accessScope.create) return;
    const onPaste = (e: ClipboardEvent) => {
      const file = Array.from(e.clipboardData?.items ?? [])
        .filter((i) => i.kind === "file" && i.type.startsWith("image/"))
        .map((i) => i.getAsFile())
        .find(Boolean);
      if (!file) return;           // plain text paste — leave the editor alone
      e.preventDefault();
      void uploadAndInsert(file);
    };
    window.addEventListener("paste", onPaste);
    return () => window.removeEventListener("paste", onPaste);
  }, [accessScope.create, uploadAndInsert]);

  const loadFolders = useCallback(async () => {
    const res = await listEDMFolders();
    if (res.success && Array.isArray(res.data)) setAllFolders(res.data);
  }, []);

  useEffect(() => { void loadFolders(); }, [loadFolders, treeToken]);

  /** Minimal but valid email scaffold — a table-based shell, since that is what
   *  actually renders consistently across mail clients. */
  const STARTER_HTML = `<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{{subject}}</title>
  </head>
  <body style="margin:0;padding:0;background:#f4f4f4;">
    <table width="100%" cellpadding="0" cellspacing="0" border="0">
      <tr>
        <td align="center" style="padding:24px 12px;">
          <table width="600" cellpadding="0" cellspacing="0" border="0" style="background:#ffffff;">
            <tr>
              <td style="padding:32px;font-family:Arial,sans-serif;font-size:15px;color:#333;">
                <p>Dear {{full_name}},</p>
                <p>Your content here.</p>
              </td>
            </tr>
          </table>
        </td>
      </tr>
    </table>
  </body>
</html>`;

  const createTemplate = async () => {
    if (!newTplName.trim() || !newTplSubject.trim()) return;
    setCreating(true);
    try {
      const res = await createEDMTemplate({
        name: newTplName.trim(),
        subject: newTplSubject.trim(),
        html_content: STARTER_HTML,
        folder_id: newTplFolder ?? rootFolderId,
        active: 1,
      } as never);
      if (!res.success || !res.data) {
        notifications.show({ color: "red", message: res.message || "Create failed" });
        return;
      }
      setNewTplOpen(false);
      setNewTplName("");
      setNewTplSubject("");
      setTreeToken((n) => n + 1);
      // Open it straight away — creating a template you then have to hunt for
      // in the tree is a pointless extra step.
      await openTemplate({ id: res.data.id, name: res.data.name });
      notifications.show({ color: "green", message: `Created ${res.data.name}` });
    } finally {
      setCreating(false);
    }
  };

  const createFolder = async () => {
    if (!newFolderName.trim()) return;
    setCreating(true);
    try {
      const res = await createEDMFolder({
        name: newFolderName.trim(),
        parent_id: newFolderParent ?? rootFolderId,
      });
      if (!res.success) {
        notifications.show({ color: "red", message: res.message || "Create failed" });
        return;
      }
      setNewFolderOpen(false);
      setNewFolderName("");
      setTreeToken((n) => n + 1);
      notifications.show({ color: "green", message: "Folder created" });
    } finally {
      setCreating(false);
    }
  };

  const handleUpload = async (file: File | null) => {
    if (!file) return;
    setUploading(true);
    try {
      const res = await uploadEDMAsset(file, rootFolderId);
      if (!res.success || !res.data) {
        notifications.show({ color: "red", message: res.message || "Upload failed" });
        return;
      }
      // Shown rather than silently copied: the author needs the URL, and the
      // storage path is the only way to find the object in the bucket later.
      setUploaded(res.data);
    } finally {
      setUploading(false);
    }
  };

  /**
   * Insert an <img> for the uploaded asset into the open template.
   *
   * Placed just before </body> — appending to the end of the string puts it
   * after </html>, outside the document, where mail clients discard it. The tag
   * looked inserted in the editor and then never appeared in the email.
   */
  const insertUploaded = () => {
    if (!uploaded || !active) return;
    const tag = `<img src="${uploaded.url}" alt="" style="display:block;max-width:100%;" />`;

    const insertInto = (html: string): string => {
      const body = html.search(/<\/body\s*>/i);
      if (body !== -1) return `${html.slice(0, body)}${tag}\n${html.slice(body)}`;
      const doc = html.search(/<\/html\s*>/i);
      if (doc !== -1) return `${html.slice(0, doc)}${tag}\n${html.slice(doc)}`;
      // A fragment with no body/html wrapper — appending is correct there.
      return `${html}\n${tag}`;
    };

    setTabs((prev) =>
      prev.map((t) => (t.id === active.id ? { ...t, html: insertInto(t.html) } : t)),
    );
    setUploaded(null);
    notifications.show({
      color: "green",
      message: "Image inserted before </body> — save to keep it",
    });
  };

  const removeTemplate = async () => {
    if (!tplDelete) return;
    setDeleting(true);
    try {
      const res = await deleteEDMTemplate(tplDelete.id);
      if (!res.success) {
        notifications.show({ color: "red", message: res.message || "Delete failed" });
        return;
      }
      // Its tab would otherwise keep accepting edits and saving to a template
      // that no longer exists.
      setTabs((prev) => prev.filter((t) => t.id !== tplDelete.id));
      setActiveId((cur) => (cur === tplDelete.id ? null : cur));
      setTplDelete(null);
      setTreeToken((n) => n + 1);
      notifications.show({ color: "green", message: `Deleted ${tplDelete.name}` });
    } finally {
      setDeleting(false);
    }
  };

  const removeAsset = async () => {
    if (!assetDelete) return;
    setDeleting(true);
    try {
      const res = await deleteEDMAsset(assetDelete.path);
      if (!res.success) {
        notifications.show({ color: "red", message: res.message || "Delete failed" });
        return;
      }
      setAssetDelete(null);
      setAsset(null);
      setTreeToken((n) => n + 1);
      notifications.show({ color: "green", message: res.message || "Image deleted" });
    } finally {
      setDeleting(false);
    }
  };

  /**
   * Drag a splitter. Listeners go on window rather than the handle so the drag
   * survives the cursor leaving the 4px strip — otherwise a fast drag detaches.
   */
  const startResize = (which: "tree" | "preview") => (e: React.MouseEvent) => {
    e.preventDefault();
    const startX = e.clientX;
    const startTree = treeWidth;
    const startPreview = previewWidth;

    const onMove = (ev: MouseEvent) => {
      const delta = ev.clientX - startX;
      if (which === "tree") {
        setTreeWidth(Math.min(500, Math.max(160, startTree + delta)));
      } else {
        // The preview sits on the right, so dragging right shrinks it.
        setPreviewWidth(Math.min(800, Math.max(280, startPreview - delta)));
      }
    };
    const onUp = () => {
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
      document.body.style.cursor = "";
      document.body.style.userSelect = "";
    };
    document.body.style.cursor = "col-resize";
    // Without this a drag selects text across the whole page.
    document.body.style.userSelect = "none";
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
  };

  const quickMatches = useMemo(() => {
    const q = quickQuery.trim().toLowerCase();
    const list = q
      ? allTemplates.filter(
          (t) => t.name.toLowerCase().includes(q) || t.id.toLowerCase().includes(q),
        )
      : allTemplates;
    return list.slice(0, 40);
  }, [allTemplates, quickQuery]);

  const openQuick = useCallback(async () => {
    setQuickOpen(true);
    setQuickQuery("");
    // Fetched on first use, not at mount: the explorer is the primary way in
    // and most sessions never need the whole flat list.
    if (allTemplates.length === 0) {
      const res = await listEDMTemplates();
      if (res.success && Array.isArray(res.data)) setAllTemplates(res.data);
    }
  }, [allTemplates.length]);

  // Cmd/Ctrl+S saves, as it would in any editor.
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === "s") {
        e.preventDefault();
        if (active && accessScope.update) void save();
      }
      if ((e.metaKey || e.ctrlKey) && e.key === "p") {
        e.preventDefault();
        void openQuick();
      }
      if ((e.metaKey || e.ctrlKey) && e.key === "b") {
        e.preventDefault();
        setShowPreview((v) => !v);
      }
      if (e.key === "Escape" && previewFull) setPreviewFull(false);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [active, accessScope.update, openQuick, previewFull]);

  return (
    <Box style={{ height: "calc(100vh - 120px)", display: "flex", flexDirection: "column" }}>
      {/* toolbar */}
      <Group justify="space-between" px="md" py={8} style={{ borderBottom: "1px solid var(--mantine-color-gray-3)" }}>
        <Group gap="xs">
          <ActionIcon variant="subtle" onClick={() => navigate("/admin/edm")}>
            <IconArrowLeft size={16} />
          </ActionIcon>
          <Text fw={600} size="sm">EDM Workspace</Text>
          {rootFolderId && (
            <Badge size="sm" variant="light" color="blue">{rootLabel}</Badge>
          )}
          {dirty.length > 0 && (
            <Badge size="sm" color="orange" variant="light">
              {dirty.length} unsaved
            </Badge>
          )}
        </Group>
        <Group gap="xs">
          {accessScope.create && (
            <>
              <Menu withinPortal position="bottom-start">
                <Menu.Target>
                  <Button size="xs" variant="light" leftSection={<IconPlus size={14} />}>
                    New
                  </Button>
                </Menu.Target>
                <Menu.Dropdown>
                  <Menu.Item
                    leftSection={<IconFilePlus size={14} />}
                    onClick={() => { setNewTplFolder(rootFolderId); setNewTplOpen(true); }}
                  >
                    Template
                  </Menu.Item>
                  <Menu.Item
                    leftSection={<IconFolderPlus size={14} />}
                    onClick={() => { setNewFolderParent(rootFolderId); setNewFolderOpen(true); }}
                  >
                    Folder
                  </Menu.Item>
                </Menu.Dropdown>
              </Menu>

              <FileButton
                onChange={handleUpload}
                accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml,image/x-icon"
              >
                {(props) => (
                  <Tooltip label="Upload an image">
                    <ActionIcon {...props} variant="subtle" loading={uploading}>
                      <IconPhotoUp size={15} />
                    </ActionIcon>
                  </Tooltip>
                )}
              </FileButton>
            </>
          )}
          {/* Approval for whichever template is open — the drawer is the same
              one the folder browser uses, so state set here is visible there. */}
          <Tooltip
            label={
              active
                ? `Approval & comments — request changes at line ${cursorLine}`
                : "Approval & comments"
            }
          >
            <ActionIcon
              variant="subtle"
              disabled={!active}
              onClick={() => active && setWorkflowId(active.id)}
            >
              <IconProgressCheck size={15} />
            </ActionIcon>
          </Tooltip>
          <Tooltip label="Quick open (⌘P)">
            <ActionIcon variant="subtle" onClick={openQuick}>
              <IconSearch size={15} />
            </ActionIcon>
          </Tooltip>
          <Tooltip label={showPreview ? "Hide preview (⌘B)" : "Show preview (⌘B)"}>
            <ActionIcon variant="subtle" onClick={() => setShowPreview((v) => !v)}>
              {showPreview ? (
                <IconLayoutSidebarRightCollapse size={15} />
              ) : (
                <IconLayoutSidebarRightExpand size={15} />
              )}
            </ActionIcon>
          </Tooltip>
          <Button
            size="xs"
            variant="light"
            leftSection={<IconSend size={14} />}
            disabled={!active}
            onClick={() => setTestOpen(true)}
          >
            Test send
          </Button>
          {accessScope.delete && active && (
            <Menu withinPortal position="bottom-end">
              <Menu.Target>
                <ActionIcon variant="subtle"><IconDots size={15} /></ActionIcon>
              </Menu.Target>
              <Menu.Dropdown>
                <Menu.Item
                  color="red"
                  leftSection={<IconTrash size={14} />}
                  onClick={() => setTplDelete({ id: active.id, name: active.name })}
                >
                  Delete “{active.name}”
                </Menu.Item>
              </Menu.Dropdown>
            </Menu>
          )}
          <Button
            size="xs"
            leftSection={<IconDeviceFloppy size={14} />}
            loading={saving}
            disabled={!active || active.html === active.savedHtml || !accessScope.update}
            onClick={save}
          >
            Save
          </Button>
        </Group>
      </Group>

      {/* three panes */}
      <Group gap={0} align="stretch" style={{ flex: 1, minHeight: 0 }}>
        {!previewFull && (
        <Box style={{ width: treeWidth, minWidth: treeWidth, overflow: "hidden" }}>
          <WorkspaceTree
            onOpenTemplate={openTemplate}
            onOpenAsset={setAsset}
            activeTemplateId={activeId}
            refreshToken={treeToken}
            rootFolderId={rootFolderId}
            rootLabel={rootLabel}
            canEdit={accessScope.update}
            onChanged={() => setTreeToken((n) => n + 1)}
          />
        </Box>
        )}
        {!previewFull && <Splitter onMouseDown={startResize("tree")} />}

        {!previewFull && (
        <Box style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
          {/* tab strip */}
          <Group gap={0} style={{ borderBottom: "1px solid var(--mantine-color-gray-3)", overflowX: "auto" }}>
            {tabs.map((t) => (
              <Group
                key={t.id}
                gap={4}
                px="sm"
                py={6}
                wrap="nowrap"
                style={{
                  cursor: "pointer",
                  borderRight: "1px solid var(--mantine-color-gray-3)",
                  backgroundColor: activeId === t.id ? "var(--mantine-color-blue-0)" : undefined,
                }}
                onClick={() => setActiveId(t.id)}
              >
                <IconMail size={12} />
                <Text size="xs" fw={activeId === t.id ? 600 : 400}>{t.name}</Text>
                {t.html !== t.savedHtml && (
                  <Box style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: "var(--mantine-color-orange-6)" }} />
                )}
                <ActionIcon
                  size="xs"
                  variant="subtle"
                  color="gray"
                  onClick={(e) => { e.stopPropagation(); closeTab(t.id); }}
                >
                  <IconX size={10} />
                </ActionIcon>
              </Group>
            ))}
            {loadingId && (
              <Group gap={4} px="sm" py={6}>
                <Loader size={12} />
                <Text size="xs" c="dimmed">opening…</Text>
              </Group>
            )}
          </Group>

          <Box
            style={{
              flex: 1,
              minHeight: 0,
              overflow: "auto",
              padding: 8,
              position: "relative",
              outline: dragOver ? "2px dashed var(--mantine-color-blue-5)" : undefined,
              outlineOffset: -4,
            }}
            onDragOver={(e) => {
              if (!accessScope.create) return;
              // Only react to files; dragging a text selection inside the editor
              // must keep working normally.
              if (!Array.from(e.dataTransfer.types).includes("Files")) return;
              e.preventDefault();
              setDragOver(true);
            }}
            onDragLeave={() => setDragOver(false)}
            onDrop={(e) => {
              if (!accessScope.create) return;
              const files = Array.from(e.dataTransfer.files).filter((f) =>
                f.type.startsWith("image/"),
              );
              if (files.length === 0) return;
              e.preventDefault();
              setDragOver(false);
              void (async () => { for (const f of files) await uploadAndInsert(f); })();
            }}
          >
            {dragOver && (
              <Box
                style={{
                  position: "absolute",
                  inset: 8,
                  zIndex: 2,
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  backgroundColor: "var(--mantine-color-blue-0)",
                  opacity: 0.92,
                  borderRadius: 8,
                  pointerEvents: "none",
                }}
              >
                <Group gap={8}>
                  <IconPhotoUp size={22} />
                  <Text size="sm" fw={500}>Drop to upload and insert</Text>
                </Group>
              </Box>
            )}
            {/*
              Open review comments for this file.
              Sits above the editor rather than in a drawer: a change request you
              have to go looking for is one that gets missed, and the line
              numbers only mean anything next to the code.
            */}
            {active && activeCrs.length > 0 && (
              <Box
                mb="xs"
                p="xs"
                style={{
                  border: "1px solid var(--mantine-color-red-2)",
                  backgroundColor: "var(--mantine-color-red-0)",
                  borderRadius: 6,
                }}
              >
                <Group gap={6} mb={4}>
                  <IconAlertCircle size={14} color="var(--mantine-color-red-7)" />
                  <Text size="xs" fw={600} c="red.8">
                    {activeCrs.length} open change request
                    {activeCrs.length === 1 ? "" : "s"}
                  </Text>
                </Group>
                <Stack gap={4}>
                  {activeCrs.map((cr) => (
                    <Group key={cr.id} gap={6} wrap="nowrap" align="flex-start">
                      {cr.line_number ? (
                        <Badge
                          size="xs"
                          variant="light"
                          color="red"
                          style={{ cursor: "pointer", fontFamily: "monospace" }}
                          onClick={() => setRevealLine(cr.line_number ?? null)}
                        >
                          L{cr.line_number}
                        </Badge>
                      ) : (
                        <Badge size="xs" variant="light" color="gray">file</Badge>
                      )}
                      <Text size="xs" style={{ flex: 1 }}>
                        {cr.description}
                        {cr.assignee_first_name && (
                          <Text span size="xs" c="dimmed">
                            {" "}— for {cr.assignee_first_name}
                          </Text>
                        )}
                      </Text>
                      <Button
                        size="compact-xs"
                        variant="subtle"
                        color="green"
                        onClick={async () => {
                          const res = await resolveChangeRequest(cr.id);
                          notifications.show({
                            color: res.success ? "green" : "red",
                            message: res.success
                              ? "Change request resolved"
                              : res.message || "Could not resolve",
                          });
                          if (res.success) await refreshCrs();
                        }}
                      >
                        Resolve
                      </Button>
                    </Group>
                  ))}
                </Stack>
              </Box>
            )}

            {active ? (
              <EDMHtmlEditor
                value={active.html}
                readOnly={!accessScope.update}
                height={520}
                markedLines={activeMarkedLines}
                onCursorLine={setCursorLine}
                revealLine={revealLine}
                onChange={(html) =>
                  setTabs((prev) => prev.map((t) => (t.id === active.id ? { ...t, html } : t)))
                }
              />
            ) : (
              <Stack align="center" justify="center" style={{ height: "100%" }} gap={4}>
                <IconMail size={36} opacity={0.2} />
                <Text size="sm" c="dimmed">Pick a template from the explorer to start editing.</Text>
                <Group gap={6}>
                  <Text size="xs" c="dimmed">or</Text>
                  <Kbd size="xs">⌘</Kbd><Kbd size="xs">P</Kbd>
                  <Text size="xs" c="dimmed">to search</Text>
                </Group>
                <Text size="xs" c="dimmed" mt={4}>
                  With a template open you can paste or drop an image straight in.
                </Text>
              </Stack>
            )}
          </Box>

          {active && (
            <WorkspaceMedia
              html={active.html}
              canUpload={accessScope.create}
              folderId={rootFolderId}
              onHtmlChange={(html) =>
                setTabs((prev) => prev.map((t) => (t.id === active.id ? { ...t, html } : t)))
              }
            />
          )}
        </Box>
        )}

        {(showPreview || previewFull) && (
          <>
            {!previewFull && <Splitter onMouseDown={startResize("preview")} />}
            <Box
              style={
                previewFull
                  ? { flex: 1, minWidth: 0, overflow: "hidden" }
                  : { width: previewWidth, minWidth: previewWidth, overflow: "hidden" }
              }
            >
              {active ? (
                <WorkspacePreview
                  html={active.html}
                  sampleData={SAMPLE_DATA}
                  fullscreen={previewFull}
                  onToggleFullscreen={() => setPreviewFull((v) => !v)}
                />
              ) : (
                <Stack align="center" justify="center" style={{ height: "100%" }}>
                  <Text size="xs" c="dimmed">No template open</Text>
                </Stack>
              )}
            </Box>
          </>
        )}
      </Group>

      {/* image preview */}
      <Modal opened={!!asset} onClose={() => setAsset(null)} title={asset?.original} centered size="lg">
        {asset && (
          <Stack gap="xs">
            <Image src={asset.url} alt={asset.original} fit="contain" mah={380} />
            <AssetField label="Public URL" value={asset.url} />
            <AssetField label="Storage path (bucket object)" value={asset.path} />
            {asset.sha256 && (
              <AssetField label="Content hash (sha256)" value={asset.sha256} />
            )}
            <Text size="xs" c="dimmed">
              {asset.usedBy.length > 0
                ? `Used by ${asset.usedBy.join(", ")}`
                : "Not referenced by any template yet — uploaded but unused."}
            </Text>
            {accessScope.delete && (
              <Group justify="flex-end">
                <Button
                  size="xs"
                  color="red"
                  variant="light"
                  leftSection={<IconTrash size={14} />}
                  onClick={() => setAssetDelete(asset)}
                >
                  Delete image
                </Button>
              </Group>
            )}
          </Stack>
        )}
      </Modal>

      {/* uploaded image */}
      <Modal opened={!!uploaded} onClose={() => setUploaded(null)} title="Image uploaded" centered size="md">
        {uploaded && (
          <Stack gap="xs">
            <Image src={uploaded.url} alt={uploaded.original} fit="contain" mah={260} />
            <AssetField label="Public URL" value={uploaded.url} />
            <AssetField label="Storage path (bucket object)" value={uploaded.path} />
            <Text size="xs" c="dimmed">
              Stored under its content hash, so re-uploading the same file reuses
              this object rather than making a copy.
            </Text>
            <Group justify="flex-end">
              <Button variant="subtle" onClick={() => setUploaded(null)}>Close</Button>
              <Tooltip
                label="Open a template first — there is nothing to insert into"
                disabled={!!active}
              >
                <Button disabled={!active} onClick={insertUploaded}>
                  Insert into template
                </Button>
              </Tooltip>
            </Group>
          </Stack>
        )}
      </Modal>

      {/* new template */}
      <Modal opened={newTplOpen} onClose={() => setNewTplOpen(false)} title="New template" centered size="sm">
        <Stack>
          <TextInput label="Name" required value={newTplName} onChange={(e) => setNewTplName(e.currentTarget.value)} data-autofocus />
          <TextInput
            label="Subject"
            required
            description="Handlebars allowed, e.g. Welcome, {{full_name}}"
            value={newTplSubject}
            onChange={(e) => setNewTplSubject(e.currentTarget.value)}
          />
          <Select
            label="Folder"
            placeholder="Top level"
            data={folderSelectOptions(allFolders)}
            value={newTplFolder}
            onChange={setNewTplFolder}
            searchable
            clearable
          />
          <Text size="xs" c="dimmed">Starts from a table-based email scaffold you can edit.</Text>
          <Group justify="flex-end">
            <Button variant="subtle" onClick={() => setNewTplOpen(false)}>Cancel</Button>
            <Button loading={creating} onClick={createTemplate}>Create and open</Button>
          </Group>
        </Stack>
      </Modal>

      {/* new folder */}
      <Modal opened={newFolderOpen} onClose={() => setNewFolderOpen(false)} title="New folder" centered size="sm">
        <Stack>
          <TextInput label="Name" required value={newFolderName} onChange={(e) => setNewFolderName(e.currentTarget.value)} data-autofocus />
          <Select
            label="Inside"
            placeholder="Top level"
            data={folderSelectOptions(allFolders)}
            value={newFolderParent}
            onChange={setNewFolderParent}
            searchable
            clearable
          />
          <Group justify="flex-end">
            <Button variant="subtle" onClick={() => setNewFolderOpen(false)}>Cancel</Button>
            <Button loading={creating} onClick={createFolder}>Create</Button>
          </Group>
        </Stack>
      </Modal>

      {/* delete template */}
      <Modal opened={!!tplDelete} onClose={() => setTplDelete(null)} title="Delete template" centered size="sm">
        <Stack>
          <Text size="sm">Delete <strong>{tplDelete?.name}</strong>?</Text>
          <Alert color="red" icon={<IconAlertTriangle size={16} />}>
            <Text size="sm">
              If a promo code, curated event or member offer references this
              template ID, that mail stops working. The reference is a plain
              string, so nothing here can check it for you.
            </Text>
          </Alert>
          <Group justify="flex-end">
            <Button variant="subtle" onClick={() => setTplDelete(null)}>Cancel</Button>
            <Button color="red" loading={deleting} onClick={removeTemplate}>Delete</Button>
          </Group>
        </Stack>
      </Modal>

      {/* delete image */}
      <Modal opened={!!assetDelete} onClose={() => setAssetDelete(null)} title="Delete image" centered size="sm">
        <Stack>
          <Text size="sm" ff="monospace" style={{ wordBreak: "break-all" }}>
            {assetDelete?.path}
          </Text>
          {assetDelete && assetDelete.usedBy.length > 0 ? (
            <Alert color="red" icon={<IconAlertTriangle size={16} />} title="Still in use">
              <Text size="sm">
                Referenced by {assetDelete.usedBy.join(", ")}. Those templates
                will show a broken image.
              </Text>
            </Alert>
          ) : (
            <Alert color="orange" icon={<IconAlertTriangle size={16} />}>
              <Text size="sm">
                No template lists this image — but a template edited by hand can
                reference a URL without it being recorded, so that is a lower
                bound, not a guarantee.
              </Text>
            </Alert>
          )}
          <Text size="xs" c="dimmed">
            Any email already delivered that embeds this URL will show a broken
            image from now on. That mail cannot be recalled or repaired.
          </Text>
          <Group justify="flex-end">
            <Button variant="subtle" onClick={() => setAssetDelete(null)}>Cancel</Button>
            <Button color="red" loading={deleting} onClick={removeAsset}>Delete image</Button>
          </Group>
        </Stack>
      </Modal>

      {/* quick open */}
      <Modal
        opened={quickOpen}
        onClose={() => setQuickOpen(false)}
        withCloseButton={false}
        centered
        size="lg"
        padding={0}
      >
        <TextInput
          placeholder="Search templates by name or ID…"
          value={quickQuery}
          onChange={(e) => setQuickQuery(e.currentTarget.value)}
          leftSection={<IconSearch size={15} />}
          variant="unstyled"
          size="md"
          px="md"
          data-autofocus
          onKeyDown={(e) => {
            // Enter opens the top match — the whole point of a quick-open.
            if (e.key === "Enter" && quickMatches[0]) {
              const t = quickMatches[0];
              void openTemplate({ id: t.id, name: t.name });
              setQuickOpen(false);
            }
            if (e.key === "Escape") setQuickOpen(false);
          }}
        />
        <Box style={{ maxHeight: 380, overflowY: "auto", borderTop: "1px solid var(--mantine-color-gray-3)" }}>
          {quickMatches.length === 0 ? (
            <Text size="sm" c="dimmed" p="md">No templates match.</Text>
          ) : (
            quickMatches.map((t) => (
              <Group
                key={t.id}
                px="md"
                py={8}
                gap="sm"
                wrap="nowrap"
                style={{ cursor: "pointer" }}
                onClick={() => {
                  void openTemplate({ id: t.id, name: t.name });
                  setQuickOpen(false);
                }}
              >
                <IconMail size={14} color="var(--mantine-color-grape-6)" />
                <Text size="sm" style={{ flex: 1 }}>{t.name}</Text>
                <Text size="xs" c="dimmed" ff="monospace">{t.id}</Text>
              </Group>
            ))
          )}
        </Box>
      </Modal>

      {/* test send */}
      <Modal opened={testOpen} onClose={() => setTestOpen(false)} title={`Test send — ${active?.name ?? ""}`} centered size="sm">
        <Stack>
          <TextInput
            label="To"
            placeholder="you@karmagroup.com, other@karmagroup.com"
            value={testTo}
            onChange={(e) => setTestTo(e.currentTarget.value)}
            data-autofocus
          />
          <Text size="xs" c="dimmed">
            Sends the <strong>saved</strong> version, not unsaved edits in the editor.
          </Text>
          <Group justify="flex-end">
            <Button variant="subtle" onClick={() => setTestOpen(false)}>Cancel</Button>
            <Button loading={sending} onClick={sendTest}>Send</Button>
          </Group>
        </Stack>
      </Modal>

      {workflowId && (
        <WorkflowSidebar
          opened
          onClose={() => setWorkflowId(null)}
          entityType="EDM_TEMPLATE"
          entityId={workflowId}
          crTargets={tabs.map((t) => ({ value: t.id, label: t.name }))}
          defaultCrTarget={workflowId}
          defaultCrLine={cursorLine}
          // Bumping the tree token repaints the approval dots in the explorer.
          onWorkflowUpdated={() => {
            setTreeToken((n) => n + 1);
            void refreshCrs();
          }}
        />
      )}
    </Box>
  );
}

/** Read-only value with a copy button — used for URLs and storage paths. */
function AssetField({ label, value }: { label: string; value: string }) {
  return (
    <Box>
      <Text size="xs" fw={500} c="dimmed">{label}</Text>
      <Group gap={4} wrap="nowrap">
        <Text size="xs" ff="monospace" style={{ wordBreak: "break-all", flex: 1 }}>
          {value}
        </Text>
        <CopyButton value={value}>
          {({ copied, copy }) => (
            <Tooltip label={copied ? "Copied" : "Copy"}>
              <ActionIcon size="xs" variant="subtle" color={copied ? "teal" : "gray"} onClick={copy}>
                {copied ? <IconCheck size={12} /> : <IconCopy size={12} />}
              </ActionIcon>
            </Tooltip>
          )}
        </CopyButton>
      </Group>
    </Box>
  );
}

/** 4px drag strip between panes. */
function Splitter({ onMouseDown }: { onMouseDown: (e: React.MouseEvent) => void }) {
  return (
    <Box
      onMouseDown={onMouseDown}
      style={{
        width: 4,
        cursor: "col-resize",
        backgroundColor: "var(--mantine-color-gray-3)",
        flexShrink: 0,
      }}
    />
  );
}
