import { useEffect, useMemo, useRef, useState } from "react"
import { convertFileSrc } from "@tauri-apps/api/core"
import { openPath } from "@tauri-apps/plugin-opener"
import ReactMarkdown from "react-markdown"
import remarkGfm from "remark-gfm"
import remarkMath from "remark-math"
import rehypeKatex from "rehype-katex"
import "katex/dist/katex.min.css"
import {
  FileText,
  Image as ImageIcon,
  Film,
  Music,
  FileSpreadsheet,
  FileQuestion,
  Code2,
  ExternalLink,
  RefreshCw,
  Maximize2,
  Minus,
  Plus,
  X,
} from "lucide-react"
import { useTranslation } from "react-i18next"
import type { PDFDocumentLoadingTask, PDFDocumentProxy, RenderTask } from "pdfjs-dist"
import { getFileSize, readFileAsBase64 } from "@/commands/fs"
import {
  getFileCategory,
  getCodeLanguage,
  getFileExtension,
  isExtractedTextPreviewFile,
} from "@/lib/file-types"
import type { FileCategory } from "@/lib/file-types"
import { getFileName, normalizePath } from "@/lib/path-utils"
import { resolveMarkdownImageSrc } from "@/lib/markdown-image-resolver"
import { transformImageEmbeds } from "@/lib/wikilink-transform"
import { detectLanguage } from "@/lib/detect-language"
import { getHtmlLang, getTextDirection } from "@/lib/language-metadata"
import { parseFrontmatter } from "@/lib/frontmatter"
import { FrontmatterPanel } from "@/components/editor/frontmatter-panel"
import { useWikiStore } from "@/stores/wiki-store"
import { MermaidDiagram, unwrapMermaidPre } from "@/components/mermaid-diagram"
import { FileHistoryButton } from "@/components/editor/file-history-panel"

interface FilePreviewProps {
  filePath: string
  textContent: string
}

export function FilePreview({ filePath, textContent }: FilePreviewProps) {
  return <div className="relative h-full min-h-0">
    <FilePreviewContent filePath={filePath} textContent={textContent} />
    <FileHistoryButton filePath={filePath} currentContent={textContent} />
  </div>
}

function FilePreviewContent({ filePath, textContent }: FilePreviewProps) {
  const category = getFileCategory(filePath)
  const fileName = getFileName(filePath)
  const extension = getFileExtension(filePath)

  switch (category) {
    case "image":
      return <ImagePreview filePath={filePath} fileName={fileName} />
    case "video":
      return <VideoPreview filePath={filePath} fileName={fileName} />
    case "audio":
      return <AudioPreview filePath={filePath} fileName={fileName} />
    case "pdf":
      return <PdfPreview filePath={filePath} content={textContent} />
    case "code":
      if (extension === "mmd" || extension === "mermaid") {
        return <StandaloneMermaidPreview filePath={filePath} content={textContent} />
      }
      if (extension === "svg" && isAgentWorkspacePath(filePath)) {
        return <ImagePreview filePath={filePath} fileName={fileName} />
      }
      if (extension === "html" || extension === "htm") {
        return <HtmlPreview filePath={filePath} fileName={fileName} content={textContent} />
      }
      return <CodePreview filePath={filePath} content={textContent} />
    case "data":
      if (extension === "csv" || extension === "tsv") {
        return <DelimitedTablePreview filePath={filePath} content={textContent} delimiter={extension === "tsv" ? "\t" : ","} />
      }
      return <CodePreview filePath={filePath} content={textContent} />
    case "text":
      return <TextPreview filePath={filePath} content={textContent} label="Text" />
    case "document":
      if (isExtractedTextPreviewFile(filePath)) {
        return <TextPreview filePath={filePath} content={textContent} label={extractedTextLabel(filePath)} />
      }
      return <BinaryPlaceholder filePath={filePath} fileName={fileName} category={category} />
    default:
      return <BinaryPlaceholder filePath={filePath} fileName={fileName} category={category} />
  }
}

