import { useState, useEffect, useRef, useCallback } from 'react';
import {
  X,
  Download,
  Loader2,
  Music,
  Film,
  Image,
  FileType2,
  Maximize2,
  Minimize2,
  Square,
  Play,
  Pause,
  Volume2,
  VolumeX,
  ZoomIn,
  ZoomOut,
  RotateCw,
  ArrowUp,
} from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import rehypeSlug from 'rehype-slug';
import { Dialog, DialogOverlay, DialogPortal } from '@/components/ui/dialog';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { cardStyle } from '@/components/Card';
import { useFiles } from 'apps/FileBrowser';
import { getHeaders } from 'hooks/useClient';
import { config } from 'config';
import { toast } from 'sonner';
import { getIcon } from 'material-file-icons';

type FileViewerProps = {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  filePath: string;
  fileName: string;
  root?: string;
};

type FileType = 'markdown' | 'audio' | 'video' | 'image' | 'pdf' | 'code' | 'text';

const AUDIO_EXTS = ['mp3', 'wav', 'flac', 'ogg', 'oga', 'opus', 'aac', 'm4a', 'wma'];
const NATIVE_VIDEO_EXTS = ['mp4', 'm4v', 'webm', 'ogv', 'mov'];
const TRANSCODE_VIDEO_EXTS = [
  'mkv',
  'avi',
  'wmv',
  'flv',
  'ts',
  'mts',
  'm2ts',
  '3gp',
  '3g2',
  'vob',
  'divx',
  'asf',
  'f4v',
  'rm',
  'rmvb',
];
const ALL_VIDEO_EXTS = [...NATIVE_VIDEO_EXTS, ...TRANSCODE_VIDEO_EXTS];
const IMAGE_EXTS = [
  'jpg',
  'jpeg',
  'png',
  'gif',
  'webp',
  'svg',
  'bmp',
  'ico',
  'tiff',
  'tif',
  'avif',
  'heic',
  'heif',
  'jfif',
  'apng',
];

// Map file extension to shiki language identifier
const EXT_TO_LANG: Record<string, string> = {
  js: 'javascript',
  mjs: 'javascript',
  cjs: 'javascript',
  jsx: 'jsx',
  ts: 'typescript',
  mts: 'typescript',
  cts: 'typescript',
  tsx: 'tsx',
  json: 'json',
  jsonc: 'jsonc',
  html: 'html',
  htm: 'html',
  css: 'css',
  scss: 'scss',
  sass: 'sass',
  less: 'less',
  py: 'python',
  rb: 'ruby',
  rs: 'rust',
  go: 'go',
  java: 'java',
  kt: 'kotlin',
  kts: 'kotlin',
  swift: 'swift',
  c: 'c',
  h: 'c',
  cpp: 'cpp',
  cc: 'cpp',
  cxx: 'cpp',
  hpp: 'cpp',
  cs: 'csharp',
  php: 'php',
  sh: 'bash',
  bash: 'bash',
  zsh: 'bash',
  sql: 'sql',
  yaml: 'yaml',
  yml: 'yaml',
  toml: 'toml',
  xml: 'xml',
  xsl: 'xml',
  lua: 'lua',
  r: 'r',
  dart: 'dart',
  vue: 'vue',
  svelte: 'svelte',
  graphql: 'graphql',
  gql: 'graphql',
  dockerfile: 'dockerfile',
  makefile: 'makefile',
  zig: 'zig',
  elixir: 'elixir',
  ex: 'elixir',
  exs: 'elixir',
};

function getFileType(name: string): FileType {
  const ext = name.split('.').pop()?.toLowerCase() ?? '';
  const baseName = name.toLowerCase();
  if (['md', 'mdx', 'markdown'].includes(ext)) return 'markdown';
  if (ext === 'pdf') return 'pdf';
  if (EXT_TO_LANG[ext]) return 'code';
  if (AUDIO_EXTS.includes(ext)) return 'audio';
  if (ALL_VIDEO_EXTS.includes(ext)) return 'video';
  if (IMAGE_EXTS.includes(ext)) return 'image';
  if (baseName === 'dockerfile' || baseName === 'makefile') return 'code';
  return 'text';
}

function getLang(name: string): string {
  const ext = name.split('.').pop()?.toLowerCase() ?? '';
  const baseName = name.toLowerCase();
  if (baseName === 'dockerfile') return 'dockerfile';
  if (baseName === 'makefile') return 'makefile';
  return EXT_TO_LANG[ext] ?? 'text';
}

function getExt(name: string): string {
  return name.split('.').pop()?.toLowerCase() ?? '';
}

function needsTranscode(name: string): boolean {
  return TRANSCODE_VIDEO_EXTS.includes(getExt(name));
}

function getRawUrl(filePath: string, root?: string): string {
  const headers = getHeaders();
  const token = headers['Authorization']?.replace('Bearer ', '') ?? '';
  const rootParam = root && root !== 'home' ? `&root=${encodeURIComponent(root)}` : '';
  return `${config.API_URL}/file-browser/raw?path=${encodeURIComponent(filePath)}&token=${encodeURIComponent(token)}${rootParam}`;
}

