<script setup lang="ts">
import {computed, ref, onMounted, onUnmounted, nextTick} from "vue";
import {listen, type EventCallback, type EventName} from "@tauri-apps/api/event";
import {convertFileSrc, invoke, isTauri} from "@tauri-apps/api/core";
import {getCurrentWindow, type CloseRequestedEvent} from "@tauri-apps/api/window";
import MarkdownEditor from "./components/MarkdownEditor.vue";
import DirectorySidebar from "./components/DirectorySidebar.vue";
import OutlinePanel from "./components/OutlinePanel.vue";

const content = ref("");
const wordCount = ref(0);
const charCount = ref(0);

type DocumentMode = "preview" | "edit" | "mixed";
type AppTheme = "system" | "warm" | "white" | "black" | "gruvbox" | "gray";
type WorkspaceFileType = "markdown" | "image" | "video";
type SyncProvider = "none" | "webdav" | "s3";
const DEFAULT_DOCUMENT_MODE_KEY = "yurou:default-document-mode";
const APP_THEME_KEY = "yurou:app-theme";
const RENDER_SINGLE_LINE_BREAKS_KEY = "yurou:render-single-line-breaks";
const PARAGRAPH_INDENT_KEY = "yurou:paragraph-indent";
const MARKDOWN_EXTENSIONS = new Set(["md"]);
const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "avif"]);
const VIDEO_EXTENSIONS = new Set(["mp4", "webm", "mov", "m4v", "ogv"]);
const VIDEO_MIME_TYPES: Record<string, string> = {
  mp4: "video/mp4",
  webm: "video/webm",
  mov: "video/quicktime",
  m4v: "video/mp4",
  ogv: "video/ogg",
};

function isDocumentMode(value: string | null): value is DocumentMode {
  return value === "preview" || value === "edit" || value === "mixed";
}

function loadDefaultDocumentMode(): DocumentMode {
  try {
    const stored = localStorage.getItem(DEFAULT_DOCUMENT_MODE_KEY);
    if (isDocumentMode(stored)) return stored;
  } catch { /* localStorage unavailable */
  }
  return "edit";
}

function saveDefaultDocumentMode(mode: DocumentMode) {
  try {
    localStorage.setItem(DEFAULT_DOCUMENT_MODE_KEY, mode);
  } catch { /* noop */
  }
}

function isAppTheme(value: string | null): value is AppTheme {
  return value === "system" || value === "warm" || value === "white" || value === "black" || value === "gruvbox" || value === "gray";
}

function loadAppTheme(): AppTheme {
  try {
    const stored = localStorage.getItem(APP_THEME_KEY);
    if (isAppTheme(stored)) return stored;
  } catch { /* localStorage unavailable */
  }
  return "warm";
}

function saveAppTheme(theme: AppTheme) {
  try {
    localStorage.setItem(APP_THEME_KEY, theme);
  } catch { /* noop */
  }
}

function applyAppTheme(theme: AppTheme) {
  document.documentElement.dataset.theme = theme;
}

// File state
const currentFilePath = ref<string | null>(null);
const currentFileType = ref<WorkspaceFileType>("markdown");
const mediaPreviewSrc = ref("");
const mediaPreviewError = ref("");
const mediaPreviewKey = ref(0);
const saveStatus = ref("");
const saveNotice = ref("");
const saveNoticeType = ref<"success" | "error">("success");
const hasDraftDocument = ref(false);
let lastSavedContent = "";

// Folder state
interface DirEntry {
  name: string;
  path: string;
  is_dir: boolean;
  children?: DirEntry[];
}

interface WorkspaceEntry {
  name: string;
  path: string;
}

interface WorkspaceChangedPayload {
  root: string;
  paths: string[];
  kind: string;
}

interface WorkspaceWatchErrorPayload {
  root: string;
  message: string;
}

function loadRenderSingleLineBreaks() {
  try {
    return localStorage.getItem(RENDER_SINGLE_LINE_BREAKS_KEY) === "true";
  } catch {
    return false;
  }
}

function saveRenderSingleLineBreaks(value: boolean) {
  try {
    localStorage.setItem(RENDER_SINGLE_LINE_BREAKS_KEY, String(value));
  } catch { /* noop */
  }
}

function loadParagraphIndent() {
  try {
    return localStorage.getItem(PARAGRAPH_INDENT_KEY) === "true";
  } catch {
    return false;
  }
}

function saveParagraphIndent(value: boolean) {
  try {
    localStorage.setItem(PARAGRAPH_INDENT_KEY, String(value));
  } catch { /* noop */
  }
}

interface SyncConfig {
  provider: SyncProvider;
  webdav: {
    endpoint: string;
    username: string;
    password: string;
    remoteDir: string;
  };
  s3: {
    endpoint: string;
    region: string;
    bucket: string;
    accessKeyId: string;
    secretAccessKey: string;
    remotePrefix: string;
    pathStyle: boolean;
    useHttps: boolean;
  };
  autoSyncOnStartup: boolean;
  autoSyncAfterSave: boolean;
  lastSyncAt: string | null;
}

interface SyncResult {
  message: string;
  lastSyncAt: string;
  stats: {
    uploaded: number;
    downloaded: number;
    deletedLocal: number;
    deletedRemote: number;
    skipped: number;
  };
}

const folderPath = ref("");
const dirEntries = ref<DirEntry[]>([]);
const LAST_WORKSPACE_PATH_KEY = "yurou:last-workspace-path";
const RECENT_WORKSPACES_KEY = "yurou:recent-workspaces";
const MAX_RECENT_WORKSPACES = 12;
const recentWorkspaces = ref<WorkspaceEntry[]>(loadRecentWorkspaces());
const editorResetKey = ref(0);
const WORKSPACE_REFRESH_DELAY_MS = 140;
let workspaceRefreshTimer: ReturnType<typeof setTimeout> | null = null;
let workspaceRefreshInFlight = false;
let workspaceRefreshQueued = false;
const syncInFlight = ref(false);
let fileOpenRequest = 0;
let saveNoticeTimer: ReturnType<typeof setTimeout> | null = null;
let closeSaveInFlight = false;

// ---- editor zoom (Ctrl+scroll) ----
const ZOOM_MIN = 0.7;
const ZOOM_MAX = 1.5;
const ZOOM_STEP = 0.1;

function loadZoomPref(): number {
  try {
    const stored = localStorage.getItem("editor-zoom");
    if (stored) {
      const val = parseFloat(stored);
      if (!isNaN(val) && val >= ZOOM_MIN && val <= ZOOM_MAX) return val;
    }
  } catch { /* localStorage unavailable */
  }
  return 1.0;
}

const editorZoom = ref(loadZoomPref());
const showZoomIndicator = ref(false);
let zoomIndicatorTimer: ReturnType<typeof setTimeout> | null = null;

function applyZoomCssVars() {
  document.documentElement.style.setProperty("--editor-zoom", String(editorZoom.value));
}

function saveZoomPref() {
  try {
    localStorage.setItem("editor-zoom", String(editorZoom.value));
  } catch { /* noop */
  }
}

