import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ChevronDown, ChevronRight, Columns2, GitBranch, ListChevronsDownUp, ListChevronsUpDown, AlignJustify, Pilcrow, RefreshCw, Trash2, WrapText } from 'lucide-react';
import { codeToHtml } from 'shiki';
import type { GitDiffFile, GitDiffMode, GitDiffResult, GitDiscardFileInfo } from '@/api/types';
import { cn } from '@/lib/utils';
import { DiffSplitView } from './DiffSplitView';
import { InlineReviewEditor } from './InlineReviewEditor';
import { InlineReviewThread } from './InlineReviewThread';
import { useDiffComments } from '@/stores/useDiffCommentStore';

interface WriteProps {
  mutating: boolean;
  mutateError: string | null;
  onStage: (files: string[]) => Promise<boolean>;
  onUnstage: (files: string[]) => Promise<boolean>;
  onCommit: (message: string, files?: string[]) => Promise<boolean>;
  onDiscard: (files: GitDiscardFileInfo[]) => Promise<boolean>;
}

interface Props extends WriteProps {
  result: GitDiffResult | null;
  loading: boolean;
  error: string | null;
  mode: GitDiffMode;
  sessionId?: string;
  onSelectMode: (m: GitDiffMode) => void;
  onRefresh: () => void;
  /** FIX 4: non-null when the repo's dirty state suggests a different mode than the pinned one. */
  modeSuggestion?: GitDiffMode | null;
  /** FIX 5: pending-changes count from the Coder pane — shown in empty state as a hint. */
  pendingCount?: number;
  layout: 'unified' | 'split';
  wrapLines: boolean;
  hideWhitespace: boolean;
  onLayoutChange: (layout: 'unified' | 'split') => void;
  onWrapLinesChange: (wrap: boolean) => void;
  onHideWhitespaceChange: (hide: boolean) => void;
}

const CHANGE_TYPE_LABELS: Record<string, string> = {
  added: 'A',
  modified: 'M',
  deleted: 'D',
  renamed: 'R',
  untracked: '?',
};

const CHANGE_TYPE_COLORS: Record<string, string> = {
  added: 'text-green-500',
  modified: 'text-yellow-500',
  deleted: 'text-red-500',
  renamed: 'text-blue-500',
  untracked: 'text-muted-foreground',
};

interface DiscardConfirmState {
  file: GitDiffFile;
}

function DiscardConfirmDialog({
  state,
  onConfirm,
  onCancel,
}: {
  state: DiscardConfirmState;
  onConfirm: () => void;
  onCancel: () => void;
}) {
  const isUntracked = state.file.change_type === 'untracked';
  return (
    <div
      role="dialog"
      aria-modal="true"
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4"
    >
      <div className="bg-popover border rounded-lg shadow-lg max-w-sm w-full p-4 flex flex-col gap-3">
        <p className="text-sm font-medium">
          {isUntracked ? 'Permanently delete file?' : 'Discard changes?'}
        </p>
        <p className="text-xs text-muted-foreground">
          {isUntracked
            ? `${state.file.path} will be permanently deleted. This cannot be undone.`
            : `Changes to ${state.file.path} will be reverted to the last commit. This cannot be undone.`}
        </p>
        <div className="flex gap-2 justify-end">
          <button
            type="button"
            onClick={onCancel}
            className="text-xs px-3 py-1.5 rounded border hover:bg-muted max-md:min-h-[44px] max-md:min-w-[44px]"
          >
            Cancel
          </button>
          <button
            type="button"
            onClick={onConfirm}
            className="text-xs px-3 py-1.5 rounded bg-destructive text-destructive-foreground hover:bg-destructive/90 max-md:min-h-[44px] max-md:min-w-[44px]"
          >
            {isUntracked ? 'Delete' : 'Discard'}
          </button>
        </div>
      </div>
    </div>
  );
}