function getTranscodeUrl(filePath: string, root?: string, t = '0'): string {
  const headers = getHeaders();
  const token = headers['Authorization']?.replace('Bearer ', '') ?? '';
  const rootParam = root && root !== 'home' ? `&root=${encodeURIComponent(root)}` : '';
  return `${config.API_URL}/file-browser/transcode?path=${encodeURIComponent(filePath)}&t=${encodeURIComponent(t)}&token=${encodeURIComponent(token)}${rootParam}`;
}

function formatTime(s: number): string {
  if (!isFinite(s) || isNaN(s)) return '0:00';
  const h = Math.floor(s / 3600);
  const m = Math.floor((s % 3600) / 60);
  const sec = Math.floor(s % 60);
  if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
  return `${m}:${sec.toString().padStart(2, '0')}`;
}

// ── Shared seek bar hook ──
function useSeekBar(mediaRef: React.RefObject<HTMLMediaElement | null>, duration: number) {
  const barRef = useRef<HTMLDivElement>(null);

  const seekTo = useCallback(
    (clientX: number) => {
      const el = mediaRef.current;
      const bar = barRef.current;
      if (!el || !bar || !duration) return;
      const rect = bar.getBoundingClientRect();
      const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
      el.currentTime = pct * duration;
    },
    [duration, mediaRef],
  );

  const onSeekDown = useCallback(
    (e: React.MouseEvent<HTMLDivElement>) => {
      e.preventDefault();
      seekTo(e.clientX);
      const onMove = (ev: MouseEvent) => seekTo(ev.clientX);
      const onUp = () => {
        window.removeEventListener('mousemove', onMove);
        window.removeEventListener('mouseup', onUp);
      };
      window.addEventListener('mousemove', onMove);
      window.addEventListener('mouseup', onUp);
    },
    [seekTo],
  );

  return { barRef, onSeekDown };
}

// ── Seek bar component ──
function SeekBar({
  barRef,
  onSeekDown,
  pct,
  trackClass = 'bg-duck-dark/10',
  fillClass = 'bg-duck-teal',
  thumbClass = 'bg-duck-teal border-white',
}: {
  barRef: React.RefObject<HTMLDivElement | null>;
  onSeekDown: (e: React.MouseEvent<HTMLDivElement>) => void;
  pct: number;
  trackClass?: string;
  fillClass?: string;
  thumbClass?: string;
}) {
  return (
    <div ref={barRef} onMouseDown={onSeekDown} className="relative h-4 flex items-center cursor-pointer group">
      <div className={`absolute left-0 right-0 h-1.5 rounded-full ${trackClass} pointer-events-none`}>
        <div className={`absolute inset-y-0 left-0 rounded-full ${fillClass}`} style={{ width: `${pct}%` }} />
      </div>
      <div
        className={`absolute top-1/2 -translate-y-1/2 w-3.5 h-3.5 rounded-full shadow-md border-2 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none ${thumbClass} ${fillClass}`}
        style={{ left: `calc(${pct}% - 7px)` }}
      />
    </div>
  );
}

// ── Copy button for code blocks ──
function CopyButton({ text }: { text: string }) {
  const [copied, setCopied] = useState(false);
  return (
    <button
      onClick={() => {
        navigator.clipboard.writeText(text);
        setCopied(true);
        setTimeout(() => setCopied(false), 1500);
      }}
      className="absolute top-2 right-2 px-2 py-1 text-[10px] font-mono rounded bg-white/10 text-white/60 hover:text-white hover:bg-white/20 transition-colors cursor-pointer"
    >
      {copied ? 'Copied!' : 'Copy'}
    </button>
  );
}

// ── Highlighted code block for markdown ──
function HighlightedCodeBlock({ code, lang }: { code: string; lang: string }) {
  const [html, setHtml] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;
    import('shiki')
      .then(({ codeToHtml }) => codeToHtml(code, { lang, theme: 'github-dark-default' }))
      .then((result) => {
        if (!cancelled) setHtml(result);
      })
      .catch(() => {});
    return () => {
      cancelled = true;
    };
  }, [code, lang]);

  if (html) {
    return (
      <div className="relative my-4">
        <CopyButton text={code} />
        {lang && (
          <span className="absolute top-2 left-3 text-[10px] font-mono text-white/30 uppercase tracking-wider z-10">
            {lang}
          </span>
        )}
        <div
          className="[&_pre]:rounded-lg [&_pre]:p-4 [&_pre]:pt-8 [&_pre]:overflow-x-auto [&_pre]:text-sm [&_pre]:font-mono [&_pre]:leading-relaxed [&_pre]:border [&_pre]:border-white/5 [&_code]:font-mono"
          dangerouslySetInnerHTML={{ __html: html }}
        />
      </div>
    );
  }

  return (
    <pre className="relative rounded-lg bg-[#0d1117] text-[#e6edf3] p-4 overflow-x-auto text-sm font-mono leading-relaxed my-4 border border-white/5">
      <CopyButton text={code} />
      {lang && (
        <span className="absolute top-2 left-3 text-[10px] font-mono text-white/30 uppercase tracking-wider">
          {lang}
        </span>
      )}
      <code className="block pt-4">{code}</code>
    </pre>
  );
}

