import { useMemo, type MutableRefObject } from "react";
import type { editor as MonacoNs } from "monaco-editor";
import { writeText as clipboardWriteText } from "@tauri-apps/plugin-clipboard-manager";
import { useAppStore } from "../../../stores/useAppStore";
import { fileBufferKey } from "../../../stores/slices/fileTreeSlice";
import { setAppSetting } from "../../../services/tauri";
import {
  EDITOR_ZOOM_MAX,
  EDITOR_ZOOM_MIN,
  EDITOR_ZOOM_STEP,
} from "../editorConstants";
import type { EditorActions } from "./editorMenuConfig";

// EDITOR_BASE_FONT_SIZE lives in ../editorConstants so MonacoEditor
// can import it without depending on this module's clipboard / store /
// setAppSetting imports. Re-exported here as a back-compat hatch — old
// imports of `EDITOR_BASE_FONT_SIZE` from `useEditorActions` keep
// working. New code should import from `../editorConstants` directly.
export { EDITOR_BASE_FONT_SIZE } from "../editorConstants";

/** Pure dependency bag consumed by `buildEditorActions`. Refactoring all
 *  side effects through this interface lets the hook stay a thin store
 *  selector while tests can pass hand-rolled mocks. */
export interface EditorActionsDeps {
  workspaceId: string;
  path: string;
  /** **Lazy** accessor for the live editor instance. Must be a function
   *  rather than a snapshot value: the menubar mounts immediately, but
   *  Monaco is lazy-loaded and `editorRef.current` flips from `null` to
   *  the editor instance asynchronously. Capturing the snapshot inside
   *  the hook's `useMemo` would freeze every Monaco-backed action
   *  (Find, Format, Undo, …) at whatever the ref held when the action
   *  bag was first built — usually `null`. Reading at call time keeps
   *  the dispatch live. */
  getEditor: () => MonacoNs.IStandaloneCodeEditor | null;
  /** FileViewer's existing save + close handlers — we route through
   *  them so the dirty-aware confirmation modal and save toast are
   *  shared with the inline Save button. */
  onSave: () => void;
  onCloseTab: () => void;

  /** Current saved-baseline for the open file. `null` when the buffer
   *  hasn't loaded yet. Revert restores the editor buffer to this. */
  getBaseline: () => string | null;
  setFileBufferContent: (workspaceId: string, path: string, value: string) => void;

  /** Reveal-in-files plumbing. */
  setAllFilesSelectedPath: (workspaceId: string, path: string | null) => void;
  setAllFilesDirExpanded: (
    workspaceId: string,
    path: string,
    expanded: boolean,
  ) => void;
  rightSidebarVisible: boolean;
  showRightSidebar: () => void;
  setRightSidebarTabForWorkspace: (workspaceId: string, tab: "files" | "changes" | "tasks") => void;

  openCommandPaletteFileMode: () => void;

  /** Editor view-state — Monaco subscribes to the store values and
   *  applies them via `updateOptions`. The action just flips the
   *  underlying flag + persists. */
  wordWrap: boolean;
  setWordWrap: (val: boolean) => void;
  minimap: boolean;
  setMinimap: (val: boolean) => void;
  lineNumbers: boolean;
  setLineNumbers: (val: boolean) => void;
  fontZoom: number;
  setFontZoom: (val: number) => void;

  /** `setAppSetting` indirection — kept here so the hook can ignore
   *  Tauri import boundaries during tests. Failures are logged but not
   *  surfaced as toasts; the in-memory store update is the authoritative
   *  signal for the menubar UX and matches the existing
   *  `EditorSettings.tsx` rollback approach for the minimap toggle. */
  persistSetting: (key: string, value: string) => Promise<unknown>;

  /** Absolute path on disk to the workspace root, for Copy Path. `null`
   *  if the workspace has no worktree yet (remote, in-progress
   *  provisioning) — Copy Path falls back to the relative path in that
   *  case so the menu item still does something useful. */
  worktreePath: string | null;
  writeToClipboard: (text: string) => Promise<void>;
  addToast: (message: string) => void;
}

