import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
import '@/lib/monaco-setup'
import { DiffEditor, type Monaco } from '@monaco-editor/react'
import { Loader2 } from 'lucide-react'
import { registerHiveTheme, HIVE_THEME_NAME } from '@/lib/monaco-theme'
import { clampMonacoLineNumber, parseHunks, getMonacoLanguage } from '@/lib/diff-utils'
import type { Hunk } from '@/lib/diff-utils'
import { MonacoDiffToolbar } from './MonacoDiffToolbar'
import type { MonacoDiffViewMode } from './MonacoDiffToolbar'
import { HunkActionGutter } from './HunkActionGutter'
import { PrCommentGutter } from './PrCommentGutter'
import { DiffCommentGutter } from './DiffCommentGutter'
import { usePRReviewStore } from '@/stores/usePRReviewStore'
import { diffCommentFileKey, useDiffCommentStore } from '@/stores/useDiffCommentStore'
import { toast } from '@/lib/toast'
import type { DiffComment, PRReviewComment } from '@shared/types/git'
import type { editor } from 'monaco-editor'
import { useI18n } from '@/i18n/useI18n'

interface MonacoDiffViewProps {
  worktreePath: string
  worktreeId?: string
  filePath: string
  fileName: string
  staged: boolean
  isUntracked: boolean
  isNewFile?: boolean
  compareBranch?: string
  scrollToLine?: number
  scrollTrigger?: number
  prReviewWorktreeId?: string
  onClose: () => void
}

const EMPTY_COMMENTS: PRReviewComment[] = []
const EMPTY_DIFF_COMMENTS: DiffComment[] = []

