import { useCallback, useEffect, useMemo, useState } from 'react';
import { ChevronRight, ChevronDown, FilePlus, FileText, Folder, PanelRightClose, PanelRightOpen } from 'lucide-react';
import { api, ApiError } from '@/api/client';
import type { FileEntry } from '@/api/types';
import { inferLanguage } from '@/lib/attachments';
import { sessionEvents } from '@/hooks/sessionEvents';
import { useRightRailDrawer } from '@/hooks/useRightRailDrawer';
import { useViewport } from '@/hooks/useViewport';
import { useProjectGit } from '@/hooks/useProjectGit';
import { useGitDiff } from '@/hooks/useGitDiff';
import { useDiffPreferences } from '@/hooks/useDiffPreferences';
import { FileViewerOverlay } from '@/components/FileViewerOverlay';
import { GitDiffView } from '@/components/GitDiffView';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog';
import { cn } from '@/lib/utils';

type RailTab = 'files' | 'git';

interface Props {
  projectId: string;
  sessionId: string;
}

const STORAGE_KEY = 'boocode.rightrail';

function basename(path: string): string {
  if (!path) return '';
  const parts = path.split('/');
  return parts[parts.length - 1] ?? path;
}

function joinPath(parent: string, name: string): string {
  if (!parent || parent === '.' || parent === '') return name;
  return `${parent}/${name}`;
}