// ── Scroll to top button ──
function ScrollToTopButton({ scrollContainer }: { scrollContainer: React.RefObject<HTMLDivElement | null> }) {
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const el = scrollContainer.current;
    if (!el) return;
    const onScroll = () => setVisible(el.scrollTop > 200);
    el.addEventListener('scroll', onScroll, { passive: true });
    return () => el.removeEventListener('scroll', onScroll);
  }, [scrollContainer]);

  if (!visible) return null;

  return (
    <button
      onClick={() => scrollContainer.current?.scrollTo({ top: 0, behavior: 'smooth' })}
      className="sticky bottom-4 float-right mr-4 z-10 p-2.5 rounded-full bg-duck-teal text-white shadow-lg hover:bg-duck-teal/90 active:scale-95 transition-all cursor-pointer"
      title="Scroll to top"
    >
      <ArrowUp className="h-4 w-4" />
    </button>
  );
}

// ── Markdown renderer ──
const MarkdownRenderer = ({
  content,
  scrollContainer,
}: {
  content: string;
  scrollContainer: React.RefObject<HTMLDivElement | null>;
}) => {
  const handleAnchorClick = useCallback(
    (ev: React.MouseEvent<HTMLElement>) => {
      const target = (ev.target as HTMLElement).closest('a');
      if (!target) return;
      const href = target.getAttribute('href');
      if (!href?.startsWith('#')) return;
      ev.preventDefault();
      const id = href.slice(1);
      const el = scrollContainer.current?.querySelector(`#${CSS.escape(id)}`);
      if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
    },
    [scrollContainer],
  );

  return (
    <article className="file-viewer-md" onClick={handleAnchorClick}>
      <ReactMarkdown
        remarkPlugins={[remarkGfm]}
        rehypePlugins={[rehypeRaw, rehypeSlug]}
        components={{
          pre({ children }) {
            return <div className="relative">{children}</div>;
          },
          code({ className, children, ...props }) {
            const isBlock = className?.startsWith('language-');
            const lang = className?.replace('language-', '') ?? '';
            const text = String(children).replace(/\n$/, '');

            if (!isBlock) {
              return (
                <code
                  className="px-1.5 py-0.5 rounded bg-duck-teal/10 text-duck-teal text-[0.85em] font-mono"
                  {...props}
                >
                  {children}
                </code>
              );
            }

            return <HighlightedCodeBlock code={text} lang={lang} />;
          },
        }}
      >
        {content}
      </ReactMarkdown>
    </article>
  );
};

// ── Text renderer ──
const TextRenderer = ({ content }: { content: string }) => (
  <pre className="whitespace-pre-wrap font-mono text-sm text-duck-dark leading-relaxed p-4">{content}</pre>
);

// ── Code renderer with syntax highlighting ──
const CodeRenderer = ({ content, lang }: { content: string; lang: string }) => {
  const [html, setHtml] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;
    import('shiki')
      .then(({ codeToHtml }) => codeToHtml(content, { lang, theme: 'github-dark-default' }))
      .then((result) => {
        if (!cancelled) setHtml(result);
      })
      .catch(() => {
        if (!cancelled) setHtml(null);
      });
    return () => {
      cancelled = true;
    };
  }, [content, lang]);

  if (html === null) {
    return <TextRenderer content={content} />;
  }

  return (
    <div
      className="code-highlight text-sm leading-relaxed [&_pre]:p-4 [&_pre]:overflow-x-auto [&_pre]:rounded-none [&_code]:font-mono"
      dangerouslySetInnerHTML={{ __html: html }}
    />
  );
};