function FileDiffRow({
  file,
  uncommitted,
  disabled,
  onStage,
  onUnstage,
  onDiscardRequest,
  layout,
  wrapLines,
  expanded,
  onToggleExpand,
  sessionId,
  diffMode,
}: {
  file: GitDiffFile;
  uncommitted: boolean;
  disabled: boolean;
  onStage: (path: string) => void;
  onUnstage: (path: string) => void;
  onDiscardRequest: (file: GitDiffFile) => void;
  layout: 'unified' | 'split';
  wrapLines: boolean;
  expanded: boolean;
  onToggleExpand: (path: string) => void;
  sessionId?: string;
  diffMode?: string;
}) {
  const [html, setHtml] = useState<string | null>(null);
  const [highlighting, setHighlighting] = useState(false);
  const highlightRef = useRef<HTMLDivElement | null>(null);
  const [showEditor, setShowEditor] = useState(false);
  const commentKey = `${file.path}:${file.change_type}`;
  const diffModeVal = diffMode ?? '';
  const { comments, addComment, updateComment, deleteComment } = useDiffComments(sessionId ?? '', diffModeVal);
  const fileComments = comments.get(commentKey) ?? [];

  useEffect(() => {
    if (!expanded || !file.diff_body) return;
    if (html !== null) return;
    let cancelled = false;
    setHighlighting(true);
    void codeToHtml(file.diff_body, { lang: 'diff', theme: 'github-dark' })
      .then((result) => { if (!cancelled) setHtml(result); })
      .catch(() => { if (!cancelled) setHtml(null); })
      .finally(() => { if (!cancelled) setHighlighting(false); });
    return () => { cancelled = true; };
  }, [expanded, file.diff_body, html]);

  useEffect(() => {
    if (highlightRef.current && html !== null) {
      // Shiki generates sanitized HTML — not user-supplied content.
      // eslint-disable-next-line no-unsanitized/property
      highlightRef.current.innerHTML = html;
    }
  }, [html]);

  const typeLabel = CHANGE_TYPE_LABELS[file.change_type] ?? '?';
  const typeColor = CHANGE_TYPE_COLORS[file.change_type] ?? 'text-muted-foreground';
  const displayPath = file.old_path ? `${file.old_path} → ${file.path}` : file.path;

  const handleAddComment = (body: string) => {
    const comment = { id: crypto.randomUUID(), body, createdAt: Date.now(), updatedAt: Date.now() };
    addComment(commentKey, comment);
    setShowEditor(false);
  };

  const handleEditComment = (id: string, body: string) => {
    updateComment(commentKey, id, body);
  };

  const handleDeleteComment = (id: string) => {
    deleteComment(commentKey, id);
  };

  return (
    <li className="border-b border-border/30 last:border-0">
      <div className="flex items-center group">
        <button
          type="button"
          className="flex-1 flex items-center gap-1.5 px-2 py-1.5 text-xs hover:bg-muted/40 text-left max-md:min-h-[44px] min-w-0"
          onClick={() => onToggleExpand(file.path)}
          aria-expanded={expanded}
        >
          {expanded
            ? <ChevronDown size={10} className="shrink-0 text-muted-foreground" />
            : <ChevronRight size={10} className="shrink-0 text-muted-foreground" />}
          <span className={cn('font-mono font-bold w-3 shrink-0', typeColor)}>{typeLabel}</span>
          <span className="truncate flex-1">{displayPath}</span>
          {(file.added_lines > 0 || file.removed_lines > 0) && (
            <span className="shrink-0 text-muted-foreground/70 font-mono text-[10px]">
              {file.added_lines > 0 && <span className="text-green-500">+{file.added_lines}</span>}
              {file.added_lines > 0 && file.removed_lines > 0 && <span className="mx-0.5">/</span>}
              {file.removed_lines > 0 && <span className="text-red-500">-{file.removed_lines}</span>}
            </span>
          )}
          {file.staged && (
            <span className="shrink-0 text-[10px] bg-blue-500/15 text-blue-400 px-1 rounded">staged</span>
          )}
        </button>

        {/* Write affordances — Uncommitted mode only */}
        {uncommitted && (
          <div className="flex items-center gap-0.5 px-1 shrink-0">
            {/* Stage / Unstage toggle */}
            {file.change_type !== 'deleted' && (
              <button
                type="button"
                disabled={disabled}
                onClick={() => file.staged ? onUnstage(file.path) : onStage(file.path)}
                className="text-[10px] px-1.5 py-0.5 rounded border border-border/50 hover:bg-muted disabled:opacity-40 max-md:min-h-[44px] max-md:min-w-[44px]"
                title={file.staged ? 'Unstage' : 'Stage'}
              >
                {file.staged ? '−' : '+'}
              </button>
            )}
            {/* Discard — separated secondary affordance */}
            <button
              type="button"
              disabled={disabled}
              onClick={() => onDiscardRequest(file)}
              className="p-1 rounded hover:bg-destructive/15 hover:text-destructive text-muted-foreground/50 disabled:opacity-40 max-md:min-h-[44px] max-md:min-w-[44px]"
              title={file.change_type === 'untracked' ? 'Delete file' : 'Discard changes'}
            >
              <Trash2 size={10} />
            </button>
          </div>
        )}
      </div>

      {expanded && (
        <div className="px-2 pb-2">
          {file.is_binary && (
            <p className="text-xs text-muted-foreground italic px-2 py-1">Binary file</p>
          )}
          {file.is_too_large && (
            <p className="text-xs text-muted-foreground italic px-2 py-1">Diff too large to display</p>
          )}
          {file.change_type === 'untracked' && (
            <p className="text-xs text-muted-foreground italic px-2 py-1">Untracked — not yet staged</p>
          )}
          {!file.is_binary && !file.is_too_large && file.diff_body && (
            layout === 'split' ? (
              <DiffSplitView file={file} wrapLines={wrapLines} />
            ) : (
              <>
                {highlighting && (
                  <p className="text-xs text-muted-foreground px-2 py-1">Highlighting…</p>
                )}
                {!highlighting && html !== null ? (
                  <div
                    ref={highlightRef}
                    className="text-[11px] overflow-x-auto rounded bg-[#0d1117] [&_pre]:!p-2 [&_pre]:!m-0 [&_pre]:overflow-x-auto"
                  />
                ) : (
                  !highlighting && (
                    <pre className={cn(
                      'text-[11px] overflow-x-auto rounded bg-muted/30 p-2',
                      wrapLines ? 'whitespace-pre-wrap break-all' : 'whitespace-pre',
                    )}>
                      {file.diff_body}
                    </pre>
                  )
                )}
                {/* Comment button */}
                <div className="flex items-center gap-1 mt-1">
                  <button
                    type="button"
                    onClick={() => setShowEditor(!showEditor)}
                    className="text-[10px] text-muted-foreground hover:text-foreground flex items-center gap-0.5 px-1 py-0.5 rounded hover:bg-muted/40"
                  >
                    <span>{showEditor ? 'Cancel' : 'Comment'}</span>
                  </button>
                  <span className="text-[10px] text-muted-foreground/50">
                    {fileComments.length > 0 && `${fileComments.length} comment${fileComments.length > 1 ? 's' : ''}`}
                  </span>
                </div>
                {showEditor && (
                  <InlineReviewEditor
                    onSave={handleAddComment}
                    onCancel={() => setShowEditor(false)}
                  />
                )}
                <InlineReviewThread
                  comments={fileComments}
                  onEditComment={handleEditComment}
                  onDeleteComment={handleDeleteComment}
                />
              </>
            )
          )}
        </div>
      )}
    </li>
  );
}