function PdfPreview({ filePath, content }: { filePath: string; content: string }) {
  const { t } = useTranslation()
  const [showText, setShowText] = useState(false)
  const [page, setPage] = useState(1)
  const [zoom, setZoom] = useState(100)
  const [reloadKey, setReloadKey] = useState(0)
  const [document, setDocument] = useState<PDFDocumentProxy | null>(null)
  const [pageCount, setPageCount] = useState(0)
  const [loadError, setLoadError] = useState<string | null>(null)
  const [loading, setLoading] = useState(true)
  const canvasRef = useRef<HTMLCanvasElement | null>(null)

  useEffect(() => {
    let disposed = false
    let loadingTask: PDFDocumentLoadingTask | null = null
    let loadedDocument: PDFDocumentProxy | null = null

    setLoading(true)
    setLoadError(null)
    setDocument(null)
    setPageCount(0)
    setPage(1)

    void (async () => {
      try {
        // PDF.js receives bytes over Tauri IPC because WebView-native PDF
        // plugins and custom asset URLs are not portable across WebView2 and
        // WebKitGTK. Base64 temporarily expands memory, so very large files
        // must stay on the streaming system-reader path instead.
        const fileSize = await getFileSize(filePath)
        if (fileSize > MAX_INLINE_PDF_BYTES) throw new Error(t("preview.pdfTooLarge"))
        const [{ getDocument, GlobalWorkerOptions }, workerModule, file] = await Promise.all([
          // The legacy build includes the URL/Promise/AbortSignal polyfills
          // required by older WebKitGTK runtimes still shipped by supported
          // Linux distributions. Windows WebView2 also uses this same path.
          import("pdfjs-dist/legacy/build/pdf.mjs"),
          import("pdfjs-dist/legacy/build/pdf.worker.min.mjs?url"),
          readFileAsBase64(filePath),
        ])
        GlobalWorkerOptions.workerSrc = workerModule.default
        loadingTask = getDocument({ data: decodeBase64(file.base64) })
        loadedDocument = await loadingTask.promise
        if (disposed) {
          await loadedDocument.destroy()
          return
        }
        setDocument(loadedDocument)
        setPageCount(loadedDocument.numPages)
      } catch (error) {
        if (!disposed) setLoadError(error instanceof Error ? error.message : String(error))
      } finally {
        if (!disposed) setLoading(false)
      }
    })()

    return () => {
      disposed = true
      if (loadingTask) void loadingTask.destroy()
      else if (loadedDocument) void loadedDocument.destroy()
    }
  }, [filePath, reloadKey, t])

  useEffect(() => {
    if (!document || showText) return
    const canvas = canvasRef.current
    if (!canvas) return
    let disposed = false
    let renderTask: RenderTask | null = null

    void (async () => {
      try {
        const pdfPage = await document.getPage(Math.min(page, document.numPages))
        if (disposed) return
        const viewport = pdfPage.getViewport({ scale: (zoom / 100) * 1.25 })
        const pixelRatio = window.devicePixelRatio || 1
        const context = canvas.getContext("2d")
        if (!context) throw new Error("Canvas 2D rendering is unavailable")
        canvas.width = Math.floor(viewport.width * pixelRatio)
        canvas.height = Math.floor(viewport.height * pixelRatio)
        canvas.style.width = `${Math.floor(viewport.width)}px`
        canvas.style.height = `${Math.floor(viewport.height)}px`
        renderTask = pdfPage.render({
          canvas,
          canvasContext: context,
          viewport,
          transform: pixelRatio === 1 ? undefined : [pixelRatio, 0, 0, pixelRatio, 0, 0],
        })
        await renderTask.promise
      } catch (error) {
        if (!disposed && !(error instanceof Error && error.name === "RenderingCancelledException")) {
          setLoadError(error instanceof Error ? error.message : String(error))
        }
      }
    })()

    return () => {
      disposed = true
      renderTask?.cancel()
    }
  }, [document, page, showText, zoom])

  return <div className="flex h-full min-h-0 flex-col p-4">
    <div className="mb-2 flex items-center gap-1.5 text-xs text-muted-foreground">
      <span className="min-w-0 flex-1 truncate" title={filePath}>{filePath}</span>
      <button type="button" className="rounded border px-2 py-1 hover:bg-muted" onClick={() => setShowText((value) => !value)}>{showText ? t("preview.pdfDocument") : t("preview.pdfText")}</button>
      {!showText && <><button type="button" className="rounded p-1 hover:bg-muted" onClick={() => setZoom((value) => Math.max(50, value - 25))}><Minus className="h-3.5 w-3.5" /></button><span className="w-10 text-center">{zoom}%</span><button type="button" className="rounded p-1 hover:bg-muted" onClick={() => setZoom((value) => Math.min(300, value + 25))}><Plus className="h-3.5 w-3.5" /></button><label className="ml-1 flex items-center gap-1">{t("preview.pdfPage")}<input value={page} min={1} max={pageCount || undefined} type="number" onChange={(event) => setPage(clampPdfPage(Number(event.target.value) || 1, pageCount))} className="w-14 rounded border bg-background px-1 py-0.5" /></label><span>/ {pageCount || "–"}</span></>}
      <button type="button" onClick={() => void openPath(filePath)} className="rounded p-1 hover:bg-muted" title={t("preview.openWithSystem")} aria-label={t("preview.openWithSystem")}><ExternalLink className="h-3.5 w-3.5" /></button>
    </div>
    <div className="min-h-0 flex-1 overflow-hidden rounded-md border bg-white">
      {showText ? <TextPreview filePath={filePath} content={content} label="PDF text" /> : loading ? (
        <div className="flex h-full items-center justify-center text-sm text-muted-foreground">{t("preview.pdfLoading")}</div>
      ) : loadError ? (
        <div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center text-sm text-muted-foreground">
          <FileQuestion className="h-8 w-8" />
          <p>{t("preview.pdfLoadError")}</p>
          <p className="max-w-xl break-words text-xs opacity-70">{loadError}</p>
          <button type="button" className="rounded border px-3 py-1.5 hover:bg-muted" onClick={() => setReloadKey((value) => value + 1)}>{t("preview.reload")}</button>
        </div>
      ) : (
        <div className="h-full overflow-auto bg-muted/30 p-4"><canvas ref={canvasRef} className="mx-auto bg-white shadow-sm" /></div>
      )}
    </div>
  </div>
}

