"use client"

import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Check, ChevronDown, ChevronRight, Loader2 } from "lucide-react"
import { useTranslations } from "next-intl"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Textarea } from "@/components/ui/textarea"
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import {
  FileTree,
  FileTreeFile,
  FileTreeFolder,
} from "@/components/ai-elements/file-tree"
import {
  gitAddFiles,
  gitCommit,
  gitRollbackFile,
  gitShowFile,
  gitStatus,
  deleteFileTreeEntry,
  readFilePreview,
} from "@/lib/tauri"
import type { GitStatusEntry } from "@/lib/types"
import { cn } from "@/lib/utils"
import { toast } from "sonner"
import { DiffViewer } from "@/components/diff/diff-viewer"
import { languageFromPath } from "@/lib/language-detect"

interface CommitWorkspaceProps {
  folderPath: string
  onCommitted?: () => void
  onCancel?: () => void
}

interface TreeFileNode {
  kind: "file"
  name: string
  path: string
  entry: GitStatusEntry
}

interface TreeDirNode {
  kind: "dir"
  name: string
  path: string
  children: TreeNode[]
}

type TreeNode = TreeFileNode | TreeDirNode

const UNTRACKED_STATUS = "??"
const DEFAULT_LEFT_PANE_WIDTH = 420
const MIN_LEFT_PANE_WIDTH = 320
const MIN_RIGHT_PANE_WIDTH = 360

function clamp(value: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, value))
}

function toPercent(pixels: number, totalPixels: number): number {
  if (totalPixels <= 0) return 0
  return (pixels / totalPixels) * 100
}

function buildFileTree(entries: GitStatusEntry[]): TreeNode[] {
  type BuildDir = {
    name: string
    path: string
    dirs: Map<string, BuildDir>
    files: TreeFileNode[]
  }

  const root: BuildDir = {
    name: "",
    path: "",
    dirs: new Map(),
    files: [],
  }

  for (const entry of entries) {
    const parts = entry.file.split("/").filter(Boolean)
    if (parts.length === 0) continue

    let current = root
    let currentPath = ""

    for (let i = 0; i < parts.length; i += 1) {
      const part = parts[i]
      const isLeaf = i === parts.length - 1
      currentPath = currentPath ? `${currentPath}/${part}` : part

      if (isLeaf) {
        current.files.push({
          kind: "file",
          name: part,
          path: currentPath,
          entry,
        })
      } else {
        const found = current.dirs.get(part)
        if (found) {
          current = found
        } else {
          const next: BuildDir = {
            name: part,
            path: currentPath,
            dirs: new Map(),
            files: [],
          }
          current.dirs.set(part, next)
          current = next
        }
      }
    }
  }

  function sortNodes(nodes: TreeNode[]) {
    return nodes.sort((a, b) => {
      if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1
      return a.name.localeCompare(b.name)
    })
  }

  function toNodes(dir: BuildDir): TreeNode[] {
    const dirs: TreeNode[] = Array.from(dir.dirs.values()).map((child) => ({
      kind: "dir",
      name: child.name,
      path: child.path,
      children: toNodes(child),
    }))

    return sortNodes([...dirs, ...dir.files])
  }

  return toNodes(root)
}

/** Collect all file paths under a tree node (recursive). */
function collectFilePaths(node: TreeNode): string[] {
  if (node.kind === "file") return [node.path]
  return node.children.flatMap(collectFilePaths)
}

/** Depth-first traversal to find the first file node (matches visual order). */
function findFirstFile(nodes: TreeNode[]): string | undefined {
  for (const node of nodes) {
    if (node.kind === "file") return node.path
    const found = findFirstFile(node.children)
    if (found) return found
  }
  return undefined
}

function collectDirPaths(entries: GitStatusEntry[]) {
  const paths = new Set<string>()

  for (const entry of entries) {
    const parts = entry.file.split("/").filter(Boolean)
    if (parts.length < 2) continue

    let currentPath = ""
    for (let i = 0; i < parts.length - 1; i += 1) {
      currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i]
      paths.add(currentPath)
    }
  }

  return paths
}

interface ConfirmState {
  open: boolean
  title: string
  description: string
  action: (() => void) | null
  variant: "default" | "destructive"
}

const CONFIRM_INITIAL: ConfirmState = {
  open: false,
  title: "",
  description: "",
  action: null,
  variant: "default",
}