function onEditorWheel(e: WheelEvent) {
  if (!e.ctrlKey && !e.metaKey) return;
  e.preventDefault();
  const delta = e.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP;
  const next = Math.round((editorZoom.value + delta) * 10) / 10;
  editorZoom.value = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, next));
  applyZoomCssVars();
  saveZoomPref();

  // Show zoom indicator and reset timer
  showZoomIndicator.value = true;
  if (zoomIndicatorTimer) clearTimeout(zoomIndicatorTimer);
  zoomIndicatorTimer = setTimeout(() => {
    showZoomIndicator.value = false;
  }, 1500);
}

// ---- toolbar state ----
const showOutline = ref(false);
const defaultDocumentMode = ref<DocumentMode>(loadDefaultDocumentMode());
const documentMode = ref<DocumentMode>(defaultDocumentMode.value);
const appTheme = ref<AppTheme>(loadAppTheme());
const renderSingleLineBreaks = ref(loadRenderSingleLineBreaks());
const paragraphIndent = ref(loadParagraphIndent());
const settingsRequest = ref(0);
const settingsInitialPage = ref<"about" | "editor" | "sync">("about");
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined; scrollToLine: (line: number) => void } | null>(null);
const directorySidebarRef = ref<{ activateSearch: () => void } | null>(null);
const isWindowMaximized = ref(false);
const isMarkdownDocument = computed(() => currentFileType.value === "markdown");
const currentVideoMimeType = computed(() => {
  if (!currentFilePath.value) return "";
  return VIDEO_MIME_TYPES[getFileExtension(currentFilePath.value)] || "";
});
const showStartActions = computed(() => {
  return !folderPath.value && !currentFilePath.value && !hasDraftDocument.value;
});

type TitlebarDragState = {
  startX: number;
  startY: number;
};

const TITLEBAR_DRAG_THRESHOLD = 4;
let titlebarDragState: TitlebarDragState | null = null;
let unlistenWindowResize: (() => void) | null = null;
let unlistenWindowCloseRequested: (() => void) | null = null;

function isInteractiveTitlebarTarget(target: EventTarget | null) {
  if (!(target instanceof Element)) return false;
  return Boolean(target.closest("button, a, input, textarea, select, [data-no-window-drag]"));
}

async function refreshWindowMaximized() {
  if (!isTauri()) return;
  try {
    isWindowMaximized.value = await getCurrentWindow().isMaximized();
  } catch (error) {
    console.error("Failed to read window state:", error);
  }
}

async function minimizeWindow() {
  if (!isTauri()) return;
  try {
    await getCurrentWindow().minimize();
  } catch (error) {
    console.error("Failed to minimize window:", error);
  }
}

async function toggleWindowMaximize() {
  if (!isTauri()) return;
  try {
    const appWindow = getCurrentWindow();
    await appWindow.toggleMaximize();
    isWindowMaximized.value = await appWindow.isMaximized();
  } catch (error) {
    console.error("Failed to toggle window maximize:", error);
  }
}

async function closeWindow() {
  if (!isTauri()) return;
  try {
    await getCurrentWindow().close();
  } catch (error) {
    console.error("Failed to close window:", error);
  }
}

function removeTitlebarDragListeners() {
  window.removeEventListener("mousemove", onTitlebarMouseMove);
  window.removeEventListener("mouseup", removeTitlebarDragListeners);
  titlebarDragState = null;
}

function onTitlebarMouseDown(event: MouseEvent) {
  if (event.button !== 0 || event.detail > 1 || isInteractiveTitlebarTarget(event.target)) return;

  event.preventDefault();
  titlebarDragState = {
    startX: event.screenX,
    startY: event.screenY,
  };
  window.addEventListener("mousemove", onTitlebarMouseMove);
  window.addEventListener("mouseup", removeTitlebarDragListeners, {once: true});
}

function onTitlebarMouseMove(event: MouseEvent) {
  if (!titlebarDragState) return;

  const movedX = Math.abs(event.screenX - titlebarDragState.startX);
  const movedY = Math.abs(event.screenY - titlebarDragState.startY);
  if (movedX < TITLEBAR_DRAG_THRESHOLD && movedY < TITLEBAR_DRAG_THRESHOLD) return;

  event.preventDefault();
  removeTitlebarDragListeners();
  if (!isTauri()) return;

  void getCurrentWindow().startDragging().catch((error) => {
    console.error("Failed to start window drag:", error);
  });
}

function onTitlebarDoubleClick(event: MouseEvent) {
  if (isInteractiveTitlebarTarget(event.target)) return;

  event.preventDefault();
  removeTitlebarDragListeners();
  void toggleWindowMaximize();
}

function openSettings(page: "about" | "editor" | "sync" = "about") {
  settingsInitialPage.value = page;
  settingsRequest.value += 1;
}

async function onWindowCloseRequested(event: CloseRequestedEvent) {
  if (closeSaveInFlight) {
    event.preventDefault();
    return;
  }

  closeSaveInFlight = true;
  const shouldShowClosingSave = isCurrentMarkdownDocumentDirty();
  const saved = await saveCurrentWorkspaceDocumentBeforeSwitch();
  if (!saved) {
    closeSaveInFlight = false;
    event.preventDefault();
    return;
  }

  if (shouldShowClosingSave) {
    await new Promise((resolve) => setTimeout(resolve, 700));
  }
}

function setDocumentMode(mode: DocumentMode) {
  documentMode.value = mode;
}

function updateDefaultDocumentMode(mode: DocumentMode) {
  defaultDocumentMode.value = mode;
  documentMode.value = mode;
  saveDefaultDocumentMode(mode);
}

function updateAppTheme(theme: AppTheme) {
  appTheme.value = theme;
  applyAppTheme(theme);
  saveAppTheme(theme);
}

onMounted(() => {
  applyAppTheme(appTheme.value);
  applyZoomCssVars();
  window.addEventListener("wheel", onEditorWheel, {passive: false});
  if (isTauri()) {
    void refreshWindowMaximized();
    void getCurrentWindow().onResized(() => {
      void refreshWindowMaximized();
    }).then((unlisten) => {
      unlistenWindowResize = unlisten;
    }).catch((error) => {
      console.error("Failed to listen for window resize:", error);
    });
    void getCurrentWindow().onCloseRequested(onWindowCloseRequested).then((unlisten) => {
      unlistenWindowCloseRequested = unlisten;
    }).catch((error) => {
      console.error("Failed to listen for window close requests:", error);
    });
  }
  void restoreLastWorkspace();
});

onUnmounted(() => {
  window.removeEventListener("wheel", onEditorWheel);
  removeTitlebarDragListeners();
  if (unlistenWindowResize) {
    unlistenWindowResize();
    unlistenWindowResize = null;
  }
  if (unlistenWindowCloseRequested) {
    unlistenWindowCloseRequested();
    unlistenWindowCloseRequested = null;
  }
  if (zoomIndicatorTimer) clearTimeout(zoomIndicatorTimer);
  if (saveNoticeTimer) clearTimeout(saveNoticeTimer);
});