export function RightRail({ projectId, sessionId }: Props) {
  const { isMobile } = useViewport();
  const { open: drawerOpen, setOpen: setDrawerOpen } = useRightRailDrawer();
  const [open, setOpen] = useState(() => {
    try { return localStorage.getItem(`${STORAGE_KEY}.open`) !== 'false'; } catch { return true; }
  });
  const [tab, setTab] = useState<RailTab>('files');
  const [filter, setFilter] = useState('');
  const [expandedDirs, setExpandedDirs] = useState<Set<string>>(new Set());
  const [cache, setCache] = useState<Map<string, FileEntry[]>>(new Map());
  const [fullFileList, setFullFileList] = useState<string[] | null>(null);
  const [viewerFile, setViewerFile] = useState<{ path: string; content: string } | null>(null);

  // Git metadata: dirty dot on the Git tab (no new fetch — reuses the 30s poll).
  const git = useProjectGit(projectId);
  const isDirty = git?.is_dirty ?? false;

  // Git diff view state (Phase 2: includes write callbacks).
  const { result: gitDiff, loading: gitLoading, error: gitError, mode: gitMode, selectMode, refresh: refreshDiff, mutating: gitMutating, mutateError: gitMutateError, stage: gitStage, unstage: gitUnstage, commit: gitCommit, discard: gitDiscard, modeSuggestion: gitModeSuggestion } = useGitDiff(projectId);
  const showGitTab = gitDiff === null || gitDiff.git_repo;

  // FIX 5: pending-changes count — fetched when git tab is active so the empty state
  // can hint that unapplied pending changes exist in the Coder pane.
  const [pendingCount, setPendingCount] = useState(0);
  useEffect(() => {
    if (tab !== 'git') return;
    const check = () => {
      fetch(`/api/coder/sessions/${sessionId}/pending`)
        .then((r) => r.ok ? r.json() as Promise<Array<{ status: string }>> : [])
        .then((data) => setPendingCount(data.filter((c) => c.status === 'pending').length))
        .catch(() => {});
    };
    check();
    return sessionEvents.subscribe((e) => {
      if (e.type === 'git_diff_refresh') check();
    });
  }, [tab, sessionId]);

  // New-file-from-pasted-text modal. Queues a pending_changes create via
  // BooCoder; it then shows in the CoderPane DiffPanel for explicit apply.
  const [newFileOpen, setNewFileOpen] = useState(false);
  const [newFilePath, setNewFilePath] = useState('');
  const [newFileContent, setNewFileContent] = useState('');
  const [creating, setCreating] = useState(false);
  const [createError, setCreateError] = useState<string | null>(null);

  // Diff toolbar state (integration with expandedPaths pending)
  const { preferences: diffPrefs, updatePreferences: updateDiffPrefs } = useDiffPreferences();

  // File editing state
  const [editingFile, setEditingFile] = useState<string | null>(null);
  const [editContent, setEditContent] = useState('');
  const [editLoading, setEditLoading] = useState(false);
  const [editError, setEditError] = useState<string | null>(null);

  const openNewFile = useCallback(() => {
    setNewFilePath('');
    setNewFileContent('');
    setCreateError(null);
    setNewFileOpen(true);
  }, []);

  const submitNewFile = useCallback(async () => {
    const path = newFilePath.trim();
    if (!path || creating) return;
    setCreating(true);
    setCreateError(null);
    try {
      await api.coder.createPendingFile(sessionId, path, newFileContent);
      setNewFileOpen(false);
      setNewFilePath('');
      setNewFileContent('');
    } catch (err) {
      // 422 WriteGuardError surfaces via ApiError.message (the route's { error }).
      setCreateError(err instanceof ApiError ? err.message : err instanceof Error ? err.message : 'Failed to create file');
    } finally {
      setCreating(false);
    }
  }, [sessionId, newFilePath, newFileContent, creating]);

  // Combined open state: on mobile use the global drawer state (toggled by
  // the Session header's FolderTree button); on desktop use the persistent
  // internal state.
  const isOpen = isMobile ? drawerOpen : open;
  const closeRail = useCallback(() => {
    if (isMobile) setDrawerOpen(false);
    else setOpen(false);
  }, [isMobile, setDrawerOpen]);
  const openRail = useCallback(() => {
    if (isMobile) setDrawerOpen(true);
    else setOpen(true);
  }, [isMobile, setDrawerOpen]);

  useEffect(() => {
    // best-effort; ignore failure because localStorage may be unavailable (quota, private mode)
    try { localStorage.setItem(`${STORAGE_KEY}.open`, String(open)); } catch {}
  }, [open]);

  useEffect(() => {
    let cancelled = false;
    api.projects.files(projectId).then((r) => {
      if (!cancelled) setFullFileList(r.files);
    }).catch(() => {});
    return () => { cancelled = true; };
  }, [projectId]);

  const loadDir = useCallback(async (dirPath: string) => {
    const apiPath = dirPath === '' ? '.' : dirPath;
    try {
      const result = await api.projects.listDir(projectId, apiPath);
      setCache((prev) => { const next = new Map(prev); next.set(dirPath, result.entries); return next; });
    } catch { /* ignore */ }
  }, [projectId]);

  useEffect(() => {
    if (!isOpen) return;
    if (!cache.has('')) void loadDir('');
  }, [isOpen, cache, loadDir]);

  function toggleDir(dirPath: string) {
    setExpandedDirs((prev) => {
      const next = new Set(prev);
      if (next.has(dirPath)) {
        next.delete(dirPath);
      } else {
        next.add(dirPath);
        if (!cache.has(dirPath)) void loadDir(dirPath);
      }
      return next;
    });
  }

  async function startEdit(path: string) {
    setEditingFile(path);
    setEditLoading(true);
    setEditError(null);
    try {
      const result = await api.projects.viewFile(projectId, path);
      setEditContent(result.content);
    } catch {
      setEditError('Failed to load file');
      setEditingFile(null);
    } finally {
      setEditLoading(false);
    }
  }

  async function saveEdit() {
    if (!editingFile) return;
    try {
      await api.projects.writeFile(projectId, editingFile, editContent);
      setEditingFile(null);
      setEditContent('');
      sessionEvents.emit({ type: 'git_diff_refresh' });
    } catch {
      setEditError('Failed to save file');
    }
  }

  function cancelEdit() {
    setEditingFile(null);
    setEditContent('');
    setEditError(null);
  }

  // Cancel edit when switching tabs
  useEffect(() => {
    if (tab !== 'files') cancelEdit();
  }, [tab]);

  async function openFile(path: string) {
    try {
      const result = await api.projects.viewFile(projectId, path);
      setViewerFile({ path, content: result.content });
    } catch { /* ignore */ }
  }

  // Filter results
  const trimmed = filter.trim().toLowerCase();
  const filterActive = trimmed.length > 0;

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

  const filterResults = useMemo<FilterResult[]>(() => {
    if (!filterActive) return [];
    if (fullFileList) {
      const filenameMatches: string[] = [];
      const pathOnly: string[] = [];
      for (const p of fullFileList) {
        const lp = p.toLowerCase();
        if (!lp.includes(trimmed)) continue;
        if (basename(p).toLowerCase().includes(trimmed)) filenameMatches.push(p);
        else pathOnly.push(p);
      }
      filenameMatches.sort((a, b) => a.localeCompare(b));
      pathOnly.sort((a, b) => a.localeCompare(b));
      return [...filenameMatches, ...pathOnly].slice(0, 50).map((p) => ({ path: p, name: basename(p) }));
    }
    return [];
  }, [filterActive, trimmed, fullFileList]);

  // Trigger a git diff refresh whenever the Git tab becomes active.
  useEffect(() => {
    if (tab === 'git') sessionEvents.emit({ type: 'git_diff_refresh' });
  }, [tab]);

  // Listen for open_file_in_browser events
  useEffect(() => {
    return sessionEvents.subscribe((event) => {
      if (event.type !== 'open_file_in_browser') return;
      if (!isOpen) openRail();
      void openFile(event.path);
    });
  }, [isOpen, openRail, projectId]);

  // Desktop closed state: render the floating chevron handle. Mobile never
  // shows the handle — the toggle lives in the Session header on mobile.
  if (!isMobile && !open) {
    return (
      <button
        type="button"
        onClick={() => setOpen(true)}
        className="shrink-0 border-l bg-sidebar p-2 hover:bg-muted"
        aria-label="Open file browser"
      >
        <PanelRightOpen size={16} />
      </button>
    );
  }

  const rootEntries = cache.get('') ?? [];

  // Mobile: render as fixed-position right-side drawer (always mounted so
  // the transform transition can animate in/out). Desktop: inline aside.
  const asideCls = isMobile
    ? cn(
        'fixed inset-y-0 right-0 z-40 w-[85vw] max-w-sm border-l bg-sidebar flex flex-col overflow-hidden',
        'motion-reduce:transition-none transition-transform duration-200 ease-out',
        drawerOpen ? 'translate-x-0' : 'translate-x-full',
      )
    : 'w-64 shrink-0 border-l bg-sidebar flex flex-col h-full overflow-hidden';

  return (
    <>
      <aside className={asideCls}>
        {/* Header: Files / Git tab strip, FilePlus (Files only), close */}
        <div className="flex items-center gap-1 px-2 py-1.5 border-b shrink-0">
          <button
            type="button"
            onClick={() => setTab('files')}
            className={cn(
              'text-xs px-2 py-0.5 rounded max-md:min-h-[44px]',
              tab === 'files' ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground hover:text-foreground',
            )}
          >
            Files
          </button>
          {showGitTab && (
            <button
              type="button"
              onClick={() => setTab('git')}
              className={cn(
                'relative text-xs px-2 py-0.5 rounded max-md:min-h-[44px] flex items-center gap-1',
                tab === 'git' ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground hover:text-foreground',
              )}
            >
              Git
              {isDirty && (
                <span className="w-1.5 h-1.5 rounded-full bg-yellow-400 shrink-0" aria-label="dirty" />
              )}
            </button>
          )}
          <div className="flex-1" />
          {tab === 'files' && (
            <button
              type="button"
              onClick={openNewFile}
              className="p-1 rounded hover:bg-muted text-muted-foreground max-md:min-h-[44px] max-md:min-w-[44px]"
              aria-label="New file from pasted text"
              title="New file"
            >
              <FilePlus size={14} />
            </button>
          )}
          <button
            type="button"
            onClick={closeRail}
            className="p-1 rounded hover:bg-muted text-muted-foreground max-md:min-h-[44px] max-md:min-w-[44px]"
            aria-label="Close file browser"
          >
            <PanelRightClose size={14} />
          </button>
        </div>

        {/* Files tab content */}
        {tab === 'files' && (
          <>
            <div className="px-2 py-1.5 shrink-0">
              <Input
                value={filter}
                onChange={(e) => setFilter(e.target.value)}
                placeholder="Filter files..."
                className="h-7 text-xs"
              />
            </div>
            <div className="flex-1 overflow-y-auto px-1 py-1">
              {filterActive ? (
                filterResults.length > 0 ? (
                  <ul className="list-none space-y-0.5">
                    {filterResults.map((r) => (
                      <li key={r.path}>
                        <button
                          type="button"
                          className="w-full flex items-center gap-1 px-2 py-1 text-xs rounded hover:bg-muted/60 text-left"
                          onClick={() => void openFile(r.path)}
                        >
                          <FileText size={12} className="text-muted-foreground shrink-0" />
                          <span className="font-bold truncate">{r.name}</span>
                          <span className="text-muted-foreground ml-1 truncate">{r.path}</span>
                        </button>
                      </li>
                    ))}
                  </ul>
                ) : (
                  <div className="text-xs text-muted-foreground px-2 py-4 text-center">No matches</div>
                )
              ) : editingFile ? (
                <div className="flex flex-col flex-1 overflow-hidden p-2 gap-2">
                  <div className="text-xs font-mono truncate text-muted-foreground">{editingFile}</div>
                  {editLoading ? (
                    <div className="flex-1 flex items-center justify-center text-xs text-muted-foreground">Loading...</div>
                  ) : (
                    <>
                      <textarea
                        value={editContent}
                        onChange={(e) => setEditContent(e.target.value)}
                        className="flex-1 font-mono text-xs p-2 rounded border bg-background resize-none outline-none focus:ring-1 focus:ring-ring"
                        onKeyDown={(e) => {
                          if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') saveEdit();
                          if (e.key === 'Escape') cancelEdit();
                        }}
                      />
                      {editError && <p className="text-xs text-destructive">{editError}</p>}
                      <div className="flex items-center gap-2 justify-end">
                        <button type="button" onClick={cancelEdit} className="text-xs px-2 py-1 rounded border hover:bg-muted">Cancel</button>
                        <button type="button" onClick={saveEdit} className="text-xs px-3 py-1 rounded bg-primary text-primary-foreground hover:bg-primary/90">Save</button>
                      </div>
                    </>
                  )}
                </div>
              ) : (
                <TreeLevel
                  parentPath=""
                  entries={rootEntries}
                  cache={cache}
                  expanded={expandedDirs}
                  depth={0}
                  onToggleDir={toggleDir}
                  onSelectFile={(path) => void openFile(path)}
                  onEditFile={startEdit}
                />
              )}
            </div>
          </>
        )}

        {/* Git tab content */}
        {tab === 'git' && (
          <GitDiffView
            result={gitDiff}
            loading={gitLoading}
            error={gitError}
            mode={gitMode}
            sessionId={sessionId}
            onSelectMode={selectMode}
            onRefresh={refreshDiff}
            mutating={gitMutating}
            mutateError={gitMutateError}
            onStage={gitStage}
            onUnstage={gitUnstage}
            onCommit={gitCommit}
            onDiscard={gitDiscard}
            modeSuggestion={gitModeSuggestion}
            pendingCount={pendingCount}
            layout={diffPrefs.layout}
            wrapLines={diffPrefs.wrapLines}
            hideWhitespace={diffPrefs.hideWhitespace}
            onLayoutChange={(layout) => updateDiffPrefs({ layout })}
            onWrapLinesChange={(wrapLines) => updateDiffPrefs({ wrapLines })}
            onHideWhitespaceChange={(hideWhitespace) => updateDiffPrefs({ hideWhitespace })}
          />
        )}
      </aside>

      {viewerFile && (
        <FileViewerOverlay
          path={viewerFile.path}
          content={viewerFile.content}
          lang={inferLanguage(viewerFile.path)}
          onClose={() => setViewerFile(null)}
        />
      )}

      <Dialog open={newFileOpen} onOpenChange={setNewFileOpen}>
        <DialogContent className="sm:max-w-lg">
          <DialogHeader>
            <DialogTitle>New file from pasted text</DialogTitle>
            <DialogDescription>
              Queues a new file as a pending change. Review and apply it from the Coder pane.
            </DialogDescription>
          </DialogHeader>
          <div className="space-y-3">
            <div className="space-y-1.5">
              <Label htmlFor="new-file-path" className="text-xs">Path (relative to project root)</Label>
              <Input
                id="new-file-path"
                value={newFilePath}
                onChange={(e) => setNewFilePath(e.target.value)}
                placeholder="src/example.ts"
              />
            </div>
            <div className="space-y-1.5">
              <Label htmlFor="new-file-content" className="text-xs">Content</Label>
              <Textarea
                id="new-file-content"
                value={newFileContent}
                onChange={(e) => setNewFileContent(e.target.value)}
                placeholder="Paste file contents here…"
                autoFocus
                className="min-h-[180px] font-mono text-xs"
              />
            </div>
            {createError && <p className="text-xs text-destructive">{createError}</p>}
          </div>
          <DialogFooter>
            <Button variant="ghost" onClick={() => setNewFileOpen(false)} disabled={creating}>
              Cancel
            </Button>
            <Button onClick={() => void submitNewFile()} disabled={creating || !newFilePath.trim()}>
              {creating ? 'Creating…' : 'Create'}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  );
}