const MAX_INLINE_PDF_BYTES = 128 * 1024 * 1024

export function decodeBase64(value: string): Uint8Array {
  const binary = atob(value)
  const bytes = new Uint8Array(binary.length)
  for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index)
  return bytes
}

export function clampPdfPage(page: number, pageCount: number): number {
  return Math.min(Math.max(1, Math.trunc(page)), Math.max(1, pageCount))
}

export function parseDelimitedContent(content: string, delimiter: string, maxRows = 500): string[][] {
  const rows: string[][] = []
  let cells: string[] = []
  let current = ""
  let quoted = false
  const normalized = content.replace(/\r\n/g, "\n")
  for (let index = 0; index < normalized.length && rows.length < maxRows; index += 1) {
    const char = normalized[index]
    if (char === '"') {
      if (quoted && normalized[index + 1] === '"') { current += '"'; index += 1 } else quoted = !quoted
    } else if (char === delimiter && !quoted) { cells.push(current); current = "" } else current += char
    if (char === "\n" && !quoted) {
      current = current.slice(0, -1)
      cells.push(current); rows.push(cells); cells = []; current = ""
    }
  }
  if ((current || cells.length > 0) && rows.length < maxRows) { cells.push(current); rows.push(cells) }
  return rows
}

function DelimitedTablePreview({ filePath, content, delimiter }: { filePath: string; content: string; delimiter: string }) {
  const rows = useMemo(() => parseDelimitedContent(content, delimiter), [content, delimiter])
  return <div className="h-full overflow-auto p-4"><div className="mb-2 text-xs text-muted-foreground">{filePath}</div><table className="min-w-full border-collapse text-xs"><thead className="sticky top-0 bg-muted">{rows[0] && <tr>{rows[0].map((cell, index) => <th key={index} className="border px-2 py-1 text-left">{cell}</th>)}</tr>}</thead><tbody>{rows.slice(1).map((row, rowIndex) => <tr key={rowIndex}>{row.map((cell, cellIndex) => <td key={cellIndex} className="max-w-80 border px-2 py-1 align-top">{cell}</td>)}</tr>)}</tbody></table></div>
}