function stripMarkdown(md: string): string {
  return md
      // Remove images ![alt](url)
      .replace(/!\[[^\]]*\]\([^)]*\)/g, "")
      // Convert links [text](url) to just text
      .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
      // Remove heading markers
      .replace(/^#{1,6}\s+/gm, "")
      // Remove bold/italic markers
      .replace(/\*{1,3}|_{1,3}/g, "")
      // Remove strikethrough
      .replace(/~~/g, "")
      // Remove inline code markers
      .replace(/`+/g, "")
      // Remove blockquote markers
      .replace(/^>\s?/gm, "")
      // Remove list markers (-, *, +, or 1.)
      .replace(/^[\s]*[-*+]\s+/gm, "")
      .replace(/^[\s]*\d+\.\s+/gm, "")
      // Remove horizontal rules
      .replace(/^[-*_]{3,}\s*$/gm, "")
      .trim();
}

function onUpdate(md: string) {
  content.value = md;
  updateDocumentMetrics(md);
}

function updateDocumentMetrics(md: string) {
  const text = stripMarkdown(md);
  charCount.value = text.length;
  wordCount.value = text.trim() ? text.trim().split(/\s+/).length : 0;
}

function setDocumentContent(md: string) {
  content.value = md;
  updateDocumentMetrics(md);
}

function resetMarkdownEditorState() {
  fileOpenRequest += 1;
  setDocumentContent("");
  lastSavedContent = "";
  currentFilePath.value = null;
  currentFileType.value = "markdown";
  resetMediaPreviewState();
  hasDraftDocument.value = false;
  saveStatus.value = "";
  showOutline.value = false;
  documentMode.value = defaultDocumentMode.value;
  editorResetKey.value += 1;
}

function onHeadingClick(heading: { text: string; level: number }) {
  const selector = `.markdown-preview h${heading.level}`;
  const headingEls = document.querySelectorAll(selector);
  for (const el of headingEls) {
    if (el.textContent?.trim() === heading.text) {
      el.scrollIntoView({behavior: "smooth", block: "start"});
      break;
    }
  }
}

function getLastWorkspacePath() {
  try {
    return localStorage.getItem(LAST_WORKSPACE_PATH_KEY);
  } catch {
    return null;
  }
}

function saveLastWorkspacePath(path: string) {
  try {
    localStorage.setItem(LAST_WORKSPACE_PATH_KEY, path);
  } catch { /* noop */
  }
}

function clearLastWorkspacePath() {
  try {
    localStorage.removeItem(LAST_WORKSPACE_PATH_KEY);
  } catch { /* noop */
  }
}

function normalizeWorkspacePath(path: string) {
  const trimmed = path.trim();
  if (!trimmed) return "";

  const normalized = trimmed.replace(/[\\/]+$/, "");
  if (!normalized) return trimmed[0] || "";
  if (/^[A-Za-z]:$/.test(normalized) && /^[A-Za-z]:[\\/]+$/.test(trimmed)) {
    return `${normalized}\\`;
  }

  return normalized;
}

function getWorkspaceIdentity(path: string) {
  return normalizeWorkspacePath(path).toLocaleLowerCase();
}

function getWorkspaceName(path: string): string {
  const normalized = normalizeWorkspacePath(path);
  return normalized.split(/[/\\]/).pop() || normalized || path;
}

function createWorkspaceEntry(path: string): WorkspaceEntry {
  const normalized = normalizeWorkspacePath(path);
  return {
    name: getWorkspaceName(normalized),
    path: normalized,
  };
}

function getStoredWorkspacePath(item: unknown) {
  if (typeof item === "string") return item;
  if (item && typeof item === "object") {
    const maybeWorkspace = item as { path?: unknown };
    if (typeof maybeWorkspace.path === "string") return maybeWorkspace.path;
  }
  return null;
}

function dedupeWorkspaces(paths: string[]) {
  const seen = new Set<string>();
  const workspaces: WorkspaceEntry[] = [];

  for (const path of paths) {
    const normalized = normalizeWorkspacePath(path);
    if (!normalized) continue;

    const identity = getWorkspaceIdentity(normalized);
    if (seen.has(identity)) continue;

    seen.add(identity);
    workspaces.push(createWorkspaceEntry(normalized));
  }

  return workspaces.slice(0, MAX_RECENT_WORKSPACES);
}

function loadRecentWorkspaces(): WorkspaceEntry[] {
  try {
    const stored = localStorage.getItem(RECENT_WORKSPACES_KEY);
    if (!stored) return [];

    const parsed = JSON.parse(stored) as unknown;
    if (!Array.isArray(parsed)) return [];

    return dedupeWorkspaces(
      parsed
        .map(getStoredWorkspacePath)
        .filter((path): path is string => Boolean(path)),
    );
  } catch {
    return [];
  }
}

function saveRecentWorkspaces(workspaces: WorkspaceEntry[] = recentWorkspaces.value) {
  try {
    localStorage.setItem(RECENT_WORKSPACES_KEY, JSON.stringify(workspaces));
  } catch { /* noop */
  }
}

function rememberWorkspace(path: string) {
  const normalized = normalizeWorkspacePath(path);
  if (!normalized) return;

  recentWorkspaces.value = dedupeWorkspaces([
    normalized,
    ...recentWorkspaces.value.map((workspace) => workspace.path),
  ]);
  saveRecentWorkspaces();
}

function forgetWorkspace(path: string) {
  const identity = getWorkspaceIdentity(path);
  recentWorkspaces.value = recentWorkspaces.value.filter((workspace) => {
    return getWorkspaceIdentity(workspace.path) !== identity;
  });
  saveRecentWorkspaces();
}

async function removeRecentWorkspace(path: string) {
  const isCurrent = Boolean(folderPath.value) && getWorkspaceIdentity(folderPath.value) === getWorkspaceIdentity(path);

  if (isCurrent) {
    const saved = await saveCurrentWorkspaceDocumentBeforeSwitch();
    if (!saved) return;

    resetWorkspaceAndEditorState();
    clearLastWorkspacePath();
  }

  forgetWorkspace(path);
}

async function loadDir(path: string): Promise<boolean> {
  const normalized = normalizeWorkspacePath(path);
  try {
    const entries = await invoke<DirEntry[]>("read_dir", {path: normalized});
    folderPath.value = normalized;
    dirEntries.value = entries;
    return true;
  } catch (e) {
    console.error("Failed to read directory:", e);
    return false;
  }
}

async function saveCurrentWorkspaceDocumentBeforeSwitch() {
  if (!isMarkdownDocument.value) return true;
  if (!currentFilePath.value && !hasDraftDocument.value) return true;

  const contentToSave = getLatestEditorContent();
  if (!currentFilePath.value && !contentToSave.trim()) return true;
  if (contentToSave === lastSavedContent) return true;

  return doSave(contentToSave, { syncAfterSave: false, trigger: "automatic" });
}

function updateRenderSingleLineBreaks(value: boolean) {
  renderSingleLineBreaks.value = value;
  saveRenderSingleLineBreaks(value);
}

function updateParagraphIndent(value: boolean) {
  paragraphIndent.value = value;
  saveParagraphIndent(value);
}

function resetWorkspaceAndEditorState(options: { stopWatcher?: boolean } = {}) {
  if (options.stopWatcher !== false) {
    void stopWorkspaceWatcher();
  } else {
    clearPendingWorkspaceRefresh();
  }
  folderPath.value = "";
  dirEntries.value = [];
  resetMarkdownEditorState();
}

async function startWorkspaceWatcher(path: string) {
  if (!isTauri()) return;
  try {
    await invoke("watch_workspace", {path});
  } catch (e) {
    console.error("Failed to watch workspace:", e);
  }
}

async function stopWorkspaceWatcher() {
  clearPendingWorkspaceRefresh();
  if (!isTauri()) return;
  try {
    await invoke("unwatch_workspace");
  } catch (e) {
    console.error("Failed to stop watching workspace:", e);
  }
}

async function switchWorkspace(path: string, options: { persist?: boolean; resetEditor?: boolean } = {}) {
  const normalized = normalizeWorkspacePath(path);

  if (options.resetEditor !== false) {
    const saved = await saveCurrentWorkspaceDocumentBeforeSwitch();
    if (!saved) return false;
    resetWorkspaceAndEditorState({stopWatcher: false});
  }

  const loaded = await loadDir(normalized);

  if (!loaded) {
    await stopWorkspaceWatcher();
    forgetWorkspace(normalized);

    const lastWorkspacePath = getLastWorkspacePath();
    if (lastWorkspacePath && getWorkspaceIdentity(lastWorkspacePath) === getWorkspaceIdentity(normalized)) {
      clearLastWorkspacePath();
    }

    return false;
  }

  void startWorkspaceWatcher(normalized);
  rememberWorkspace(normalized);

  if (options.persist !== false) {
    saveLastWorkspacePath(normalized);
  }

  return true;
}

async function loadSyncConfig(): Promise<SyncConfig | null> {
  try {
    return await invoke<SyncConfig>("load_sync_config");
  } catch (e) {
    console.error("Failed to load sync config:", e);
    return null;
  }
}

function shouldRunSync(config: SyncConfig | null, trigger: "startup" | "save") {
  if (!config || config.provider === "none") return false;
  if (trigger === "startup") return config.autoSyncOnStartup;
  return config.autoSyncAfterSave;
}

function describeSyncResult(result: SyncResult) {
  const { stats } = result;
  return `同步完成: 上传 ${stats.uploaded}, 下载 ${stats.downloaded}, 删除本地 ${stats.deletedLocal}, 删除远端 ${stats.deletedRemote}`;
}

async function refreshCurrentOpenFileFromDisk() {
  if (!currentFilePath.value) return;
  if (!folderPath.value) return;

  try {
    if (isMarkdownDocument.value) {
      const fileContent = await invoke<string>("read_file", { path: currentFilePath.value });
      setDocumentContent(fileContent);
      lastSavedContent = fileContent;
      hasDraftDocument.value = false;
      editorResetKey.value += 1;
      return;
    }

    if (currentFileType.value === "image" || currentFileType.value === "video") {
      mediaPreviewSrc.value = getLocalAssetSrc(currentFilePath.value);
      mediaPreviewError.value = "";
      mediaPreviewKey.value += 1;
    }
  } catch (e) {
    console.error("Failed to refresh current file after sync:", e);
    resetMarkdownEditorState();
  }
}

async function runConfiguredSync(trigger: "startup" | "save" | "manual") {
  if (!folderPath.value || syncInFlight.value) return false;

  const config = await loadSyncConfig();
  if (trigger !== "manual" && !shouldRunSync(config, trigger)) return false;
  if (!config || config.provider === "none") {
    if (trigger === "manual") {
      showSaveNotice("请先在设置中配置同步方式", "error");
    }
    return false;
  }

  if (trigger !== "save") {
    const saved = await saveCurrentWorkspaceDocumentBeforeSwitch();
    if (!saved) return false;
  }

  syncInFlight.value = true;
  showSaveNotice("正在同步...", "success");
  try {
    const result = await invoke<SyncResult>("sync_now", {
      workspace: folderPath.value,
      config,
    });
    saveStatus.value = describeSyncResult(result);
    showSaveNotice(describeSyncResult(result), "success");
    await refreshWorkspaceTree();
    await refreshCurrentOpenFileFromDisk();
    return true;
  } catch (e) {
    console.error("Sync failed:", e);
    saveStatus.value = `同步失败: ${e}`;
    showSaveNotice(`同步失败: ${e}`, "error");
    return false;
  } finally {
    syncInFlight.value = false;
  }
}

async function handleWorkspaceSynced() {
  await refreshWorkspaceTree();
  await refreshCurrentOpenFileFromDisk();
}

async function prepareWorkspaceSync(done: (saved: boolean) => void) {
  try {
    done(await saveCurrentWorkspaceDocumentBeforeSwitch());
  } catch (e) {
    console.error("Failed to prepare workspace sync:", e);
    done(false);
  }
}

async function restoreLastWorkspace() {
  const lastWorkspacePath = getLastWorkspacePath();
  if (!lastWorkspacePath) return;

  const restored = await switchWorkspace(lastWorkspacePath, {
    persist: false,
    resetEditor: true,
  });
  if (restored) {
    void runConfiguredSync("startup");
  }
}

async function onOpenFolder() {
  try {
    const chosen = await invoke<string | null>("pick_folder");
    if (chosen) {
      await switchWorkspace(chosen);
    }
  } catch (e) {
    console.error("Failed to pick folder:", e);
  }
}

function getFileName(path: string): string {
  return path.split(/[/\\]/).pop() || path;
}

function getFileExtension(path: string): string {
  const fileName = getFileName(path);
  const dotIndex = fileName.lastIndexOf(".");
  if (dotIndex < 0) return "";

  return fileName.slice(dotIndex + 1).toLowerCase();
}

function getWorkspaceFileType(path: string): WorkspaceFileType {
  const ext = getFileExtension(path);
  if (IMAGE_EXTENSIONS.has(ext)) return "image";
  if (VIDEO_EXTENSIONS.has(ext)) return "video";
  if (MARKDOWN_EXTENSIONS.has(ext)) return "markdown";

  return "markdown";
}

function getLocalAssetSrc(path: string): string {
  if (isTauri()) return convertFileSrc(path);
  if (/^[A-Za-z]:[\\/]/.test(path)) return `file:///${path.replace(/\\/g, "/")}`;
  if (path.startsWith("\\\\")) return `file:${path.replace(/\\/g, "/")}`;

  return path;
}

function resetMediaPreviewState() {
  mediaPreviewSrc.value = "";
  mediaPreviewError.value = "";
  mediaPreviewKey.value += 1;
}

function openMediaPreview(path: string, fileType: Extract<WorkspaceFileType, "image" | "video">) {
  currentFilePath.value = path;
  currentFileType.value = fileType;
  mediaPreviewSrc.value = getLocalAssetSrc(path);
  mediaPreviewError.value = "";
  mediaPreviewKey.value += 1;
  setDocumentContent("");
  lastSavedContent = "";
  hasDraftDocument.value = false;
  saveStatus.value = "";
  showOutline.value = false;
  editorResetKey.value += 1;
}

function clearMediaPreviewError() {
  mediaPreviewError.value = "";
}

function markMediaPreviewError() {
  const kind = currentFileType.value === "video" ? "视频" : "图片";
  mediaPreviewError.value = `无法预览此${kind}`;
}

// ---- file tree helpers ----
function findEntry(entries: DirEntry[], path: string): DirEntry | null {
  for (const entry of entries) {
    if (entry.path === path) return entry;
    if (entry.children) {
      const found = findEntry(entry.children, path);
      if (found) return found;
    }
  }
  return null;
}

async function mergeLoadedDirectoryEntries(freshEntries: DirEntry[], currentEntries: DirEntry[]): Promise<DirEntry[]> {
  const currentByPath = new Map(
    currentEntries.map((entry) => [getWorkspaceIdentity(entry.path), entry]),
  );

  return Promise.all(
    freshEntries.map(async (freshEntry) => {
      const currentEntry = currentByPath.get(getWorkspaceIdentity(freshEntry.path));
      if (!freshEntry.is_dir || currentEntry?.children === undefined) {
        return freshEntry;
      }

      try {
        const children = await invoke<DirEntry[]>("read_dir", {path: freshEntry.path});
        freshEntry.children = await mergeLoadedDirectoryEntries(children, currentEntry.children);
      } catch (e) {
        console.error("Failed to refresh subdirectory:", e);
        freshEntry.children = currentEntry.children;
      }

      return freshEntry;
    }),
  );
}

function clearPendingWorkspaceRefresh() {
  if (workspaceRefreshTimer) {
    clearTimeout(workspaceRefreshTimer);
    workspaceRefreshTimer = null;
  }
  workspaceRefreshQueued = false;
}

function scheduleWorkspaceRefresh(root = folderPath.value) {
  const expectedRoot = normalizeWorkspacePath(root);
  if (!expectedRoot) return;

  if (workspaceRefreshTimer) {
    clearTimeout(workspaceRefreshTimer);
  }

  workspaceRefreshTimer = setTimeout(() => {
    workspaceRefreshTimer = null;
    void refreshWorkspaceTree(expectedRoot);
  }, WORKSPACE_REFRESH_DELAY_MS);
}

async function refreshWorkspaceTree(expectedRoot = folderPath.value) {
  const normalizedRoot = normalizeWorkspacePath(expectedRoot);
  if (!normalizedRoot || !folderPath.value) return;
  if (getWorkspaceIdentity(folderPath.value) !== getWorkspaceIdentity(normalizedRoot)) return;

  if (workspaceRefreshInFlight) {
    workspaceRefreshQueued = true;
    return;
  }

  workspaceRefreshInFlight = true;
  try {
    const entries = await invoke<DirEntry[]>("read_dir", {path: normalizedRoot});
    if (!folderPath.value || getWorkspaceIdentity(folderPath.value) !== getWorkspaceIdentity(normalizedRoot)) return;
    dirEntries.value = await mergeLoadedDirectoryEntries(entries, dirEntries.value);
  } catch (e) {
    console.error("Failed to refresh workspace:", e);
  } finally {
    workspaceRefreshInFlight = false;
    if (workspaceRefreshQueued) {
      workspaceRefreshQueued = false;
      scheduleWorkspaceRefresh(normalizedRoot);
    }
  }
}

async function onToggleFolder(path: string) {
  const entry = findEntry(dirEntries.value, path);
  if (!entry || entry.children !== undefined) return;
  try {
    const children = await invoke<DirEntry[]>("read_dir", {path});
    entry.children = children;
  } catch (e) {
    console.error("Failed to read subdirectory:", e);
  }
}

async function onCreateFolder(path: string) {
  try {
    await invoke("create_folder", { path });
  } catch (e) {
    console.error("Failed to create folder:", e);
  }
  scheduleWorkspaceRefresh();
}

async function onCreateFile(path: string) {
  try {
    await invoke("create_file", { path });
  } catch (e) {
    console.error("Failed to create file:", e);
  }
  scheduleWorkspaceRefresh();
}

async function onDeleteEntry(path: string) {
  try {
    await invoke("delete_entry", { path });
  } catch (e) {
    console.error("Failed to delete entry:", e);
  }
  // Close the file if it's the one being deleted
  if (currentFilePath.value != null && getWorkspaceIdentity(currentFilePath.value) === getWorkspaceIdentity(path)) {
    resetMarkdownEditorState();
    currentFilePath.value = null;
  }
  scheduleWorkspaceRefresh();
}

async function onRenameEntry(path: string, newName: string) {
  try {
    const newPath = await invoke<string>("rename_entry", { path, newName });
    // Update current file path if it was renamed
    if (currentFilePath.value != null && getWorkspaceIdentity(currentFilePath.value) === getWorkspaceIdentity(path)) {
      currentFilePath.value = newPath;
    }
  } catch (e) {
    console.error("Failed to rename entry:", e);
  }
  scheduleWorkspaceRefresh();
}

async function onOpenFile(path: string) {
  const request = ++fileOpenRequest;

  if (currentFilePath.value && getWorkspaceIdentity(currentFilePath.value) === getWorkspaceIdentity(path)) {
    return;
  }

  const saved = await saveCurrentWorkspaceDocumentBeforeSwitch();
  if (!saved || request !== fileOpenRequest) return;

  const fileType = getWorkspaceFileType(path);
  if (fileType === "image" || fileType === "video") {
    openMediaPreview(path, fileType);
    return;
  }

  try {
    const fileContent = await invoke<string>("read_file", {path});
    if (request !== fileOpenRequest) return;
    currentFileType.value = "markdown";
    resetMediaPreviewState();
    setDocumentContent(fileContent);
    lastSavedContent = fileContent;
    currentFilePath.value = path;
    hasDraftDocument.value = false;
    saveStatus.value = "";
  } catch (e) {
    console.error("Failed to open file:", e);
  }
}

function getLatestEditorContent(contentOverride?: string) {
  if (typeof contentOverride === "string") return contentOverride;
  if (!isMarkdownDocument.value) return content.value;
  return markdownEditorRef.value?.flushPendingChanges() ?? content.value;
}

function isCurrentMarkdownDocumentDirty() {
  if (!isMarkdownDocument.value) return false;
  if (!currentFilePath.value && !hasDraftDocument.value) return false;

  const latestContent = getLatestEditorContent();
  if (!currentFilePath.value && !latestContent.trim()) return false;
  return latestContent !== lastSavedContent;
}

function showSaveNotice(message: string, type: "success" | "error" = "success") {
  if (saveNoticeTimer) clearTimeout(saveNoticeTimer);
  saveNotice.value = message;
  saveNoticeType.value = type;
  saveNoticeTimer = setTimeout(() => {
    saveNotice.value = "";
    saveNoticeTimer = null;
  }, 3000);
}

async function doSave(
  contentOverride?: string,
  options: { syncAfterSave?: boolean; trigger?: "manual" | "automatic" } = {},
) {
  if (!isMarkdownDocument.value) return false;

  const contentToSave = getLatestEditorContent(contentOverride);

  if (!currentFilePath.value) {
    // No file path yet — prompt user to pick a save location
    try {
      const chosen = await invoke<string | null>("pick_save_file");
      if (!chosen) {
        showSaveNotice(options.trigger === "automatic" ? "已取消自动保存" : "已取消保存", "error");
        return false;
      }
      currentFilePath.value = chosen;
    } catch (e) {
      console.error("Save dialog failed:", e);
      showSaveNotice(`保存失败: ${e}`, "error");
      return false;
    }
  }

  try {
    await invoke("write_file", {path: currentFilePath.value, content: contentToSave});
    const fileName = getFileName(currentFilePath.value);
    const message = options.trigger === "automatic" ? `已自动保存: ${fileName}` : `已手动保存: ${fileName}`;
    saveStatus.value = message;
    showSaveNotice(message);
    lastSavedContent = contentToSave;
    setTimeout(() => {
      saveStatus.value = "";
    }, 3000);
    hasDraftDocument.value = false;
    if (options.syncAfterSave !== false) {
      void runConfiguredSync("save");
    }
    return true;
  } catch (e) {
    console.error("Save failed:", e);
    saveStatus.value = `保存失败: ${e}`;
    showSaveNotice(saveStatus.value, "error");
    return false;
  }
}

async function doSaveAs() {
  if (!isMarkdownDocument.value) return;

  const contentToSave = getLatestEditorContent();

  try {
    const chosen = await invoke<string | null>("pick_save_file");
    if (!chosen) {
      showSaveNotice("已取消另存为", "error");
      return;
    }
    currentFilePath.value = chosen;
    await invoke("write_file", {path: currentFilePath.value, content: contentToSave});
    saveStatus.value = `已手动保存: ${getFileName(currentFilePath.value)}`;
    showSaveNotice(saveStatus.value);
    lastSavedContent = contentToSave;
    setTimeout(() => {
      saveStatus.value = "";
    }, 3000);
    hasDraftDocument.value = false;
    void runConfiguredSync("save");
  } catch (e) {
    console.error("Save failed:", e);
    saveStatus.value = `保存失败: ${e}`;
    showSaveNotice(saveStatus.value, "error");
  }
}

async function doNew() {
  fileOpenRequest += 1;
  const saved = await saveCurrentWorkspaceDocumentBeforeSwitch();
  if (!saved) return;

  currentFileType.value = "markdown";
  resetMediaPreviewState();
  setDocumentContent("");
  lastSavedContent = "";
  currentFilePath.value = null;
  hasDraftDocument.value = true;
  saveStatus.value = "";
  documentMode.value = defaultDocumentMode.value;
  editorResetKey.value += 1;
}

function registerTauriListener<T>(eventName: EventName, handler: EventCallback<T>) {
  if (!isTauri()) return;

  void listen<T>(eventName, handler).catch((error) => {
    console.error(`Failed to listen for ${eventName}:`, error);
  });
}

// Listen for menu events from Rust
registerTauriListener<string>("folder-opened", (event) => {
  void switchWorkspace(event.payload);
});

registerTauriListener<WorkspaceChangedPayload>("workspace-changed", (event) => {
  if (!folderPath.value) return;
  if (getWorkspaceIdentity(event.payload.root) !== getWorkspaceIdentity(folderPath.value)) return;
  scheduleWorkspaceRefresh(event.payload.root);
});

registerTauriListener<WorkspaceWatchErrorPayload>("workspace-watch-error", (event) => {
  if (!folderPath.value) return;
  if (getWorkspaceIdentity(event.payload.root) !== getWorkspaceIdentity(folderPath.value)) return;
  console.error("Workspace watcher error:", event.payload.message);
  scheduleWorkspaceRefresh(event.payload.root);
});

registerTauriListener("menu-save", () => {
  doSave();
});

registerTauriListener("menu-save-as", () => {
  doSaveAs();
});

registerTauriListener("menu-new", () => {
  void doNew();
});

registerTauriListener("menu-quit", () => {
  void closeWindow();
});

registerTauriListener("show-about", () => {
  openSettings("about");
});

registerTauriListener("menu-event", (event) => {
  console.log("Menu event:", event.payload);
});
registerTauriListener("menu-search", () => {
  directorySidebarRef.value?.activateSearch();
});

async function handleSearchResultClick(filePath: string, lineNumber: number) {
  await onOpenFile(filePath);
  await nextTick();
  markdownEditorRef.value?.scrollToLine(lineNumber);
}
</script>

<template>
  <div class="h-screen app-root flex flex-col">
    <!-- Three-column body -->
    <div class="flex-1 flex overflow-hidden app-root">
      <!-- Left: Directory sidebar -->
      <DirectorySidebar
          ref="directorySidebarRef"
          :folder-path="folderPath"
          :entries="dirEntries"
          :current-file-path="currentFilePath"
          :settings-request="settingsRequest"
          :settings-initial-page="settingsInitialPage"
          :default-document-mode="defaultDocumentMode"
          :app-theme="appTheme"
          :render-single-line-breaks="renderSingleLineBreaks"
          :paragraph-indent="paragraphIndent"
          :recent-workspaces="recentWorkspaces"
          @open-folder="onOpenFolder"
          @switch-workspace="(path: string) => switchWorkspace(path)"
          @remove-workspace="removeRecentWorkspace"
          @toggle-folder="onToggleFolder"
          @open-file="onOpenFile"
          @create-folder="onCreateFolder"
          @create-file="onCreateFile"
          @delete-entry="onDeleteEntry"
          @rename-entry="onRenameEntry"
          @search-result-click="handleSearchResultClick"
          @update-default-document-mode="updateDefaultDocumentMode"
          @update-app-theme="updateAppTheme"
          @update-render-single-line-breaks="updateRenderSingleLineBreaks"
          @update-paragraph-indent="updateParagraphIndent"
          @prepare-workspace-sync="prepareWorkspaceSync"
          @workspace-synced="handleWorkspaceSynced"
      />

      <!-- Right: Editor area -->
      <main class="flex-1 flex flex-col overflow-hidden relative editor-area">
        <!-- ════════════════ Custom title bar / toolbar ════════════════ -->
        <div
            class="editor-titlebar flex w-full items-center h-10 pl-3 pr-0 select-none shrink-0"
            @mousedown="onTitlebarMouseDown"
            @dblclick="onTitlebarDoubleClick"
        >
          <!-- Left: Outline toggle -->
          <div
              v-if="!showStartActions && isMarkdownDocument"
              class="flex items-center shrink-0"
              data-no-window-drag
          >
            <button
                class="toolbar-btn"
                :class="{ 'toolbar-btn-active': showOutline }"
                title="切换大纲"
                aria-label="切换大纲"
                @click="showOutline = !showOutline"
            >
              <i class="ri-menu-line text-base"></i>
            </button>
          </div>
          <div v-else class="titlebar-brand shrink-0 pointer-events-none">
            <img src="/yurou-logo.png" alt="yurou" class="titlebar-logo" />
          </div>

          <!-- Center: Filename -->
          <div class="titlebar-drag-surface flex-1 min-w-0 px-3">
            <div class="app-muted-text titlebar-file-title text-center text-sm font-medium truncate pointer-events-none">
            {{ showStartActions ? '未选择工作目录' : currentFilePath ? getFileName(currentFilePath) : '未命名文档' }}
            </div>
          </div>

          <!-- Right: Actions -->
          <div
              v-if="!showStartActions"
              class="flex items-center gap-0.5 shrink-0"
              data-no-window-drag
          >
            <!-- Document mode -->
            <div v-if="isMarkdownDocument" class="mode-toggle" role="group" aria-label="文档模式">
              <button
                  class="mode-toggle-btn"
                  :class="{ 'mode-toggle-btn-active': documentMode === 'preview' }"
                  title="预览模式"
                  @click="setDocumentMode('preview')"
              >
                预览
              </button>
              <button
                  class="mode-toggle-btn"
                  :class="{ 'mode-toggle-btn-active': documentMode === 'edit' }"
                  title="编辑模式"
                  @click="setDocumentMode('edit')"
              >
                编辑
              </button>
              <button
                  class="mode-toggle-btn"
                  :class="{ 'mode-toggle-btn-active': documentMode === 'mixed' }"
                  title="混合模式"
                  @click="setDocumentMode('mixed')"
              >
                混合
              </button>
            </div>

            <!-- Sync button -->
            <button
                v-if="folderPath"
                class="toolbar-btn"
                :title="syncInFlight ? '正在同步...' : '同步工作目录'"
                :disabled="syncInFlight"
                @click="runConfiguredSync('manual')"
            >
              <i :class="syncInFlight ? 'ri-loader-4-line sync-spinning' : 'ri-loop-left-line'"></i>
            </button>
          </div>

          <!-- Window controls -->
          <div class="window-controls flex items-stretch self-stretch ml-2 shrink-0" data-no-window-drag>
            <button
                class="window-control-btn"
                title="最小化"
                aria-label="最小化"
                @click="minimizeWindow"
            >
              <i class="ri-subtract-line"></i>
            </button>
            <button
                class="window-control-btn"
                :title="isWindowMaximized ? '还原' : '最大化'"
                :aria-label="isWindowMaximized ? '还原' : '最大化'"
                @click="toggleWindowMaximize"
            >
              <i :class="isWindowMaximized ? 'ri-checkbox-multiple-blank-line' : 'ri-checkbox-blank-line'"></i>
            </button>
            <button
                class="window-control-btn window-control-close"
                title="关闭"
                aria-label="关闭"
                @click="closeWindow"
            >
              <i class="ri-close-line"></i>
            </button>
          </div>
        </div>
        <div class="flex-1 flex min-h-0 overflow-hidden">
          <Transition name="outline-slide">
            <OutlinePanel
                v-if="showOutline && !showStartActions && isMarkdownDocument"
                :markdown-content="content"
                @heading-click="onHeadingClick"
            />
          </Transition>

          <MarkdownEditor
              v-if="!showStartActions && isMarkdownDocument"
              :key="editorResetKey"
              ref="markdownEditorRef"
              :model-value="content"
              :document-mode="documentMode"
              :file-path="currentFilePath"
              :render-single-line-breaks="renderSingleLineBreaks"
              :paragraph-indent="paragraphIndent"
              placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等"
              @update:model-value="onUpdate"
              @save-shortcut="doSave"
              class="flex-1 w-full min-w-0"
          />
          <div
              v-else-if="!showStartActions && currentFileType === 'image'"
              class="media-preview flex-1 min-w-0"
          >
            <div class="media-preview-stage">
              <img
                  :key="mediaPreviewKey"
                  class="media-preview-image"
                  :src="mediaPreviewSrc"
                  :alt="getFileName(currentFilePath || '')"
                  @load="clearMediaPreviewError"
                  @error="markMediaPreviewError"
              />
              <div v-if="mediaPreviewError" class="media-preview-error">
                <i class="ri-image-line"></i>
                <span>{{ mediaPreviewError }}</span>
              </div>
            </div>
          </div>
          <div
              v-else-if="!showStartActions && currentFileType === 'video'"
              class="media-preview flex-1 min-w-0"
          >
            <div class="media-preview-stage">
              <video
                  :key="mediaPreviewKey"
                  class="media-preview-video"
                  controls
                  preload="metadata"
                  @loadeddata="clearMediaPreviewError"
                  @error="markMediaPreviewError"
              >
                <source
                    :src="mediaPreviewSrc"
                    :type="currentVideoMimeType || undefined"
                />
              </video>
              <div v-if="mediaPreviewError" class="media-preview-error">
                <i class="ri-film-line"></i>
                <span>{{ mediaPreviewError }}</span>
              </div>
            </div>
          </div>
          <div
              v-else
              class="start-actions flex-1 flex items-center justify-center px-8"
          >
            <div class="flex flex-col items-center gap-3">
              <button class="start-action-btn" @click="onOpenFolder">
                选择工作目录
              </button>
              <button class="start-action-btn" @click="doNew">
                新建文件
              </button>
            </div>
          </div>
        </div>

        <!-- Zoom indicator -->
        <Transition name="zoom-fade">
          <div
              v-if="showZoomIndicator"
              class="zoom-indicator fixed top-4 right-4 z-50 px-2.5 py-1 rounded-md text-xs font-medium pointer-events-none select-none backdrop-blur-sm"
          >
            {{ Math.round(editorZoom * 100) }}%
          </div>
        </Transition>
        <Transition name="save-notice">
          <div
              v-if="saveNotice"
              class="save-notice fixed top-12 right-4 z-50 pointer-events-none select-none"
              :class="{ 'save-notice-error': saveNoticeType === 'error' }"
              role="status"
              aria-live="polite"
          >
            <i :class="saveNoticeType === 'error' ? 'ri-error-warning-line' : 'ri-checkbox-circle-line'"></i>
            <span>{{ saveNotice }}</span>
          </div>
        </Transition>
      </main>
    </div>
<!--    <footer class="app-footer backdrop-blur-sm">-->
<!--      <div class="flex items-center gap-4 px-4 py-1.5">-->

<!--        &lt;!&ndash; Current file / save status &ndash;&gt;-->
<!--        <span-->
<!--            v-if="saveStatus"-->
<!--            class="text-xs"-->
<!--            :class="saveStatus.startsWith('保存失败') ? 'text-red-500' : 'text-emerald-600'"-->
<!--        >{{ saveStatus }}</span>-->
<!--        <span v-else-if="currentFilePath" class="app-muted-text text-xs">-->
<!--          {{ currentFilePath.split(/[/\\]/).pop() }}-->
<!--        </span>-->
<!--        <span v-else class="app-subtle-text text-xs">未保存</span>-->

<!--        <span class="flex-1"/>-->

<!--        <span class="app-muted-text text-xs">{{ wordCount.toLocaleString() }} 词</span>-->
<!--        <span class="app-divider w-px h-3"/>-->
<!--        <span class="app-muted-text text-xs">{{ charCount.toLocaleString() }} 字</span>-->
<!--      </div>-->
<!--    </footer>-->
  </div>
</template>

<style>
.zoom-fade-enter-active,
.zoom-fade-leave-active {
  transition: opacity 0.2s ease;
}

.zoom-fade-enter-from,
.zoom-fade-leave-to {
  opacity: 0;
}

.save-notice-enter-active,
.save-notice-leave-active {
  transition: opacity 0.18s ease, transform 0.18s ease;
}

.save-notice-enter-from,
.save-notice-leave-to {
  opacity: 0;
  transform: translateY(-6px);
}

.outline-slide-enter-active,
.outline-slide-leave-active {
  transition:
    width 0.18s ease,
    opacity 0.18s ease;
}

.outline-slide-enter-from,
.outline-slide-leave-to {
  width: 0;
  opacity: 0;
}

.app-root {
  background: var(--app-bg);
  color: var(--app-text);
}

.editor-titlebar {
  background: var(--app-bg);
  border-bottom: 1px solid var(--app-border-strong);
}

.editor-area {
  background: var(--app-bg);
}

.app-footer {
  border-top: 1px solid var(--app-border);
  background: color-mix(in srgb, var(--app-sidebar-bg) 72%, transparent);
}

.app-muted-text {
  color: var(--app-muted);
}

.app-subtle-text {
  color: var(--app-subtle);
}

.app-divider {
  background: var(--app-border-strong);
}

.zoom-indicator {
  background: var(--app-toast-bg);
  color: var(--app-control-contrast);
}

.save-notice {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  max-width: min(360px, calc(100vw - 32px));
  min-height: 32px;
  padding: 7px 11px;
  border: 1px solid #86efac;
  border-radius: 6px;
  background: #f0fdf4;
  color: #14532d;
  box-shadow:
    0 10px 30px rgba(0, 0, 0, 0.28),
    0 0 0 1px rgba(255, 255, 255, 0.5);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.35;
}

.save-notice i {
  flex: 0 0 auto;
  color: #15803d;
  font-size: 16px;
}

.save-notice span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.save-notice-error {
  border-color: #fca5a5;
  background: #fef2f2;
  color: #7f1d1d;
}

.save-notice-error i {
  color: #b91c1c;
}

/* ── Media preview ──────────────────────────────────────────────── */

.media-preview {
  overflow: auto;
  background:
    linear-gradient(45deg, color-mix(in srgb, var(--app-border) 46%, transparent) 25%, transparent 25%),
    linear-gradient(-45deg, color-mix(in srgb, var(--app-border) 46%, transparent) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, color-mix(in srgb, var(--app-border) 46%, transparent) 75%),
    linear-gradient(-45deg, transparent 75%, color-mix(in srgb, var(--app-border) 46%, transparent) 75%),
    var(--app-bg);
  background-position: 0 0, 0 10px, 10px -10px, -10px 0;
  background-size: 20px 20px;
}

.media-preview-stage {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100%;
  width: 100%;
  padding: 32px;
}

.media-preview-image,
.media-preview-video {
  display: block;
  max-width: 100%;
  max-height: calc(100vh - 104px);
  border: 1px solid var(--app-border-strong);
  border-radius: 6px;
  background: var(--app-surface);
  box-shadow: 0 16px 44px color-mix(in srgb, var(--app-shadow) 64%, transparent);
}

.media-preview-image {
  object-fit: contain;
}

.media-preview-video {
  width: min(100%, 1040px);
  /*aspect-ratio: 16 / 9;*/
}

.media-preview-error {
  position: absolute;
  inset: 32px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  border: 1px solid var(--app-border-strong);
  border-radius: 8px;
  background: color-mix(in srgb, var(--app-surface) 88%, transparent);
  color: var(--app-muted);
  font-size: 13px;
  text-align: center;
}

.media-preview-error i {
  color: var(--app-subtle);
  font-size: 28px;
  line-height: 1;
}

/* ── Toolbar ───────────────────────────────────────────────────── */

.titlebar-drag-surface {
  height: 100%;
  min-width: 32px;
  display: flex;
  align-items: center;
}

.titlebar-brand {
  height: 100%;
  min-width: 68px;
  display: inline-flex;
  align-items: center;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0;
}

.titlebar-logo {
  height: 26px;
  width: auto;
}

.titlebar-file-title {
  width: 100%;
  line-height: 1.2;
}

.toolbar-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 6px;
  color: var(--app-muted);
  background: transparent;
  border: none;
  cursor: pointer;
  transition: background-color 0.15s, color 0.15s;
  flex-shrink: 0;
}

