import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { open, save } from "@tauri-apps/plugin-dialog";
import { open as openExternal } from "@tauri-apps/plugin-shell";
import { writeText as writeClipboardText } from "@tauri-apps/plugin-clipboard-manager";
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
import { Menu } from "@tauri-apps/api/menu";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { appDataDir } from "@tauri-apps/api/path";
import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/core";
import historyIcon from "./assets/history.png";
import historyIconBright from "./assets/history_b.png";
import { markdownToHTML } from "./markdown/markdown";
import "./markdown/markdown-render.css";
import {
  createFolder,
  createText,
  deleteText,
  deleteTextVersion,
  discardDraft,
  getDraft,
  getLatestManualVersion,
  getText,
  listFolders,
  listTexts,
  listVersions,
  moveFolder,
  moveTextToFolder,
  saveManualVersion,
  searchTexts,
  setFolderOrder,
  setTextOrder,
  updateFolderName,
  updateTextTitle,
  upsertDraft,
  type Folder,
  type Text,
} from "./lib/db";

const formatDate = (timestamp: number) => {
  if (!timestamp) return "";
  return new Date(timestamp).toLocaleString();
};

type HistorySnapshot = {
  body: string;
  lastPersistedBody: string;
  lastPersistedTitle: string;
  hasDraft: boolean;
  restoredDraft: boolean;
  draftBaseVersionId: string | null;
  latestManualVersionId: string | null;
};

type ConfirmState = {
  title: string;
  message: string;
  actionLabel?: string;
  onConfirm: () => Promise<void> | void;
};

type HistoryEntry = {
  id: string;
  created_at: number;
  kind: "manual" | "draft";
  body: string;
  baseVersionId?: string | null;
};

const DEFAULT_TITLE = "Untitled Text";