// ── Audio renderer ──
const AudioRenderer = ({ src, fileName }: { src: string; fileName: string }) => {
  const audioRef = useRef<HTMLAudioElement>(null);
  const [playing, setPlaying] = useState(false);
  const [currentTime, setCurrent] = useState(0);
  const [duration, setDuration] = useState(0);
  const [volume, setVolume] = useState(1);
  const [muted, setMuted] = useState(false);
  const [loaded, setLoaded] = useState(false);
  const [error, setError] = useState(false);
  const { barRef, onSeekDown } = useSeekBar(audioRef, duration);

  const ext = fileName.split('.').pop()?.toUpperCase() ?? 'AUDIO';

  useEffect(() => {
    const a = audioRef.current;
    if (!a) return;
    const onLoaded = () => {
      setDuration(a.duration);
      setLoaded(true);
    };
    const onTime = () => setCurrent(a.currentTime);
    const onEnded = () => setPlaying(false);
    const onError = () => setError(true);
    a.addEventListener('loadedmetadata', onLoaded);
    a.addEventListener('timeupdate', onTime);
    a.addEventListener('ended', onEnded);
    a.addEventListener('error', onError);
    return () => {
      a.removeEventListener('loadedmetadata', onLoaded);
      a.removeEventListener('timeupdate', onTime);
      a.removeEventListener('ended', onEnded);
      a.removeEventListener('error', onError);
    };
  }, []);

  const togglePlay = useCallback(() => {
    const a = audioRef.current;
    if (!a) return;
    if (playing) a.pause();
    else a.play();
    setPlaying(!playing);
  }, [playing]);

  const changeVolume = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    const v = parseFloat(e.target.value);
    setVolume(v);
    setMuted(v === 0);
    if (audioRef.current) audioRef.current.volume = v;
  }, []);

  const toggleMute = useCallback(() => {
    const a = audioRef.current;
    if (!a) return;
    if (muted) {
      a.volume = volume || 1;
      setMuted(false);
    } else {
      a.volume = 0;
      setMuted(true);
    }
  }, [muted, volume]);

  const pct = duration > 0 ? (currentTime / duration) * 100 : 0;

  if (error) {
    return (
      <div className="flex items-center justify-center h-full">
        <span className="text-sm text-red-500">Failed to load audio file</span>
      </div>
    );
  }

  return (
    <div className="flex items-center justify-center h-full px-8">
      <audio ref={audioRef} src={src} preload="metadata" />
      <div className="w-full max-w-xl space-y-6">
        <div className="flex flex-col items-center gap-4">
          <div className="relative w-32 h-32 rounded-2xl bg-gradient-to-br from-duck-teal/20 via-duck-forest/10 to-duck-yellow/20 border-2 border-duck-dark/10 flex items-center justify-center shadow-lg">
            <Music className="h-12 w-12 text-duck-teal/60" />
            <span className="absolute bottom-2 right-2 text-[9px] font-mono font-bold text-duck-dark/30 tracking-wider">
              {ext}
            </span>
          </div>
          <div className="text-center">
            <p className="text-sm font-semibold text-duck-dark truncate max-w-xs">{fileName}</p>
            {loaded && <p className="text-xs text-duck-dark/40 mt-0.5">{formatTime(duration)}</p>}
          </div>
        </div>
        <div className="space-y-1.5">
          <SeekBar barRef={barRef} onSeekDown={onSeekDown} pct={pct} />
          <div className="flex justify-between text-[10px] font-mono text-duck-dark/40">
            <span>{formatTime(currentTime)}</span>
            <span>{formatTime(duration)}</span>
          </div>
        </div>
        <div className="flex items-center justify-center gap-6">
          <div className="flex items-center gap-2">
            <button
              onClick={toggleMute}
              className="p-1.5 text-duck-dark/40 hover:text-duck-dark transition-colors cursor-pointer"
            >
              {muted ? <VolumeX className="h-4 w-4" /> : <Volume2 className="h-4 w-4" />}
            </button>
            <input
              type="range"
              min={0}
              max={1}
              step={0.01}
              value={muted ? 0 : volume}
              onChange={changeVolume}
              className="w-20 h-1 accent-duck-teal cursor-pointer"
            />
          </div>
          <button
            onClick={togglePlay}
            disabled={!loaded}
            className="w-14 h-14 rounded-full bg-duck-teal text-white flex items-center justify-center shadow-lg hover:bg-duck-teal/90 disabled:opacity-40 transition-all cursor-pointer active:scale-95"
          >
            {!loaded ? (
              <Loader2 className="h-6 w-6 animate-spin" />
            ) : playing ? (
              <Pause className="h-6 w-6" />
            ) : (
              <Play className="h-6 w-6 ml-0.5" />
            )}
          </button>
          <div className="w-[104px]" />
        </div>
      </div>
    </div>
  );
};