export function GitDiffView({
  result,
  loading,
  error,
  mode,
  onSelectMode,
  onRefresh,
  mutating,
  mutateError,
  onStage,
  onUnstage,
  onCommit,
  onDiscard,
  modeSuggestion,
  pendingCount,
  layout,
  wrapLines,
  hideWhitespace,
  onLayoutChange,
  onWrapLinesChange,
  onHideWhitespaceChange,
  sessionId,
}: Props) {
  const [commitMessage, setCommitMessage] = useState('');
  const [discardTarget, setDiscardTarget] = useState<DiscardConfirmState | null>(null);
  const [lastAction, setLastAction] = useState<string | null>(null);
  const lastActionTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());

  const allExpandedComputed = useMemo(
    () => result !== null && (result.files?.length ?? 0) > 0 && result.files.every((f) => expandedFiles.has(f.path)),
    [result, expandedFiles],
  );

  const handleExpandAllChange = useCallback((expand: boolean) => {
    if (expand && result?.files) {
      setExpandedFiles(new Set(result.files.map((f) => f.path)));
    } else {
      setExpandedFiles(new Set());
    }
  }, [result?.files]);

  const handleToggleExpand = useCallback((path: string) => {
    setExpandedFiles((prev) => {
      const next = new Set(prev);
      if (next.has(path)) next.delete(path);
      else next.add(path);
      return next;
    });
  }, []);

  function flashAction(msg: string) {
    setLastAction(msg);
    if (lastActionTimer.current) clearTimeout(lastActionTimer.current);
    lastActionTimer.current = setTimeout(() => setLastAction(null), 2000);
  }

  const uncommitted = mode === 'uncommitted';
  const inProgress = result?.in_progress_op ?? null;
  const writeDisabled = mutating || !!inProgress;
  const stagedFiles = result?.files.filter((f) => f.staged) ?? [];
  const canDoCommit = uncommitted && stagedFiles.length > 0 && commitMessage.trim().length > 0 && !writeDisabled;

  async function handleStage(path: string) {
    const ok = await onStage([path]);
    if (ok) flashAction('Staged');
  }

  async function handleUnstage(path: string) {
    const ok = await onUnstage([path]);
    if (ok) flashAction('Unstaged');
  }

  function handleDiscardRequest(file: GitDiffFile) {
    setDiscardTarget({ file });
  }

  async function handleDiscardConfirm() {
    if (!discardTarget) return;
    const { file } = discardTarget;
    setDiscardTarget(null);
    const info: GitDiscardFileInfo = {
      path: file.path,
      change_type: file.change_type,
      staged: file.staged,
    };
    const ok = await onDiscard([info]);
    if (ok) flashAction(file.change_type === 'untracked' ? 'Deleted' : 'Discarded');
  }

  async function handleCommit() {
    const msg = commitMessage.trim();
    if (!msg) return;
    const ok = await onCommit(msg);
    if (ok) {
      setCommitMessage('');
      flashAction('Committed');
    }
  }

  if (loading && !result) {
    return (
      <div className="flex-1 flex items-center justify-center text-xs text-muted-foreground">
        Loading diff…
      </div>
    );
  }

  if (error) {
    return (
      <div className="flex-1 flex flex-col items-center justify-center gap-2 px-4 text-center">
        <p className="text-xs text-destructive">{error}</p>
        <button
          type="button"
          onClick={onRefresh}
          className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 max-md:min-h-[44px]"
        >
          <RefreshCw size={12} />
          Refresh
        </button>
      </div>
    );
  }

  if (!result || !result.git_repo) {
    return (
      <div className="flex-1 flex items-center justify-center text-xs text-muted-foreground px-4 text-center">
        Not a git repository
      </div>
    );
  }

  const { files, base_label } = result;

  return (
    <div className="flex flex-col flex-1 overflow-hidden">
      {/* Mode selector */}
      <div className="flex items-center gap-1 px-2 py-1.5 border-b shrink-0">
        <button
          type="button"
          onClick={() => onSelectMode('uncommitted')}
          className={cn(
            'text-xs px-2 py-0.5 rounded max-md:min-h-[44px]',
            mode === 'uncommitted'
              ? 'bg-muted text-foreground font-medium'
              : 'text-muted-foreground hover:text-foreground',
          )}
        >
          Uncommitted
        </button>
        <button
          type="button"
          onClick={() => onSelectMode('committed')}
          className={cn(
            'text-xs px-2 py-0.5 rounded max-md:min-h-[44px]',
            mode === 'committed'
              ? 'bg-muted text-foreground font-medium'
              : 'text-muted-foreground hover:text-foreground',
          )}
        >
          Committed
        </button>
        <div className="flex-1" />
        {(loading || mutating) && (
          <span className="text-[10px] text-muted-foreground">{mutating ? 'Working…' : 'Refreshing…'}</span>
        )}
        {lastAction && !mutating && (
          <span className="text-[10px] text-green-500">{lastAction}</span>
        )}
        <button
          type="button"
          onClick={onRefresh}
          disabled={loading || mutating}
          className="p-1 rounded hover:bg-muted text-muted-foreground disabled:opacity-40 max-md:min-h-[44px] max-md:min-w-[44px]"
          aria-label="Refresh diff"
          title="Refresh"
        >
          <RefreshCw size={12} />
        </button>
      </div>

      {/* Diff toolbar */}
      <div className="flex items-center gap-1 px-2 py-1 border-b shrink-0">
        <button
          type="button"
          onClick={() => onLayoutChange('unified')}
          className={cn(
            'text-xs px-2 py-0.5 rounded flex items-center gap-1 max-md:min-h-[44px]',
            layout === 'unified'
              ? 'bg-muted text-foreground font-medium'
              : 'text-muted-foreground hover:text-foreground',
          )}
          title="Unified diff"
        >
          <AlignJustify size={12} />
          Unified
        </button>
        <button
          type="button"
          onClick={() => onLayoutChange('split')}
          className={cn(
            'text-xs px-2 py-0.5 rounded flex items-center gap-1 max-md:min-h-[44px]',
            layout === 'split'
              ? 'bg-muted text-foreground font-medium'
              : 'text-muted-foreground hover:text-foreground',
          )}
          title="Split diff"
        >
          <Columns2 size={12} />
          Split
        </button>
        <button
          type="button"
          onClick={() => onHideWhitespaceChange(!hideWhitespace)}
          className={cn(
            'p-1 rounded max-md:min-h-[44px] max-md:min-w-[44px]',
            hideWhitespace
              ? 'bg-muted text-foreground font-medium'
              : 'text-muted-foreground hover:text-foreground',
          )}
          title={hideWhitespace ? 'Show whitespace' : 'Hide whitespace'}
        >
          <Pilcrow size={12} />
        </button>
        <button
          type="button"
          onClick={() => onWrapLinesChange(!wrapLines)}
          className={cn(
            'p-1 rounded max-md:min-h-[44px] max-md:min-w-[44px]',
            wrapLines
              ? 'bg-muted text-foreground font-medium'
              : 'text-muted-foreground hover:text-foreground',
          )}
          title={wrapLines ? 'Unwrap lines' : 'Wrap lines'}
        >
          <WrapText size={12} />
        </button>
        <div className="flex-1" />
        <button
          type="button"
          onClick={() => handleExpandAllChange(!allExpandedComputed)}
          className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground max-md:min-h-[44px] max-md:min-w-[44px]"
          title={allExpandedComputed ? 'Collapse all' : 'Expand all'}
        >
          {allExpandedComputed ? <ListChevronsDownUp size={12} /> : <ListChevronsUpDown size={12} />}
        </button>
        <button
          type="button"
          onClick={onRefresh}
          disabled={loading || mutating}
          className="p-1 rounded hover:bg-muted text-muted-foreground disabled:opacity-40 max-md:min-h-[44px] max-md:min-w-[44px]"
          aria-label="Refresh diff"
          title="Refresh"
        >
          <RefreshCw size={12} />
        </button>
      </div>

      {/* Committed-mode base label */}
      {result.mode === 'committed' && base_label && (
        <div className="px-2 py-1 text-[10px] text-muted-foreground border-b flex items-center gap-1 shrink-0">
          <GitBranch size={10} />
          <span className="truncate">vs {base_label}</span>
        </div>
      )}

      {/* FIX 2: Fallback label — committed was requested but no base branch found */}
      {result.mode === 'uncommitted' && result.base_label && (
        <div className="px-2 py-1 text-[10px] text-amber-600 dark:text-amber-400 border-b flex items-center gap-1 shrink-0">
          <GitBranch size={10} />
          <span className="truncate">{result.base_label}</span>
        </div>
      )}

      {/* FIX 4: Mode suggestion — shown when pinned mode diverges from auto-selected mode */}
      {modeSuggestion && (
        <div className="px-2 py-1 text-[10px] text-muted-foreground border-b shrink-0 flex items-center gap-1">
          <span>Repo is now {modeSuggestion === 'uncommitted' ? 'dirty' : 'clean'} —</span>
          <button
            type="button"
            onClick={() => onSelectMode(modeSuggestion)}
            className="underline hover:text-foreground"
          >
            switch to {modeSuggestion === 'uncommitted' ? 'Uncommitted' : 'Committed'}
          </button>
        </div>
      )}

      {/* In-progress op banner */}
      {inProgress && (
        <div className="px-2 py-1 text-[10px] text-yellow-500 bg-yellow-500/10 border-b shrink-0">
          {inProgress} in progress — write actions disabled
        </div>
      )}

      {/* Mutation error */}
      {mutateError && (
        <div className="px-2 py-1 text-[10px] text-destructive bg-destructive/10 border-b shrink-0 truncate">
          {mutateError}
        </div>
      )}

      {/* File list */}
      <div className="flex-1 overflow-y-auto">
        {files.length === 0 ? (
          <div className="flex flex-col items-center justify-center px-4 py-8 text-xs text-muted-foreground text-center gap-1.5">
            <span>{mode === 'uncommitted' ? 'No uncommitted changes' : 'No changes vs. the base branch'}</span>
            {/* FIX 5: hint when pending changes exist in the Coder pane */}
            {!!pendingCount && (
              <span className="text-[10px]">
                {pendingCount} pending {pendingCount === 1 ? 'change' : 'changes'} visible in the Coder pane
              </span>
            )}
          </div>
        ) : (
          <ul className="list-none">
            {files.map((file) => (
              <FileDiffRow
                key={file.path}
                file={file}
                uncommitted={uncommitted}
                disabled={writeDisabled}
                onStage={handleStage}
                onUnstage={handleUnstage}
                onDiscardRequest={handleDiscardRequest}
                layout={layout}
                wrapLines={wrapLines}
                expanded={expandedFiles.has(file.path)}
                onToggleExpand={handleToggleExpand}
                sessionId={sessionId}
                diffMode={mode}
              />
            ))}
          </ul>
        )}
      </div>

      {/* Commit panel — Uncommitted mode only */}
      {uncommitted && (
        <div className="shrink-0 border-t px-2 py-2 flex flex-col gap-1.5">
          <textarea
            value={commitMessage}
            onChange={(e) => setCommitMessage(e.target.value)}
            disabled={writeDisabled}
            placeholder="Commit message…"
            rows={2}
            className="w-full text-xs rounded border bg-background px-2 py-1 resize-none focus:outline-none focus:ring-1 focus:ring-ring disabled:opacity-40 placeholder:text-muted-foreground"
          />
          <div className="flex items-center gap-1.5">
            <span className="text-[10px] text-muted-foreground flex-1">
              {stagedFiles.length > 0
                ? `${stagedFiles.length} file${stagedFiles.length > 1 ? 's' : ''} staged`
                : 'No files staged'}
            </span>
            <button
              type="button"
              disabled={!canDoCommit}
              onClick={handleCommit}
              className="text-xs px-3 py-1 rounded bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-40 max-md:min-h-[44px]"
            >
              Commit
            </button>
          </div>
        </div>
      )}

      {/* Discard confirmation dialog */}
      {discardTarget && (
        <DiscardConfirmDialog
          state={discardTarget}
          onConfirm={handleDiscardConfirm}
          onCancel={() => setDiscardTarget(null)}
        />
      )}
    </div>
  );
}