interface TreeLevelProps {
  parentPath: string;
  entries: FileEntry[];
  cache: Map<string, FileEntry[]>;
  expanded: Set<string>;
  depth: number;
  onToggleDir: (dirPath: string) => void;
  onSelectFile: (path: string) => void;
  onEditFile?: (path: string) => void;
}

function TreeLevel({ parentPath, entries, cache, expanded, depth, onToggleDir, onSelectFile, onEditFile }: TreeLevelProps) {
  const sorted = useMemo(() => {
    const copy = [...entries];
    copy.sort((a, b) => {
      if (a.kind !== b.kind) return a.kind === 'dir' ? -1 : 1;
      return a.name.localeCompare(b.name);
    });
    return copy;
  }, [entries]);

  return (
    <ul className="list-none">
      {sorted.map((entry) => {
        const fullPath = joinPath(parentPath, entry.name);
        const isExpanded = entry.kind === 'dir' && expanded.has(fullPath);
        return (
          <li key={fullPath}>
            <div
              className="flex items-center gap-1 px-1 py-0.5 text-xs cursor-default rounded hover:bg-muted/60"
              style={{ paddingLeft: 4 + depth * 12 }}
              onClick={() => {
                if (entry.kind === 'dir') onToggleDir(fullPath);
                else onSelectFile(fullPath);
              }}
              onDoubleClick={() => {
                if (entry.kind === 'file') onEditFile?.(fullPath);
              }}
            >
              {entry.kind === 'dir' ? (
                isExpanded ? <ChevronDown size={10} className="shrink-0" /> : <ChevronRight size={10} className="shrink-0" />
              ) : (
                <span className="w-[10px] shrink-0" />
              )}
              {entry.kind === 'dir' ? (
                <Folder size={12} className="text-muted-foreground shrink-0" />
              ) : (
                <FileText size={12} className="text-muted-foreground shrink-0" />
              )}
              <span className="truncate">{entry.name}</span>
            </div>
            {entry.kind === 'dir' && isExpanded && cache.has(fullPath) && (
              <TreeLevel
                parentPath={fullPath}
                entries={cache.get(fullPath) ?? []}
                cache={cache}
                expanded={expanded}
                depth={depth + 1}
                onToggleDir={onToggleDir}
                onSelectFile={onSelectFile}
                onEditFile={onEditFile}
              />
            )}
          </li>
        );
      })}
    </ul>
  );
}