// ── Video renderer ──
const VideoRenderer = ({ src, fileName }: { src: string; fileName: string }) => {
  const videoRef = useRef<HTMLVideoElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const hideTimer = useRef<ReturnType<typeof setTimeout>>(null);
  const [playing, setPlaying] = useState(false);
  const [currentTime, setCurrent] = useState(0);
  const [duration, setDuration] = useState(0);
  const [volume, setVolume] = useState(1);
  const [muted, setMuted] = useState(false);
  const [loaded, setLoaded] = useState(false);
  const [error, setError] = useState(false);
  const [showControls, setShowControls] = useState(true);
  const [isFullscreen, setIsFullscreen] = useState(false);
  const { barRef, onSeekDown } = useSeekBar(videoRef, duration);

  useEffect(() => {
    const v = videoRef.current;
    if (!v) return;
    const onLoaded = () => {
      setDuration(v.duration);
      setLoaded(true);
    };
    const onTime = () => setCurrent(v.currentTime);
    const onPlay = () => setPlaying(true);
    const onPause = () => setPlaying(false);
    const onEnded = () => setPlaying(false);
    const onError = () => setError(true);
    v.addEventListener('loadedmetadata', onLoaded);
    v.addEventListener('timeupdate', onTime);
    v.addEventListener('play', onPlay);
    v.addEventListener('pause', onPause);
    v.addEventListener('ended', onEnded);
    v.addEventListener('error', onError);
    return () => {
      v.removeEventListener('loadedmetadata', onLoaded);
      v.removeEventListener('timeupdate', onTime);
      v.removeEventListener('play', onPlay);
      v.removeEventListener('pause', onPause);
      v.removeEventListener('ended', onEnded);
      v.removeEventListener('error', onError);
    };
  }, []);

  // Fullscreen change detection
  useEffect(() => {
    const onChange = () => setIsFullscreen(!!document.fullscreenElement);
    document.addEventListener('fullscreenchange', onChange);
    return () => document.removeEventListener('fullscreenchange', onChange);
  }, []);

  // Auto-hide controls
  const resetHideTimer = useCallback(() => {
    setShowControls(true);
    if (hideTimer.current) clearTimeout(hideTimer.current);
    hideTimer.current = setTimeout(() => {
      if (videoRef.current && !videoRef.current.paused) setShowControls(false);
    }, 2500);
  }, []);

  const togglePlay = useCallback(() => {
    const v = videoRef.current;
    if (!v) return;
    if (v.paused) v.play();
    else v.pause();
  }, []);

  const changeVolume = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    const val = parseFloat(e.target.value);
    setVolume(val);
    setMuted(val === 0);
    if (videoRef.current) videoRef.current.volume = val;
  }, []);

  const toggleMute = useCallback(() => {
    const v = videoRef.current;
    if (!v) return;
    if (muted) {
      v.volume = volume || 1;
      setMuted(false);
    } else {
      v.volume = 0;
      setMuted(true);
    }
  }, [muted, volume]);

  const toggleFullscreen = useCallback(() => {
    const c = containerRef.current;
    if (!c) return;
    if (document.fullscreenElement) document.exitFullscreen();
    else c.requestFullscreen();
  }, []);

  const pct = duration > 0 ? (currentTime / duration) * 100 : 0;

  if (error) {
    return (
      <div className="flex items-center justify-center h-full">
        <span className="text-sm text-red-500">Failed to load video</span>
      </div>
    );
  }

  return (
    <div
      ref={containerRef}
      className="relative w-full h-full bg-black flex items-center justify-center group"
      onMouseMove={resetHideTimer}
      onMouseLeave={() => {
        if (playing) setShowControls(false);
      }}
    >
      <video ref={videoRef} src={src} preload="metadata" className="max-w-full max-h-full" onClick={togglePlay} />

      {/* Big center play button when paused */}
      {loaded && !playing && (
        <button onClick={togglePlay} className="absolute inset-0 flex items-center justify-center cursor-pointer">
          <div className="w-16 h-16 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center">
            <Play className="h-8 w-8 text-white ml-1" />
          </div>
        </button>
      )}

      {/* Loading spinner */}
      {!loaded && (
        <div className="absolute inset-0 flex items-center justify-center">
          <Loader2 className="h-8 w-8 text-white animate-spin" />
        </div>
      )}

      {/* Bottom controls overlay */}
      <div
        className={`absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent pt-12 pb-3 px-4 transition-opacity duration-300 ${
          showControls || !playing ? 'opacity-100' : 'opacity-0 pointer-events-none'
        }`}
      >
        {/* Seek bar */}
        <SeekBar
          barRef={barRef}
          onSeekDown={onSeekDown}
          pct={pct}
          trackClass="bg-white/20"
          fillClass="bg-duck-teal"
          thumbClass="bg-duck-teal border-white"
        />

        {/* Controls row */}
        <div className="flex items-center gap-3 mt-1">
          <button onClick={togglePlay} className="p-1 text-white hover:text-duck-teal transition-colors cursor-pointer">
            {playing ? <Pause className="h-5 w-5" /> : <Play className="h-5 w-5" />}
          </button>

          <div className="flex items-center gap-1.5">
            <button
              onClick={toggleMute}
              className="p-1 text-white/70 hover:text-white transition-colors cursor-pointer"
            >
              {muted ? <VolumeX className="h-4 w-4" /> : <Volume2 className="h-4 w-4" />}
            </button>
            <input
              type="range"
              min={0}
              max={1}
              step={0.01}
              value={muted ? 0 : volume}
              onChange={changeVolume}
              className="w-16 h-1 accent-duck-teal cursor-pointer"
            />
          </div>

          <span className="text-xs font-mono text-white/60 select-none">
            {formatTime(currentTime)} / {formatTime(duration)}
          </span>

          <div className="flex-1" />

          <span className="text-[9px] font-mono text-white/30 uppercase tracking-wider">{getExt(fileName)}</span>

          <button
            onClick={toggleFullscreen}
            className="p-1 text-white/70 hover:text-white transition-colors cursor-pointer"
          >
            {isFullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
          </button>
        </div>
      </div>
    </div>
  );
};