function HtmlPreview({
  filePath,
  fileName,
  content,
}: {
  filePath: string
  fileName: string
  content: string
}) {
  const { t } = useTranslation()
  const src = convertFileSrc(filePath)
  const [showSource, setShowSource] = useState(false)
  const [reloadKey, setReloadKey] = useState(0)
  return (
    <div className="flex h-full flex-col p-4" data-preview-kind="html">
      <div className="mb-3 flex items-center gap-1.5 text-xs text-muted-foreground">
        <span className="min-w-0 flex-1 truncate" title={filePath}>{filePath}</span>
        <span className="rounded bg-muted px-1.5 py-0.5 text-[10px] uppercase">HTML</span>
        <button
          type="button"
          onClick={() => setShowSource((current) => !current)}
          className="rounded p-1 hover:bg-accent hover:text-foreground"
          title={showSource ? t("preview.showRendered") : t("preview.showSource")}
          aria-label={showSource ? t("preview.showRendered") : t("preview.showSource")}
        >
          <Code2 className="h-3.5 w-3.5" />
        </button>
        {!showSource && (
          <button
            type="button"
            onClick={() => setReloadKey((current) => current + 1)}
            className="rounded p-1 hover:bg-accent hover:text-foreground"
            title={t("preview.reload")}
            aria-label={t("preview.reload")}
          >
            <RefreshCw className="h-3.5 w-3.5" />
          </button>
        )}
        <button
          type="button"
          onClick={() => void openPath(filePath)}
          className="rounded p-1 hover:bg-accent hover:text-foreground"
          title={t("preview.openWithSystem")}
          aria-label={t("preview.openWithSystem")}
        >
          <ExternalLink className="h-3.5 w-3.5" />
        </button>
      </div>
      <div className="min-h-0 flex-1 overflow-hidden rounded-lg border border-border bg-background">
        {showSource ? (
          <pre className="h-full overflow-auto whitespace-pre-wrap break-words p-4 font-mono text-xs">
            {content}
          </pre>
        ) : (
          <iframe
            key={reloadKey}
            title={fileName}
            src={src}
            className="h-full w-full bg-white"
            // Generated HTML is untrusted Agent output. Scripts are useful for
            // interactive reports, but same-origin access stays disabled so the
            // document cannot reach the parent DOM or authenticated app APIs.
            sandbox="allow-scripts"
            referrerPolicy="no-referrer"
          />
        )}
      </div>
    </div>
  )
}

function StandaloneMermaidPreview({ filePath, content }: { filePath: string; content: string }) {
  return (
    <div className="flex h-full min-h-0 flex-col overflow-auto p-6" data-preview-kind="mermaid">
      <div className="mb-2 flex items-center gap-2 text-xs text-muted-foreground">
        <span className="min-w-0 flex-1 truncate" title={filePath}>{filePath}</span>
        <span className="rounded bg-muted px-1.5 py-0.5 text-[10px] uppercase">Mermaid</span>
      </div>
      <MermaidDiagram code={content} />
    </div>
  )
}

function isAgentWorkspacePath(filePath: string): boolean {
  return normalizePath(filePath).split("/").includes("agent-workspace")
}

function extractedTextLabel(filePath: string): string {
  switch (getFileExtension(filePath)) {
    case "doc":
      return "Word DOC (extracted text)"
    case "docx":
      return "Word DOCX (extracted text)"
    case "pptx":
      return "PowerPoint (extracted text)"
    case "xls":
    case "xlsx":
      return "Spreadsheet (extracted text)"
    case "odt":
    case "ods":
    case "odp":
      return "OpenDocument (extracted text)"
    default:
      return "Extracted text"
  }
}