.toolbar-btn:hover {
  background-color: var(--app-hover);
  color: var(--app-text);
}

.toolbar-btn-active {
  color: var(--app-accent);
  background-color: var(--app-surface-muted);
}

.window-controls {
  color: var(--app-muted);
}

.window-control-btn {
  width: 46px;
  height: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 0;
  background: transparent;
  color: inherit;
  cursor: pointer;
  font-size: 15px;
  line-height: 1;
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.window-control-btn:hover {
  background-color: var(--app-hover);
  color: var(--app-text);
}

.window-control-btn:focus-visible {
  outline: 1px solid var(--app-accent);
  outline-offset: -3px;
}

.window-control-close:hover {
  background-color: #c42b1c;
  color: #ffffff;
}

.mode-toggle {
  display: inline-flex;
  align-items: center;
  height: 30px;
  padding: 2px;
  border: 1px solid var(--app-border-strong);
  border-radius: 7px;
  background-color: var(--app-surface-muted);
  flex-shrink: 0;
}

.mode-toggle-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 24px;
  min-width: 42px;
  padding: 0 9px;
  border: none;
  border-radius: 5px;
  background: transparent;
  color: var(--app-muted);
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  transition:
    background-color 0.15s,
    color 0.15s,
    box-shadow 0.15s;
}

.mode-toggle-btn:hover {
  color: var(--app-text);
}

.mode-toggle-btn-active {
  background-color: var(--app-surface);
  color: var(--app-accent);
  box-shadow: 0 1px 3px var(--app-shadow);
}


.start-actions {
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--app-surface) 35%, transparent), color-mix(in srgb, var(--app-sidebar-bg) 20%, transparent)),
    var(--app-bg);
}

.start-action-btn {
  border: none;
  background: transparent;
  color: var(--app-muted);
  font-size: 15px;
  line-height: 1.4;
  cursor: pointer;
  padding: 4px 2px;
  transition:
    color 0.15s ease,
    text-decoration-color 0.15s ease;
  text-decoration: underline;
  text-underline-offset: 6px;
  text-decoration-thickness: 1px;
  text-decoration-color: transparent;
}

.start-action-btn:hover,
.start-action-btn:focus-visible {
  color: var(--app-accent);
  text-decoration-color: currentColor;
  outline: none;
}

/* Sync button spin animation */
@keyframes sync-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.sync-spinning {
  display: inline-block;
  animation: sync-spin 1s linear infinite;
}
</style>
