"use client"

import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import dynamic from "next/dynamic"
import { ChevronDown, ChevronRight, FileCode2, FileIcon } from "lucide-react"
import type { editor as MonacoEditorNs } from "monaco-editor"
import { useTranslations } from "next-intl"
import { useFolderContext } from "@/contexts/folder-context"
import { useWorkspaceContext } from "@/contexts/workspace-context"
import { ImagePreview } from "@/components/files/image-preview"
import { DiffViewer } from "@/components/diff/diff-viewer"
import { UnifiedDiffPreview } from "@/components/diff/unified-diff-preview"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { cjk } from "@streamdown/cjk"
import { code } from "@streamdown/code"
import { math } from "@streamdown/math"
import { mermaid } from "@streamdown/mermaid"
import { Streamdown } from "streamdown"
import { readFileBase64 } from "@/lib/api"
import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes"
import "@/lib/monaco-local"

const previewPlugins = { cjk, code, math, mermaid }

function resolveRelativePath(base: string, relative: string): string {
  // Strip URL fragment (e.g. #gh-light-mode-only) and query string
  const cleaned = relative.replace(/[#?].*$/, "")
  // Preserve leading "/" for absolute paths, filter empty segments
  const isAbsolute = base.startsWith("/")
  const parts = base.split("/").filter(Boolean)
  for (const seg of cleaned.split("/")) {
    if (seg === "..") {
      if (parts.length > 0) parts.pop()
    } else if (seg !== "." && seg !== "") {
      parts.push(seg)
    }
  }
  return (isAbsolute ? "/" : "") + parts.join("/")
}

/**
 * Pre-resolve relative paths in markdown image/link syntax before Streamdown.
 *
 * rehype-harden resolves "../foo" via `new URL("../foo", "http://example.com")`
 * which loses directory context (e.g. "../images/a.png" from "docs/readme/"
 * becomes "/images/a.png" instead of "/docs/images/a.png").
 *
 * This function resolves relative paths against the file's directory BEFORE
 * Streamdown processes them, using "./" prefix so rehype-harden preserves them.
 */
function preprocessMarkdownPaths(
  content: string,
  relativeFileDir: string
): string {
  const resolveUrl = (url: string): string => {
    // Skip absolute URLs, anchors, and already-root-relative paths
    if (/^https?:\/\/|^data:|^blob:|^#|^\//.test(url)) return url
    // Separate fragment/query from path
    const fragIdx = url.search(/[#?]/)
    const pathPart = fragIdx >= 0 ? url.slice(0, fragIdx) : url
    const fragment = fragIdx >= 0 ? url.slice(fragIdx) : ""
    // Resolve relative to file directory within project
    const parts = relativeFileDir.split("/").filter(Boolean)
    for (const seg of pathPart.split("/")) {
      if (seg === "..") {
        if (parts.length > 0) parts.pop()
      } else if (seg !== "." && seg !== "") {
        parts.push(seg)
      }
    }
    // "./" prefix ensures rehype-harden recognizes it as relative
    return "./" + parts.join("/") + fragment
  }

  // Pre-resolve image paths: ![alt](url) or ![alt](url "title")
  let result = content.replace(
    /!\[([^\]]*)\]\(([^)\s"']+)([^)]*)\)/g,
    (match, alt, url, rest) => {
      const resolved = resolveUrl(url)
      if (resolved === url) return match
      return `![${alt}](${resolved}${rest})`
    }
  )

  // Pre-resolve image-wrapped link paths: [![alt](img)](url)
  result = result.replace(
    /\[(!\[[^\]]*\]\([^)]*\))\]\(([^)\s"']+)([^)]*)\)/g,
    (match, imgPart, url, rest) => {
      const resolved = resolveUrl(url)
      if (resolved === url) return match
      return `[${imgPart}](${resolved}${rest})`
    }
  )

  // Pre-resolve link paths: [text](url) — negative lookbehind to skip images
  result = result.replace(
    /(?<!!)\[([^\]]*)\]\(([^)\s"']+)([^)]*)\)/g,
    (match, text, url, rest) => {
      const resolved = resolveUrl(url)
      if (resolved === url) return match
      return `[${text}](${resolved}${rest})`
    }
  )

  // Pre-resolve HTML <a href="..."> and <img src="..."> tags
  result = result.replace(
    /<(a\s[^>]*?href|img\s[^>]*?src)=(["'])([^"']+)\2/gi,
    (match, prefix, quote, url) => {
      const resolved = resolveUrl(url)
      if (resolved === url) return match
      return `<${prefix}=${quote}${resolved}${quote}`
    }
  )

  return result
}

const MIME_BY_EXT: Record<string, string> = {
  png: "image/png",
  jpg: "image/jpeg",
  jpeg: "image/jpeg",
  gif: "image/gif",
  svg: "image/svg+xml",
  webp: "image/webp",
  bmp: "image/bmp",
  ico: "image/x-icon",
}

function useLocalImageSrc(
  src: string | undefined,
  fileDir: string | null,
  folderPath: string | null
): string | undefined {
  const [dataUrl, setDataUrl] = useState<string | undefined>(undefined)

  const isLocal = src && fileDir && !/^https?:\/\/|^data:|^blob:/.test(src)

  useEffect(() => {
    if (!isLocal || !src || !fileDir) return
    let cancelled = false
    // rehype-harden resolves "../foo" to "/foo" via new URL(src, "http://example.com")
    // Root-relative paths (starting with "/") should resolve against folderPath
    const absPath =
      src.startsWith("/") && folderPath
        ? resolveRelativePath(folderPath, src)
        : resolveRelativePath(fileDir, src)
    const ext = absPath.split(".").pop()?.toLowerCase() ?? ""
    const mime = MIME_BY_EXT[ext] ?? "image/png"

    readFileBase64(absPath)
      .then((b64) => {
        if (!cancelled) {
          setDataUrl(`data:${mime};base64,${b64}`)
        }
      })
      .catch((err) => {
        console.error(
          `[PreviewImage] readFileBase64 failed for "${absPath}":`,
          typeof err === "object" ? JSON.stringify(err) : err
        )
      })
    return () => {
      cancelled = true
    }
  }, [isLocal, src, fileDir, folderPath])

  if (!isLocal) return src
  return dataUrl
}

function PreviewImage({
  fileDir,
  folderPath,
  ...props
}: React.ComponentProps<"img"> & {
  fileDir: string | null
  folderPath: string | null
}) {
  const src = typeof props.src === "string" ? props.src : undefined
  const resolvedSrc = useLocalImageSrc(src, fileDir, folderPath)

  // eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text
  return <img {...props} src={resolvedSrc} />
}

const AUTO_SAVE_DELAY_MS = 5000

function buildMonacoModelPath(path: string | null, id: string): string {
  if (!path) return `inmemory://model/${encodeURIComponent(id)}`
  const normalized = path.replace(/\\/g, "/")
  const encoded = normalized.split("/").map(encodeURIComponent).join("/")
  return `file:///${encoded}`
}

interface DiffOutlineFile {
  key: string
  path: string
  startLine: number
  endLine: number
  additions: number
  deletions: number
  hunks: DiffOutlineHunk[]
}

interface DiffOutlineHunk {
  key: string
  startLine: number
  endLine: number
  header: string
  additions: number
  deletions: number
}

interface DiffOutline {
  files: DiffOutlineFile[]
  totalAdditions: number
  totalDeletions: number
  totalHunks: number
}

type DiffListContext =
  | { kind: "commit"; commitHash: string; commitMessage: string | null }
  | { kind: "working"; path: string }
  | { kind: "branch"; branch: string; path: string }

function decodeDiffTabToken(token: string): string {
  try {
    return decodeURIComponent(token)
  } catch {
    return token
  }
}

function normalizeDiffPath(rawPath: string): string | null {
  const trimmed = rawPath.trim().replace(/^"|"$/g, "")
  if (!trimmed || trimmed === "/dev/null") return null

  if (trimmed.startsWith("a/") || trimmed.startsWith("b/")) {
    return trimmed.slice(2).replace(/\\/g, "/")
  }

  return trimmed.replace(/\\/g, "/")
}

function normalizeWorkspacePath(path: string): string {
  return path.replace(/\\/g, "/")
}

function parsePathFromDiffGitLine(line: string): string | null {
  if (!line.startsWith("diff --git ")) return null
  const match = line.match(/^diff --git\s+(.+?)\s+(.+)$/)
  if (!match) return null

  return normalizeDiffPath(match[2]) ?? normalizeDiffPath(match[1])
}

function parsePathFromFileHeader(
  line: string,
  prefix: "--- " | "+++ "
): string | null {
  if (!line.startsWith(prefix)) return null
  return normalizeDiffPath(line.slice(prefix.length))
}

function parsePathFromApplyPatchLine(line: string): string | null {
  const prefixes = ["*** Update File: ", "*** Add File: ", "*** Delete File: "]

  for (const prefix of prefixes) {
    if (line.startsWith(prefix)) {
      return normalizeDiffPath(line.slice(prefix.length))
    }
  }

  return null
}

function parseMovedPathFromApplyPatchLine(line: string): string | null {
  if (!line.startsWith("*** Move to: ")) return null
  return normalizeDiffPath(line.slice(13))
}

function buildDiffOutline(content: string): DiffOutline | null {
  if (!content.trim()) return null

  const lines = content.split("\n")
  const files: DiffOutlineFile[] = []

  let current: DiffOutlineFile | null = null
  let currentHunk: {
    startLine: number
    header: string
    additions: number
    deletions: number
  } | null = null
  let fileIndex = 1
  let hunkIndex = 1

  const createFile = (
    lineNumber: number,
    path: string | null
  ): DiffOutlineFile => {
    const entry: DiffOutlineFile = {
      key: `${lineNumber}-${fileIndex}`,
      path: path ?? `Diff #${fileIndex}`,
      startLine: lineNumber,
      endLine: lineNumber,
      additions: 0,
      deletions: 0,
      hunks: [],
    }
    fileIndex += 1
    return entry
  }

  const flushHunk = (endLine: number) => {
    if (!current || !currentHunk) return

    const normalizedEnd = Math.max(currentHunk.startLine, endLine)
    current.hunks.push({
      key: `${current.key}:hunk-${hunkIndex}`,
      startLine: currentHunk.startLine,
      endLine: normalizedEnd,
      header: currentHunk.header,
      additions: currentHunk.additions,
      deletions: currentHunk.deletions,
    })
    hunkIndex += 1
    currentHunk = null
  }

  const flushFile = () => {
    if (!current) return

    if (currentHunk) {
      flushHunk(current.endLine)
    }

    if (
      current.hunks.length === 0 &&
      (current.additions > 0 || current.deletions > 0)
    ) {
      current.hunks.push({
        key: `${current.key}:hunk-${hunkIndex}`,
        startLine: current.startLine,
        endLine: current.endLine,
        header: "Change",
        additions: current.additions,
        deletions: current.deletions,
      })
      hunkIndex += 1
    }

    files.push(current)
    current = null
  }

  for (const [index, line] of lines.entries()) {
    const lineNumber = index + 1

    const diffGitPath = parsePathFromDiffGitLine(line)
    if (diffGitPath) {
      flushFile()
      current = createFile(lineNumber, diffGitPath)
      continue
    }

    const applyPatchPath = parsePathFromApplyPatchLine(line)
    if (applyPatchPath) {
      flushFile()
      current = createFile(lineNumber, applyPatchPath)
      continue
    }

    if (line.startsWith("--- ") && currentHunk) {
      flushHunk(lineNumber - 1)
    }

    const movedPath = parseMovedPathFromApplyPatchLine(line)
    if (movedPath && current) {
      current.path = movedPath
    }

    if (!current) {
      const minusPath = parsePathFromFileHeader(line, "--- ")
      if (minusPath) {
        current = createFile(lineNumber, minusPath)
      } else {
        const plusPath = parsePathFromFileHeader(line, "+++ ")
        if (plusPath) current = createFile(lineNumber, plusPath)
      }
    } else {
      const plusPath = parsePathFromFileHeader(line, "+++ ")
      if (plusPath) current.path = plusPath
    }

    if (!current) continue
    current.endLine = lineNumber

    if (line.startsWith("@@ ")) {
      if (currentHunk) {
        flushHunk(lineNumber - 1)
      }
      currentHunk = {
        startLine: lineNumber,
        header: line,
        additions: 0,
        deletions: 0,
      }
      continue
    }

    if (line.startsWith("+") && !line.startsWith("+++")) {
      current.additions += 1
      if (currentHunk) currentHunk.additions += 1
    }
    if (line.startsWith("-") && !line.startsWith("---")) {
      current.deletions += 1
      if (currentHunk) currentHunk.deletions += 1
    }
  }

  flushFile()

  if (files.length === 0) return null

  const totalAdditions = files.reduce((sum, file) => sum + file.additions, 0)
  const totalDeletions = files.reduce((sum, file) => sum + file.deletions, 0)
  const totalHunks = files.reduce((sum, file) => sum + file.hunks.length, 0)

  return {
    files,
    totalAdditions,
    totalDeletions,
    totalHunks,
  }
}

function setEditorHiddenAreas(
  editor: MonacoEditorNs.IStandaloneCodeEditor,
  ranges: {
    startLineNumber: number
    startColumn: number
    endLineNumber: number
    endColumn: number
  }[]
) {
  const hiddenAreaEditor = editor as unknown as {
    setHiddenAreas?: (
      hiddenRanges: {
        startLineNumber: number
        startColumn: number
        endLineNumber: number
        endColumn: number
      }[]
    ) => void
  }

  hiddenAreaEditor.setHiddenAreas?.(ranges)
}

const MonacoEditor = dynamic(async () => import("@monaco-editor/react"), {
  ssr: false,
})

function normalizeLineEndings(text: string): string {
  return text.replace(/\r\n/g, "\n")
}

function splitDiffLines(text: string): string[] {
  if (!text) return []
  return normalizeLineEndings(text).split("\n")
}

function lowerBound(values: number[], target: number): number {
  let lo = 0
  let hi = values.length

  while (lo < hi) {
    const mid = (lo + hi) >>> 1
    if (values[mid] < target) {
      lo = mid + 1
    } else {
      hi = mid
    }
  }

  return lo
}

function computeLcsMatches(
  baseLines: string[],
  currentLines: string[]
): Array<{ baseIndex: number; currentIndex: number }> | null {
  const MAX_MATCHES_PER_LINE = 256
  const MAX_TOKENS = 200_000
  const basePositions = new Map<string, number[]>()

  for (const [index, line] of baseLines.entries()) {
    const positions = basePositions.get(line)
    if (positions) {
      positions.push(index)
    } else {
      basePositions.set(line, [index])
    }
  }

  const tokens: Array<{ baseIndex: number; currentIndex: number }> = []
  for (const [currentIndex, line] of currentLines.entries()) {
    const positions = basePositions.get(line)
    if (!positions) continue
    if (positions.length > MAX_MATCHES_PER_LINE) continue
    for (let pos = positions.length - 1; pos >= 0; pos -= 1) {
      tokens.push({ baseIndex: positions[pos], currentIndex })
      if (tokens.length > MAX_TOKENS) return null
    }
  }

  if (tokens.length === 0) return []

  const tails: number[] = []
  const tailsTokenIndex: number[] = []
  const prevTokenIndex = Array<number>(tokens.length).fill(-1)

  for (const [tokenIndex, token] of tokens.entries()) {
    const len = lowerBound(tails, token.baseIndex)
    tails[len] = token.baseIndex
    tailsTokenIndex[len] = tokenIndex
    if (len > 0) {
      prevTokenIndex[tokenIndex] = tailsTokenIndex[len - 1]
    }
  }

  const matches: Array<{ baseIndex: number; currentIndex: number }> = []
  let cursor = tailsTokenIndex[tails.length - 1]
  while (cursor >= 0) {
    const token = tokens[cursor]
    matches.push({
      baseIndex: token.baseIndex,
      currentIndex: token.currentIndex,
    })
    cursor = prevTokenIndex[cursor]
  }

  matches.reverse()
  return matches
}

interface GitGutterLineMarkers {
  added: number[]
  modified: number[]
  deleted: number[]
}

const EMPTY_GIT_GUTTER_LINE_MARKERS: GitGutterLineMarkers = {
  added: [],
  modified: [],
  deleted: [],
}

function toSortedUniqueLineNumbers(lineNumbers: number[]): number[] {
  if (lineNumbers.length <= 1) return lineNumbers
  return [...new Set(lineNumbers)].sort((a, b) => a - b)
}

function appendLineRange(
  target: number[],
  startIndexInclusive: number,
  endIndexExclusive: number
) {
  for (let index = startIndexInclusive; index < endIndexExclusive; index += 1) {
    target.push(index + 1)
  }
}

function computeGitGutterLineMarkers(
  baseContent: string,
  currentContent: string
): GitGutterLineMarkers {
  const MAX_TOTAL_LINES = 20_000
  if (baseContent === currentContent) {
    return EMPTY_GIT_GUTTER_LINE_MARKERS
  }

  const baseLines = splitDiffLines(baseContent)
  const currentLines = splitDiffLines(currentContent)
  if (baseLines.length + currentLines.length > MAX_TOTAL_LINES) {
    return EMPTY_GIT_GUTTER_LINE_MARKERS
  }

  if (
    normalizeLineEndings(baseContent) === normalizeLineEndings(currentContent)
  ) {
    return EMPTY_GIT_GUTTER_LINE_MARKERS
  }
  if (baseLines.length === 0) {
    return {
      added: currentLines.map((_, index) => index + 1),
      modified: [],
      deleted: [],
    }
  }
  if (currentLines.length === 0) {
    return {
      added: [],
      modified: [],
      deleted: [1],
    }
  }

  const matches = computeLcsMatches(baseLines, currentLines)
  if (matches === null) {
    return EMPTY_GIT_GUTTER_LINE_MARKERS
  }
  const added: number[] = []
  const modified: number[] = []
  const deleted: number[] = []

  let previousBase = -1
  let previousCurrent = -1
  const sentinels = [
    ...matches,
    { baseIndex: baseLines.length, currentIndex: currentLines.length },
  ]

  for (const match of sentinels) {
    const baseGap = match.baseIndex - previousBase - 1
    const currentGap = match.currentIndex - previousCurrent - 1

    if (baseGap === 0 && currentGap > 0) {
      appendLineRange(added, previousCurrent + 1, match.currentIndex)
    } else if (baseGap > 0 && currentGap === 0) {
      const anchorLine = Math.max(
        1,
        Math.min(currentLines.length, match.currentIndex + 1)
      )
      deleted.push(anchorLine)
    } else if (baseGap > 0 && currentGap > 0) {
      appendLineRange(modified, previousCurrent + 1, match.currentIndex)
    }

    previousBase = match.baseIndex
    previousCurrent = match.currentIndex
  }

  return {
    added: toSortedUniqueLineNumbers(added),
    modified: toSortedUniqueLineNumbers(modified),
    deleted: toSortedUniqueLineNumbers(deleted),
  }
}

function DiffFileList({
  diffOutline,
  badge,
  description,
  onOpenDiff,
  openFilePreview,
}: {
  diffOutline: DiffOutline
  badge?: string | null
  description?: string | null
  onOpenDiff: (path: string) => Promise<void>
  openFilePreview: (path: string) => Promise<void>
}) {
  const t = useTranslations("Folder.fileWorkspacePanel")
  return (
    <div className="h-full flex flex-col min-h-0">
      <div className="border-b border-border bg-muted/25 px-3 py-2 space-y-1">
        <div className="text-[11px] text-muted-foreground flex items-center gap-3">
          {badge && (
            <span className="font-medium text-foreground/80 font-mono">
              {badge}
            </span>
          )}
          <span>{t("fileCount", { count: diffOutline.files.length })}</span>
          <span className="font-mono text-green-600 dark:text-green-400">
            +{diffOutline.totalAdditions}
          </span>
          <span className="font-mono text-red-600 dark:text-red-400">
            -{diffOutline.totalDeletions}
          </span>
        </div>
        {description && (
          <p className="text-xs text-foreground/70 line-clamp-2 leading-snug">
            {description}
          </p>
        )}
      </div>
      <div className="flex-1 min-h-0 overflow-y-auto">
        <div className="py-1">
          {diffOutline.files.map((file) => (
            <ContextMenu key={file.key}>
              <ContextMenuTrigger asChild>
                <button
                  type="button"
                  className="w-full flex items-center gap-2 px-3 py-1.5 text-left hover:bg-muted/50 transition-colors group"
                  onClick={() => {
                    void onOpenDiff(file.path)
                  }}
                  title={file.path}
                >
                  <FileIcon className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
                  <span className="text-xs truncate flex-1 min-w-0 font-mono">
                    {file.path}
                  </span>
                  <span className="shrink-0 flex items-center gap-2 text-[10px] font-mono">
                    {file.additions > 0 && (
                      <span className="text-green-600 dark:text-green-400">
                        +{file.additions}
                      </span>
                    )}
                    {file.deletions > 0 && (
                      <span className="text-red-600 dark:text-red-400">
                        -{file.deletions}
                      </span>
                    )}
                  </span>
                </button>
              </ContextMenuTrigger>
              <ContextMenuContent>
                <ContextMenuItem
                  onSelect={() => {
                    void onOpenDiff(file.path)
                  }}
                >
                  {t("viewDiff")}
                </ContextMenuItem>
                <ContextMenuItem
                  onSelect={() => {
                    void openFilePreview(file.path)
                  }}
                >
                  {t("openFile")}
                </ContextMenuItem>
              </ContextMenuContent>
            </ContextMenu>
          ))}
        </div>
      </div>
    </div>
  )
}

export function FileWorkspacePanel() {
  const t = useTranslations("Folder.fileWorkspacePanel")
  const {
    activeFileTab,
    consumePendingFileReveal,
    pendingFileReveal,
    openBranchDiff,
    openCommitDiff,
    openFilePreview,
    openWorkingTreeDiff,
    previewFileTabIds,
    saveActiveFile,
    updateActiveFileContent,
  } = useWorkspaceContext()
  const { folder } = useFolderContext()
  const folderPath = folder?.path ?? null
  const activeScope = activeFileTab?.id ?? "__default__"
  const editorRef = useRef<MonacoEditorNs.IStandaloneCodeEditor | null>(null)
  const cursorListenerRef = useRef<{ dispose: () => void } | null>(null)
  const gitChangeDecorationsRef = useRef<string[]>([])
  const editorTheme = useMonacoThemeSync()
  const [editorMountVersion, setEditorMountVersion] = useState(0)
  const [cursorLine, setCursorLine] = useState(1)
  const [collapsedFiles, setCollapsedFiles] = useState<Record<string, boolean>>(
    {}
  )
  const [collapsedHunks, setCollapsedHunks] = useState<Record<string, boolean>>(
    {}
  )
  const renderedContent = activeFileTab?.content ?? ""
  const isFileTab = activeFileTab?.kind === "file"
  const fileReadonly = isFileTab ? Boolean(activeFileTab.readonly) : true
  const fileSaveState = isFileTab ? (activeFileTab.saveState ?? "idle") : "idle"
  const fileIsDirty = isFileTab ? Boolean(activeFileTab.isDirty) : false
  const canEdit = isFileTab && !fileReadonly
  const autoSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const autoSaveGuardRef = useRef({
    canEdit: false,
    fileIsDirty: false,
    fileSaveState: "idle" as "idle" | "saving" | "error",
  })
  const diffListContext = useMemo<DiffListContext | null>(() => {
    if (!activeFileTab) return null
    if (activeFileTab.kind !== "diff") return null

    const commitMatch = activeFileTab.id.match(/^diff:commit:([^:]+):all$/)
    if (commitMatch) {
      return {
        kind: "commit",
        commitHash: commitMatch[1],
        commitMessage: activeFileTab.description,
      }
    }

    const workingOverviewMatch = activeFileTab.id.match(
      /^diff:working-overview:(.+)$/
    )
    if (workingOverviewMatch) {
      return {
        kind: "working",
        path: decodeDiffTabToken(workingOverviewMatch[1]),
      }
    }

    const branchOverviewMatch = activeFileTab.id.match(
      /^diff:branch-overview:([^:]+):(.+)$/
    )
    if (branchOverviewMatch) {
      return {
        kind: "branch",
        branch: decodeDiffTabToken(branchOverviewMatch[1]),
        path: decodeDiffTabToken(branchOverviewMatch[2]),
      }
    }

    return null
  }, [activeFileTab])
  const diffOutline = useMemo(() => {
    if (!activeFileTab || activeFileTab.kind !== "diff") return null
    return buildDiffOutline(activeFileTab.content)
  }, [activeFileTab])
  const allHunks = useMemo(
    () =>
      diffListContext
        ? []
        : (diffOutline?.files.flatMap((file) => file.hunks) ?? []),
    [diffListContext, diffOutline]
  )
  const activeHunkIndex = useMemo(() => {
    if (allHunks.length === 0) return -1

    const containingIndex = allHunks.findIndex(
      (hunk) => cursorLine >= hunk.startLine && cursorLine <= hunk.endLine
    )
    if (containingIndex >= 0) return containingIndex

    const firstAfterIndex = allHunks.findIndex(
      (hunk) => hunk.startLine > cursorLine
    )
    if (firstAfterIndex < 0) return allHunks.length - 1
    return firstAfterIndex - 1
  }, [allHunks, cursorLine])

  const lineNumbersMinChars = useMemo(() => {
    const lineCount = renderedContent.split("\n").length
    const digits = String(Math.max(1, lineCount)).length

    // Keep a one-character buffer so numbers don't visually hug the gutter edge.
    return Math.max(3, digits + 1)
  }, [renderedContent])

  const hasGitBaseSnapshot =
    isFileTab && activeFileTab?.gitBaseContent !== undefined
  const gitBaseContent = hasGitBaseSnapshot
    ? (activeFileTab?.gitBaseContent ?? "")
    : ""
  const gitGutterLineMarkers = useMemo(() => {
    if (!hasGitBaseSnapshot) return EMPTY_GIT_GUTTER_LINE_MARKERS
    return computeGitGutterLineMarkers(gitBaseContent, renderedContent)
  }, [gitBaseContent, hasGitBaseSnapshot, renderedContent])

  const applyGitChangeDecorations = useCallback(() => {
    const editorInstance = editorRef.current
    if (!editorInstance) return

    const { added, modified, deleted } = gitGutterLineMarkers

    if (
      !isFileTab ||
      (added.length === 0 && modified.length === 0 && deleted.length === 0)
    ) {
      gitChangeDecorationsRef.current = editorInstance.deltaDecorations(
        gitChangeDecorationsRef.current,
        []
      )
      return
    }

    const model = editorInstance.getModel()
    if (!model) return

    const maxLine = model.getLineCount()
    const toRange = (lineNumber: number) => ({
      startLineNumber: lineNumber,
      startColumn: 1,
      endLineNumber: lineNumber,
      endColumn: 1,
    })
    const addedDecorations = added
      .filter((lineNumber) => lineNumber >= 1 && lineNumber <= maxLine)
      .map((lineNumber) => ({
        range: toRange(lineNumber),
        options: {
          isWholeLine: true,
          linesDecorationsClassName:
            "codeg-dirty-diff-glyph codeg-dirty-diff-added",
        },
      }))
    const modifiedDecorations = modified
      .filter((lineNumber) => lineNumber >= 1 && lineNumber <= maxLine)
      .map((lineNumber) => ({
        range: toRange(lineNumber),
        options: {
          isWholeLine: true,
          linesDecorationsClassName:
            "codeg-dirty-diff-glyph codeg-dirty-diff-modified",
        },
      }))
    const deletedDecorations = deleted
      .filter((lineNumber) => lineNumber >= 1 && lineNumber <= maxLine)
      .map((lineNumber) => ({
        range: {
          startLineNumber: lineNumber,
          startColumn: Number.MAX_VALUE,
          endLineNumber: lineNumber,
          endColumn: Number.MAX_VALUE,
        },
        options: {
          isWholeLine: false,
          linesDecorationsClassName:
            "codeg-dirty-diff-glyph codeg-dirty-diff-deleted",
        },
      }))
    const decorations = [
      ...addedDecorations,
      ...modifiedDecorations,
      ...deletedDecorations,
    ]

    gitChangeDecorationsRef.current = editorInstance.deltaDecorations(
      gitChangeDecorationsRef.current,
      decorations
    )
  }, [gitGutterLineMarkers, isFileTab])

  const applyHiddenAreas = useCallback(() => {
    const editorInstance = editorRef.current
    if (!editorInstance) return

    if (!diffOutline || diffListContext) {
      setEditorHiddenAreas(editorInstance, [])
      return
    }

    const model = editorInstance.getModel()
    if (!model) return

    const maxLine = model.getLineCount()
    const ranges: {
      startLineNumber: number
      startColumn: number
      endLineNumber: number
      endColumn: number
    }[] = []

    const addRange = (startLine: number, endLine: number) => {
      const safeStart = Math.max(1, startLine)
      const safeEnd = Math.min(maxLine, endLine)
      if (safeStart > safeEnd) return

      ranges.push({
        startLineNumber: safeStart,
        startColumn: 1,
        endLineNumber: safeEnd,
        endColumn: 1,
      })
    }

    for (const file of diffOutline.files) {
      const fileCollapsed = Boolean(
        collapsedFiles[`${activeScope}:${file.key}`]
      )
      if (fileCollapsed) {
        addRange(file.startLine + 1, file.endLine)
        continue
      }

      for (const hunk of file.hunks) {
        if (!collapsedHunks[`${activeScope}:${hunk.key}`]) continue
        addRange(hunk.startLine + 1, hunk.endLine)
      }
    }

    setEditorHiddenAreas(editorInstance, ranges)
  }, [
    activeScope,
    collapsedFiles,
    collapsedHunks,
    diffListContext,
    diffOutline,
  ])

  const handleEditorMount = useCallback(
    (editorInstance: MonacoEditorNs.IStandaloneCodeEditor) => {
      editorRef.current = editorInstance
      cursorListenerRef.current?.dispose()
      cursorListenerRef.current = editorInstance.onDidChangeCursorPosition(
        (event) => {
          setCursorLine(event.position.lineNumber)
        }
      )
      setEditorMountVersion((prev) => prev + 1)
      setCursorLine(editorInstance.getPosition()?.lineNumber ?? 1)
      applyHiddenAreas()
      applyGitChangeDecorations()
    },
    [applyGitChangeDecorations, applyHiddenAreas]
  )

  const jumpToLine = useCallback((lineNumber: number) => {
    const editorInstance = editorRef.current
    if (!editorInstance) return false

    const model = editorInstance.getModel()
    if (!model) return false
    const maxLine = model.getLineCount()
    const targetLine = Math.min(Math.max(1, lineNumber), maxLine)

    editorInstance.revealLineInCenter(targetLine)
    editorInstance.setPosition({ lineNumber: targetLine, column: 1 })
    editorInstance.focus()
    return true
  }, [])

  const jumpToHunk = useCallback(
    (hunkIndex: number) => {
      const hunk = allHunks[hunkIndex]
      if (!hunk) return
      jumpToLine(hunk.startLine)
    },
    [allHunks, jumpToLine]
  )

  const handlePrevHunk = useCallback(() => {
    if (allHunks.length === 0) return
    if (activeHunkIndex <= 0) return
    jumpToHunk(activeHunkIndex - 1)
  }, [activeHunkIndex, allHunks.length, jumpToHunk])

  const handleNextHunk = useCallback(() => {
    if (allHunks.length === 0) return
    if (activeHunkIndex >= allHunks.length - 1) return
    jumpToHunk(activeHunkIndex + 1)
  }, [activeHunkIndex, allHunks.length, jumpToHunk])

  const toggleFileCollapsed = useCallback(
    (fileKey: string) => {
      setCollapsedFiles((prev) => {
        const scopedKey = `${activeScope}:${fileKey}`
        return {
          ...prev,
          [scopedKey]: !prev[scopedKey],
        }
      })
    },
    [activeScope]
  )

  const toggleHunkCollapsed = useCallback(
    (hunkKey: string) => {
      setCollapsedHunks((prev) => {
        const scopedKey = `${activeScope}:${hunkKey}`
        return {
          ...prev,
          [scopedKey]: !prev[scopedKey],
        }
      })
    },
    [activeScope]
  )

  useEffect(() => {
    applyHiddenAreas()
  }, [applyHiddenAreas])

  useEffect(() => {
    applyGitChangeDecorations()
  }, [activeFileTab?.id, applyGitChangeDecorations])

  useEffect(() => {
    if (!pendingFileReveal) return
    if (!isFileTab || !activeFileTab || activeFileTab.loading) return
    if (!activeFileTab.path) return
    if (
      normalizeWorkspacePath(activeFileTab.path) !==
      normalizeWorkspacePath(pendingFileReveal.path)
    ) {
      return
    }

    const jumped = jumpToLine(pendingFileReveal.line)
    if (!jumped) return

    consumePendingFileReveal(pendingFileReveal.requestId)
  }, [
    activeFileTab,
    consumePendingFileReveal,
    editorMountVersion,
    isFileTab,
    jumpToLine,
    pendingFileReveal,
  ])

  useEffect(() => {
    autoSaveGuardRef.current = {
      canEdit,
      fileIsDirty,
      fileSaveState,
    }
  }, [canEdit, fileIsDirty, fileSaveState])

  useEffect(() => {
    if (autoSaveTimerRef.current) {
      clearTimeout(autoSaveTimerRef.current)
      autoSaveTimerRef.current = null
    }

    if (!canEdit || !fileIsDirty || fileSaveState !== "idle") return

    autoSaveTimerRef.current = setTimeout(() => {
      const guard = autoSaveGuardRef.current
      if (
        !guard.canEdit ||
        !guard.fileIsDirty ||
        guard.fileSaveState !== "idle"
      ) {
        return
      }
      void saveActiveFile()
    }, AUTO_SAVE_DELAY_MS)

    return () => {
      if (autoSaveTimerRef.current) {
        clearTimeout(autoSaveTimerRef.current)
        autoSaveTimerRef.current = null
      }
    }
  }, [canEdit, fileIsDirty, fileSaveState, saveActiveFile, renderedContent])

  useEffect(() => {
    if (!isFileTab) return

    const saveOnDeactivation = () => {
      const guard = autoSaveGuardRef.current
      if (
        !guard.canEdit ||
        !guard.fileIsDirty ||
        guard.fileSaveState === "saving"
      ) {
        return
      }
      void saveActiveFile()
    }

    const onWindowBlur = () => {
      saveOnDeactivation()
    }

    const onVisibilityChange = () => {
      if (document.visibilityState !== "hidden") return
      saveOnDeactivation()
    }

    window.addEventListener("blur", onWindowBlur)
    document.addEventListener("visibilitychange", onVisibilityChange)
    return () => {
      window.removeEventListener("blur", onWindowBlur)
      document.removeEventListener("visibilitychange", onVisibilityChange)
    }
  }, [isFileTab, saveActiveFile])

  useEffect(() => {
    if (!isFileTab) return

    const onKeyDown = (event: KeyboardEvent) => {
      const isSaveShortcut =
        (event.metaKey || event.ctrlKey) && event.key === "s"
      if (!isSaveShortcut) return
      event.preventDefault()
      if (!canEdit) return
      void saveActiveFile()
    }

    window.addEventListener("keydown", onKeyDown)
    return () => {
      window.removeEventListener("keydown", onKeyDown)
    }
  }, [canEdit, isFileTab, saveActiveFile])

  useEffect(
    () => () => {
      if (editorRef.current) {
        editorRef.current.deltaDecorations(gitChangeDecorationsRef.current, [])
      }
      gitChangeDecorationsRef.current = []
      cursorListenerRef.current?.dispose()
      cursorListenerRef.current = null
    },
    []
  )

  if (!activeFileTab) {
    return (
      <div className="h-full flex flex-col items-center justify-center text-center px-6">
        <FileCode2 className="h-8 w-8 text-muted-foreground/60 mb-3" />
        <p className="text-sm text-muted-foreground">{t("openFileOrDiff")}</p>
      </div>
    )
  }

  if (activeFileTab.kind === "rich-diff") {
    const isCommitDiff = activeFileTab.id.startsWith("diff:commit:")
    const isExternalConflictDiff = activeFileTab.id.startsWith(
      "diff:external-conflict:"
    )
    const commitHash = isCommitDiff
      ? (activeFileTab.id.split(":")[2]?.slice(0, 7) ?? "")
      : ""
    const origLabel = isCommitDiff
      ? `${commitHash}~1`
      : isExternalConflictDiff
        ? t("disk")
        : t("head")
    const modLabel = isCommitDiff
      ? commitHash
      : isExternalConflictDiff
        ? t("unsaved")
        : t("workingTree")

    return (
      <div className="h-full relative">
        {activeFileTab.loading && (
          <div className="absolute top-2 right-3 z-10 rounded-md bg-background/70 px-2 py-1 text-[11px] text-muted-foreground backdrop-blur-sm">
            {t("loading")}
          </div>
        )}
        {!activeFileTab.loading && (
          <DiffViewer
            key={activeFileTab.id}
            original={activeFileTab.originalContent ?? ""}
            modified={activeFileTab.modifiedContent ?? ""}
            originalLabel={origLabel}
            modifiedLabel={modLabel}
            language={activeFileTab.language}
            className="h-full"
          />
        )}
      </div>
    )
  }

  if (
    activeFileTab.kind === "diff" &&
    activeFileTab.id.startsWith("diff:session:")
  ) {
    return (
      <div className="h-full relative">
        {activeFileTab.loading && (
          <div className="absolute top-2 right-3 z-10 rounded-md bg-background/70 px-2 py-1 text-[11px] text-muted-foreground backdrop-blur-sm">
            {t("loading")}
          </div>
        )}
        <UnifiedDiffPreview
          diffText={activeFileTab.content}
          className="h-full p-3"
        />
      </div>
    )
  }

  // Preview mode for markdown files
  const isPreviewMode =
    isFileTab &&
    activeFileTab &&
    previewFileTabIds.has(activeFileTab.id) &&
    activeFileTab.language === "markdown"

  // Diff overview list view (commit / directory)
  if (diffListContext && diffOutline) {
    const badge =
      diffListContext.kind === "commit"
        ? diffListContext.commitHash.slice(0, 7)
        : diffListContext.kind === "branch"
          ? diffListContext.branch
          : t("workingTree")

    const description =
      diffListContext.kind === "commit"
        ? diffListContext.commitMessage
        : diffListContext.kind === "branch"
          ? t("compareWithBranch", {
              path: diffListContext.path,
              branch: diffListContext.branch,
            })
          : (activeFileTab.description ?? diffListContext.path)

    const handleOpenDiff = async (path: string) => {
      if (diffListContext.kind === "commit") {
        await openCommitDiff(diffListContext.commitHash, path)
        return
      }

      if (diffListContext.kind === "branch") {
        await openBranchDiff(diffListContext.branch, path)
        return
      }

      await openWorkingTreeDiff(path)
    }

    return (
      <div className="h-full relative">
        {activeFileTab.loading && (
          <div className="absolute top-2 right-3 z-10 rounded-md bg-background/70 px-2 py-1 text-[11px] text-muted-foreground backdrop-blur-sm">
            {t("loading")}
          </div>
        )}
        {!activeFileTab.loading && (
          <DiffFileList
            diffOutline={diffOutline}
            badge={badge}
            description={description}
            onOpenDiff={handleOpenDiff}
            openFilePreview={openFilePreview}
          />
        )}
      </div>
    )
  }

  // Image preview
  if (isFileTab && activeFileTab && activeFileTab.language === "image") {
    return <ImagePreview key={activeFileTab.id} tab={activeFileTab} />
  }

  if (isPreviewMode && activeFileTab) {
    const absFilePath =
      activeFileTab.path && folderPath
        ? `${folderPath}/${activeFileTab.path}`
        : null
    const fileDir = absFilePath
      ? absFilePath.replace(/\/[^/]*$/, "")
      : folderPath
    // Pre-resolve relative paths before Streamdown/rehype-harden mangles them
    const relativeFileDir = activeFileTab.path?.includes("/")
      ? activeFileTab.path.replace(/\/[^/]*$/, "")
      : ""
    const preprocessedContent = preprocessMarkdownPaths(
      renderedContent,
      relativeFileDir
    )

    return (
      <div className="h-full relative">
        {activeFileTab.loading && (
          <div className="absolute top-2 right-3 z-10 rounded-md bg-background/70 px-2 py-1 text-[11px] text-muted-foreground backdrop-blur-sm">
            {t("loading")}
          </div>
        )}
        <div className="h-full overflow-auto p-6 [&_a_img]:inline">
          <Streamdown
            plugins={previewPlugins}
            components={{
              // eslint-disable-next-line @typescript-eslint/no-unused-vars
              img: ({ node, ...imgProps }) => (
                <PreviewImage
                  {...imgProps}
                  fileDir={fileDir}
                  folderPath={folderPath}
                />
              ),
              // eslint-disable-next-line @typescript-eslint/no-unused-vars
              a: ({ node, href, children, ...aProps }) => {
                const isRelative = href && !/^[a-z][a-z0-9+.-]*:|^#/i.test(href)
                if (isRelative && href) {
                  return (
                    <a
                      {...aProps}
                      href="#"
                      onClick={(e) => {
                        e.preventDefault()
                        // After preprocessing + rehype-harden, paths are
                        // root-relative like "/docs/images/foo.png"
                        const clean = href.replace(/[#?].*$/, "")
                        const target = clean
                          .replace(/^\/+/, "")
                          .replace(/\/\/+/g, "/")
                        openFilePreview(target)
                      }}
                    >
                      {children}
                    </a>
                  )
                }
                return (
                  <a
                    {...aProps}
                    href={href}
                    target="_blank"
                    rel="noopener noreferrer"
                  >
                    {children}
                  </a>
                )
              },
            }}
          >
            {preprocessedContent}
          </Streamdown>
        </div>
      </div>
    )
  }

  return (
    <div className="h-full relative">
      {activeFileTab.loading && (
        <div className="absolute top-2 right-3 z-10 rounded-md bg-background/70 px-2 py-1 text-[11px] text-muted-foreground backdrop-blur-sm">
          {t("loading")}
        </div>
      )}
      <div className="h-full flex flex-col min-h-0">
        {diffOutline && (
          <div className="border-b border-border bg-muted/25">
            <div className="px-3 py-1.5 text-[11px] text-muted-foreground flex items-center gap-3">
              <span>{t("fileCount", { count: diffOutline.files.length })}</span>
              <span className="font-mono text-green-600 dark:text-green-400">
                +{diffOutline.totalAdditions}
              </span>
              <span className="font-mono text-red-600 dark:text-red-400">
                -{diffOutline.totalDeletions}
              </span>
              {diffOutline.totalHunks > 0 && (
                <span>{t("hunkCount", { count: diffOutline.totalHunks })}</span>
              )}
              {allHunks.length > 0 && (
                <div className="ml-auto flex items-center gap-1">
                  <button
                    type="button"
                    onClick={handlePrevHunk}
                    disabled={activeHunkIndex <= 0}
                    className="rounded border border-border bg-background px-2 py-0.5 text-[10px] disabled:opacity-40 hover:bg-muted transition-colors inline-flex items-center gap-1"
                  >
                    <ChevronRight className="h-3 w-3 rotate-180" />
                    {t("prev")}
                  </button>
                  <button
                    type="button"
                    onClick={handleNextHunk}
                    disabled={
                      activeHunkIndex < 0 ||
                      activeHunkIndex >= allHunks.length - 1
                    }
                    className="rounded border border-border bg-background px-2 py-0.5 text-[10px] disabled:opacity-40 hover:bg-muted transition-colors inline-flex items-center gap-1"
                  >
                    {t("next")}
                    <ChevronRight className="h-3 w-3" />
                  </button>
                </div>
              )}
            </div>
            <div className="px-2 pb-2 space-y-1 max-h-52 overflow-y-auto">
              {diffOutline.files.map((file) => {
                const fileCollapsed = Boolean(
                  collapsedFiles[`${activeScope}:${file.key}`]
                )
                return (
                  <div
                    key={file.key}
                    className="rounded-md border border-border/80 bg-background/80"
                  >
                    <button
                      type="button"
                      onClick={() => toggleFileCollapsed(file.key)}
                      className="w-full px-2 py-1.5 text-[11px] flex items-center gap-1 hover:bg-muted/60 transition-colors"
                    >
                      <ChevronRight
                        className={`h-3 w-3 shrink-0 transition-transform ${
                          fileCollapsed ? "" : "rotate-90"
                        }`}
                      />
                      <span
                        className="font-mono text-left truncate"
                        title={file.path}
                        onClick={(event) => {
                          event.stopPropagation()
                          jumpToLine(file.startLine)
                        }}
                      >
                        {file.path}
                      </span>
                      <span className="ml-auto shrink-0 flex items-center gap-2 text-[10px]">
                        <span className="font-mono text-green-600 dark:text-green-400">
                          +{file.additions}
                        </span>
                        <span className="font-mono text-red-600 dark:text-red-400">
                          -{file.deletions}
                        </span>
                        <span>{file.hunks.length}h</span>
                      </span>
                    </button>
                    {!fileCollapsed && file.hunks.length > 0 && (
                      <div className="px-2 pb-2 space-y-1">
                        {file.hunks.map((hunk) => {
                          const hunkCollapsed = Boolean(
                            collapsedHunks[`${activeScope}:${hunk.key}`]
                          )
                          const isActive =
                            activeHunkIndex >= 0 &&
                            allHunks[activeHunkIndex]?.key === hunk.key

                          return (
                            <div
                              key={hunk.key}
                              className={`flex items-center gap-1 rounded border px-1.5 py-1 text-[10px] ${
                                isActive
                                  ? "border-primary/50 bg-primary/10"
                                  : "border-border/70 bg-muted/30"
                              }`}
                            >
                              <button
                                type="button"
                                onClick={() => toggleHunkCollapsed(hunk.key)}
                                className="inline-flex items-center gap-1 min-w-0 flex-1 text-left hover:opacity-80"
                                title={hunk.header}
                              >
                                <ChevronDown
                                  className={`h-3 w-3 shrink-0 transition-transform ${
                                    hunkCollapsed ? "-rotate-90" : ""
                                  }`}
                                />
                                <span className="font-mono truncate">
                                  {hunk.header}
                                </span>
                              </button>
                              <button
                                type="button"
                                onClick={() => jumpToLine(hunk.startLine)}
                                className="shrink-0 rounded border border-border bg-background px-1.5 py-0.5 hover:bg-muted transition-colors"
                                title={t("jumpToLine", {
                                  line: hunk.startLine,
                                })}
                              >
                                L{hunk.startLine}
                              </button>
                            </div>
                          )
                        })}
                      </div>
                    )}
                  </div>
                )
              })}
              {diffOutline.files.length === 0 && (
                <div className="text-[11px] text-muted-foreground px-1 py-0.5">
                  {t("noParsedDiffSections")}
                </div>
              )}
            </div>
          </div>
        )}
        <div className="flex-1 min-h-0">
          {activeFileTab.loading ? (
            <div className="h-full flex items-center justify-center text-xs text-muted-foreground">
              {t("loadingEditor")}
            </div>
          ) : (
            <MonacoEditor
              beforeMount={defineMonacoThemes}
              onMount={handleEditorMount}
              path={buildMonacoModelPath(activeFileTab.path, activeFileTab.id)}
              value={renderedContent}
              onChange={(value) => {
                if (!isFileTab) return
                updateActiveFileContent(value ?? "")
              }}
              language={activeFileTab.language}
              theme={editorTheme}
              loading={
                <div className="h-full flex items-center justify-center text-xs text-muted-foreground">
                  {t("loadingEditor")}
                </div>
              }
              options={{
                readOnly: !canEdit,
                minimap: { enabled: false },
                automaticLayout: true,
                fontSize: 13,
                lineNumbersMinChars,
                lineDecorationsWidth: 10,
                wordWrap: "off",
                scrollBeyondLastLine: false,
                scrollBeyondLastColumn: 8,
                renderLineHighlight: "line",
                scrollbar: {
                  horizontal: "auto",
                },
              }}
            />
          )}
        </div>
      </div>
    </div>
  )
}