export default function MonacoDiffView({
  worktreePath,
  worktreeId,
  filePath,
  fileName,
  staged,
  isUntracked,
  compareBranch,
  scrollToLine,
  scrollTrigger,
  prReviewWorktreeId,
  onClose
}: MonacoDiffViewProps): React.JSX.Element {
  const { t } = useI18n()
  const [originalContent, setOriginalContent] = useState<string | null>(null)
  const [modifiedContent, setModifiedContent] = useState<string | null>(null)
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  // PR review diffs default to inline mode so comment view zones render naturally.
  const [viewMode, setViewMode] = useState<MonacoDiffViewMode>(
    prReviewWorktreeId ? 'inline' : 'split'
  )
  const [hunks, setHunks] = useState<Hunk[]>([])
  const [refreshKey, setRefreshKey] = useState(0)
  const [draftCommentLine, setDraftCommentLine] = useState<number | null>(null)
  const isInitialLoad = useRef(true)
  const recentActionRef = useRef(false)

  const diffEditorRef = useRef<editor.IStandaloneDiffEditor | null>(null)
  const modifiedEditorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
  const [editorReady, setEditorReady] = useState(false)
  const [zonesReady, setZonesReady] = useState(!prReviewWorktreeId)

  // PR review comments for this file (when in PR review mode)
  const allPrComments = usePRReviewStore(
    (s) => (prReviewWorktreeId ? s.comments.get(prReviewWorktreeId) : undefined) ?? EMPTY_COMMENTS
  )
  const fileComments = useMemo(
    () => (prReviewWorktreeId ? allPrComments.filter((c) => c.path === filePath) : EMPTY_COMMENTS),
    [allPrComments, filePath, prReviewWorktreeId]
  )
  const diffCommentScope = useMemo(
    () => ({ staged, compareBranch: compareBranch ?? null }),
    [compareBranch, staged]
  )
  const diffCommentsKey = worktreeId
    ? diffCommentFileKey(worktreeId, filePath, diffCommentScope)
    : null
  const diffComments = useDiffCommentStore(
    (s) =>
      (diffCommentsKey ? s.commentsByFile.get(diffCommentsKey) : undefined) ?? EMPTY_DIFF_COMMENTS
  )
  const loadDiffComments = useDiffCommentStore((s) => s.loadComments)
  const createDiffComment = useDiffCommentStore((s) => s.createComment)
  const updateDiffComment = useDiffCommentStore((s) => s.updateComment)
  const deleteDiffComment = useDiffCommentStore((s) => s.deleteComment)
  const attachDiffComment = useDiffCommentStore((s) => s.attachComment)

  useEffect(() => {
    if (worktreeId) {
      loadDiffComments(worktreeId, filePath, diffCommentScope)
    }
  }, [worktreeId, filePath, diffCommentScope, loadDiffComments])

  // Fetch file contents for the diff
  const fetchContent = useCallback(async () => {
    // Only show loading spinner on initial mount, not on refresh
    if (isInitialLoad.current) {
      setIsLoading(true)
    }
    setError(null)

    try {
      if (compareBranch) {
        // Branch diff: original = branch ref, modified = working tree
        const [origResult, modResult] = await Promise.all([
          window.gitOps.getBranchBaseContent(worktreePath, compareBranch, filePath),
          window.gitOps.getFileContent(worktreePath, filePath)
        ])

        // File added (doesn't exist in branch) — empty original
        setOriginalContent(origResult.success ? (origResult.content ?? '') : '')
        // File deleted (doesn't exist in working tree) — empty modified
        setModifiedContent(modResult.success ? (modResult.content ?? '') : '')
      } else if (staged) {
        // Staged diff: original = HEAD, modified = Index (staged)
        const [origResult, modResult] = await Promise.all([
          window.gitOps.getRefContent(worktreePath, 'HEAD', filePath),
          window.gitOps.getRefContent(worktreePath, '', filePath)
        ])

        if (!origResult.success && !origResult.error?.includes('does not exist')) {
          setError(origResult.error || t('diffUi.errors.loadHeadVersion'))
          return
        }
        if (!modResult.success) {
          setError(modResult.error || t('diffUi.errors.loadStagedVersion'))
          return
        }

        setOriginalContent(origResult.content ?? '')
        setModifiedContent(modResult.content ?? '')
      } else {
        // Unstaged diff: original = Index (or HEAD if nothing staged), modified = Working tree
        const [origResult, modResult] = await Promise.all([
          window.gitOps
            .getRefContent(worktreePath, '', filePath)
            .catch(() => window.gitOps.getRefContent(worktreePath, 'HEAD', filePath)),
          window.gitOps.getFileContent(worktreePath, filePath)
        ])

        if (!origResult.success && !origResult.error?.includes('does not exist')) {
          setError(origResult.error || t('diffUi.errors.loadOriginalVersion'))
          return
        }
        if (!modResult.success) {
          setError(modResult.error || t('diffUi.errors.loadFileContent'))
          return
        }

        setOriginalContent(origResult.content ?? '')
        setModifiedContent(modResult.content ?? '')
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : t('diffUi.errors.loadDiffContent'))
    } finally {
      setIsLoading(false)
      isInitialLoad.current = false
    }
  }, [worktreePath, filePath, staged, compareBranch, t])

  // Fetch on mount and when refresh is triggered
  useEffect(() => {
    fetchContent()
  }, [fetchContent, refreshKey])

  // Listen for external file changes (but skip if we just did a manual action)
  useEffect(() => {
    const cleanup = window.gitOps.onStatusChanged((event) => {
      if (event.worktreePath === worktreePath && !recentActionRef.current) {
        setRefreshKey((k) => k + 1)
      }
    })
    return cleanup
  }, [worktreePath])

  // Handle Monaco mount
  const handleEditorDidMount = useCallback((diffEd: editor.IStandaloneDiffEditor) => {
    diffEditorRef.current = diffEd
    modifiedEditorRef.current = diffEd.getModifiedEditor()

    // Get initial diff changes
    const changes = diffEd.getLineChanges()
    setHunks(parseHunks(changes))

    // Listen for diff updates
    diffEd.onDidUpdateDiff(() => {
      const newChanges = diffEd.getLineChanges()
      setHunks(parseHunks(newChanges))
    })

    // Signal that the editor is mounted and ready for scrolling
    setEditorReady(true)
  }, [])

  // Auto-scroll to the target line (e.g. when navigating from a PR comment).
  // Waits for: editor mounted (editorReady), content loaded (!isLoading),
  // and view zones created + sized (zonesReady — signalled by PrCommentGutter).
  // scrollTrigger changes on every navigation so re-clicking the same comment
  // (same scrollToLine value) still triggers a scroll.
  useEffect(() => {
    if (!scrollToLine || !editorReady || isLoading || !zonesReady) return
    const modEditor = modifiedEditorRef.current
    if (!modEditor) return

    const targetLine = clampMonacoLineNumber(scrollToLine, modEditor)
    modEditor.revealLineInCenter(targetLine)
    modEditor.setPosition({ lineNumber: targetLine, column: 1 })
  }, [scrollToLine, scrollTrigger, editorReady, isLoading, zonesReady])

  // Register theme before Monaco loads
  const handleBeforeMount = useCallback((monaco: Monaco) => {
    registerHiveTheme(monaco)
  }, [])

  // Hunk navigation — scroll to next/prev hunk in the modified editor
  const handleNextHunk = useCallback(() => {
    const modEditor = modifiedEditorRef.current
    if (!modEditor || hunks.length === 0) return
    const currentLine = modEditor.getPosition()?.lineNumber ?? 0
    const next = hunks.find((h) => h.modifiedStartLine > currentLine)
    const target = next ?? hunks[0] // wrap around
    const targetLine = clampMonacoLineNumber(target.modifiedStartLine, modEditor)
    modEditor.revealLineInCenter(targetLine)
    modEditor.setPosition({ lineNumber: targetLine, column: 1 })
  }, [hunks])

  const handlePrevHunk = useCallback(() => {
    const modEditor = modifiedEditorRef.current
    if (!modEditor || hunks.length === 0) return
    const currentLine = modEditor.getPosition()?.lineNumber ?? Infinity
    const prev = [...hunks].reverse().find((h) => h.modifiedStartLine < currentLine)
    const target = prev ?? hunks[hunks.length - 1] // wrap around
    const targetLine = clampMonacoLineNumber(target.modifiedStartLine, modEditor)
    modEditor.revealLineInCenter(targetLine)
    modEditor.setPosition({ lineNumber: targetLine, column: 1 })
  }, [hunks])

  // Keyboard shortcuts
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent): void => {
      if (e.key === 'Escape') {
        e.preventDefault()
        onClose()
      } else if (e.altKey && e.key === 'ArrowDown') {
        e.preventDefault()
        handleNextHunk()
      } else if (e.altKey && e.key === 'ArrowUp') {
        e.preventDefault()
        handlePrevHunk()
      }
    }
    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [onClose, handleNextHunk, handlePrevHunk])

  // Copy diff content
  const handleCopy = useCallback(async () => {
    if (compareBranch) {
      const result = await window.gitOps.getBranchFileDiff(worktreePath, compareBranch, filePath)
      if (result.success && result.diff) {
        await window.projectOps.copyToClipboard(result.diff)
      }
    } else {
      // Get the unified diff via existing IPC
      const result = await window.gitOps.getDiff(worktreePath, filePath, staged, isUntracked)
      if (result.success && result.diff) {
        await window.projectOps.copyToClipboard(result.diff)
      }
    }
  }, [worktreePath, filePath, staged, isUntracked, compareBranch])

  const handleAddCommentAtCurrentLine = useCallback(() => {
    const modEditor = modifiedEditorRef.current
    const currentLine =
      modEditor?.getPosition()?.lineNumber ?? hunks[0]?.modifiedStartLine ?? scrollToLine ?? 1
    const targetLine = clampMonacoLineNumber(currentLine, modEditor)
    setDraftCommentLine(targetLine)
    window.requestAnimationFrame(() => {
      if (modEditor) {
        modEditor.revealLineInCenter(targetLine)
      }
    })
  }, [hunks, scrollToLine])

  const handleCreateDiffComment = useCallback(
    async (lineNumber: number, body: string) => {
      if (!worktreeId) return
      await createDiffComment({
        worktreeId,
        filePath,
        side: 'modified',
        lineNumber,
        compareBranch: compareBranch ?? null,
        staged,
        body
      })
      setDraftCommentLine(null)
      toast.success(t('diffComments.toasts.created'))
    },
    [worktreeId, filePath, compareBranch, staged, createDiffComment, t]
  )

  const handleUpdateDiffComment = useCallback(
    async (id: string, body: string) => {
      await updateDiffComment(id, { body })
      toast.success(t('diffComments.toasts.updated'))
    },
    [updateDiffComment, t]
  )

  const handleDeleteDiffComment = useCallback(
    async (id: string) => {
      await deleteDiffComment(id)
      toast.success(t('diffComments.toasts.deleted'))
    },
    [deleteDiffComment, t]
  )

  const handleToggleDiffCommentResolved = useCallback(
    async (comment: DiffComment) => {
      await updateDiffComment(comment.id, { resolved: !comment.resolved })
    },
    [updateDiffComment]
  )

  const handleAttachDiffComment = useCallback(
    (comment: DiffComment) => {
      attachDiffComment(comment)
      toast.success(t('diffComments.toasts.attached'))
    },
    [attachDiffComment, t]
  )

  // Trigger re-fetch after hunk actions — suppress watcher duplicate for 500ms
  const handleContentChanged = useCallback(() => {
    recentActionRef.current = true
    setRefreshKey((k) => k + 1)
    setTimeout(() => {
      recentActionRef.current = false
    }, 500)
  }, [])

  const language = getMonacoLanguage(filePath)
  const renderSideBySide = viewMode === 'split'
  const hideUnchangedRegions = viewMode === 'hunk'
  const canUseDiffComments = !!worktreeId

  if (isLoading) {
    return (
      <div className="flex-1 flex flex-col min-h-0" data-testid="monaco-diff-view">
        <MonacoDiffToolbar
          fileName={fileName}
          staged={staged}
          isUntracked={isUntracked}
          compareBranch={compareBranch}
          viewMode={viewMode}
          onViewModeChange={setViewMode}
          commentCount={diffComments.length}
          onAddComment={canUseDiffComments ? handleAddCommentAtCurrentLine : undefined}
          onPrevHunk={handlePrevHunk}
          onNextHunk={handleNextHunk}
          onCopy={handleCopy}
          onClose={onClose}
        />
        <div className="flex-1 flex items-center justify-center" data-testid="monaco-diff-loading">
          <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
        </div>
      </div>
    )
  }

  if (error) {
    return (
      <div className="flex-1 flex flex-col min-h-0" data-testid="monaco-diff-view">
        <MonacoDiffToolbar
          fileName={fileName}
          staged={staged}
          isUntracked={isUntracked}
          compareBranch={compareBranch}
          viewMode={viewMode}
          onViewModeChange={setViewMode}
          commentCount={diffComments.length}
          onAddComment={canUseDiffComments ? handleAddCommentAtCurrentLine : undefined}
          onPrevHunk={handlePrevHunk}
          onNextHunk={handleNextHunk}
          onCopy={handleCopy}
          onClose={onClose}
        />
        <div
          className="flex-1 flex items-center justify-center text-destructive"
          data-testid="monaco-diff-error"
        >
          {error}
        </div>
      </div>
    )
  }

  return (
    <div className="flex-1 flex flex-col min-h-0" data-testid="monaco-diff-view">
      <MonacoDiffToolbar
        fileName={fileName}
        staged={staged}
        isUntracked={isUntracked}
        compareBranch={compareBranch}
        viewMode={viewMode}
        onViewModeChange={setViewMode}
        commentCount={diffComments.length}
        onAddComment={canUseDiffComments ? handleAddCommentAtCurrentLine : undefined}
        onPrevHunk={handlePrevHunk}
        onNextHunk={handleNextHunk}
        onCopy={handleCopy}
        onClose={onClose}
      />
      <div className="flex-1 relative min-h-0">
        <DiffEditor
          original={originalContent ?? ''}
          modified={modifiedContent ?? ''}
          language={language}
          theme={HIVE_THEME_NAME}
          onMount={handleEditorDidMount}
          beforeMount={handleBeforeMount}
          options={{
            readOnly: true,
            originalEditable: false,
            renderSideBySide,
            enableSplitViewResizing: true,
            ignoreTrimWhitespace: false,
            renderIndicators: true,
            renderMarginRevertIcon: false,
            diffAlgorithm: 'advanced',
            hideUnchangedRegions: {
              enabled: hideUnchangedRegions,
              contextLineCount: 3,
              minimumLineCount: 4,
              revealLineCount: 16
            },
            minimap: { enabled: false },
            scrollBeyondLastLine: false,
            fontSize: 12,
            lineHeight: 20,
            fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
            automaticLayout: true,
            scrollbar: {
              verticalScrollbarSize: 10,
              horizontalScrollbarSize: 10
            }
          }}
          loading={
            <div className="flex items-center justify-center h-full">
              <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
            </div>
          }
        />
        {!compareBranch && originalContent !== null && modifiedContent !== null && (
          <HunkActionGutter
            hunks={hunks}
            staged={staged}
            worktreePath={worktreePath}
            filePath={filePath}
            originalContent={originalContent}
            modifiedContent={modifiedContent}
            modifiedEditor={modifiedEditorRef.current}
            onContentChanged={handleContentChanged}
          />
        )}
        {prReviewWorktreeId &&
          fileComments.length > 0 &&
          originalContent !== null &&
          modifiedContent !== null && (
            <PrCommentGutter
              comments={fileComments}
              modifiedEditor={modifiedEditorRef.current}
              highlightLine={scrollToLine}
              onZonesReady={() => setZonesReady(true)}
            />
          )}
        {canUseDiffComments && originalContent !== null && modifiedContent !== null && (
          <DiffCommentGutter
            comments={diffComments}
            modifiedEditor={modifiedEditorRef.current}
            draftLineNumber={draftCommentLine}
            onCancelDraft={() => setDraftCommentLine(null)}
            onCreate={handleCreateDiffComment}
            onUpdate={handleUpdateDiffComment}
            onDelete={handleDeleteDiffComment}
            onToggleResolved={handleToggleDiffCommentResolved}
            onAttach={handleAttachDiffComment}
          />
        )}
      </div>
    </div>
  )
}