function ImagePreview({ filePath, fileName }: { filePath: string; fileName: string }) {
  const src = convertFileSrc(filePath)
  const [expanded, setExpanded] = useState(false)
  const [zoom, setZoom] = useState(1)
  return (
    <div className="flex h-full flex-col p-6">
      <div className="mb-4 flex items-center gap-2 text-xs text-muted-foreground"><span className="min-w-0 flex-1 truncate">{filePath}</span><button type="button" onClick={() => setExpanded(true)} className="rounded p-1 hover:bg-muted"><Maximize2 className="h-4 w-4" /></button></div>
      <div className="flex flex-1 items-center justify-center overflow-auto rounded-lg bg-muted/30">
        <img
          src={src}
          alt={fileName}
          className="max-h-full max-w-full object-contain"
        />
      </div>
      {expanded && <div className="fixed inset-0 z-[100] flex flex-col bg-background/95 p-4 backdrop-blur-sm"><div className="flex justify-end gap-1"><button type="button" className="rounded p-2 hover:bg-muted" onClick={() => setZoom((value) => Math.max(.25, value - .25))}><Minus className="h-4 w-4" /></button><button type="button" className="rounded p-2 hover:bg-muted" onClick={() => setZoom((value) => Math.min(5, value + .25))}><Plus className="h-4 w-4" /></button><button type="button" className="rounded p-2 hover:bg-muted" onClick={() => setExpanded(false)}><X className="h-4 w-4" /></button></div><div className="min-h-0 flex-1 overflow-auto text-center"><img src={src} alt={fileName} className="mx-auto max-w-none object-contain" style={{ width: `${zoom * 100}%` }} /></div></div>}
    </div>
  )
}

function VideoPreview({ filePath, fileName }: { filePath: string; fileName: string }) {
  const src = convertFileSrc(filePath)
  return (
    <div className="flex h-full flex-col p-6">
      <div className="mb-4 text-xs text-muted-foreground">{filePath}</div>
      <div className="flex flex-1 items-center justify-center overflow-auto rounded-lg bg-black">
        <video
          src={src}
          controls
          className="max-h-full max-w-full"
        >
          <track kind="captions" label={fileName} />
        </video>
      </div>
    </div>
  )
}

function AudioPreview({ filePath, fileName }: { filePath: string; fileName: string }) {
  const src = convertFileSrc(filePath)
  return (
    <div className="flex h-full flex-col items-center justify-center gap-4 p-6">
      <div className="text-xs text-muted-foreground">{filePath}</div>
      <Music className="h-16 w-16 text-muted-foreground/50" />
      <p className="text-sm font-medium">{fileName}</p>
      <audio src={src} controls className="w-full max-w-md">
        <track kind="captions" label={fileName} />
      </audio>
    </div>
  )
}

function CodePreview({ filePath, content }: { filePath: string; content: string }) {
  const lang = getCodeLanguage(filePath)
  return (
    <div className="h-full overflow-auto p-6">
      <div className="mb-2 flex items-center gap-2 text-xs text-muted-foreground">
        <span>{filePath}</span>
        <span className="rounded bg-muted px-1.5 py-0.5 text-[10px] uppercase">{lang}</span>
      </div>
      <pre className="whitespace-pre-wrap rounded-lg bg-muted/30 p-4 font-mono text-sm">
        {content}
      </pre>
    </div>
  )
}

