import { useState, useEffect, useRef } from 'react';
import { useSearchParams, useNavigate } from 'react-router';
import { toast } from 'sonner';
import { useFilesAPI, type DirEntry } from '../../../hooks/useFilesAPI';
import { useTasks, type TaskSummary } from '../useTasks';
import { useAgents, type AgentSummary } from '../useAgents';
import { useUserState } from 'state/useUserState';
import { useAuth } from 'hooks/useAuth';
import { useFilesRefresh } from '../../../channels';
import { useNewDashboardDraft } from '../../Dashboards/useNewDashboardDraft';
import { copyToClipboard, canReadClipboard } from 'helpers/clipboard';

/**
 * Where the browser is looking, when it is the one browser that owns the address bar. `urlPath` is opt-in
 * because the same app mounts as a dashboard panel, and a dashboard can hold two of them — one shared
 * param would move both. The /files screen holds exactly one, so there it is the source of truth.
 */
export const FILES_PATH_PARAM = 'path';

/** The base folder is the bare address, so it drops the param rather than spelling itself out. */
const withPath = (prev: URLSearchParams, path: string, basePath: string) => {
  const next = new URLSearchParams(prev);
  if (path === basePath) next.delete(FILES_PATH_PARAM);
  else next.set(FILES_PATH_PARAM, path);
  return next;
};

