"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,
      }}
    />
  );
}