function TextPreview({ filePath, content, label }: { filePath: string; content: string; label: string }) {
  const projectPath = useWikiStore((s) => s.project?.path ?? null)
  const pendingScrollImageSrc = useWikiStore((s) => s.pendingScrollImageSrc)
  const setPendingScrollImageSrc = useWikiStore((s) => s.setPendingScrollImageSrc)
  const scrollRootRef = useRef<HTMLDivElement | null>(null)

  const { frontmatter, body } = useMemo(() => parseFrontmatter(content), [content])
  // Rewrite Obsidian image embeds (`![[…]]`) into standard markdown
  // so raw-source previews (e.g. skill-exported docs) actually show
  // their images instead of dumping the embed syntax as text.
  const renderBody = useMemo(() => transformImageEmbeds(body), [body])
  // Directory of this file (project-absolute) so relative image
  // references (`../assets/x.png`) resolve against the file's own
  // location, Obsidian-style.
  const currentFileDir = useMemo(() => {
    const norm = normalizePath(filePath)
    const dir = norm.slice(0, norm.lastIndexOf("/"))
    return dir || null
  }, [filePath])
  const renderLanguage = useMemo(() => detectLanguage(body), [body])
  const direction = getTextDirection(renderLanguage)
  const htmlLang = getHtmlLang(renderLanguage)

  // Consume `pendingScrollImageSrc` once the file has rendered.
  // We re-scan the DOM whenever:
  //   - file content changes (different page just loaded), OR
  //   - the pending target changes (user clicked a different image)
  // Image loading is async, so we also subscribe to `load` events
  // and rescroll once the actual layout settles — the first
  // `scrollIntoView` lands on a 0-height placeholder otherwise.
  useEffect(() => {
    if (!pendingScrollImageSrc) return
    const root = scrollRootRef.current
    if (!root) return
    // Match by `data-mdsrc` (literal markdown URL) — the post-
    // resolver `src` is a tauri:// URL we don't want to bake into
    // the search-result data.
    // Inline-escape `"` and `\` for the attribute-VALUE position
    // of a CSS selector (CSS.escape is for IDENTIFIER context and
    // would over-escape here). Image URLs can in principle contain
    // either, so doing this is correctness, not paranoia.
    const escapedSrc = pendingScrollImageSrc
      .replace(/\\/g, "\\\\")
      .replace(/"/g, '\\"')
    const target = root.querySelector<HTMLImageElement>(
      `img[data-mdsrc="${escapedSrc}"]`,
    )
    if (!target) {
      // Page may not actually contain this image — clear the
      // pending so a future page-open doesn't get an unexpected
      // scroll. Fail silently: the user navigated, we just don't
      // know where to send them.
      setPendingScrollImageSrc(null)
      return
    }
    // Initial scroll. The image may not have loaded its bytes yet
    // (lazy loading + remote PNG decode) so this lands on a
    // 0-height box. After load, recompute.
    target.scrollIntoView({ behavior: "auto", block: "center" })
    if (!target.complete) {
      const onLoad = () => {
        target.scrollIntoView({ behavior: "smooth", block: "center" })
        target.removeEventListener("load", onLoad)
      }
      target.addEventListener("load", onLoad)
    }
    // Briefly highlight the target so the user sees where they
    // landed — the page might be long and the image might be in
    // a section visually similar to its neighbors.
    target.classList.add("ring-2", "ring-primary", "ring-offset-2")
    const tHighlight = setTimeout(() => {
      target.classList.remove("ring-2", "ring-primary", "ring-offset-2")
    }, 1800)
    setPendingScrollImageSrc(null)
    return () => clearTimeout(tHighlight)
  }, [pendingScrollImageSrc, content, setPendingScrollImageSrc])

  return (
    <div ref={scrollRootRef} className="h-full overflow-auto p-6">
      <div className="mb-2 flex items-center gap-2 text-xs text-muted-foreground">
        <span>{filePath}</span>
        <span className="rounded bg-muted px-1.5 py-0.5 text-[10px] uppercase">{label}</span>
      </div>
      {frontmatter && <FrontmatterPanel data={frontmatter} />}
      <div
        className="prose prose-sm max-w-none dark:prose-invert"
        dir={direction}
        lang={htmlLang}
        style={{ textAlign: "start" }}
      >
        <ReactMarkdown
          remarkPlugins={[remarkGfm, remarkMath]}
          rehypePlugins={[rehypeKatex]}
          components={{
            // Resolve `![](media/...)` references generated by the
            // ingest image-extraction step. Without this, the
            // browser tries to load `media/...` relative to the
            // webview origin and silently 404s.
            //
            // `data-mdsrc` preserves the ORIGINAL markdown URL
            // (pre-resolver) so the search-result jump-to-image
            // path can find the rendered <img> by its source-of-
            // truth identifier rather than the resolved tauri://
            // URL (which differs per platform).
            img: ({ src, alt, ...props }) => (
              <img
                src={typeof src === "string" ? resolveMarkdownImageSrc(src, projectPath, currentFileDir) : undefined}
                data-mdsrc={typeof src === "string" ? src : undefined}
                alt={alt ?? ""}
                className="max-w-full rounded border border-border/40 transition-all"
                loading="lazy"
                {...props}
              />
            ),
            table: ({ children, ...props }) => (
              <div className="my-2 overflow-x-auto rounded border border-border">
                <table className="w-full border-collapse text-xs" {...props}>{children}</table>
              </div>
            ),
            thead: ({ children, ...props }) => (
              <thead className="bg-muted" {...props}>{children}</thead>
            ),
            th: ({ children, ...props }) => (
              <th className="border border-border/80 px-3 py-1.5 text-start font-semibold bg-muted" {...props}>{children}</th>
            ),
            td: ({ children, ...props }) => (
              <td className="border border-border/60 px-3 py-1.5" {...props}>{children}</td>
            ),
            pre: ({ children, ...props }) => {
              const mermaid = unwrapMermaidPre(children)
              if (mermaid) return <>{mermaid}</>
              return <pre dir="ltr" style={{ textAlign: "left" }} {...props}>{children}</pre>
            },
            code: ({ className, children, ...props }) => {
              const lang = className?.replace("language-", "")
              const codeText = String(children).replace(/\n$/, "")
              if (lang === "mermaid") return <MermaidDiagram code={codeText} />
              return <code dir="ltr" className={className} {...props}>{children}</code>
            },
          }}
        >
          {renderBody}
        </ReactMarkdown>
      </div>
    </div>
  )
}

function BinaryPlaceholder({
  filePath,
  fileName,
  category,
}: {
  filePath: string
  fileName: string
  category: FileCategory
}) {
  const { t } = useTranslation()
  const [text, setText] = useState<string | null>(null)
  const [loading, setLoading] = useState(false)
  const [loadError, setLoadError] = useState<string | null>(null)
  const iconMap: Record<string, typeof FileText> = {
    document: FileSpreadsheet,
    unknown: FileQuestion,
    image: ImageIcon,
    video: Film,
  }
  const Icon = iconMap[category] ?? FileQuestion

  if (text !== null) {
    return <CodePreview filePath={filePath} content={text} />
  }

  const viewAsText = async () => {
    setLoading(true)
    setLoadError(null)
    try {
      // read_file is size-bounded on the Rust side. The explicit user action is
      // the opt-in for long-tail text formats; decoding failures remain visible
      // here instead of being mistaken for an empty file.
      const { readFile } = await import("@/commands/fs")
      setText(await readFile(filePath))
    } catch (error) {
      setLoadError(error instanceof Error ? error.message : String(error))
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="flex h-full flex-col items-center justify-center gap-4 p-6 text-center">
      <Icon className="h-16 w-16 text-muted-foreground/30" />
      <div>
        <p className="text-sm font-medium">{fileName}</p>
        <p className="mt-1 text-xs text-muted-foreground">{filePath}</p>
      </div>
      <p className="text-sm text-muted-foreground">
        {t("preview.notAvailable")}
      </p>
      <div className="flex flex-wrap items-center justify-center gap-2">
        <button
          type="button"
          onClick={() => void viewAsText()}
          disabled={loading}
          className="rounded-md border border-border bg-background px-3 py-1.5 text-xs hover:bg-accent disabled:opacity-50"
        >
          {loading ? t("preview.loadingText") : t("preview.viewAsText")}
        </button>
        <button
          type="button"
          onClick={() => void openPath(filePath)}
          className="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-xs hover:bg-accent"
        >
          <ExternalLink className="h-3.5 w-3.5" />
          {t("preview.openWithSystem")}
        </button>
      </div>
      {loadError && <p className="max-w-lg text-xs text-destructive">{loadError}</p>}
    </div>
  )
}