// ── Image renderer ──
const ZOOM_STEPS = [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 5];

const ImageRenderer = ({ src, fileName }: { src: string; fileName: string }) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const minZoom = ZOOM_STEPS[0] ?? 1;
  const maxZoom = ZOOM_STEPS[ZOOM_STEPS.length - 1] ?? 1;
  const [zoom, setZoom] = useState(1);
  const [rotation, setRotation] = useState(0);
  const [loaded, setLoaded] = useState(false);
  const [error, setError] = useState(false);
  const [dragging, setDragging] = useState(false);
  const [offset, setOffset] = useState({ x: 0, y: 0 });
  const dragStart = useRef({ x: 0, y: 0, ox: 0, oy: 0 });

  const zoomIn = useCallback(() => {
    setZoom((z) => {
      const next = ZOOM_STEPS.find((s) => s > z);
      return next ?? z;
    });
  }, []);

  const zoomOut = useCallback(() => {
    setZoom((z) => {
      const prev = [...ZOOM_STEPS].reverse().find((s) => s < z);
      return prev ?? z;
    });
  }, []);

  const resetView = useCallback(() => {
    setZoom(1);
    setRotation(0);
    setOffset({ x: 0, y: 0 });
  }, []);

  const rotate = useCallback(() => {
    setRotation((r) => (r + 90) % 360);
  }, []);

  // Scroll to zoom
  const onWheel = useCallback(
    (e: React.WheelEvent) => {
      e.preventDefault();
      if (e.deltaY < 0) zoomIn();
      else zoomOut();
    },
    [zoomIn, zoomOut],
  );

  // Pan with mouse drag when zoomed
  const onMouseDown = useCallback(
    (e: React.MouseEvent) => {
      if (zoom <= 1) return;
      e.preventDefault();
      setDragging(true);
      dragStart.current = { x: e.clientX, y: e.clientY, ox: offset.x, oy: offset.y };

      const onMove = (ev: MouseEvent) => {
        setOffset({
          x: dragStart.current.ox + (ev.clientX - dragStart.current.x),
          y: dragStart.current.oy + (ev.clientY - dragStart.current.y),
        });
      };
      const onUp = () => {
        setDragging(false);
        window.removeEventListener('mousemove', onMove);
        window.removeEventListener('mouseup', onUp);
      };
      window.addEventListener('mousemove', onMove);
      window.addEventListener('mouseup', onUp);
    },
    [zoom, offset],
  );

  // Reset offset when zoom goes to 1
  useEffect(() => {
    if (zoom <= 1) setOffset({ x: 0, y: 0 });
  }, [zoom]);

  if (error) {
    return (
      <div className="flex items-center justify-center h-full">
        <span className="text-sm text-red-500">Failed to load image</span>
      </div>
    );
  }

  return (
    <div className="relative w-full h-full flex flex-col">
      {/* Image area */}
      <div
        ref={containerRef}
        className={`flex-1 min-h-0 flex items-center justify-center overflow-hidden bg-[repeating-conic-gradient(#e5e7eb_0%_25%,transparent_0%_50%)] bg-[length:16px_16px] ${
          zoom > 1 ? (dragging ? 'cursor-grabbing' : 'cursor-grab') : 'cursor-zoom-in'
        }`}
        onWheel={onWheel}
        onMouseDown={zoom > 1 ? onMouseDown : undefined}
        onClick={zoom <= 1 ? zoomIn : undefined}
      >
        {!loaded && (
          <div className="absolute inset-0 flex items-center justify-center">
            <Loader2 className="h-6 w-6 text-duck-teal animate-spin" />
          </div>
        )}
        <img
          src={src}
          alt={fileName}
          onLoad={() => setLoaded(true)}
          onError={() => setError(true)}
          className="transition-transform duration-150 select-none"
          draggable={false}
          style={{
            transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom}) rotate(${rotation}deg)`,
            maxWidth: zoom <= 1 ? '100%' : 'none',
            maxHeight: zoom <= 1 ? '100%' : 'none',
            opacity: loaded ? 1 : 0,
          }}
        />
      </div>

      {/* Toolbar */}
      <div className="shrink-0 flex items-center justify-center gap-1 py-2 border-t border-duck-dark/10 bg-white/80">
        <button
          onClick={zoomOut}
          disabled={zoom <= minZoom}
          className="p-1.5 rounded-md text-duck-dark/50 hover:text-duck-dark hover:bg-duck-dark/5 disabled:opacity-30 transition-colors cursor-pointer"
        >
          <ZoomOut className="h-4 w-4" />
        </button>
        <button
          onClick={resetView}
          className="px-2 py-1 rounded-md text-xs font-mono text-duck-dark/50 hover:text-duck-dark hover:bg-duck-dark/5 transition-colors cursor-pointer min-w-[4rem] text-center"
        >
          {Math.round(zoom * 100)}%
        </button>
        <button
          onClick={zoomIn}
          disabled={zoom >= maxZoom}
          className="p-1.5 rounded-md text-duck-dark/50 hover:text-duck-dark hover:bg-duck-dark/5 disabled:opacity-30 transition-colors cursor-pointer"
        >
          <ZoomIn className="h-4 w-4" />
        </button>
        <div className="w-px h-4 bg-duck-dark/10 mx-1" />
        <button
          onClick={rotate}
          className="p-1.5 rounded-md text-duck-dark/50 hover:text-duck-dark hover:bg-duck-dark/5 transition-colors cursor-pointer"
        >
          <RotateCw className="h-4 w-4" />
        </button>
      </div>
    </div>
  );
};

// ── PDF renderer ──
const PdfRenderer = ({ src }: { src: string }) => (
  <iframe src={src} className="w-full h-full border-0" title="PDF viewer" />
);

// ── File Viewer modal ──
export const FileViewer = ({ open, onOpenChange, filePath, fileName, root }: FileViewerProps) => {
  const [content, setContent] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [expanded, setExpanded] = useState(false);
  const [ttsLoading, setTtsLoading] = useState(false);
  const [ttsPlaying, setTtsPlaying] = useState(false);
  const ttsAudioRef = useRef<HTMLAudioElement | null>(null);
  const ttsUrlRef = useRef<string | null>(null);
  const scrollRef = useRef<HTMLDivElement>(null);
  const files = useFiles(root);
  const fileType = getFileType(fileName);

  useEffect(() => {
    if (!open) {
      setContent(null);
      setError(null);
      setExpanded(false);
      if (ttsAudioRef.current) {
        ttsAudioRef.current.pause();
        ttsAudioRef.current = null;
      }
      if (ttsUrlRef.current) {
        URL.revokeObjectURL(ttsUrlRef.current);
        ttsUrlRef.current = null;
      }
      setTtsLoading(false);
      setTtsPlaying(false);
      return;
    }

    // Media types stream via URL — no text fetch needed
    if (fileType === 'audio' || fileType === 'video' || fileType === 'image' || fileType === 'pdf') {
      setLoading(false);
      return;
    }

    setLoading(true);
    setError(null);
    files
      .readFile(filePath)
      .then((res) => setContent(res.content))
      .catch(() => setError('Failed to read file'))
      .finally(() => setLoading(false));
  }, [open, filePath]);

  const handleDownload = () => {
    const url = getRawUrl(filePath, root);
    const a = document.createElement('a');
    a.href = url;
    a.download = fileName;
    a.click();
  };

  const handleReadAloud = async () => {
    if (ttsPlaying) {
      if (ttsAudioRef.current) {
        ttsAudioRef.current.pause();
        ttsAudioRef.current = null;
      }
      if (ttsUrlRef.current) {
        URL.revokeObjectURL(ttsUrlRef.current);
        ttsUrlRef.current = null;
      }
      setTtsPlaying(false);
      return;
    }

    setTtsLoading(true);
    try {
      const res = await fetch('http://alpha:9051/v1/audio/speech', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ model: 'kokoro', input: content, voice: 'af_heart', response_format: 'mp3' }),
      });
      if (!res.ok) throw new Error('TTS request failed');
      const blob = await res.blob();
      const url = URL.createObjectURL(blob);
      ttsUrlRef.current = url;
      const audio = new Audio(url);
      ttsAudioRef.current = audio;
      audio.addEventListener('ended', () => {
        setTtsPlaying(false);
        ttsAudioRef.current = null;
        URL.revokeObjectURL(url);
        ttsUrlRef.current = null;
      });
      await audio.play();
      setTtsPlaying(true);
    } catch {
      toast.error('Failed to generate speech audio');
      if (ttsUrlRef.current) {
        URL.revokeObjectURL(ttsUrlRef.current);
        ttsUrlRef.current = null;
      }
      ttsAudioRef.current = null;
      setTtsPlaying(false);
    } finally {
      setTtsLoading(false);
    }
  };

  const headerIcon =
    fileType === 'audio' ? (
      <Music className="h-4 w-4 text-duck-teal shrink-0" />
    ) : fileType === 'video' ? (
      <Film className="h-4 w-4 text-duck-orange shrink-0" />
    ) : fileType === 'image' ? (
      <Image className="h-4 w-4 text-duck-yellow shrink-0" />
    ) : fileType === 'pdf' ? (
      <FileType2 className="h-4 w-4 text-red-500 shrink-0" />
    ) : (
      <span className="inline-flex h-4 w-4 shrink-0" dangerouslySetInnerHTML={{ __html: getIcon(fileName).svg }} />
    );

  const videoSrc =
    fileType === 'video'
      ? needsTranscode(fileName)
        ? getTranscodeUrl(filePath, root)
        : getRawUrl(filePath, root)
      : '';

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogPortal>
        <DialogOverlay className="z-[600] bg-black/60 backdrop-blur-sm" />
        <DialogPrimitive.Content
          onOpenAutoFocus={(ev) => ev.preventDefault()}
          className={`fixed left-[50%] top-[50%] z-[600] translate-x-[-50%] translate-y-[-50%] flex flex-col overflow-hidden rounded-xl border-2 border-duck-dark/30 shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 ${
            expanded ? 'w-[95vw] h-[95vh]' : 'w-[90vw] max-w-4xl h-[85vh]'
          }`}
          style={
            fileType === 'video'
              ? { backgroundColor: '#000' }
              : cardStyle({
                  backgroundColor: 'rgba(255, 255, 255, 0.97)',
                  backgroundImage: `
                  linear-gradient(to right, rgba(20, 83, 45, 0.04) 1px, transparent 1px),
                  linear-gradient(to bottom, rgba(20, 83, 45, 0.04) 1px, transparent 1px)
                `,
                })
          }
        >
          {/* Header */}
          <div
            className={`shrink-0 flex items-center gap-3 px-5 py-3 border-b ${
              fileType === 'video' ? 'border-white/10 bg-black/80' : 'border-duck-dark/10 bg-white/60'
            }`}
          >
            {headerIcon}
            <span
              className={`text-sm font-semibold truncate flex-1 ${
                fileType === 'video' ? 'text-white' : 'text-duck-dark'
              }`}
            >
              {fileName}
            </span>
            <span
              className={`text-[10px] font-mono uppercase tracking-wider shrink-0 ${
                fileType === 'video' ? 'text-white/40' : 'text-duck-dark/40'
              }`}
            >
              {fileType === 'code' ? getLang(fileName) : fileType}
            </span>
            {(fileType === 'markdown' || fileType === 'code' || fileType === 'text') && content && (
              <button
                onClick={handleReadAloud}
                disabled={ttsLoading}
                className="p-1.5 rounded-md text-duck-dark/40 hover:text-duck-dark hover:bg-duck-dark/5 disabled:opacity-40 transition-colors cursor-pointer"
                title="Read Aloud"
              >
                {ttsLoading ? (
                  <Loader2 className="h-4 w-4 animate-spin" />
                ) : ttsPlaying ? (
                  <Square className="h-4 w-4" />
                ) : (
                  <Volume2 className="h-4 w-4" />
                )}
              </button>
            )}
            <button
              onClick={handleDownload}
              className={`p-1.5 rounded-md transition-colors cursor-pointer ${
                fileType === 'video'
                  ? 'text-white/40 hover:text-white hover:bg-white/10'
                  : 'text-duck-dark/40 hover:text-duck-dark hover:bg-duck-dark/5'
              }`}
              title="Download"
            >
              <Download className="h-4 w-4" />
            </button>
            <button
              onClick={() => setExpanded((e) => !e)}
              className={`p-1.5 rounded-md transition-colors cursor-pointer ${
                fileType === 'video'
                  ? 'text-white/40 hover:text-white hover:bg-white/10'
                  : 'text-duck-dark/40 hover:text-duck-dark hover:bg-duck-dark/5'
              }`}
              title={expanded ? 'Collapse' : 'Expand'}
            >
              {expanded ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
            </button>
            <DialogPrimitive.Close
              className={`p-1.5 rounded-md transition-colors cursor-pointer ${
                fileType === 'video'
                  ? 'text-white/40 hover:text-white hover:bg-white/10'
                  : 'text-duck-dark/40 hover:text-duck-dark hover:bg-duck-dark/5'
              }`}
            >
              <X className="h-4 w-4" />
            </DialogPrimitive.Close>
          </div>

          {/* Content */}
          <div ref={scrollRef} className="flex-1 min-h-0 overflow-auto">
            {loading ? (
              <div className="flex items-center justify-center h-full">
                <Loader2 className="h-6 w-6 text-duck-teal animate-spin" />
              </div>
            ) : error ? (
              <div className="flex items-center justify-center h-full">
                <span className="text-sm text-red-500">{error}</span>
              </div>
            ) : fileType === 'pdf' ? (
              <PdfRenderer src={getRawUrl(filePath, root)} />
            ) : fileType === 'image' ? (
              <ImageRenderer src={getRawUrl(filePath, root)} fileName={fileName} />
            ) : fileType === 'video' ? (
              <VideoRenderer src={videoSrc} fileName={fileName} />
            ) : fileType === 'audio' ? (
              <AudioRenderer src={getRawUrl(filePath, root)} fileName={fileName} />
            ) : content !== null ? (
              fileType === 'code' ? (
                <div>
                  <CodeRenderer content={content} lang={getLang(fileName)} />
                  <ScrollToTopButton scrollContainer={scrollRef} />
                </div>
              ) : (
                <div className="px-8 py-6">
                  {fileType === 'markdown' ? (
                    <MarkdownRenderer content={content} scrollContainer={scrollRef} />
                  ) : (
                    <TextRenderer content={content} />
                  )}
                  <ScrollToTopButton scrollContainer={scrollRef} />
                </div>
              )
            ) : null}
          </div>
        </DialogPrimitive.Content>
      </DialogPortal>
    </Dialog>
  );
};
