import { useCallback, useEffect, useRef, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import type { FileDiff, FileDiffDetail, DiffLine, ReviewComment } from "./types";
import { FileCard } from "./FileCard";
import { trpc } from "@/lib/trpc";

function getFileCommentMap(
  commentsByLine: Map<string, ReviewComment[]>,
  filePath: string,
): Map<string, ReviewComment[]> {
  const result = new Map<string, ReviewComment[]>();
  for (const [key, val] of commentsByLine) {
    if (key.startsWith(`${filePath}:`)) result.set(key, val);
  }
  return result;
}

interface DiffViewerProps {
  files: (FileDiff | FileDiffDetail)[];
  phaseId: string;
  commitMode: boolean;
  commentsByLine: Map<string, ReviewComment[]>;
  onAddComment: (
    filePath: string,
    lineNumber: number,
    lineType: DiffLine["type"],
    body: string,
  ) => void;
  onResolveComment: (commentId: string) => void;
  onUnresolveComment: (commentId: string) => void;
  onReplyComment?: (parentCommentId: string, body: string) => void;
  onEditComment?: (commentId: string, body: string) => void;
  viewedFiles?: Set<string>;
  onToggleViewed?: (filePath: string) => void;
  onRegisterRef?: (filePath: string, el: HTMLDivElement | null) => void;
  expandAll?: boolean;
}

export function DiffViewer({
  files,
  phaseId,
  commitMode,
  commentsByLine,
  onAddComment,
  onResolveComment,
  onUnresolveComment,
  onReplyComment,
  onEditComment,
  viewedFiles,
  onToggleViewed,
  onRegisterRef,
  expandAll,
}: DiffViewerProps) {
  // Set of file paths currently intersecting (or near) the viewport
  const visibleFiles = useRef<Set<string>>(new Set());
  // Map from filePath → wrapper div ref
  const wrapperRefs = useRef<Map<string, HTMLDivElement>>(new Map());
  // Increment to trigger re-render when visibility changes
  const [visibilityVersion, setVisibilityVersion] = useState(0);

  // Single IntersectionObserver for all wrappers
  const observerRef = useRef<IntersectionObserver | null>(null);

  useEffect(() => {
    if (files.length === 1) return; // skip for single file

    observerRef.current = new IntersectionObserver(
      (entries) => {
        let changed = false;
        for (const entry of entries) {
          const filePath = (entry.target as HTMLDivElement).dataset['filePath'];
          if (!filePath) continue;
          if (entry.isIntersecting) {
            if (!visibleFiles.current.has(filePath)) {
              visibleFiles.current.add(filePath);
              changed = true;
            }
          } else {
            if (visibleFiles.current.has(filePath)) {
              visibleFiles.current.delete(filePath);
              changed = true;
            }
          }
        }
        if (changed) setVisibilityVersion((v) => v + 1);
      },
      { rootMargin: '100% 0px 100% 0px' }, // 1× viewport above and below
    );

    // Observe all current wrapper divs
    for (const el of wrapperRefs.current.values()) {
      observerRef.current.observe(el);
    }

    return () => {
      observerRef.current?.disconnect();
    };
  }, [files]); // re-create observer when file list changes

  // Register wrapper ref — observes the div, registers with parent
  const registerWrapper = useCallback(
    (filePath: string, el: HTMLDivElement | null) => {
      if (el) {
        wrapperRefs.current.set(filePath, el);
        observerRef.current?.observe(el);
      } else {
        const prev = wrapperRefs.current.get(filePath);
        if (prev) observerRef.current?.unobserve(prev);
        wrapperRefs.current.delete(filePath);
      }
      onRegisterRef?.(filePath, el);
    },
    [onRegisterRef],
  );

  // expandAll batch loading
  const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());
  const queryClient = useQueryClient();
  const utils = trpc.useUtils();

  useEffect(() => {
    if (!expandAll || files.length === 0) return;

    const BATCH = 10;
    let cancelled = false;

    async function batchExpand() {
      const chunks: (FileDiff | FileDiffDetail)[][] = [];
      for (let i = 0; i < files.length; i += BATCH) {
        chunks.push(files.slice(i, i + BATCH));
      }

      for (const chunk of chunks) {
        if (cancelled) break;
        // Mark this batch as expanded (triggers FileCard renders + queries)
        setExpandedFiles((prev) => {
          const next = new Set(prev);
          for (const f of chunk) {
            if (f.status !== 'binary') next.add(f.newPath);
          }
          return next;
        });
        // Eagerly prefetch via React Query to saturate network
        await Promise.all(
          chunk
            .filter((f) => f.status !== 'binary' && !('hunks' in f))
            .map((f) =>
              utils.getFileDiff
                .fetch({ phaseId, filePath: encodeURIComponent(f.newPath) })
                .catch(() => null), // swallow per-file errors; FileCard shows its own error state
            ),
        );
      }
    }

    batchExpand();
    return () => {
      cancelled = true;
    };
  }, [expandAll]); // only re-run when expandAll toggles
  // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally only on expandAll

  // Suppress unused variable warning — used only to force re-render on visibility change
  void visibilityVersion;
  void queryClient; // imported for type alignment; actual prefetch goes through trpc utils

  const isSingleFile = files.length === 1;

  return (
    <div className="space-y-4">
      {files.map((file) => {
        const isVisible = isSingleFile || visibleFiles.current.has(file.newPath);
        const isExpandedOverride = expandedFiles.has(file.newPath) ? true : undefined;
        return (
          <div
            key={file.newPath}
            ref={(el) => registerWrapper(file.newPath, el)}
            data-file-path={file.newPath}
          >
            {isVisible ? (
              <FileCard
                file={file as FileDiff}
                detail={'hunks' in file ? (file as FileDiffDetail) : undefined}
                phaseId={phaseId}
                commitMode={commitMode}
                commentsByLine={getFileCommentMap(commentsByLine, file.newPath)}
                isExpandedOverride={isExpandedOverride}
                onAddComment={onAddComment}
                onResolveComment={onResolveComment}
                onUnresolveComment={onUnresolveComment}
                onReplyComment={onReplyComment}
                onEditComment={onEditComment}
                isViewed={viewedFiles?.has(file.newPath) ?? false}
                onToggleViewed={() => onToggleViewed?.(file.newPath)}
              />
            ) : (
              <div style={{ height: '48px' }} aria-hidden />
            )}
          </div>
        );
      })}
    </div>
  );
}