/** True when `path` is already absolute and can be returned verbatim
 *  by `joinWorktreePath`. Covers Unix-style leading slash and Windows
 *  forms (`C:\foo`, `C:/foo`, `\\server\share`, `//server/share`).
 *  Mirrors the absolute-path detection used by Rust's `Path::is_absolute`. */
function isAbsolutePath(path: string): boolean {
  if (path.startsWith("/") || path.startsWith("\\")) return true;
  // Drive-letter prefix: `C:`, `D:\`, `Z:/something`.
  if (path.length >= 2 && /^[A-Za-z]:/.test(path)) return true;
  return false;
}

/** Join a worktree root and a workspace-relative path so the result is
 *  usable from a shell ("paste into terminal").
 *
 *  Uses the **root's** separator (backslash on Windows roots, forward
 *  slash elsewhere) so paste-into-terminal gets a consistent native
 *  path rather than a `C:/foo/bar/baz.txt`-style mix that confuses
 *  cmd.exe / pwsh. Already-absolute `relative` values (Unix root,
 *  drive letter, UNC) are returned verbatim so a caller that already
 *  did the join doesn't get it doubled up. */
export function joinWorktreePath(root: string, relative: string): string {
  if (isAbsolutePath(relative)) return relative;
  // Pick the separator from the root so the result is internally
  // consistent. A Windows root carries backslashes; everything else
  // uses forward slashes (Unix and inside-WSL paths alike).
  const sep = root.includes("\\") ? "\\" : "/";
  const trimmed = root.replace(/[\\/]+$/, "");
  // Normalize the relative path's separators to match the chosen one
  // so `src/foo.ts` from the workspace store joins cleanly under a
  // backslash root: `C:\repo\src\foo.ts` rather than `C:\repo\src/foo.ts`.
  const normalizedRelative =
    sep === "\\" ? relative.replace(/\//g, "\\") : relative.replace(/\\/g, "/");
  return `${trimmed}${sep}${normalizedRelative}`;
}

/** Step out of every parent segment of a workspace-relative path so
 *  Reveal-in-Files can expand them in the tree. Returns parents in
 *  outer-to-inner order with a **trailing slash** on each segment so the
 *  keys line up with `buildFileTree`'s dir nodes — those store their
 *  identifier as `src/components/` (slash terminated), and `FileTree`
 *  reads expansion state via `expanded[node.path]`. Dropping the slash
 *  would write to a parallel-but-unread key. */
export function ancestorDirs(relative: string): string[] {
  const parts = relative.split("/").filter(Boolean);
  if (parts.length <= 1) return [];
  const acc: string[] = [];
  for (let i = 1; i < parts.length; i++) {
    acc.push(`${parts.slice(0, i).join("/")}/`);
  }
  return acc;
}

function clampZoom(value: number): number {
  if (!Number.isFinite(value)) return 1;
  return Math.min(EDITOR_ZOOM_MAX, Math.max(EDITOR_ZOOM_MIN, value));
}

/** Build the handler bag consumed by `buildEditorMenus`. Pure with
 *  respect to its `deps` — every observable effect is delegated to the
 *  passed-in functions/refs, so unit tests can swap in spies. */
export function buildEditorActions(deps: EditorActionsDeps): EditorActions {
  const runMonacoAction = (actionId: string) => {
    const editor = deps.getEditor();
    if (!editor) return;
    const action = editor.getAction(actionId);
    if (action) void action.run();
  };

  const triggerMonacoCommand = (commandId: string) => {
    deps.getEditor()?.trigger("editor-menubar", commandId, null);
  };

  const onRevert = () => {
    const baseline = deps.getBaseline();
    if (baseline === null) return;
    deps.setFileBufferContent(deps.workspaceId, deps.path, baseline);
  };

  const onRevealInFiles = () => {
    if (!deps.rightSidebarVisible) deps.showRightSidebar();
    deps.setRightSidebarTabForWorkspace(deps.workspaceId, "files");
    for (const dir of ancestorDirs(deps.path)) {
      deps.setAllFilesDirExpanded(deps.workspaceId, dir, true);
    }
    deps.setAllFilesSelectedPath(deps.workspaceId, deps.path);
  };

  const persistFireAndForget = (key: string, value: string) => {
    void deps.persistSetting(key, value).catch((err) => {
      // We intentionally don't roll the store back — losing a single
      // persistence write is a far better UX than a flicker. The error
      // lands in the console so it can still be diagnosed.
      console.error(`Failed to persist editor setting ${key}:`, err);
    });
  };

  const onToggleWordWrap = () => {
    const next = !deps.wordWrap;
    deps.setWordWrap(next);
    persistFireAndForget("editor_word_wrap", next ? "true" : "false");
  };

  const onToggleMinimap = () => {
    const next = !deps.minimap;
    deps.setMinimap(next);
    persistFireAndForget("editor_minimap_enabled", next ? "true" : "false");
  };

  const onToggleLineNumbers = () => {
    const next = !deps.lineNumbers;
    deps.setLineNumbers(next);
    persistFireAndForget("editor_line_numbers", next ? "true" : "false");
  };

  const applyZoom = (nextZoom: number) => {
    const clamped = clampZoom(nextZoom);
    deps.setFontZoom(clamped);
    persistFireAndForget("editor_font_zoom", clamped.toFixed(2));
  };

  const onZoomIn = () => applyZoom(deps.fontZoom + EDITOR_ZOOM_STEP);
  const onZoomOut = () => applyZoom(deps.fontZoom - EDITOR_ZOOM_STEP);
  const onZoomReset = () => applyZoom(1);

  const onCopyContents = async () => {
    const editor = deps.getEditor();
    if (!editor) return;
    const model = editor.getModel();
    if (!model) return;
    const value = model.getValue();
    try {
      await deps.writeToClipboard(value);
      deps.addToast("Copied file contents");
    } catch (err) {
      console.error("Copy contents failed:", err);
      deps.addToast(`Copy failed: ${String(err)}`);
    }
  };

  const onCopyPath = async () => {
    const target = deps.worktreePath
      ? joinWorktreePath(deps.worktreePath, deps.path)
      : deps.path;
    try {
      await deps.writeToClipboard(target);
      deps.addToast("Copied path");
    } catch (err) {
      console.error("Copy path failed:", err);
      deps.addToast(`Copy failed: ${String(err)}`);
    }
  };

  const onCopyRelativePath = async () => {
    try {
      await deps.writeToClipboard(deps.path);
      deps.addToast("Copied relative path");
    } catch (err) {
      console.error("Copy relative path failed:", err);
      deps.addToast(`Copy failed: ${String(err)}`);
    }
  };

  return {
    onSave: deps.onSave,
    onRevert,
    onRevealInFiles,
    onCloseTab: deps.onCloseTab,
    onUndo: () => triggerMonacoCommand("undo"),
    onRedo: () => triggerMonacoCommand("redo"),
    onFind: () => runMonacoAction("actions.find"),
    onReplace: () => runMonacoAction("editor.action.startFindReplaceAction"),
    onFormat: () => runMonacoAction("editor.action.formatDocument"),
    onCopyContents,
    onCopyPath,
    onCopyRelativePath,
    onToggleWordWrap,
    onToggleMinimap,
    onToggleLineNumbers,
    onZoomIn,
    onZoomOut,
    onZoomReset,
    onGoToFile: deps.openCommandPaletteFileMode,
    onGoToLine: () => runMonacoAction("editor.action.gotoLine"),
    onGoToSymbol: () => runMonacoAction("editor.action.quickOutline"),
  };
}

interface UseEditorActionsParams {
  workspaceId: string;
  path: string;
  editorRef: MutableRefObject<MonacoNs.IStandaloneCodeEditor | null>;
  onSave: () => void;
  onCloseTab: () => void;
}

/** React hook that wires `buildEditorActions` to the live store + Tauri
 *  surface. Keep this thin — all logic lives in `buildEditorActions`. */
export function useEditorActions(params: UseEditorActionsParams): EditorActions {
  const { workspaceId, path, editorRef, onSave, onCloseTab } = params;

  const setFileBufferContent = useAppStore((s) => s.setFileBufferContent);
  const setAllFilesSelectedPath = useAppStore((s) => s.setAllFilesSelectedPath);
  const setAllFilesDirExpanded = useAppStore((s) => s.setAllFilesDirExpanded);
  const rightSidebarVisible = useAppStore((s) => s.rightSidebarVisible);
  const toggleRightSidebar = useAppStore((s) => s.toggleRightSidebar);
  const setRightSidebarTabForWorkspace = useAppStore((s) => s.setRightSidebarTabForWorkspace);
  const openCommandPaletteFileMode = useAppStore(
    (s) => s.openCommandPaletteFileMode,
  );
  const wordWrap = useAppStore((s) => s.editorWordWrap);
  const setWordWrap = useAppStore((s) => s.setEditorWordWrap);
  const minimap = useAppStore((s) => s.editorMinimapEnabled);
  const setMinimap = useAppStore((s) => s.setEditorMinimapEnabled);
  const lineNumbers = useAppStore((s) => s.editorLineNumbersEnabled);
  const setLineNumbers = useAppStore((s) => s.setEditorLineNumbersEnabled);
  const fontZoom = useAppStore((s) => s.editorFontZoom);
  const setFontZoom = useAppStore((s) => s.setEditorFontZoom);
  const addToast = useAppStore((s) => s.addToast);
  const worktreePath = useAppStore(
    (s) =>
      s.workspaces.find((w) => w.id === workspaceId)?.worktree_path ?? null,
  );

  return useMemo(
    () =>
      buildEditorActions({
        workspaceId,
        path,
        // Pass the ref reader, not the snapshot — the action bag is
        // built before Monaco mounts, so a snapshot would freeze every
        // editor-backed handler at `null` for the lifetime of this
        // memoization. The reader closes over the ref (stable identity)
        // and reads the live value at call time.
        getEditor: () => editorRef.current,
        onSave,
        onCloseTab,
        getBaseline: () => {
          const buf = useAppStore.getState().fileBuffers[
            fileBufferKey(workspaceId, path)
          ];
          return buf && buf.loaded ? buf.baseline : null;
        },
        setFileBufferContent,
        setAllFilesSelectedPath,
        setAllFilesDirExpanded,
        rightSidebarVisible,
        showRightSidebar: () => {
          if (!useAppStore.getState().rightSidebarVisible) toggleRightSidebar();
        },
        setRightSidebarTabForWorkspace,
        openCommandPaletteFileMode,
        wordWrap,
        setWordWrap,
        minimap,
        setMinimap,
        lineNumbers,
        setLineNumbers,
        fontZoom,
        setFontZoom,
        persistSetting: (key, value) => setAppSetting(key, value),
        worktreePath,
        // Use Tauri's clipboard plugin (the codebase's clipboard
        // standard — see attachmentDownload.ts and useCopyToClipboard)
        // instead of `navigator.clipboard.writeText`. The plugin sidesteps
        // WebKit's secure-context / permission requirements that have
        // bitten us in past releases.
        writeToClipboard: (text) => clipboardWriteText(text),
        addToast,
      }),
    [
      workspaceId,
      path,
      editorRef,
      onSave,
      onCloseTab,
      setFileBufferContent,
      setAllFilesSelectedPath,
      setAllFilesDirExpanded,
      rightSidebarVisible,
      toggleRightSidebar,
      setRightSidebarTabForWorkspace,
      openCommandPaletteFileMode,
      wordWrap,
      setWordWrap,
      minimap,
      setMinimap,
      lineNumbers,
      setLineNumbers,
      fontZoom,
      setFontZoom,
      worktreePath,
      addToast,
    ],
  );
}