export default function App() {
  const [texts, setTexts] = useState<Text[]>([]);
  const [selectedTextId, setSelectedTextId] = useState<string | null>(null);
  const [search, setSearch] = useState("");
  const [loadingTexts, setLoadingTexts] = useState(true);
  const [folders, setFolders] = useState<Folder[]>([]);
  const [loadingFolders, setLoadingFolders] = useState(true);

  const [title, setTitle] = useState("");
  const [lastPersistedTitle, setLastPersistedTitle] = useState("");
  const [body, setBody] = useState("");
  const [lastPersistedBody, setLastPersistedBody] = useState("");
  const [hasDraft, setHasDraft] = useState(false);
  const [restoredDraft, setRestoredDraft] = useState(false);
  const [latestManualVersionId, setLatestManualVersionId] = useState<string | null>(null);
  const [draftBaseVersionId, setDraftBaseVersionId] = useState<string | null>(null);

  const [historyOpen, setHistoryOpen] = useState(false);
  const [historyItems, setHistoryItems] = useState<HistoryEntry[]>([]);
  const [viewingVersion, setViewingVersion] = useState<HistoryEntry | null>(null);
  const [selectedHistoryId, setSelectedHistoryId] = useState<string | null>(null);
  const [confirmState, setConfirmState] = useState<ConfirmState | null>(null);
  const [settingsOpen, setSettingsOpen] = useState(false);
  const [markdownPreview, setMarkdownPreview] = useState(false);
  const [expandedFolders, setExpandedFolders] = useState<Set<string>>(() => {
    const stored = localStorage.getItem("textdb.expandedFolders");
    if (!stored) return new Set();
    try {
      const parsed = JSON.parse(stored);
      if (Array.isArray(parsed)) {
        return new Set(parsed.filter((value) => typeof value === "string"));
      }
    } catch {
      return new Set();
    }
    return new Set();
  });
  const [theme, setTheme] = useState<"default" | "light">(() => {
    const storedTheme = localStorage.getItem("textdb.theme");
    return storedTheme === "light" ? "light" : "default";
  });
  const [textSize, setTextSize] = useState(() => {
    const storedSize = Number(localStorage.getItem("textdb.textSize"));
    if (!Number.isNaN(storedSize) && storedSize >= 12 && storedSize <= 18) {
      return storedSize;
    }
    return 16;
  });
  const [showLineNumbers, setShowLineNumbers] = useState(() => {
    return localStorage.getItem("textdb.lineNumbers") === "true";
  });
  const [lineHeights, setLineHeights] = useState<number[]>([]);
  const [measureTick, setMeasureTick] = useState(0);
  const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
    return localStorage.getItem("textdb.sidebarCollapsed") === "true";
  });

  const bodyRef = useRef(body);
  const textareaRef = useRef<HTMLTextAreaElement | null>(null);
  const lineNumbersRef = useRef<HTMLDivElement | null>(null);
  const measureRef = useRef<HTMLDivElement | null>(null);
  const historySnapshotRef = useRef<HistorySnapshot | null>(null);
  const recentOpenRef = useRef(new Map<string, number>());
  const dragItemRef = useRef<{ type: "text" | "folder"; id: string; parentId: string | null } | null>(
    null
  );


  useEffect(() => {
    bodyRef.current = body;
  }, [body]);


  useEffect(() => {
    document.body.dataset.theme = theme;
    localStorage.setItem("textdb.theme", theme);
  }, [theme]);

  useEffect(() => {
    document.documentElement.style.setProperty("--base-font-size", `${textSize}px`);
    localStorage.setItem("textdb.textSize", String(textSize));
  }, [textSize]);

  useEffect(() => {
    localStorage.setItem("textdb.sidebarCollapsed", String(sidebarCollapsed));
  }, [sidebarCollapsed]);

  useEffect(() => {
    localStorage.setItem("textdb.lineNumbers", String(showLineNumbers));
  }, [showLineNumbers]);

  useEffect(() => {
    localStorage.setItem(
      "textdb.expandedFolders",
      JSON.stringify(Array.from(expandedFolders))
    );
  }, [expandedFolders]);

  useEffect(() => {
    if (selectedTextId) {
      localStorage.setItem("textdb.selectedTextId", selectedTextId);
    }
  }, [selectedTextId]);

  const isViewingHistory = viewingVersion !== null;
  const isDirty = !isViewingHistory && body !== lastPersistedBody;
  const hasText = body.trim().length > 0;
  const showLineNumbersActive = showLineNumbers && !markdownPreview;
  const hasSearch = search.trim().length > 0;

  const sortByOrderThenUpdated = useCallback(
    <T extends { sort_order: number | null; updated_at: number }>(a: T, b: T) => {
      const aHasOrder = a.sort_order !== null && a.sort_order !== undefined;
      const bHasOrder = b.sort_order !== null && b.sort_order !== undefined;
      if (aHasOrder && bHasOrder) {
        return (a.sort_order ?? 0) - (b.sort_order ?? 0);
      }
      if (aHasOrder !== bHasOrder) {
        return aHasOrder ? -1 : 1;
      }
      return b.updated_at - a.updated_at;
    },
    []
  );

  const folderById = useMemo(() => {
    const map = new Map<string, Folder>();
    for (const folder of folders) {
      map.set(folder.id, folder);
    }
    return map;
  }, [folders]);

  const foldersByParent = useMemo(() => {
    const map = new Map<string | null, Folder[]>();
    for (const folder of folders) {
      const key = folder.parent_id ?? null;
      const list = map.get(key);
      if (list) {
        list.push(folder);
      } else {
        map.set(key, [folder]);
      }
    }
    for (const [key, list] of map.entries()) {
      map.set(key, [...list].sort(sortByOrderThenUpdated));
    }
    return map;
  }, [folders, sortByOrderThenUpdated]);

  const textsByFolder = useMemo(() => {
    const map = new Map<string | null, Text[]>();
    for (const text of texts) {
      const key = text.folder_id ?? null;
      const list = map.get(key);
      if (list) {
        list.push(text);
      } else {
        map.set(key, [text]);
      }
    }
    for (const [key, list] of map.entries()) {
      map.set(key, [...list].sort(sortByOrderThenUpdated));
    }
    return map;
  }, [texts, sortByOrderThenUpdated]);

  const visibleFolderIds = useMemo(() => {
    if (!hasSearch) return null;
    const visible = new Set<string>();
    for (const text of texts) {
      let current = text.folder_id ?? null;
      while (current) {
        if (visible.has(current)) break;
        visible.add(current);
        current = folderById.get(current)?.parent_id ?? null;
      }
    }
    return visible;
  }, [folderById, hasSearch, texts]);

  const handleMarkdownPreviewClick = useCallback(
    (event: React.MouseEvent<HTMLDivElement>) => {
      const target = event.target as HTMLElement | null;
      const copyButton = target?.closest?.(".md-codeblock__copy") as HTMLElement | null;
      if (copyButton) {
        event.preventDefault();
        const encoded = copyButton.getAttribute("data-copy-code") ?? "";
        const text = decodeURIComponent(encoded);
        if (!text) return;
        writeClipboardText(text).catch(() => {
          if (navigator?.clipboard?.writeText) {
            navigator.clipboard.writeText(text);
          }
        });
        return;
      }
      const link = target?.closest?.("a");
      if (!link) return;
      const href = link.getAttribute("href");
      if (!href || href.startsWith("#") || href.startsWith("/")) return;
      event.preventDefault();
      openExternal(href);
    },
    []
  );

  const handlePrintMarkdown = useCallback(() => {
    if (!markdownPreview) return;
    document.body.classList.add("print-markdown");
    const cleanup = () => {
      document.body.classList.remove("print-markdown");
      window.removeEventListener("afterprint", cleanup);
    };
    window.addEventListener("afterprint", cleanup);
    requestAnimationFrame(() => {
      window.print();
    });
  }, [markdownPreview]);

  const statusKey = useMemo(() => {
    if (isViewingHistory) return "history";
    if (isDirty) return "unsaved";
    if (hasDraft) return "draft";
    return "saved";
  }, [hasDraft, isDirty, isViewingHistory]);

  const canSave = !isViewingHistory && (isDirty || hasDraft);

  const statusLabel = useMemo(() => {
    switch (statusKey) {
      case "history":
        return "Viewing history";
      case "unsaved":
        return "Unsaved";
      case "draft":
        return "Draft autosaved";
      default:
        return "Saved";
    }
  }, [statusKey]);

  const historyIconSrc = theme === "light" ? historyIconBright : historyIcon;

  const lines = useMemo(() => body.split("\n"), [body]);
  const lineNumbers = useMemo(() => lines.map((_, index) => index + 1), [lines]);

  const handleTextareaScroll = useCallback((event: React.UIEvent<HTMLTextAreaElement>) => {
    if (!showLineNumbersActive) return;
    if (lineNumbersRef.current) {
      lineNumbersRef.current.scrollTop = event.currentTarget.scrollTop;
    }
  }, [showLineNumbersActive]);

  useEffect(() => {
    if (!showLineNumbersActive) return;
    const textarea = textareaRef.current;
    if (!textarea || typeof ResizeObserver === "undefined") return;
    const observer = new ResizeObserver(() => {
      setMeasureTick((tick) => tick + 1);
    });
    observer.observe(textarea);
    return () => observer.disconnect();
  }, [showLineNumbersActive]);

  useEffect(() => {
    if (!showLineNumbersActive) return;
    let raf = 0;
    const handleResize = () => {
      if (raf) cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => {
        setMeasureTick((tick) => tick + 1);
      });
    };
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize", handleResize);
      if (raf) cancelAnimationFrame(raf);
    };
  }, [showLineNumbersActive]);

  useLayoutEffect(() => {
    if (!showLineNumbersActive) return;
    const textarea = textareaRef.current;
    const measure = measureRef.current;
    if (!textarea || !measure) return;
    measure.style.width = `${textarea.clientWidth}px`;
    const heights = Array.from(measure.children).map((child) =>
      Math.ceil((child as HTMLElement).getBoundingClientRect().height)
    );
    setLineHeights(heights);
    if (lineNumbersRef.current) {
      lineNumbersRef.current.scrollTop = textarea.scrollTop;
    }
  }, [lines, showLineNumbersActive, textSize, measureTick, sidebarCollapsed, historyOpen]);

  useEffect(() => {
    if (showLineNumbersActive && textareaRef.current && lineNumbersRef.current) {
      lineNumbersRef.current.scrollTop = textareaRef.current.scrollTop;
    }
  }, [showLineNumbersActive, body]);


  const refreshTexts = useCallback(async () => {
    setLoadingTexts(true);
    try {
      const trimmed = search.trim();
      const rows = trimmed ? await searchTexts(trimmed) : await listTexts();
      setTexts(rows);
    } finally {
      setLoadingTexts(false);
    }
  }, [search]);

  const refreshFolders = useCallback(async () => {
    setLoadingFolders(true);
    try {
      const rows = await listFolders();
      setFolders(rows);
    } finally {
      setLoadingFolders(false);
    }
  }, []);

  const getNextTextSortOrder = useCallback(
    (folderId: string | null) => {
      const list = textsByFolder.get(folderId ?? null) ?? [];
      const hasManualOrder = list.some((text) => text.sort_order !== null);
      if (!hasManualOrder) return null;
      return list.length;
    },
    [textsByFolder]
  );

  const getNextFolderSortOrder = useCallback(
    (parentId: string | null) => {
      const list = foldersByParent.get(parentId ?? null) ?? [];
      const hasManualOrder = list.some((folder) => folder.sort_order !== null);
      if (!hasManualOrder) return null;
      return list.length;
    },
    [foldersByParent]
  );

  const isFolderExpanded = useCallback(
    (folderId: string) => {
      if (hasSearch) {
        return visibleFolderIds?.has(folderId) ?? false;
      }
      return expandedFolders.has(folderId);
    },
    [expandedFolders, hasSearch, visibleFolderIds]
  );

  const toggleFolderExpanded = useCallback((folderId: string) => {
    setExpandedFolders((prev) => {
      const next = new Set(prev);
      if (next.has(folderId)) {
        next.delete(folderId);
      } else {
        next.add(folderId);
      }
      return next;
    });
  }, []);

  const refreshVersions = useCallback(async () => {
    if (!selectedTextId || !historyOpen) return;
    const [manualRows, draft] = await Promise.all([
      listVersions(selectedTextId),
      getDraft(selectedTextId)
    ]);
    const manualItems: HistoryEntry[] = manualRows.map((row) => ({
      id: row.id,
      created_at: row.created_at,
      kind: "manual",
      body: row.body
    }));
    const draftItem: HistoryEntry[] = draft
      ? [
          {
            id: `draft:${selectedTextId}`,
            created_at: draft.updated_at,
            kind: "draft",
            body: draft.body,
            baseVersionId: draft.base_version_id ?? null
          }
        ]
      : [];
    const combined = [...draftItem, ...manualItems].sort(
      (a, b) => b.created_at - a.created_at
    );
    setHistoryItems(combined);
  }, [historyOpen, selectedTextId]);

  useEffect(() => {
    refreshTexts().catch((error) => {
      console.error("Failed to load texts", error);
    });
  }, [refreshTexts]);

  useEffect(() => {
    refreshFolders().catch((error) => {
      console.error("Failed to load folders", error);
    });
  }, [refreshFolders]);

  useEffect(() => {
    if (selectedTextId || texts.length === 0) return;
    const storedId = localStorage.getItem("textdb.selectedTextId");
    const fallback = texts[0].id;
    const resolved = storedId && texts.some((text) => text.id === storedId)
      ? storedId
      : fallback;
    setSelectedTextId(resolved);
  }, [selectedTextId, texts]);

  useEffect(() => {
    if (!historyOpen) {
      setHistoryItems([]);
      return;
    }
    refreshVersions().catch((error) => {
      console.error("Failed to load versions", error);
    });
  }, [historyOpen, refreshVersions]);

  useEffect(() => {
    if (!selectedTextId) {
      setTitle("");
      setLastPersistedTitle("");
      setBody("");
      setLastPersistedBody("");
      setHasDraft(false);
      setRestoredDraft(false);
      setLatestManualVersionId(null);
      setDraftBaseVersionId(null);
      setViewingVersion(null);
      setSelectedHistoryId(null);
      setMarkdownPreview(false);
      historySnapshotRef.current = null;
      return;
    }

    let cancelled = false;
    const loadText = async () => {
      const [text, manualVersion, draft] = await Promise.all([
        getText(selectedTextId),
        getLatestManualVersion(selectedTextId),
        getDraft(selectedTextId)
      ]);

      if (cancelled) return;

      if (!text) {
        setSelectedTextId(null);
        localStorage.removeItem("textdb.selectedTextId");
        return;
      }

      const resolvedTitle = text.title || DEFAULT_TITLE;
      const resolvedBody = draft?.body ?? manualVersion?.body ?? "";
      const baseVersionId = draft?.base_version_id ?? manualVersion?.id ?? null;

      setTitle(resolvedTitle);
      setLastPersistedTitle(resolvedTitle);
      setBody(resolvedBody);
      setLastPersistedBody(resolvedBody);
      setHasDraft(Boolean(draft));
      setRestoredDraft(Boolean(draft));
      setLatestManualVersionId(manualVersion?.id ?? null);
      setDraftBaseVersionId(baseVersionId);
      setViewingVersion(null);
      setSelectedHistoryId(
        draft ? `draft:${selectedTextId}` : manualVersion?.id ?? null
      );
      setMarkdownPreview(false);
      historySnapshotRef.current = null;
    };

    loadText().catch((error) => {
      console.error("Failed to load text", error);
    });

    return () => {
      cancelled = true;
    };
  }, [selectedTextId]);

  useEffect(() => {
    if (!selectedTextId || !isDirty || isViewingHistory) return;

    const currentBody = body;
    const handle = window.setTimeout(() => {
      upsertDraft(selectedTextId, currentBody, draftBaseVersionId)
        .then(() => {
          if (bodyRef.current !== currentBody) return;
          setHasDraft(true);
          setLastPersistedBody(currentBody);
          setSelectedHistoryId(`draft:${selectedTextId}`);
          if (historyOpen) {
            refreshVersions().catch((error) => {
              console.error("Failed to refresh history", error);
            });
          }
        })
        .catch((error) => {
          console.error("Failed to autosave draft", error);
        });
    }, 600);

    return () => window.clearTimeout(handle);
  }, [
    body,
    draftBaseVersionId,
    historyOpen,
    isDirty,
    isViewingHistory,
    refreshVersions,
    selectedTextId
  ]);

  const handleNewText = useCallback(async () => {
    const sortOrder = getNextTextSortOrder(null);
    const { textId } = await createText(DEFAULT_TITLE, "", null, sortOrder);
    await refreshTexts();
    setSelectedTextId(textId);
  }, [getNextTextSortOrder, refreshTexts]);

  const handleNewFolder = useCallback(async () => {
    const name = window.prompt("Folder name");
    const trimmed = name?.trim();
    if (!trimmed) return;
    const sortOrder = getNextFolderSortOrder(null);
    const { folderId } = await createFolder(trimmed, null, sortOrder);
    await refreshFolders();
    setExpandedFolders((prev) => {
      const next = new Set(prev);
      next.add(folderId);
      return next;
    });
  }, [getNextFolderSortOrder, refreshFolders]);

  const buildFolderPath = useCallback(
    (folderId: string) => {
      const names: string[] = [];
      let current: string | null = folderId;
      const seen = new Set<string>();
      while (current) {
        if (seen.has(current)) break;
        seen.add(current);
        const folder = folderById.get(current);
        if (!folder) break;
        names.unshift(folder.name);
        current = folder.parent_id ?? null;
      }
      return names.join(" / ");
    },
    [folderById]
  );

  const folderPathList = useMemo(() => {
    return folders
      .map((folder) => ({
        id: folder.id,
        label: buildFolderPath(folder.id)
      }))
      .sort((a, b) => a.label.localeCompare(b.label));
  }, [buildFolderPath, folders]);

  const handleMoveTextToFolder = useCallback(
    async (textId: string, folderId: string | null) => {
      await moveTextToFolder(textId, folderId, null);
      await refreshTexts();
    },
    [refreshTexts]
  );

  const handleTextContextMenu = useCallback(
    async (event: React.MouseEvent, textId: string) => {
      event.preventDefault();
      const items = [
        {
          text: "Top level",
          action: () => {
            handleMoveTextToFolder(textId, null).catch((error) => {
              console.error("Failed to move text", error);
            });
          }
        },
        ...folderPathList.map((folder) => ({
          text: folder.label,
          action: () => {
            handleMoveTextToFolder(textId, folder.id).catch((error) => {
              console.error("Failed to move text", error);
            });
          }
        }))
      ];

      const menu = await Menu.new({
        items: [
          {
            text: "Move to folder",
            items
          }
        ]
      });
      await menu.popup(undefined, getCurrentWindow());
    },
    [folderPathList, handleMoveTextToFolder]
  );

  const reorderIds = useCallback((ids: string[], draggedId: string, targetId: string) => {
    const next = ids.filter((id) => id !== draggedId);
    const targetIndex = next.indexOf(targetId);
    const insertIndex = targetIndex === -1 ? next.length : targetIndex;
    next.splice(insertIndex, 0, draggedId);
    return next;
  }, []);

  const isDescendantFolder = useCallback(
    (folderId: string, potentialAncestorId: string) => {
      let current: string | null = folderId;
      while (current) {
        if (current === potentialAncestorId) return true;
        current = folderById.get(current)?.parent_id ?? null;
      }
      return false;
    },
    [folderById]
  );

  const handleTextDrop = useCallback(
    async (event: React.DragEvent, targetTextId: string, targetFolderId: string | null) => {
      event.preventDefault();
      event.stopPropagation();
      const dragItem = dragItemRef.current;
      if (!dragItem || dragItem.type !== "text") return;
      const currentList = textsByFolder.get(targetFolderId ?? null) ?? [];
      let ids = currentList.map((text) => text.id);
      if (!ids.includes(dragItem.id)) {
        const targetIndex = ids.indexOf(targetTextId);
        ids.splice(targetIndex === -1 ? ids.length : targetIndex, 0, dragItem.id);
      } else {
        ids = reorderIds(ids, dragItem.id, targetTextId);
      }
      await moveTextToFolder(dragItem.id, targetFolderId, 0);
      await setTextOrder(ids);
      await refreshTexts();
      dragItemRef.current = null;
    },
    [refreshTexts, reorderIds, textsByFolder]
  );

  const handleFolderDrop = useCallback(
    async (event: React.DragEvent, targetFolder: Folder) => {
      event.preventDefault();
      event.stopPropagation();
      const dragItem = dragItemRef.current;
      if (!dragItem) return;

      if (dragItem.type === "text") {
        const list = textsByFolder.get(targetFolder.id) ?? [];
        const ids = list.map((text) => text.id);
        if (!ids.includes(dragItem.id)) {
          ids.push(dragItem.id);
        }
        await moveTextToFolder(dragItem.id, targetFolder.id, 0);
        await setTextOrder(ids);
        await refreshTexts();
        dragItemRef.current = null;
        return;
      }

      if (dragItem.type === "folder") {
        if (dragItem.id === targetFolder.id) return;
        if (isDescendantFolder(targetFolder.id, dragItem.id)) return;
        const sameParent = dragItem.parentId === targetFolder.parent_id;
        if (sameParent) {
          const siblings = foldersByParent.get(targetFolder.parent_id ?? null) ?? [];
          const ids = siblings.map((folder) => folder.id);
          const next = reorderIds(ids, dragItem.id, targetFolder.id);
          await setFolderOrder(next);
          await refreshFolders();
        } else {
          const children = foldersByParent.get(targetFolder.id) ?? [];
          const ids = children.map((folder) => folder.id);
          ids.push(dragItem.id);
          await moveFolder(dragItem.id, targetFolder.id, 0);
          await setFolderOrder(ids);
          await refreshFolders();
        }
        dragItemRef.current = null;
      }
    },
    [foldersByParent, isDescendantFolder, refreshFolders, refreshTexts, reorderIds, textsByFolder]
  );

  const handleRootDrop = useCallback(
    async (event: React.DragEvent) => {
      event.preventDefault();
      const dragItem = dragItemRef.current;
      if (!dragItem) return;
      if (dragItem.type === "text") {
        const list = textsByFolder.get(null) ?? [];
        const ids = list.map((text) => text.id);
        if (!ids.includes(dragItem.id)) {
          ids.push(dragItem.id);
        }
        await moveTextToFolder(dragItem.id, null, 0);
        await setTextOrder(ids);
        await refreshTexts();
      } else if (dragItem.type === "folder") {
        const list = foldersByParent.get(null) ?? [];
        const ids = list.map((folder) => folder.id);
        if (!ids.includes(dragItem.id)) {
          ids.push(dragItem.id);
        }
        await moveFolder(dragItem.id, null, 0);
        await setFolderOrder(ids);
        await refreshFolders();
      }
      dragItemRef.current = null;
    },
    [foldersByParent, refreshFolders, refreshTexts, textsByFolder]
  );

  const handleDragStartText = useCallback((event: React.DragEvent, text: Text) => {
    dragItemRef.current = {
      type: "text",
      id: text.id,
      parentId: text.folder_id ?? null
    };
    event.dataTransfer.effectAllowed = "move";
    event.dataTransfer.setData("text/plain", text.id);
  }, []);

  const handleDragStartFolder = useCallback(
    (event: React.DragEvent, folder: Folder) => {
      dragItemRef.current = {
        type: "folder",
        id: folder.id,
        parentId: folder.parent_id ?? null
      };
      event.dataTransfer.effectAllowed = "move";
      event.dataTransfer.setData("text/plain", folder.id);
    },
    []
  );

  const handleDragEnd = useCallback(() => {
    dragItemRef.current = null;
  }, []);

  const createTextFromFile = useCallback(
    async (filePath: string) => {
      try {
        const filename = filePath.split(/[\/]/).pop() || DEFAULT_TITLE;
        const title = filename.replace(/\.(txt|md)$/i, "") || DEFAULT_TITLE;
        const contents = await readTextFile(filePath);
        const sortOrder = getNextTextSortOrder(null);
        const { textId } = await createText(title, contents, null, sortOrder);
        await refreshTexts();
        setSelectedTextId(textId);
      } catch (error) {
        console.error("Failed to open text file", error);
      }
    },
    [getNextTextSortOrder, refreshTexts]
  );

  const handleFilePaths = useCallback(
    async (paths: string[]) => {
      const now = Date.now();
      const txtPaths = paths.filter((path) => {
        const lower = path.toLowerCase();
        return lower.endsWith(".txt") || lower.endsWith(".md");
      });
      const recent = recentOpenRef.current;
      for (const path of txtPaths) {
        const key = path.toLowerCase();
        const last = recent.get(key);
        if (last && now - last < 1000) continue;
        recent.set(key, now);
        await createTextFromFile(path);
      }
      for (const [key, timestamp] of recent.entries()) {
        if (now - timestamp > 2000) recent.delete(key);
      }
    },
    [createTextFromFile]
  );

  useEffect(() => {
    let unlisten: (() => void) | null = null;
    getCurrentWindow()
      .onDragDropEvent(async (event) => {
        if (event.payload.type !== "drop") return;
        await handleFilePaths(event.payload.paths ?? []);
      })
      .then((cleanup) => {
        unlisten = cleanup;
      })
      .catch((error) => {
        console.error("Failed to register drag/drop handler", error);
      });
    return () => {
      if (unlisten) unlisten();
    };
  }, [handleFilePaths]);

  useEffect(() => {
    let unlisten: (() => void) | null = null;
    listen<string[]>("file-opened", async (event) => {
      await handleFilePaths(event.payload ?? []);
    })
      .then((cleanup) => {
        unlisten = cleanup;
      })
      .catch((error) => {
        console.error("Failed to register file-open listener", error);
      });

    invoke<string[]>("take_pending_opens")
      .then((paths) => handleFilePaths(paths ?? []))
      .catch((error) => {
        console.error("Failed to load pending file opens", error);
      });

    return () => {
      if (unlisten) unlisten();
    };
  }, [handleFilePaths]);

  const handleOpenText = useCallback(async () => {
    const baseDir = await appDataDir();
    const path = await open({
      multiple: false,
      directory: false,
      filters: [{ name: "Text/Markdown", extensions: ["txt", "md"] }],
      defaultPath: baseDir
    });
    if (!path || Array.isArray(path)) return;
    await createTextFromFile(path);
  }, [createTextFromFile]);

  const handleDeleteText = useCallback(
    async (promptId: string) => {
      await deleteText(promptId);
      await refreshTexts();
      if (selectedTextId === promptId) {
        setSelectedTextId(null);
        localStorage.removeItem("textdb.selectedTextId");
      }
    },
    [refreshTexts, selectedTextId]
  );

  const handleSaveVersion = useCallback(async () => {
    if (!selectedTextId || !canSave) return;
    const normalizedTitle = title.trim() || DEFAULT_TITLE;
    if (normalizedTitle !== title) {
      setTitle(normalizedTitle);
    }
    const result = await saveManualVersion(selectedTextId, normalizedTitle, body);
    setLastPersistedBody(body);
    setLastPersistedTitle(normalizedTitle);
    setHasDraft(false);
    setRestoredDraft(false);
    setLatestManualVersionId(result.versionId);
    setDraftBaseVersionId(result.versionId);
    setSelectedHistoryId(result.versionId);
    await refreshTexts();
    await refreshVersions();
  }, [body, canSave, refreshTexts, refreshVersions, selectedTextId, title]);

  const applyTitleUpdate = useCallback((promptId: string, nextTitle: string) => {
    const now = Date.now();
    setTexts((prev) => {
      let updated: Text | null = null;
      const remaining: Text[] = [];
      for (const text of prev) {
        if (text.id === promptId) {
          updated = { ...text, title: nextTitle, updated_at: now };
        } else {
          remaining.push(text);
        }
      }
      if (!updated) return prev;
      return [updated, ...remaining];
    });
  }, []);

  const handleTitleChange = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      const nextTitle = event.target.value;
      setTitle(nextTitle);
      if (!selectedTextId || isViewingHistory) return;
      applyTitleUpdate(selectedTextId, nextTitle);
      setLastPersistedTitle(nextTitle);
      updateTextTitle(selectedTextId, nextTitle).catch((error) => {
        console.error("Failed to update title", error);
      });
    },
    [applyTitleUpdate, isViewingHistory, selectedTextId]
  );

  const handleDiscardDraft = useCallback(async () => {
    if (!selectedTextId || isViewingHistory) return;
    await discardDraft(selectedTextId);
    const manualVersion = await getLatestManualVersion(selectedTextId);
    const resolvedBody = manualVersion?.body ?? "";
    setBody(resolvedBody);
    setLastPersistedBody(resolvedBody);
    setHasDraft(false);
    setRestoredDraft(false);
    setLatestManualVersionId(manualVersion?.id ?? null);
    setDraftBaseVersionId(manualVersion?.id ?? null);
    setSelectedHistoryId(manualVersion?.id ?? null);
    await refreshVersions();
  }, [isViewingHistory, refreshVersions, selectedTextId]);

  const handleExportText = useCallback(async () => {
    if (!hasText) return;
    const filename = `${title.trim() || DEFAULT_TITLE}.txt`;
    const path = await save({
      defaultPath: filename,
      filters: [{ name: "Text", extensions: ["txt"] }]
    });
    if (!path) return;
    const finalPath = path.endsWith(".txt") ? path : `${path}.txt`;
    await writeTextFile(finalPath, body);
  }, [body, hasText, title]);

  const handleExitHistory = useCallback(() => {
    const snapshot = historySnapshotRef.current;
    if (snapshot) {
      setBody(snapshot.body);
      setLastPersistedBody(snapshot.lastPersistedBody);
      setLastPersistedTitle(snapshot.lastPersistedTitle);
      setHasDraft(snapshot.hasDraft);
      setRestoredDraft(snapshot.restoredDraft);
      setDraftBaseVersionId(snapshot.draftBaseVersionId);
      setLatestManualVersionId(snapshot.latestManualVersionId);
    }
    setViewingVersion(null);
    setSelectedHistoryId(null);
    historySnapshotRef.current = null;
  }, []);

  const applyVersionAsCurrent = useCallback(
    (version: HistoryEntry) => {
      const snapshot = historySnapshotRef.current;
      if (snapshot) {
        setLastPersistedBody(snapshot.lastPersistedBody);
        setLastPersistedTitle(snapshot.lastPersistedTitle);
        setHasDraft(snapshot.hasDraft);
        setRestoredDraft(snapshot.restoredDraft);
        setLatestManualVersionId(snapshot.latestManualVersionId);
      }
      setDraftBaseVersionId(
        version.kind === "manual" ? version.id : version.baseVersionId ?? null
      );
      setBody(version.body);
      setLastPersistedBody(version.body);
      setViewingVersion(null);
      setSelectedHistoryId(version.id);
      historySnapshotRef.current = null;
    },
    []
  );

  const handleDeleteVersion = useCallback(
    async (version: HistoryEntry) => {
      if (!selectedTextId) return;

      const currentItems = historyItems;
      const index = currentItems.findIndex((item) => item.id === version.id);
      const olderCandidate =
        index >= 0 ? currentItems[index + 1] ?? null : null;
      const fallbackCandidate =
        index > 0 ? currentItems[index - 1] ?? null : null;
      const shouldAutoSelect =
        historyOpen && (!viewingVersion || viewingVersion.id === version.id);

      if (version.kind === "draft") {
        await discardDraft(selectedTextId);
        setHasDraft(false);
        setRestoredDraft(false);
        setDraftBaseVersionId(latestManualVersionId ?? null);
        if (historySnapshotRef.current) {
          historySnapshotRef.current.hasDraft = false;
          historySnapshotRef.current.restoredDraft = false;
          historySnapshotRef.current.draftBaseVersionId =
            latestManualVersionId ?? null;
        }
      } else {
        await deleteTextVersion(selectedTextId, version.id);
      }
      await refreshVersions();

      if (version.kind === "manual" && latestManualVersionId === version.id) {
        const manualVersion = await getLatestManualVersion(selectedTextId);
        const resolvedBody = manualVersion?.body ?? "";
        const nextManualId = manualVersion?.id ?? null;
        setLatestManualVersionId(nextManualId);
        setDraftBaseVersionId(nextManualId);
        if (historySnapshotRef.current) {
          historySnapshotRef.current.latestManualVersionId = nextManualId;
          historySnapshotRef.current.draftBaseVersionId = nextManualId;
        }
        if (!hasDraft && !isViewingHistory) {
          setBody(resolvedBody);
          setLastPersistedBody(resolvedBody);
        }
      }

      if (shouldAutoSelect) {
        if (olderCandidate) {
          applyVersionAsCurrent(olderCandidate);
        } else if (fallbackCandidate) {
          applyVersionAsCurrent(fallbackCandidate);
        } else {
          handleExitHistory();
        }
      }
    },
    [
      applyVersionAsCurrent,
      handleExitHistory,
      hasDraft,
      isViewingHistory,
      historyOpen,
      latestManualVersionId,
      refreshVersions,
      selectedTextId,
      historyItems,
      viewingVersion
    ]
  );

  const handleConfirm = useCallback(async () => {
    if (!confirmState) return;
    const action = confirmState.onConfirm;
    setConfirmState(null);
    try {
      await action();
    } catch (error) {
      console.error("Delete failed", error);
    }
  }, [confirmState]);

  const handleTitleBlur = useCallback(async () => {
    if (!selectedTextId || isViewingHistory) return;
    const normalizedTitle = title.trim() || DEFAULT_TITLE;
    if (normalizedTitle !== title) {
      setTitle(normalizedTitle);
      applyTitleUpdate(selectedTextId, normalizedTitle);
    }
    if (normalizedTitle === lastPersistedTitle) return;
    await updateTextTitle(selectedTextId, normalizedTitle);
    setLastPersistedTitle(normalizedTitle);
    await refreshTexts();
  }, [
    applyTitleUpdate,
    isViewingHistory,
    lastPersistedTitle,
    refreshTexts,
    selectedTextId,
    title
  ]);

  const handleToggleHistory = useCallback(() => {
    if (historyOpen && viewingVersion) {
      const snapshot = historySnapshotRef.current;
      if (snapshot) {
        setBody(snapshot.body);
        setLastPersistedBody(snapshot.lastPersistedBody);
        setLastPersistedTitle(snapshot.lastPersistedTitle);
        setHasDraft(snapshot.hasDraft);
        setRestoredDraft(snapshot.restoredDraft);
        setDraftBaseVersionId(snapshot.draftBaseVersionId);
        setLatestManualVersionId(snapshot.latestManualVersionId);
      }
      setViewingVersion(null);
      historySnapshotRef.current = null;
    }
    setHistoryOpen((prev) => !prev);
  }, [historyOpen, viewingVersion]);

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      const isSave =
        (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s";
      if (isSave) {
        event.preventDefault();
        handleSaveVersion().catch((error) => {
          console.error("Failed to save version", error);
        });
        return;
      }

      if (
        event.key === "Tab" &&
        !event.metaKey &&
        !event.ctrlKey &&
        !event.altKey &&
        selectedTextId &&
        !settingsOpen &&
        !confirmState
      ) {
        event.preventDefault();
        setMarkdownPreview((value) => !value);
      }
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [confirmState, handleSaveVersion, selectedTextId, settingsOpen]);

  return (
    <div className={`app app--theme-${theme}${sidebarCollapsed ? " app--sidebar-collapsed" : ""}`}>
      {!sidebarCollapsed ? (
      <aside className="sidebar">
        <div className="sidebar__header">
          <div className="sidebar__title-row">
            <div className="app-title">TextDB</div>
            <div className="sidebar__actions">
              <button
                className="icon-button icon-button--ghost"
                onClick={() => setSettingsOpen(true)}
                aria-label="Open settings"
                title="Settings"
                type="button"
              >
                <span className="icon-button__glyph icon-button__glyph--large" aria-hidden="true">⚙</span>
              </button>
              <button
                className="icon-button icon-button--ghost"
                onClick={() => setSidebarCollapsed(true)}
                aria-label="Collapse sidebar"
                title="Collapse sidebar"
                type="button"
              >
                <span aria-hidden="true">&lt;</span>
              </button>
            </div>
          </div>
          <input
            className="search"
            placeholder="Search texts"
            value={search}
            onChange={(event) => setSearch(event.target.value)}
          />
        </div>
        <div className="prompt-list">
          <div className="prompt-list__inner">
            {loadingTexts ? (
              <div className="empty">Loading texts…</div>
            ) : texts.length === 0 ? (
              <div className="empty">No texts yet.</div>
            ) : (
              texts.map((text) => (
                <div
                  key={text.id}
                  className={`prompt-item${
                    text.id === selectedTextId ? " is-active" : ""
                  }`}
                  onClick={() => setSelectedTextId(text.id)}
                  role="button"
                  tabIndex={0}
                  onKeyDown={(event) => {
                    if (event.key === "Enter" || event.key === " ") {
                      event.preventDefault();
                      setSelectedTextId(text.id);
                    }
                  }}
                >
                  <div className="prompt-item__content">
                    <div className="prompt-item__title">{text.title}</div>
                    <div className="prompt-item__meta">
                      Updated {formatDate(text.updated_at)}
                    </div>
                  </div>
                  <button
                    className="prompt-item__delete"
                    onClick={(event) => {
                      event.stopPropagation();
                      setConfirmState({
                        title: "Delete text",
                        message: `Delete \"${text.title}\"? This removes all versions and drafts.`,
                        actionLabel: "Delete text",
                        onConfirm: () => handleDeleteText(text.id)
                      });
                    }}
                    aria-label="Delete text"
                    title="Delete text"
                  >
                    ×
                  </button>
                </div>
              ))
            )}
          </div>
        </div>
        <div className="sidebar__footer">
          <button className="button button--primary" onClick={handleNewText}>
            New Text
          </button>
          <button className="button" onClick={handleOpenText}>
            Open Text
          </button>
        </div>
      </aside>
      ) : null}

      <main className="workspace">
        {sidebarCollapsed ? (
          <div className="workspace__topbar">
            <button
              className="icon-button icon-button--ghost"
              onClick={() => setSidebarCollapsed(false)}
              aria-label="Expand sidebar"
              title="Expand sidebar"
              type="button"
            >
              <span aria-hidden="true">&gt;</span>
            </button>
          </div>
        ) : null}
        {!selectedTextId ? (
          <div className="empty-state">
            <div className="empty-state__title">Create your first text</div>
            <div className="empty-state__subtitle">
              Everything stays offline in a single SQLite database.
            </div>
            <button className="button button--primary" onClick={handleNewText}>
              New Text
            </button>
          </div>
        ) : (
          <div
            className={`workspace__content${
              historyOpen ? " workspace__content--history" : ""
            }`}
          >
            <section className="editor">
              <div className="editor__header">
                <div className="editor__title-row">
                  <input
                    className="title-input"
                    value={title}
                    onChange={handleTitleChange}
                    onBlur={handleTitleBlur}
                    placeholder="Text title"
                    disabled={isViewingHistory}
                  />
                  <button
                    className={`icon-button${historyOpen ? " is-active" : ""}`}
                    onClick={handleToggleHistory}
                    aria-label={historyOpen ? "Close history" : "Open history"}
                    title={historyOpen ? "Close history" : "Open history"}
                    type="button"
                  >
                    <img src={historyIconSrc} alt="" className="icon-button__img" />
                  </button>
                </div>
              </div>

              <div
                className={`editor__textarea-wrap${
                  markdownPreview ? " editor__textarea-wrap--preview" : ""
                }`}
              >
                {showLineNumbersActive ? (
                  <div className="line-measure" ref={measureRef} aria-hidden="true">
                    {lines.map((line, index) => (
                      <div key={index} className="line-measure__line">
                        {line.length > 0 ? line : " "}
                      </div>
                    ))}
                  </div>
                ) : null}
                {showLineNumbersActive ? (
                  <div className="line-numbers" ref={lineNumbersRef}>
                    {lineNumbers.map((line, index) => (
                      <div
                        key={line}
                        className="line-numbers__line"
                        style={{ height: lineHeights[index] ? `${lineHeights[index]}px` : undefined }}
                      >
                        {line}
                      </div>
                    ))}
                  </div>
                ) : null}
                {markdownPreview ? (
                  <div
                    className="markdown-preview md-root"
                    dangerouslySetInnerHTML={{ __html: markdownToHTML(body) }}
                    onClick={handleMarkdownPreviewClick}
                  />
                ) : (
                  <textarea
                    ref={textareaRef}
                    className="editor__textarea"
                    value={body}
                    onChange={(event) => setBody(event.target.value)}
                    onScroll={handleTextareaScroll}
                    placeholder="Write your text here…"
                    readOnly={isViewingHistory}
                  />
                )}
              </div>

              <div className="editor__footer">
                {hasText ? (
                  <>
                    <button
                      className="button"
                      type="button"
                      onClick={() => setMarkdownPreview((value) => !value)}
                    >
                      {markdownPreview ? "Edit" : "Preview Markdown"}
                    </button>
                    <button className="button" onClick={handleExportText}>
                      Export Text
                    </button>
                    {markdownPreview ? (
                      <>
                        <button className="button" type="button" onClick={handlePrintMarkdown}>
                          Print
                        </button>
                      </>
                    ) : null}
                  </>
                ) : null}
                {hasDraft && !isViewingHistory ? (
                  <button
                    className="button"
                    onClick={() =>
                      setConfirmState({
                        title: "Discard draft",
                        message: "Discard this draft? This cannot be undone.",
                        actionLabel: "Discard draft",
                        onConfirm: handleDiscardDraft
                      })
                    }
                  >
                    Discard Draft
                  </button>
                ) : null}
                <div className="editor__footer-status">
                  <div className="status-line">
                    <span className={`status status--${statusKey}`}></span>
                    {statusLabel}
                  </div>
                </div>
                <button
                  className="button button--primary button--save"
                  onClick={handleSaveVersion}
                  disabled={!canSave}
                >
                  Save Version (⌘S)
                </button>
              </div>
            </section>

            {historyOpen ? (
              <aside className="history">
                <div className="history__header">
                  <span>History</span>
                  <button
                    className="history__close"
                    onClick={handleToggleHistory}
                    aria-label="Close history"
                    title="Close history"
                    type="button"
                  >
                    ×
                  </button>
                </div>
                <div className="history__list">
                  {historyItems.length === 0 ? (
                    <div className="empty">No versions yet.</div>
                  ) : (
                    historyItems.map((version) => (
                      <div
                        key={version.id}
                        className={`history__item${
                          selectedHistoryId === version.id ? " is-active" : ""
                        }`}
                        onClick={() => applyVersionAsCurrent(version)}
                        role="button"
                        tabIndex={0}
                        onKeyDown={(event) => {
                          if (event.key === "Enter" || event.key === " ") {
                            event.preventDefault();
                            applyVersionAsCurrent(version);
                          }
                        }}
                      >
                        <div className="history__item-content">
                          <div className="history__item-title">
                            {formatDate(version.created_at)}
                          </div>
                          <div className="history__item-meta">
                            {version.kind === "draft" ? "Draft" : "Manual save"}
                          </div>
                        </div>
                        <button
                          className="history__item-delete"
                          onClick={(event) => {
                            event.stopPropagation();
                            setConfirmState({
                              title:
                                version.kind === "draft"
                                  ? "Discard draft"
                                  : "Delete version",
                              message:
                                version.kind === "draft"
                                  ? "Discard this draft? This cannot be undone."
                                  : "Delete this version? This cannot be undone.",
                              actionLabel:
                                version.kind === "draft"
                                  ? "Discard draft"
                                  : "Delete version",
                              onConfirm: () => handleDeleteVersion(version)
                            });
                          }}
                          aria-label={
                            version.kind === "draft"
                              ? "Discard draft"
                              : "Delete version"
                          }
                          title={
                            version.kind === "draft"
                              ? "Discard draft"
                              : "Delete version"
                          }
                        >
                          ×
                        </button>
                      </div>
                    ))
                  )}
                </div>
              </aside>
            ) : null}
          </div>
        )}
      </main>

      {settingsOpen ? (
        <div className="settings-overlay">
          <div
            className="settings-overlay__backdrop"
            onClick={() => setSettingsOpen(false)}
          />
          <div className="settings-panel" role="dialog" aria-modal="true">
            <div className="settings-panel__header">
              <div className="settings-panel__title">Settings</div>
              <button
                className="icon-button icon-button--ghost"
                onClick={() => setSettingsOpen(false)}
                aria-label="Close settings"
                title="Close settings"
                type="button"
              >
                <span aria-hidden="true">×</span>
              </button>
            </div>
            <div className="settings-panel__section">
              <label className="settings-panel__label" htmlFor="theme-select">
                Theme
              </label>
              <select
                id="theme-select"
                className="settings-panel__select"
                value={theme}
                onChange={(event) =>
                  setTheme(event.target.value as "default" | "light")
                }
              >
                <option value="default">Default</option>
                <option value="light">Bright</option>
              </select>
            </div>
            <div className="settings-panel__section settings-panel__section--row">
              <label className="settings-panel__label" htmlFor="line-numbers-toggle">
                Line numbers
              </label>
              <input
                id="line-numbers-toggle"
                type="checkbox"
                checked={showLineNumbers}
                onChange={(event) => setShowLineNumbers(event.target.checked)}
              />
            </div>
            <div className="settings-panel__section">
              <label className="settings-panel__label" htmlFor="text-size">
                Text size
              </label>
              <div className="settings-panel__slider-row">
                <input
                  id="text-size"
                  className="settings-panel__range"
                  type="range"
                  min={12}
                  max={18}
                  step={1}
                  value={textSize}
                  onChange={(event) => setTextSize(Number(event.target.value))}
                />
                <div className="settings-panel__value">{textSize}px</div>
              </div>
            </div>
          </div>
        </div>
      ) : null}

      {confirmState ? (
        <div className="modal">
          <div className="modal__overlay" onClick={() => setConfirmState(null)} />
          <div className="modal__card" role="dialog" aria-modal="true">
            <div className="modal__title">{confirmState.title}</div>
            <div className="modal__message">{confirmState.message}</div>
            <div className="modal__actions">
              <button className="button" onClick={() => setConfirmState(null)}>
                Cancel
              </button>
              <button className="button button--danger" onClick={handleConfirm}>
                {confirmState.actionLabel ?? "Delete"}
              </button>
            </div>
          </div>
        </div>
      ) : null}
    </div>
  );
}