export function CommitWorkspace({
  folderPath,
  onCommitted,
  onCancel,
}: CommitWorkspaceProps) {
  const t = useTranslations("Folder.commitDialog")
  const tCommon = useTranslations("Folder.common")
  const [entries, setEntries] = useState<GitStatusEntry[]>([])
  const containerRef = useRef<HTMLDivElement>(null)
  const [containerWidth, setContainerWidth] = useState(0)
  const [selected, setSelected] = useState<Set<string>>(new Set())
  const [diffOriginal, setDiffOriginal] = useState("")
  const [diffModified, setDiffModified] = useState("")
  const [diffLanguage, setDiffLanguage] = useState("plaintext")
  const [diffFile, setDiffFile] = useState<string | null>(null)
  const messageRef = useRef("")
  const [hasMessage, setHasMessage] = useState(false)
  const [messageInputKey, setMessageInputKey] = useState(0)
  const [loadingStatus, setLoadingStatus] = useState(false)
  const [loadingDiff, setLoadingDiff] = useState(false)
  const [committing, setCommitting] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [untrackedOpen, setUntrackedOpen] = useState(false)
  const [expandedTrackedDirs, setExpandedTrackedDirs] = useState<Set<string>>(
    new Set()
  )
  const [expandedUntrackedDirs, setExpandedUntrackedDirs] = useState<
    Set<string>
  >(new Set())
  const [confirm, setConfirm] = useState<ConfirmState>(CONFIRM_INITIAL)

  // Use refs to track mutable values without causing callback recreation
  const diffFileRef = useRef(diffFile)
  diffFileRef.current = diffFile
  const entriesRef = useRef(entries)
  entriesRef.current = entries

  const folderName = useMemo(() => {
    const parts = folderPath.replace(/[/\\]+$/, "").split(/[/\\]/)
    return parts[parts.length - 1] || folderPath
  }, [folderPath])

  const trackedEntries = useMemo(
    () => entries.filter((entry) => entry.status !== UNTRACKED_STATUS),
    [entries]
  )
  const untrackedEntries = useMemo(
    () => entries.filter((entry) => entry.status === UNTRACKED_STATUS),
    [entries]
  )
  const trackedTree = useMemo(
    () => buildFileTree(trackedEntries),
    [trackedEntries]
  )
  const untrackedTree = useMemo(
    () => buildFileTree(untrackedEntries),
    [untrackedEntries]
  )
  const filePathSet = useMemo(
    () => new Set(entries.map((entry) => entry.file)),
    [entries]
  )
  const trackedFiles = useMemo(
    () => trackedEntries.map((entry) => entry.file),
    [trackedEntries]
  )
  const untrackedFiles = useMemo(
    () => untrackedEntries.map((entry) => entry.file),
    [untrackedEntries]
  )

  // Shared diff loading logic — extracted to avoid duplication
  const loadDiff = useCallback(
    async (file: string, allEntries?: GitStatusEntry[]) => {
      if (!folderPath) return
      setDiffFile(file)
      setDiffLanguage(languageFromPath(file))
      setLoadingDiff(true)
      setDiffOriginal("")
      setDiffModified("")

      try {
        const statusSource = allEntries ?? entriesRef.current
        const isUntracked =
          statusSource.find((e) => e.file === file)?.status === UNTRACKED_STATUS

        const [originalContent, modifiedContent] = await Promise.all([
          isUntracked
            ? Promise.resolve("")
            : gitShowFile(folderPath, file).catch(() => ""),
          readFilePreview(folderPath, file)
            .then((r) => r.content)
            .catch(() => ""),
        ])

        setDiffOriginal(originalContent)
        setDiffModified(modifiedContent)
      } catch {
        setDiffOriginal("")
        setDiffModified("")
      } finally {
        setLoadingDiff(false)
      }
    },
    [folderPath]
  )

  const loadStatus = useCallback(async () => {
    if (!folderPath) return
    setLoadingStatus(true)
    setError(null)
    try {
      const result = await gitStatus(folderPath)
      setEntries(result)
      const tracked = result.filter(
        (entry) => entry.status !== UNTRACKED_STATUS
      )
      const untracked = result.filter(
        (entry) => entry.status === UNTRACKED_STATUS
      )
      setSelected(new Set(tracked.map((entry) => entry.file)))
      const trackedDirs = collectDirPaths(tracked)
      trackedDirs.add(folderName)
      setExpandedTrackedDirs(trackedDirs)
      const untrackedDirs = collectDirPaths(untracked)
      untrackedDirs.add(folderName)
      setExpandedUntrackedDirs(untrackedDirs)

      // Auto-select the first file in visual tree order for diff preview
      const firstFile =
        findFirstFile(buildFileTree(tracked)) ??
        findFirstFile(buildFileTree(untracked))
      if (firstFile) {
        await loadDiff(firstFile, result)
      }
    } catch (err) {
      setError(String(err))
      setEntries([])
      setExpandedTrackedDirs(new Set())
      setExpandedUntrackedDirs(new Set())
    } finally {
      setLoadingStatus(false)
    }
  }, [folderPath, folderName, loadDiff])

  useEffect(() => {
    if (!folderPath) return
    setDiffOriginal("")
    setDiffModified("")
    setDiffLanguage("plaintext")
    setDiffFile(null)
    messageRef.current = ""
    setHasMessage(false)
    setMessageInputKey((key) => key + 1)
    setUntrackedOpen(false)
    void loadStatus()
  }, [folderPath, loadStatus])

  const handleViewDiff = useCallback(
    (file: string) => {
      if (!folderPath || diffFileRef.current === file) return
      void loadDiff(file)
    },
    [folderPath, loadDiff]
  )

  const toggleFile = useCallback((file: string) => {
    setSelected((prev) => {
      const next = new Set(prev)
      if (next.has(file)) {
        next.delete(file)
      } else {
        next.add(file)
      }
      return next
    })
  }, [])

  const toggleAll = useCallback(() => {
    setSelected((prev) => {
      if (prev.size === entries.length) {
        return new Set<string>()
      }
      return new Set(entries.map((entry) => entry.file))
    })
  }, [entries])

  const toggleGroup = useCallback((files: string[]) => {
    setSelected((prev) => {
      const next = new Set(prev)
      const allInGroupSelected = files.every((file) => next.has(file))
      if (allInGroupSelected) {
        files.forEach((file) => next.delete(file))
      } else {
        files.forEach((file) => next.add(file))
      }
      return next
    })
  }, [])

  const handleSelectPath = useCallback(
    (path: string) => {
      if (!filePathSet.has(path)) return
      handleViewDiff(path)
    },
    [filePathSet, handleViewDiff]
  )

  const handleCommit = useCallback(async () => {
    const commitMessage = messageRef.current.trim()
    if (!commitMessage || selected.size === 0 || !folderPath) return
    setCommitting(true)
    setError(null)
    try {
      const result = await gitCommit(
        folderPath,
        commitMessage,
        Array.from(selected)
      )
      toast.success(t("toasts.commitCompleted"), {
        description: t("toasts.committedFiles", {
          count: result.committed_files,
        }),
      })
      onCommitted?.()
    } catch (err) {
      setError(String(err))
    } finally {
      setCommitting(false)
    }
  }, [folderPath, onCommitted, selected, t])

  // --- Context menu actions ---

  const handleAddToVcs = useCallback(
    async (file: string) => {
      if (!folderPath) return
      try {
        await gitAddFiles(folderPath, [file])
        toast.success(t("toasts.addedToVcs"), { description: file })
        void loadStatus()
      } catch (err) {
        toast.error(t("toasts.addToVcsFailed"), { description: String(err) })
      }
    },
    [folderPath, loadStatus, t]
  )

  const handleDeleteFile = useCallback(
    (file: string) => {
      setConfirm({
        open: true,
        title: t("confirm.deleteTitle"),
        description: t("confirm.deleteDescription", { file }),
        variant: "destructive",
        action: () => {
          void (async () => {
            if (!folderPath) return
            try {
              await deleteFileTreeEntry(folderPath, file)
              toast.success(t("toasts.fileDeleted"), { description: file })
              // If deleted file was being viewed, clear the diff
              if (diffFileRef.current === file) {
                setDiffFile(null)
                setDiffOriginal("")
                setDiffModified("")
              }
              setSelected((prev) => {
                if (!prev.has(file)) return prev
                const next = new Set(prev)
                next.delete(file)
                return next
              })
              void loadStatus()
            } catch (err) {
              toast.error(t("toasts.deleteFailed"), {
                description: String(err),
              })
            }
          })()
        },
      })
    },
    [folderPath, loadStatus, t]
  )

  const handleRollbackFile = useCallback(
    (file: string) => {
      setConfirm({
        open: true,
        title: t("confirm.rollbackTitle"),
        description: t("confirm.rollbackDescription", { file }),
        variant: "destructive",
        action: () => {
          void (async () => {
            if (!folderPath) return
            try {
              await gitRollbackFile(folderPath, file)
              toast.success(t("toasts.fileRolledBack"), { description: file })
              if (diffFileRef.current === file) {
                setDiffFile(null)
                setDiffOriginal("")
                setDiffModified("")
              }
              setSelected((prev) => {
                if (!prev.has(file)) return prev
                const next = new Set(prev)
                next.delete(file)
                return next
              })
              void loadStatus()
            } catch (err) {
              toast.error(t("toasts.rollbackFailed"), {
                description: String(err),
              })
            }
          })()
        },
      })
    },
    [folderPath, loadStatus, t]
  )

  const handleRollbackDir = useCallback(
    (dirPath: string, files: string[], displayName?: string) => {
      const label = displayName ?? dirPath
      setConfirm({
        open: true,
        title: t("confirm.rollbackTitle"),
        description: t("confirm.rollbackDirDescription", { dir: label }),
        variant: "destructive",
        action: () => {
          void (async () => {
            if (!folderPath) return
            try {
              await gitRollbackFile(folderPath, dirPath)
              toast.success(t("toasts.dirRolledBack"), {
                description: label,
              })
              if (diffFileRef.current && files.includes(diffFileRef.current)) {
                setDiffFile(null)
                setDiffOriginal("")
                setDiffModified("")
              }
              setSelected((prev) => {
                const next = new Set(prev)
                files.forEach((f) => next.delete(f))
                return next
              })
              void loadStatus()
            } catch (err) {
              toast.error(t("toasts.rollbackFailed"), {
                description: String(err),
              })
            }
          })()
        },
      })
    },
    [folderPath, loadStatus, t]
  )

  const handleDeleteDir = useCallback(
    (dirPath: string, files: string[], displayName?: string) => {
      const label = displayName ?? dirPath
      setConfirm({
        open: true,
        title: t("confirm.deleteTitle"),
        description: t("confirm.deleteDirDescription", { dir: label }),
        variant: "destructive",
        action: () => {
          void (async () => {
            if (!folderPath) return
            try {
              await deleteFileTreeEntry(folderPath, dirPath)
              toast.success(t("toasts.dirDeleted"), {
                description: label,
              })
              if (diffFileRef.current && files.includes(diffFileRef.current)) {
                setDiffFile(null)
                setDiffOriginal("")
                setDiffModified("")
              }
              setSelected((prev) => {
                const next = new Set(prev)
                files.forEach((f) => next.delete(f))
                return next
              })
              void loadStatus()
            } catch (err) {
              toast.error(t("toasts.deleteFailed"), {
                description: String(err),
              })
            }
          })()
        },
      })
    },
    [folderPath, loadStatus, t]
  )

  const handleAddDirToVcs = useCallback(
    async (dirPath: string, files: string[], displayName?: string) => {
      if (!folderPath) return
      const label = displayName ?? dirPath
      try {
        await gitAddFiles(folderPath, files)
        toast.success(t("toasts.addedToVcs"), { description: label })
        void loadStatus()
      } catch (err) {
        toast.error(t("toasts.addToVcsFailed"), { description: String(err) })
      }
    },
    [folderPath, loadStatus, t]
  )

  const closeConfirm = useCallback(() => {
    setConfirm(CONFIRM_INITIAL)
  }, [])

  const confirmActionRef = useRef(confirm.action)
  confirmActionRef.current = confirm.action

  const executeConfirmAction = useCallback(() => {
    confirmActionRef.current?.()
    setConfirm(CONFIRM_INITIAL)
  }, [])

  const allSelected = useMemo(
    () => entries.length > 0 && selected.size === entries.length,
    [entries.length, selected.size]
  )
  const trackedAllSelected = useMemo(
    () =>
      trackedFiles.length > 0 &&
      trackedFiles.every((file) => selected.has(file)),
    [trackedFiles, selected]
  )
  const untrackedAllSelected = useMemo(
    () =>
      untrackedFiles.length > 0 &&
      untrackedFiles.every((file) => selected.has(file)),
    [untrackedFiles, selected]
  )

  const handleMessageChange = useCallback(
    (e: React.ChangeEvent<HTMLTextAreaElement>) => {
      const nextValue = e.target.value
      messageRef.current = nextValue
      const nextHasMessage = nextValue.trim().length > 0
      setHasMessage((prev) => (prev === nextHasMessage ? prev : nextHasMessage))
    },
    []
  )

  useEffect(() => {
    const container = containerRef.current
    if (!container) return

    const updateWidth = (next: number) => {
      setContainerWidth((prev) => (Math.abs(prev - next) < 1 ? prev : next))
    }

    updateWidth(container.clientWidth)
    const observer = new ResizeObserver((entries) => {
      updateWidth(entries[0]?.contentRect.width ?? container.clientWidth)
    })

    observer.observe(container)
    return () => {
      observer.disconnect()
    }
  }, [])

  const safeContainerWidth =
    containerWidth > 0
      ? containerWidth
      : DEFAULT_LEFT_PANE_WIDTH + MIN_RIGHT_PANE_WIDTH + 240
  const leftMinSize = clamp(
    toPercent(MIN_LEFT_PANE_WIDTH, safeContainerWidth),
    5,
    95
  )
  const rightMinSize = clamp(
    toPercent(MIN_RIGHT_PANE_WIDTH, safeContainerWidth),
    5,
    95
  )
  const leftMaxSize = Math.max(leftMinSize, 100 - rightMinSize)
  const leftDefaultSize = clamp(
    toPercent(DEFAULT_LEFT_PANE_WIDTH, safeContainerWidth),
    leftMinSize,
    leftMaxSize
  )

  // --- Render helpers for file tree nodes ---

  const renderTrackedNode = useCallback(
    function renderNode(node: TreeNode): React.ReactNode {
      if (node.kind === "dir") {
        const dirFiles = collectFilePaths(node)
        const hasNonDeleted = node.children.some(
          (child) =>
            child.kind === "file" &&
            child.entry.status !== " D" &&
            child.entry.status !== "D"
        )
        return (
          <ContextMenu key={`tracked:${node.path}`}>
            <ContextMenuTrigger>
              <FileTreeFolder name={node.name} path={node.path}>
                {node.children.map(renderNode)}
              </FileTreeFolder>
            </ContextMenuTrigger>
            <ContextMenuContent>
              {hasNonDeleted && (
                <ContextMenuItem
                  onClick={() => handleRollbackDir(node.path, dirFiles)}
                >
                  {t("actions.rollback")}
                </ContextMenuItem>
              )}
              <ContextMenuItem
                variant="destructive"
                onClick={() => handleDeleteDir(node.path, dirFiles)}
              >
                {tCommon("delete")}
              </ContextMenuItem>
            </ContextMenuContent>
          </ContextMenu>
        )
      }

      const isDeleted = node.entry.status === " D" || node.entry.status === "D"

      return (
        <ContextMenu key={`tracked:${node.path}`}>
          <ContextMenuTrigger>
            <FileTreeFile
              name={node.name}
              path={node.path}
              className="gap-1 px-1.5 py-1"
            >
              <button
                type="button"
                onClick={(e) => {
                  e.stopPropagation()
                  toggleFile(node.path)
                }}
                className={cn(
                  "flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors",
                  selected.has(node.path)
                    ? "border-primary bg-primary text-primary-foreground"
                    : "border-input"
                )}
                aria-label={t("aria.selectFile", {
                  action: selected.has(node.path)
                    ? t("actions.unselect")
                    : t("actions.select"),
                  path: node.path,
                })}
              >
                {selected.has(node.path) && <Check className="h-3 w-3" />}
              </button>
              <button
                type="button"
                className="flex-1 truncate text-left hover:underline"
                onClick={(e) => {
                  e.stopPropagation()
                  handleViewDiff(node.path)
                }}
                title={node.path}
              >
                {node.name}
              </button>
              <span className="w-6 shrink-0 text-right text-xs font-medium text-muted-foreground">
                {node.entry.status}
              </span>
            </FileTreeFile>
          </ContextMenuTrigger>
          <ContextMenuContent>
            {!isDeleted && (
              <ContextMenuItem onClick={() => handleRollbackFile(node.path)}>
                {t("actions.rollback")}
              </ContextMenuItem>
            )}
            <ContextMenuItem
              variant="destructive"
              onClick={() => handleDeleteFile(node.path)}
            >
              {tCommon("delete")}
            </ContextMenuItem>
          </ContextMenuContent>
        </ContextMenu>
      )
    },
    [
      selected,
      toggleFile,
      handleViewDiff,
      handleRollbackFile,
      handleRollbackDir,
      handleDeleteFile,
      handleDeleteDir,
      t,
      tCommon,
    ]
  )

  const renderUntrackedNode = useCallback(
    function renderNode(node: TreeNode): React.ReactNode {
      if (node.kind === "dir") {
        const dirFiles = collectFilePaths(node)
        return (
          <ContextMenu key={`untracked:${node.path}`}>
            <ContextMenuTrigger>
              <FileTreeFolder name={node.name} path={node.path}>
                {node.children.map(renderNode)}
              </FileTreeFolder>
            </ContextMenuTrigger>
            <ContextMenuContent>
              <ContextMenuItem
                onClick={() => {
                  void handleAddDirToVcs(node.path, dirFiles)
                }}
              >
                {t("actions.addToVcs")}
              </ContextMenuItem>
              <ContextMenuSeparator />
              <ContextMenuItem
                variant="destructive"
                onClick={() => handleDeleteDir(node.path, dirFiles)}
              >
                {tCommon("delete")}
              </ContextMenuItem>
            </ContextMenuContent>
          </ContextMenu>
        )
      }

      return (
        <ContextMenu key={`untracked:${node.path}`}>
          <ContextMenuTrigger>
            <FileTreeFile
              name={node.name}
              path={node.path}
              className="gap-1 px-1.5 py-1"
            >
              <button
                type="button"
                onClick={(e) => {
                  e.stopPropagation()
                  toggleFile(node.path)
                }}
                className={cn(
                  "flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors",
                  selected.has(node.path)
                    ? "border-primary bg-primary text-primary-foreground"
                    : "border-input"
                )}
                aria-label={t("aria.selectFile", {
                  action: selected.has(node.path)
                    ? t("actions.unselect")
                    : t("actions.select"),
                  path: node.path,
                })}
              >
                {selected.has(node.path) && <Check className="h-3 w-3" />}
              </button>
              <button
                type="button"
                className="flex-1 truncate text-left hover:underline"
                onClick={(e) => {
                  e.stopPropagation()
                  handleViewDiff(node.path)
                }}
                title={node.path}
              >
                {node.name}
              </button>
            </FileTreeFile>
          </ContextMenuTrigger>
          <ContextMenuContent>
            <ContextMenuItem onClick={() => handleAddToVcs(node.path)}>
              {t("actions.addToVcs")}
            </ContextMenuItem>
            <ContextMenuSeparator />
            <ContextMenuItem
              variant="destructive"
              onClick={() => handleDeleteFile(node.path)}
            >
              {tCommon("delete")}
            </ContextMenuItem>
          </ContextMenuContent>
        </ContextMenu>
      )
    },
    [
      selected,
      toggleFile,
      handleViewDiff,
      handleAddToVcs,
      handleAddDirToVcs,
      handleDeleteFile,
      handleDeleteDir,
      t,
      tCommon,
    ]
  )

  const toggleTrackedGroup = useCallback(
    () => toggleGroup(trackedFiles),
    [toggleGroup, trackedFiles]
  )
  const toggleUntrackedGroup = useCallback(
    () => toggleGroup(untrackedFiles),
    [toggleGroup, untrackedFiles]
  )
  const toggleUntrackedOpen = useCallback(
    () => setUntrackedOpen((open) => !open),
    []
  )

  return (
    <div
      ref={containerRef}
      className="flex h-full min-h-0 overflow-hidden rounded-lg border bg-card"
    >
      <ResizablePanelGroup
        direction="horizontal"
        className="h-full min-h-0 min-w-0"
      >
        <ResizablePanel
          defaultSize={leftDefaultSize}
          minSize={leftMinSize}
          maxSize={leftMaxSize}
        >
          <div className="flex h-full min-h-0 flex-col">
            {error && (
              <div className="border-b border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
                {error}
              </div>
            )}

            <div className="flex h-9 items-center gap-2 border-b bg-muted/50 px-3">
              <button
                type="button"
                onClick={toggleAll}
                className={cn(
                  "flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors",
                  allSelected
                    ? "border-primary bg-primary text-primary-foreground"
                    : "border-input"
                )}
                aria-label={
                  allSelected
                    ? t("aria.unselectAllFiles")
                    : t("aria.selectAllFiles")
                }
              >
                {allSelected && <Check className="h-3 w-3" />}
              </button>
              <span className="text-xs text-muted-foreground">
                {loadingStatus
                  ? t("loading")
                  : t("selectionCount", {
                      selected: selected.size,
                      total: entries.length,
                    })}
              </span>
            </div>

            <div className="min-h-0 flex-1">
              <ScrollArea className="h-full">
                {entries.length === 0 && !loadingStatus ? (
                  <div className="px-3 py-4 text-center text-sm text-muted-foreground">
                    {t("emptyFiles")}
                  </div>
                ) : (
                  <div className="space-y-3 p-2">
                    {trackedEntries.length > 0 && (
                      <section className="space-y-1">
                        <div className="flex items-center gap-2 px-1 text-[11px] text-muted-foreground">
                          <button
                            type="button"
                            onClick={toggleTrackedGroup}
                            className={cn(
                              "flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border transition-colors",
                              trackedAllSelected
                                ? "border-primary bg-primary text-primary-foreground"
                                : "border-input"
                            )}
                            aria-label={
                              trackedAllSelected
                                ? t("aria.unselectTracked")
                                : t("aria.selectTracked")
                            }
                          >
                            {trackedAllSelected && (
                              <Check className="h-2.5 w-2.5" />
                            )}
                          </button>
                          <span>
                            {t("trackedChanges", {
                              count: trackedEntries.length,
                            })}
                          </span>
                        </div>
                        <FileTree
                          className="rounded-none border-0 bg-transparent font-sans text-sm [&>div]:p-1"
                          expanded={expandedTrackedDirs}
                          onExpandedChange={setExpandedTrackedDirs}
                          selectedPath={diffFile ?? undefined}
                          onSelect={handleSelectPath}
                        >
                          <ContextMenu>
                            <ContextMenuTrigger>
                              <FileTreeFolder
                                name={folderName}
                                path={folderName}
                              >
                                {trackedTree.map(renderTrackedNode)}
                              </FileTreeFolder>
                            </ContextMenuTrigger>
                            <ContextMenuContent>
                              <ContextMenuItem
                                onClick={() =>
                                  handleRollbackDir(
                                    ".",
                                    trackedFiles,
                                    folderName
                                  )
                                }
                              >
                                {t("actions.rollback")}
                              </ContextMenuItem>
                              <ContextMenuItem
                                variant="destructive"
                                onClick={() =>
                                  handleDeleteDir(".", trackedFiles, folderName)
                                }
                              >
                                {tCommon("delete")}
                              </ContextMenuItem>
                            </ContextMenuContent>
                          </ContextMenu>
                        </FileTree>
                      </section>
                    )}

                    {untrackedEntries.length > 0 && (
                      <section className="space-y-1">
                        <div className="flex w-full items-center gap-2 px-1 py-0.5 text-[11px] text-muted-foreground">
                          <button
                            type="button"
                            onClick={toggleUntrackedGroup}
                            className={cn(
                              "flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border transition-colors",
                              untrackedAllSelected
                                ? "border-primary bg-primary text-primary-foreground"
                                : "border-input"
                            )}
                            aria-label={
                              untrackedAllSelected
                                ? t("aria.unselectUntracked")
                                : t("aria.selectUntracked")
                            }
                          >
                            {untrackedAllSelected && (
                              <Check className="h-2.5 w-2.5" />
                            )}
                          </button>
                          <button
                            type="button"
                            className="flex items-center gap-1 hover:text-foreground"
                            onClick={toggleUntrackedOpen}
                          >
                            {untrackedOpen ? (
                              <ChevronDown className="h-3.5 w-3.5 shrink-0" />
                            ) : (
                              <ChevronRight className="h-3.5 w-3.5 shrink-0" />
                            )}
                            <span>
                              {t("untrackedFiles", {
                                count: untrackedEntries.length,
                              })}
                            </span>
                          </button>
                        </div>
                        {untrackedOpen && (
                          <FileTree
                            className="rounded-none border-0 bg-transparent font-sans text-sm [&>div]:p-1"
                            expanded={expandedUntrackedDirs}
                            onExpandedChange={setExpandedUntrackedDirs}
                            selectedPath={diffFile ?? undefined}
                            onSelect={handleSelectPath}
                          >
                            <ContextMenu>
                              <ContextMenuTrigger>
                                <FileTreeFolder
                                  name={folderName}
                                  path={folderName}
                                >
                                  {untrackedTree.map(renderUntrackedNode)}
                                </FileTreeFolder>
                              </ContextMenuTrigger>
                              <ContextMenuContent>
                                <ContextMenuItem
                                  onClick={() => {
                                    void handleAddDirToVcs(
                                      ".",
                                      untrackedFiles,
                                      folderName
                                    )
                                  }}
                                >
                                  {t("actions.addToVcs")}
                                </ContextMenuItem>
                                <ContextMenuSeparator />
                                <ContextMenuItem
                                  variant="destructive"
                                  onClick={() =>
                                    handleDeleteDir(
                                      ".",
                                      untrackedFiles,
                                      folderName
                                    )
                                  }
                                >
                                  {tCommon("delete")}
                                </ContextMenuItem>
                              </ContextMenuContent>
                            </ContextMenu>
                          </FileTree>
                        )}
                      </section>
                    )}
                  </div>
                )}
              </ScrollArea>
            </div>

            <div className="border-t p-3">
              <div className="mb-2 text-xs text-muted-foreground">
                {t("commitMessage")}
              </div>
              <Textarea
                key={messageInputKey}
                placeholder={t("commitMessagePlaceholder")}
                defaultValue=""
                onChange={handleMessageChange}
                className="min-h-[90px] resize-y"
              />
              <div className="mt-3 flex items-center justify-end gap-2">
                <Button variant="outline" onClick={onCancel}>
                  {tCommon("cancel")}
                </Button>
                <Button
                  disabled={committing || !hasMessage || selected.size === 0}
                  onClick={handleCommit}
                >
                  {committing && (
                    <Loader2 className="mr-1 h-4 w-4 animate-spin" />
                  )}
                  {t("commitButton", { count: selected.size })}
                </Button>
              </div>
            </div>
          </div>
        </ResizablePanel>

        <ResizableHandle />

        <ResizablePanel
          defaultSize={100 - leftDefaultSize}
          minSize={rightMinSize}
        >
          <div className="flex h-full min-h-0 flex-1 flex-col overflow-hidden">
            {!diffFile ? (
              <>
                <div className="flex h-9 items-center gap-3 border-b bg-muted/50 px-3 text-xs text-muted-foreground">
                  <span className="font-medium">{t("head")}</span>
                  <span className="text-muted-foreground/60">↔</span>
                  <span className="font-medium">{t("workingTree")}</span>
                </div>
                <div className="flex min-h-0 flex-1 items-center justify-center text-sm text-muted-foreground">
                  {t("clickFileToDiff")}
                </div>
              </>
            ) : loadingDiff ? (
              <>
                <div className="flex h-9 items-center gap-3 border-b bg-muted/50 px-3 text-xs text-muted-foreground">
                  <span className="font-medium">{t("head")}</span>
                  <span className="text-muted-foreground/60">↔</span>
                  <span className="font-medium">{t("workingTree")}</span>
                </div>
                <div className="flex min-h-0 flex-1 items-center justify-center text-sm text-muted-foreground">
                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
                  {t("loadingDiff")}
                </div>
              </>
            ) : (
              <DiffViewer
                key={diffFile}
                original={diffOriginal}
                modified={diffModified}
                originalLabel={t("head")}
                modifiedLabel={t("workingTree")}
                language={diffLanguage}
                className="h-full [&>div:first-child]:h-9 [&>div:first-child]:py-0"
              />
            )}
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>

      <AlertDialog
        open={confirm.open}
        onOpenChange={(open) => {
          if (!open) closeConfirm()
        }}
      >
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>{confirm.title}</AlertDialogTitle>
            <AlertDialogDescription>
              {confirm.description}
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>{tCommon("cancel")}</AlertDialogCancel>
            <AlertDialogAction
              variant={
                confirm.variant === "destructive" ? "destructive" : "default"
              }
              onClick={executeConfirmAction}
            >
              {tCommon("confirm")}
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </div>
  )
}