export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPath = false) => {
  const { user } = useAuth();
  const navigate = useNavigate();
  const openNewDashboard = useNewDashboardDraft();
  const [searchParams, setSearchParams] = useSearchParams();
  const homeRoot = 'home';
  const [, setGlobalPath] = useUserState<string>('files/currentPath', '/');
  const [localPath, setLocalPath] = useState(basePath);
  const scoped = basePath !== '/';
  // Navigation is session-local for a panel — it always opens at its base and never restores the last
  // path. On /files it is the URL instead, so back/forward work and a folder can be linked to; a fresh
  // visit still opens at home, because a URL with no `path` means the base. Either way the main
  // (unscoped) browser mirrors its folder into files/currentPath so the Create Dashboard flow can
  // default a new dashboard's cwd to it.
  const currentPath = urlPath ? (searchParams.get(FILES_PATH_PARAM) ?? basePath) : localPath;
  const setCurrentPath = (path: string) => {
    if (urlPath) setSearchParams((prev) => withPath(prev, path, basePath));
    else setLocalPath(path);
    if (!scoped) setGlobalPath(path);
  };

  /** Query string for a folder — the current params with `path` set, so an open viewer survives a crumb. */
  const searchForPath = (path: string) => withPath(searchParams, path, basePath).toString();

  /**
   * The ephemeral viewer params replace each other wholesale: opening a file closes whatever overlay was
   * open. `path` is not one of them — it is where you are, not what is on top of it — so it survives.
   */
  const setViewerParams = (params: Record<string, string>) =>
    setSearchParams((prev) => {
      const next = new URLSearchParams(params);
      const keep = prev.get(FILES_PATH_PARAM);
      if (keep) next.set(FILES_PATH_PARAM, keep);
      return next;
    });
  const [entries, setEntries] = useState<DirEntry[]>([]);
  const [rootDir, setRootDir] = useState('');
  const [loading, setLoading] = useState(true);
  const [viewMode, setViewMode] = useUserState<'grid' | 'list'>('files/viewMode', 'grid');
  const [showHidden, setShowHidden] = useUserState<boolean>('files/showHidden', false);
  const [uploadProgress, setUploadProgress] = useState<number | null>(null);
  const [selected, setSelected] = useState<Set<string>>(new Set());
  const [clipboard, setClipboard] = useState<ClipboardState>(null);
  const [renamingName, setRenamingName] = useState<string | null>(null);
  const [searchQuery, setSearchQuery] = useState('');
  const [searchResults, setSearchResults] = useState<DirEntry[] | null>(null);
  const [searching, setSearching] = useState(false);
  const [showCloneInput, setShowCloneInput] = useState(false);
  const [cloneUrl, setCloneUrl] = useState('');
  const [cloning, setCloning] = useState(false);
  const [dragging, setDragging] = useState(false);
  const [runningTask, setRunningTask] = useState<{
    task: TaskSummary;
    entry: DirEntry;
    selectedNames?: string[];
  } | null>(null);
  // Agents are their own item type with their own modal — never folded into runningTask.
  const [runningAgent, setRunningAgent] = useState<{ agent: AgentSummary; entry: DirEntry } | null>(null);
  const [showVideoDownload, setShowVideoDownload] = useState(false);
  const [showDictate, setShowDictate] = useState(false);
  const dragCounter = useRef(0);
  const { getMatchingTasks, getMatchingTaskGroups } = useTasks();
  const { getMatchingAgentGroups } = useAgents();
  const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const searchInputRef = useRef<HTMLInputElement | null>(null);
  const fileScrollRef = useRef<HTMLDivElement | null>(null);
  const viewPath = searchParams.get('view');
  const files = useFilesAPI(rootOverride ?? homeRoot);
  const filesRef = useRef(files);
  filesRef.current = files;
  const currentPathRef = useRef(currentPath);
  currentPathRef.current = currentPath;
  const visibleEntries = showHidden ? entries : entries.filter((e) => !e.name.startsWith('.'));

  const entryPath = (name: string) => (currentPath === '/' ? `/${name}` : `${currentPath}/${name}`);
  const selectedPaths = () => Array.from(selected).map(entryPath);

  const refresh = async () => {
    setLoading(true);
    try {
      const data = await filesRef.current.listDir(currentPathRef.current);
      if (data.reset) {
        setCurrentPath('/');
        return;
      }
      setEntries(data.entries);
      if (data.rootDir) setRootDir(data.rootDir);
    } catch (err: any) {
      console.error('[FileBrowser] refresh error:', err);
      toast.error(err?.message || 'Failed to load directory');
    } finally {
      setLoading(false);
    }
  };

  const [refreshSignal] = useFilesRefresh();

  useEffect(() => {
    if (basePath !== '/' && !currentPath.startsWith(basePath)) {
      setCurrentPath(basePath);
      return;
    }
    refresh();
  }, [currentPath, homeRoot, refreshSignal]);

  useEffect(() => {
    setSelected(new Set());
  }, [currentPath]);

  // Debounced search
  useEffect(() => {
    if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
    const q = searchQuery.trim();
    if (!q) {
      setSearchResults(null);
      setSearching(false);
      return;
    }
    setSearching(true);
    searchTimerRef.current = setTimeout(async () => {
      try {
        const data = await filesRef.current.search(q, currentPath);
        setSearchResults(data.results);
      } catch {
        toast.error('Search failed');
        setSearchResults(null);
      } finally {
        setSearching(false);
      }
    }, 300);
    return () => {
      if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
    };
  }, [searchQuery]);

  // Clipboard image paste
  useEffect(() => {
    const handler = (ev: ClipboardEvent) => {
      const tag = (ev.target as HTMLElement).tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA') return;

      const items = ev.clipboardData?.items;
      if (!items) return;

      const imageFiles: File[] = [];
      for (let i = 0; i < items.length; i++) {
        const item = items[i]!;
        if (item.type.startsWith('image/')) {
          const file = item.getAsFile();
          if (file) imageFiles.push(file);
        }
      }
      if (imageFiles.length === 0) return;

      ev.preventDefault();
      const now = new Date();
      const pad = (n: number) => String(n).padStart(2, '0');
      const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;

      const dt = new DataTransfer();
      imageFiles.forEach((file, i) => {
        const ext = file.type.split('/')[1] ?? 'png';
        const name = imageFiles.length === 1 ? `clipboard-${ts}.${ext}` : `clipboard-${ts}-${i + 1}.${ext}`;
        dt.items.add(new File([file], name, { type: file.type }));
      });
      handleUpload(dt.files);
    };

    window.addEventListener('paste', handler);
    return () => window.removeEventListener('paste', handler);
  }, [currentPath, homeRoot]);

  // Keyboard shortcuts
  useEffect(() => {
    const handler = (ev: KeyboardEvent) => {
      const target = ev.target as HTMLElement;
      const tag = target.tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA') return;
      if (target.closest('.monaco-editor')) return;

      if (ev.key === 'Escape') {
        if (viewPath) return;
        if (searchQuery) {
          setSearchQuery('');
          searchInputRef.current?.blur();
          return;
        }
        setSelected(new Set());
        return;
      }

      if ((ev.ctrlKey || ev.metaKey) && ev.key === 'a') {
        ev.preventDefault();
        setSelected(new Set(visibleEntries.map((e) => e.name)));
        return;
      }

      if ((ev.ctrlKey || ev.metaKey) && ev.key === 'c') {
        ev.preventDefault();
        if (selected.size > 0) {
          const paths = selectedPaths();
          setClipboard({ paths, mode: 'copy' });
          toast.success(`Copied ${paths.length} item${paths.length !== 1 ? 's' : ''}`);
        }
        return;
      }

      if ((ev.ctrlKey || ev.metaKey) && ev.key === 'x') {
        ev.preventDefault();
        if (selected.size > 0) {
          const paths = selectedPaths();
          setClipboard({ paths, mode: 'cut' });
          toast.success(`Cut ${paths.length} item${paths.length !== 1 ? 's' : ''}`);
        }
        return;
      }

      if ((ev.ctrlKey || ev.metaKey) && ev.key === 'v') {
        if (clipboard) {
          ev.preventDefault();
          handlePaste();
        }
        return;
      }

      if (ev.key === 'Delete' || ev.key === 'Backspace') {
        if (selected.size > 0) {
          ev.preventDefault();
          handleDeleteSelected();
        }
        return;
      }

      if (ev.key === 'F2') {
        if (selected.size === 1) {
          ev.preventDefault();
          setRenamingName(Array.from(selected)[0]!);
        }
        return;
      }

      // (Printable keys are handled by FileGrid's type-ahead: jump-to-item, or flip to the filter.)
    };

    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [visibleEntries, selected, clipboard, currentPath, viewPath]);

  // ── Handlers ──

  const handleSearchResultClick = (entry: DirEntry) => {
    if (!entry.path) return;
    if (entry.type === 'directory') {
      setCurrentPath(entry.path);
    } else {
      const parentPath = entry.path.substring(0, entry.path.lastIndexOf('/')) || '/';
      setCurrentPath(parentPath);
      setViewerParams({ view: entry.path });
    }
    setSearchQuery('');
  };

  const handleNavigate = (path: string) => {
    if (basePath !== '/' && !path.startsWith(basePath)) {
      setCurrentPath(basePath);
      return;
    }
    setCurrentPath(path);
  };

  const handleOpen = (entry: DirEntry) => {
    setSelected(new Set());
    if (entry.type === 'directory') {
      const next = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`;
      setCurrentPath(next);
    } else {
      const filePath = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`;
      setViewerParams({ view: filePath });
    }
  };

  const handleCreateDir = async (name: string) => {
    const dirPath = currentPath === '/' ? `/${name}` : `${currentPath}/${name}`;
    try {
      await files.createDir(dirPath);
      await refresh();
      toast.success(`Created folder "${name}"`);
    } catch {
      toast.error('Failed to create folder');
    }
  };

  const handleUpload = async (fileList: FileList | File[]) => {
    const count = fileList.length;
    setUploadProgress(0);
    try {
      await files.uploadFiles(currentPath, fileList, (pct) => setUploadProgress(pct));
      await refresh();
      toast.success(`Uploaded ${count} file${count !== 1 ? 's' : ''}`);
    } catch {
      toast.error('Failed to upload files');
    } finally {
      setUploadProgress(null);
    }
  };

  const handleRename = async (entry: DirEntry, newName: string) => {
    const path = entryPath(entry.name);
    try {
      await files.rename(path, newName);
      await refresh();
      toast.success(`Renamed to "${newName}"`);
    } catch {
      toast.error('Failed to rename');
    }
  };

  const handleDelete = async (entry: DirEntry) => {
    if (selected.has(entry.name) && selected.size > 1) {
      handleDeleteSelected();
      return;
    }
    if (!confirm(`Delete "${entry.name}"?`)) return;
    const path = entryPath(entry.name);
    try {
      await files.remove(path);
      setSelected((prev) => {
        const next = new Set(prev);
        next.delete(entry.name);
        return next;
      });
      await refresh();
      toast.success(`Deleted "${entry.name}"`);
    } catch {
      toast.error('Failed to delete');
    }
  };

  const handleDeleteSelected = async () => {
    const names = Array.from(selected);
    if (names.length === 0) return;
    if (!confirm(`Delete ${names.length} item${names.length !== 1 ? 's' : ''}?`)) return;
    try {
      for (const name of names) {
        await files.remove(entryPath(name));
      }
      setSelected(new Set());
      await refresh();
      toast.success(`Deleted ${names.length} item${names.length !== 1 ? 's' : ''}`);
    } catch {
      toast.error('Failed to delete some items');
      await refresh();
    }
  };

  const absPath = (relPath: string) => {
    const rel = relPath.replace(/^\/+/, '');
    return rel ? `${rootDir}/${rel}` : rootDir;
  };

  const handleChat = (entry: DirEntry) => {
    const path = entryPath(entry.name);
    const type = entry.type === 'directory' ? 'folder' : 'file';
    setSearchParams((prev) => {
      const next = new URLSearchParams(prev);
      next.set('chatContext', path);
      next.set('chatType', type);
      return next;
    });
  };

  const handleDownload = async (entry: DirEntry) => {
    const path = entryPath(entry.name);
    try {
      await files.download([path]);
    } catch {
      toast.error('Failed to download');
    }
  };

  const handleDownloadSelected = async () => {
    const paths = selectedPaths();
    if (paths.length === 0) return;
    try {
      await files.download(paths);
    } catch {
      toast.error('Failed to download');
    }
  };

  const handleRunTask = (task: TaskSummary, entry: DirEntry) => {
    // When several items are selected and the task is run on one of them, run it on the whole
    // selection (scoped via INPUT_INCLUDE — files match by path, folders by prefix).
    const names =
      selected.size > 1 && selected.has(entry.name)
        ? visibleEntries.filter((e) => selected.has(e.name)).map((e) => e.name)
        : [];
    setRunningTask({ task, entry, selectedNames: names.length > 1 ? names : undefined });
  };

  // An agent takes exactly one target, so a multi-selection is not folded in the way a task's is.
  const handleRunAgent = (agent: AgentSummary, entry: DirEntry) => setRunningAgent({ agent, entry });

  const handleCreateDashboard = (entry: DirEntry) => {
    const folderPath = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`;
    openNewDashboard({ name: entry.name, path: folderPath });
  };

  const handleCopyPath = (entry: DirEntry) => {
    copyToClipboard(`~${entryPath(entry.name)}`);
    toast.success('Path copied');
  };

  const handleCopyCurrentPath = () => {
    copyToClipboard(`~${currentPath}`);
    toast.success('Path copied');
  };

  const handleChatHere = () => {
    setSearchParams((prev) => {
      const next = new URLSearchParams(prev);
      next.set('chatContext', currentPath);
      next.set('chatType', 'folder');
      return next;
    });
  };

  const handleCreateDashboardHere = () => {
    const dirName = currentPath === '/' ? '' : currentPath.split('/').pop()!;
    openNewDashboard({ name: dirName, path: currentPath });
  };

  const handleReadAloud = async (entry: DirEntry) => {
    const filePath = entryPath(entry.name);
    const toastId = toast.loading('Generating speech audio...');
    try {
      const { audioPath } = await files.tts(filePath, { saveNextTo: true });
      toast.dismiss(toastId);
      refresh();
      setViewerParams({ view: filePath, ephemeral: audioPath });
    } catch {
      toast.error('Failed to generate speech audio', { id: toastId });
    }
  };

  const handleExtract = async (entry: DirEntry) => {
    const filePath = entryPath(entry.name);
    const toastId = toast.loading('Extracting archive...');
    try {
      const { extractedPath } = await files.extract(filePath);
      const folderName = extractedPath.split('/').pop() ?? extractedPath;
      toast.success(`Extracted to "${folderName}"`, { id: toastId });
      await refresh();
    } catch {
      toast.error('Failed to extract archive', { id: toastId });
    }
  };

  const handleGitClone = async () => {
    const url = cloneUrl.trim();
    if (!url) return;
    setCloning(true);
    try {
      await files.gitClone(url, currentPath);
      await refresh();
      toast.success('Repository cloned');
    } catch {
      toast.error('Failed to clone repository');
    } finally {
      setCloning(false);
      setCloneUrl('');
      setShowCloneInput(false);
    }
  };

  const handleCut = () => {
    const paths = selected.size > 0 ? selectedPaths() : [];
    if (paths.length === 0) return;
    setClipboard({ paths, mode: 'cut' });
    toast.success(`Cut ${paths.length} item${paths.length !== 1 ? 's' : ''}`);
  };

  const handleCopy = () => {
    const paths = selected.size > 0 ? selectedPaths() : [];
    if (paths.length === 0) return;
    setClipboard({ paths, mode: 'copy' });
    toast.success(`Copied ${paths.length} item${paths.length !== 1 ? 's' : ''}`);
  };

  const handlePaste = async () => {
    if (clipboard) {
      try {
        if (clipboard.mode === 'copy') {
          await files.copy(clipboard.paths, currentPath);
        } else {
          await files.move(clipboard.paths, currentPath);
          setClipboard(null);
        }
        await refresh();
        toast.success(`Pasted ${clipboard.paths.length} item${clipboard.paths.length !== 1 ? 's' : ''}`);
      } catch {
        toast.error('Failed to paste');
      }
      return;
    }

    // Reading the clipboard is the one half with no fallback. `document.execCommand('paste')` was
    // never permitted from script, so on an insecure origin — which a tailnet http:// URL is — there
    // is no way to pull clipboard contents on demand. Pasting an image has to come from a real paste
    // event instead. Refuse clearly rather than throwing "Cannot read properties of undefined".
    if (!canReadClipboard()) {
      toast.error('Pasting from the clipboard needs a secure context (https). Drag the file in instead.');
      return;
    }

    try {
      const clipboardItems = await navigator.clipboard.read();
      const imageFiles: File[] = [];
      for (const item of clipboardItems) {
        for (const type of item.types) {
          if (type.startsWith('image/')) {
            const blob = await item.getType(type);
            const ext = type.split('/')[1] ?? 'png';
            const now = new Date();
            const pad = (n: number) => String(n).padStart(2, '0');
            const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
            imageFiles.push(new File([blob], `clipboard-${ts}.${ext}`, { type }));
          }
        }
      }
      if (imageFiles.length > 0) {
        const dt = new DataTransfer();
        imageFiles.forEach((f) => dt.items.add(f));
        await handleUpload(dt.files);
      }
    } catch {
      // Clipboard API not available or permission denied
    }
  };

  const handleDragEnter = (ev: React.DragEvent) => {
    ev.preventDefault();
    dragCounter.current++;
    if (ev.dataTransfer.types.includes('Files')) {
      setDragging(true);
    }
  };

  const handleDragLeave = (ev: React.DragEvent) => {
    ev.preventDefault();
    dragCounter.current--;
    if (dragCounter.current === 0) {
      setDragging(false);
    }
  };

  const handleDragOver = (ev: React.DragEvent) => {
    ev.preventDefault();
  };

  const handleDrop = async (ev: React.DragEvent) => {
    ev.preventDefault();
    dragCounter.current = 0;
    setDragging(false);

    const items = ev.dataTransfer.items;
    if (!items || items.length === 0) return;

    const fsEntries: FileSystemEntry[] = [];
    for (let i = 0; i < items.length; i++) {
      const entry = items[i]!.webkitGetAsEntry?.();
      if (entry) fsEntries.push(entry);
    }

    if (fsEntries.length === 0) {
      if (ev.dataTransfer.files.length > 0) {
        handleUpload(ev.dataTransfer.files);
      }
      return;
    }

    const allFiles: File[] = [];

    const readEntry = (entry: FileSystemEntry, path: string): Promise<void> => {
      if (entry.isFile) {
        return new Promise((resolve) => {
          (entry as FileSystemFileEntry).file((file) => {
            allFiles.push(new File([file], path + file.name, { type: file.type, lastModified: file.lastModified }));
            resolve();
          });
        });
      }
      if (entry.isDirectory) {
        const reader = (entry as FileSystemDirectoryEntry).createReader();
        return new Promise((resolve) => {
          const readBatch = () => {
            reader.readEntries(async (batch) => {
              if (batch.length === 0) {
                resolve();
                return;
              }
              for (const child of batch) {
                await readEntry(child, path + entry.name + '/');
              }
              readBatch();
            });
          };
          readBatch();
        });
      }
      return Promise.resolve();
    };

    for (const entry of fsEntries) {
      await readEntry(entry, '');
    }

    if (allFiles.length > 0) {
      handleUpload(allFiles);
    }
  };

  const handleBackgroundClick = (ev: React.MouseEvent) => {
    if ((ev.target as HTMLElement).closest('[data-file-item]')) return;
    setSelected(new Set());
  };

  return {
    // Auth
    user,
    // Navigation
    homeRoot,
    currentPath,
    setCurrentPath,
    // Null when this browser does not own the address bar — the breadcrumb then stays buttons.
    searchForPath: urlPath ? searchForPath : null,
    // Directory listing
    visibleEntries,
    loading,
    refresh,
    // View
    viewMode,
    setViewMode,
    showHidden,
    setShowHidden,
    // Selection
    selected,
    setSelected,
    clipboard,
    renamingName,
    setRenamingName,
    // Search
    searchQuery,
    setSearchQuery,
    searchResults,
    searching,
    searchInputRef,
    // Upload
    uploadProgress,
    // Clone
    showCloneInput,
    setShowCloneInput,
    cloneUrl,
    setCloneUrl,
    cloning,
    // Drag & drop
    dragging,
    // Task runner
    runningTask,
    setRunningTask,
    getMatchingTasks,
    getMatchingTaskGroups,
    // Agent runner
    runningAgent,
    setRunningAgent,
    getMatchingAgentGroups,
    // Files API (for self-contained dialogs like the video downloader)
    files,
    // Video download
    showVideoDownload,
    setShowVideoDownload,
    // Dictate
    showDictate,
    setShowDictate,
    // Refs
    fileScrollRef,
    // Handlers
    handleSearchResultClick,
    handleNavigate,
    handleOpen,
    handleCreateDir,
    handleUpload,
    handleRename,
    handleDelete,
    handleDeleteSelected,
    handleChat,
    handleCopyPath,
    handleCopyCurrentPath,
    handleChatHere,
    handleDownload,
    handleDownloadSelected,
    handleRunTask,
    handleRunAgent,
    handleCreateDashboard,
    handleCreateDashboardHere,
    handleReadAloud,
    handleExtract,
    handleGitClone,
    handleCut,
    handleCopy,
    handlePaste,
    handleDragEnter,
    handleDragLeave,
    handleDragOver,
    handleDrop,
    handleBackgroundClick,
    getEntryAbsPath: (name: string) => absPath(entryPath(name)),
  };
};

export type UseFileBrowserAppType = ReturnType<typeof useFileBrowserApp>;

// ── Types ──

type ClipboardState = { paths: string[]; mode: 'copy' | 'cut' } | null;
