"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { MasonryPhotoAlbum } from "react-photo-album";
import Header from "../components/header";

type Upload = {
  _id: string;
  tweetId: string;
  mediaIndex: number;
  mediaUrl: string;
  s3Key: string;
  mediaType?: "image" | "video";
  thumbnailUrl?: string;
  author?: string;
  tweet: {
    url: string;
  };
};

type TagItem = {
  _id: string;
  name: string;
  usageCount: number;
};

type GalleryPhoto = {
  src: string;
  width: number;
  height: number;
  key: string;
  alt: string;
  author: string;
  source: string;
  mediaType?: "image" | "video";
  thumbnailUrl?: string;
};

type Me = {
  role: "admin" | "writer" | "reader";
};

type ContextMenuState = {
  x: number;
  y: number;
  photo: GalleryPhoto;
};

const DEFAULT_LIST_SIZE = 8;
const EXTRA_PREFETCH_ROWS = 2;

function getColumnsForWidth(containerWidth: number) {
  if (containerWidth < 520) return 2;
  if (containerWidth < 900) return 3;
  if (containerWidth < 1280) return 4;
  return 5;
}

function calculateListSize(viewportWidth: number, viewportHeight: number) {
  const columns = getColumnsForWidth(viewportWidth);
  const columnWidth = viewportWidth / columns;
  // grid item uses aspect-4/5, so height is width * (5 / 4)
  const itemHeight = columnWidth * (5 / 4);
  const rowsInViewport = Math.ceil(viewportHeight / itemHeight);
  const size = columns * (rowsInViewport + EXTRA_PREFETCH_ROWS);
  return Math.max(DEFAULT_LIST_SIZE, size);
}

export default function App() {
  const [uploads, setUploads] = useState<Upload[]>([]);
  const [tags, setTags] = useState<TagItem[]>([]);
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
  const [total, setTotal] = useState<number>(0);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const [loadingMore, setLoadingMore] = useState(false);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [photos, setPhotos] = useState<GalleryPhoto[]>([]);
  const [viewerRole, setViewerRole] = useState<Me["role"] | "guest">("guest");
  const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
  const [deleteTarget, setDeleteTarget] = useState<GalleryPhoto | null>(null);
  const [isDeleting, setIsDeleting] = useState(false);
  const [listSize, setListSize] = useState<number>(() => {
    if (typeof window === "undefined") {
      return DEFAULT_LIST_SIZE;
    }
    return calculateListSize(window.innerWidth, window.innerHeight);
  });
  const sentinelRef = useRef<HTMLDivElement | null>(null);
  const isFetchingMoreRef = useRef(false);
  const loadMoreRef = useRef<() => Promise<void>>(async () => { });
  const wasIntersectingRef = useRef(false);

  function buildQuery(page: number, queryTags: string[], size?: number) {
    const params = new URLSearchParams();
    params.set("page", String(page));
    if (typeof size === "number") {
      params.set("size", String(size));
    }
    for (const tag of queryTags) {
      params.append("tags", tag);
    }
    return params.toString();
  }

  function toggleTag(tagName: string) {
    setSelectedTags((current) => {
      if (current.includes(tagName)) {
        return current.filter((tag) => tag !== tagName);
      }
      return [...current, tagName];
    });
  }

  useEffect(() => {
    let active = true;

    async function loadTags() {
      try {
        const tagsResponse = await fetch("/api/post/tags", { cache: "no-store" });
        if (!tagsResponse.ok) {
          throw new Error(`Failed to load tags: ${tagsResponse.status}`);
        }

        const tagsData = (await tagsResponse.json()) as TagItem[];
        if (active) {
          setTags(tagsData);
        }
      } catch (err) {
        if (active) {
          setError(err instanceof Error ? err.message : "Failed to load tags");
        }
      }
    }

    loadTags();

    return () => {
      active = false;
    };
  }, []);

  useEffect(() => {
    let active = true;

    async function loadMe() {
      try {
        const response = await fetch("/api/auth/me", { cache: "no-store" });
        if (!response.ok) {
          if (active) setViewerRole("guest");
          return;
        }

        const me = (await response.json()) as Me;
        if (active) {
          setViewerRole(me.role);
        }
      } catch {
        if (active) {
          setViewerRole("guest");
        }
      }
    }

    void loadMe();
    return () => {
      active = false;
    };
  }, []);

  useEffect(() => {
    function closeContextMenu() {
      setContextMenu(null);
    }

    function closeOnEscape(event: KeyboardEvent) {
      if (event.key === "Escape") {
        setContextMenu(null);
        if (!isDeleting) {
          setDeleteTarget(null);
        }
      }
    }

    document.addEventListener("click", closeContextMenu);
    document.addEventListener("scroll", closeContextMenu, true);
    document.addEventListener("keydown", closeOnEscape);
    return () => {
      document.removeEventListener("click", closeContextMenu);
      document.removeEventListener("scroll", closeContextMenu, true);
      document.removeEventListener("keydown", closeOnEscape);
    };
  }, [isDeleting]);

  const canManagePost = viewerRole === "admin" || viewerRole === "writer";

  async function deletePhoto(photo: GalleryPhoto) {
    try {
      setIsDeleting(true);
      const response = await fetch(`/api/post/delete/${photo.key}`, {
        method: "DELETE",
      });

      if (!response.ok) {
        throw new Error(`Failed to delete post: ${response.status}`);
      }

      setUploads((current) => current.filter((upload) => upload._id !== photo.key));
      setTotal((current) => Math.max(0, current - 1));
      setDeleteTarget(null);
    } catch (err) {
      alert(err instanceof Error ? err.message : "Failed to delete post");
    } finally {
      setIsDeleting(false);
    }
  }

  async function copyImage(photo: GalleryPhoto) {
    try {
      const response = await fetch(photo.src);
      if (!response.ok) throw new Error("Failed to fetch image");

      let blob = await response.blob();

      // 대부분의 브라우저는 클립보드 복사 시 image/png만 지원하므로,
      // 형식이 다를 경우 canvas를 이용해 PNG로 변환합니다.
      if (blob.type !== "image/png") {
        const img = new Image();
        const url = URL.createObjectURL(blob);
        img.src = url;

        await new Promise((resolve, reject) => {
          img.onload = resolve;
          img.onerror = reject;
        });

        const canvas = document.createElement("canvas");
        canvas.width = img.width;
        canvas.height = img.height;
        const ctx = canvas.getContext("2d");
        if (!ctx) throw new Error("Canvas context를 가져오지 못했습니다.");
        ctx.drawImage(img, 0, 0);

        const pngBlob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, "image/png"));
        URL.revokeObjectURL(url);

        if (!pngBlob) throw new Error("PNG 변환에 실패했습니다.");
        blob = pngBlob;
      }

      const clipboardItem = new ClipboardItem({
        [blob.type]: blob
      });

      await navigator.clipboard.write([clipboardItem]);
      // alert("이미지가 클립보드에 복사되었습니다!");
    } catch (error) {
      console.error("Failed to copy image:", error);
      alert("이미지 복사에 실패했습니다. 브라우저 호환성 문제일 수 있습니다.");
    }
  }

  function saveImage(photo: GalleryPhoto) {
    const link = document.createElement("a");
    link.href = photo.src;
    link.download = `${photo.key}.jpg`;
    link.target = "_blank";
    link.rel = "noopener noreferrer";
    document.body.appendChild(link);
    link.click();
    link.remove();
  }

  useEffect(() => {
    function updateListSize() {
      const nextSize = calculateListSize(window.innerWidth, window.innerHeight);
      setListSize((current) => (current === nextSize ? current : nextSize));
    }

    updateListSize();
    window.addEventListener("resize", updateListSize);
    return () => window.removeEventListener("resize", updateListSize);
  }, []);

  const loadMore = useCallback(async () => {
    if (isFetchingMoreRef.current || loading || loadingMore || !hasMore) {
      return;
    }

    isFetchingMoreRef.current = true;
    setLoadingMore(true);

    try {
      const nextPage = page + 1;
      const query = buildQuery(nextPage, selectedTags, listSize);
      const response = await fetch(`/api/post/list?${query}`, { cache: "no-store" });

      if (!response.ok) {
        throw new Error(`Failed to load more gallery: ${response.status}`);
      }

      const data = (await response.json()) as Upload[];

      if (data.length === 0) {
        setHasMore(false);
        return;
      }

      setUploads((current) => {
        const existingIds = new Set(current.map((item) => item._id));
        const appended = data.filter((item) => !existingIds.has(item._id));
        const merged = [...current, ...appended];
        setHasMore(merged.length < total && data.length >= listSize);
        return merged;
      });

      setPage(nextPage);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to load more gallery");
      setHasMore(false);
    } finally {
      isFetchingMoreRef.current = false;
      setLoadingMore(false);
    }
  }, [hasMore, listSize, loading, loadingMore, page, selectedTags, total]);

  useEffect(() => {
    loadMoreRef.current = loadMore;
  }, [loadMore]);

  useEffect(() => {
    if (!hasMore || loading) {
      return;
    }

    const target = sentinelRef.current;
    if (!target) {
      return;
    }

    const observer = new IntersectionObserver(
      (entries) => {
        const entry = entries[0];
        if (!entry) {
          return;
        }

        if (entry.isIntersecting && !wasIntersectingRef.current) {
          wasIntersectingRef.current = true;
          void loadMoreRef.current();
          return;
        }

        if (!entry.isIntersecting) {
          wasIntersectingRef.current = false;
        }
      },
      { rootMargin: "200px 0px" },
    );

    observer.observe(target);
    return () => {
      observer.disconnect();
      wasIntersectingRef.current = false;
    };
  }, [hasMore, loading]);

  useEffect(() => {
    let active = true;

    async function loadUploads() {
      try {
        setLoading(true);
        setLoadingMore(false);
        setPage(1);
        setHasMore(true);
        isFetchingMoreRef.current = false;
        setError(null);
        const query = buildQuery(1, selectedTags, listSize);
        const [listResponse, totalResponse] = await Promise.all([
          fetch(`/api/post/list?${query}`, { cache: "no-store" }),
          fetch(`/api/post/total?${buildQuery(1, selectedTags)}`, { cache: "no-store" }),
        ]);

        if (!listResponse.ok) {
          throw new Error(`Failed to load gallery: ${listResponse.status}`);
        }

        if (!totalResponse.ok) {
          throw new Error(`Failed to load total: ${totalResponse.status}`);
        }

        const [data, totalData] = await Promise.all([
          listResponse.json() as Promise<Upload[]>,
          totalResponse.json() as Promise<number | { total?: number; count?: number }>,
        ]);

        const resolvedTotal = typeof totalData === "number"
          ? totalData
          : (totalData.total ?? totalData.count ?? 0);

        if (active) {
          setUploads(data);
          setTotal(resolvedTotal);
          setHasMore(data.length > 0 && data.length < resolvedTotal);
        }
      } catch (err) {
        if (active) {
          setError(err instanceof Error ? err.message : "Failed to load gallery");
        }
      } finally {
        if (active) {
          setLoading(false);
        }
      }
    }

    loadUploads();

    return () => {
      active = false;
    };
  }, [listSize, selectedTags]);

  const items = useMemo(() => uploads.filter((upload) => Boolean(upload.mediaUrl)), [uploads]);

  useEffect(() => {
    let cancelled = false;

    async function loadPhotoSizes() {
      if (items.length === 0) {
        setPhotos([]);
        return;
      }

      const nextPhotos = await Promise.all(
        items.map(
          (upload) =>
            new Promise<GalleryPhoto | null>((resolve) => {
              const image = new Image();
              const srcToLoad = (upload.mediaType === "video" && upload.thumbnailUrl) ? upload.thumbnailUrl : upload.mediaUrl;
              image.src = srcToLoad;
              image.onload = () => {
                resolve({
                  src: upload.mediaUrl,
                  width: image.naturalWidth,
                  height: image.naturalHeight,
                  key: upload._id,
                  alt: `tweet ${upload.tweetId} media ${upload.mediaIndex + 1}`,
                  author: upload.author?.trim() || "unknown",
                  source: upload.tweet?.url || "",
                  mediaType: upload.mediaType,
                  thumbnailUrl: upload.thumbnailUrl,
                });
              };
              image.onerror = () => {
                resolve({
                  src: upload.mediaUrl,
                  width: 1,
                  height: 1,
                  key: upload._id,
                  alt: `tweet ${upload.tweetId} media ${upload.mediaIndex + 1}`,
                  author: upload.author?.trim() || "unknown",
                  source: upload.tweet?.url || "",
                  mediaType: upload.mediaType,
                  thumbnailUrl: upload.thumbnailUrl,
                });
              };
            }),
        ),
      );

      if (!cancelled) {
        setPhotos(nextPhotos.filter((photo): photo is GalleryPhoto => photo !== null));
      }
    }

    loadPhotoSizes();

    return () => {
      cancelled = true;
    };
  }, [items]);

  return (
    <div className="min-h-screen bg-main-gradient text-foreground transition-colors duration-500">
      <div className="sticky top-0 z-50">
        <Header />
        <div className="border-b border-border bg-background/85 backdrop-blur px-6 py-2">
          <div className="flex w-full items-center justify-between overflow-x-auto text-sm text-foreground/70">
            <div id="filter" className="flex flex-nowrap items-center gap-2 h-6 overflow-x-auto overflow-y-hidden mr-4">
              <button
                type="button"
                onClick={() => setSelectedTags([])}
                className={selectedTags.length === 0 ? "text-foreground" : "text-foreground/50"}
              >
                all
              </button>
              {tags.map((tag) => (
                <button
                  key={tag._id}
                  type="button"
                  onClick={() => toggleTag(tag.name)}
                  className={(selectedTags.includes(tag.name) ? "text-foreground" : "text-foreground/50") + " w-fit whitespace-nowrap"}
                  style={{ writingMode: "horizontal-tb" }}
                >
                  {tag.name}
                </button>
              ))}
            </div>
            <span className="shrink-0 text-xs uppercase tracking-[0.3em] text-foreground/40">
              {total} items
            </span>
          </div>
        </div>
      </div>
      <main className="w-full">
        {error ? <p className="text-red-500">{error}</p> : null}

        {loading ? (
          <div className="grid w-full grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
            {Array.from({ length: 12 }).map((_, index) => (
              <div
                key={index}
                className="aspect-4/5 animate-pulse bg-muted/50"
              />
            ))}
          </div>
        ) : (
          <div className="w-full">
            <MasonryPhotoAlbum<GalleryPhoto>
              photos={photos}
              spacing={0}
              columns={(containerWidth) => getColumnsForWidth(containerWidth)}
              render={{
                photo: ({ onClick }, { photo }) => (
                  <a
                    key={photo.key}
                    href={`/detail/${photo.key}`}
                    onClick={onClick}
                    onContextMenu={(event) => {
                      event.preventDefault();
                      event.stopPropagation();
                      const safeX = Math.min(event.clientX, window.innerWidth - 200);
                      const safeY = Math.min(event.clientY, window.innerHeight - 220);
                      setContextMenu({ x: Math.max(12, safeX), y: Math.max(12, safeY), photo });
                    }}
                    className="group relative block overflow-hidden image-scale"
                    title={`작가 ${photo.author}`}
                  >
                    {photo.mediaType === "video" ? (
                      <video
                        src={photo.src}
                        poster={photo.thumbnailUrl}
                        autoPlay
                        muted
                        loop
                        playsInline
                        className="block w-full"
                      />
                    ) : (
                      <img
                        src={photo.src}
                        alt={photo.alt}
                        loading="lazy"
                        decoding="async"
                        className="block w-full"
                      />
                    )}
                    <div className="absolute inset-x-3 bottom-3 translate-y-3 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100">
                      <button
                        type="button"
                        onClick={(event) => {
                          event.preventDefault();
                          event.stopPropagation();
                          window.open(photo.source, "_blank", "noopener,noreferrer");
                        }}
                        className="inline-flex max-w-full items-center rounded-t-sm rounded-b-lg bg-black/72 px-3 py-1.5 text-xs text-white backdrop-blur-sm pointer-events-auto"
                        title="원본 이미지 열기"
                      >
                        <span className="truncate">© {photo.author}</span>
                      </button>
                    </div>
                  </a>
                ),
              }}
              componentsProps={{
                container: { className: "!w-full" },
              }}
            />
          </div>
        )
        }
      </main >
      {contextMenu ? (
        <div
          className="fixed z-100 min-w-44 overflow-hidden rounded-lg border border-border bg-background/95 p-1 shadow-xl backdrop-blur"
          style={{ left: contextMenu.x, top: contextMenu.y }}
          onClick={(event) => event.stopPropagation()}
        >
          {canManagePost ? (
            <>
              <button
                type="button"
                className="block w-full rounded px-3 py-2 text-left text-sm hover:bg-black/5"
                onClick={() => {
                  window.location.href = `/edit/${contextMenu.photo.key}`;
                  setContextMenu(null);
                }}
              >
                수정
              </button>
              <button
                type="button"
                className="block w-full rounded px-3 py-2 text-left text-sm text-red-600 hover:bg-red-50"
                onClick={() => {
                  setDeleteTarget(contextMenu.photo);
                  setContextMenu(null);
                }}
              >
                삭제
              </button>
              <div className="my-1 h-px bg-border" />
            </>
          ) : null}
          <button
            type="button"
            className="block w-full rounded px-3 py-2 text-left text-sm hover:bg-accent"
            onClick={() => {
              navigator.clipboard.writeText(contextMenu.photo.source).then(() => {
                setContextMenu(null);
              }).catch((err) => {
                console.error("Failed to copy link:", err);
                alert("링크 복사에 실패했습니다.");
              });
            }}
          >
            원본 링크 복사
          </button>
          <div className="my-1 h-px bg-border" />

          <button
            type="button"
            className="block w-full rounded px-3 py-2 text-left text-sm hover:bg-accent"
            onClick={async () => {
              await copyImage(contextMenu.photo);
              setContextMenu(null);
            }}
          >
            이미지 복사
          </button>
          <button
            type="button"
            className="block w-full rounded px-3 py-2 text-left text-sm hover:bg-accent"
            onClick={() => {
              saveImage(contextMenu.photo);
              setContextMenu(null);
            }}
          >
            이미지 저장
          </button>
        </div>
      ) : null}
      {!loading && hasMore ? <div ref={sentinelRef} className="h-1 w-full" /> : null}
      {
        loadingMore ? (
          <div className="grid w-full grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
            {Array.from({ length: 5 }).map((_, index) => (
              <div
                key={`loading-more-${index}`}
                className="aspect-4/5 animate-pulse bg-muted/50"
              />
            ))}
          </div>
        ) : null
      }
      {deleteTarget ? (
        <div
          className="fixed inset-0 z-120 flex items-center justify-center bg-black/35 px-4"
          onClick={() => {
            if (!isDeleting) {
              setDeleteTarget(null);
            }
          }}
        >
          <div
            className="w-full max-w-sm rounded-xl border border-border bg-background p-5 shadow-2xl"
            onClick={(event) => event.stopPropagation()}
          >
            <p className="text-base font-semibold text-foreground">게시물을 삭제할까요?</p>
            <p className="mt-2 text-sm text-foreground/70">삭제 후에는 되돌릴 수 없습니다.</p>
            <div className="mt-5 flex justify-end gap-2">
              <button
                type="button"
                className="rounded px-4 py-2 text-sm text-foreground/70 hover:bg-accent disabled:opacity-50"
                onClick={() => setDeleteTarget(null)}
                disabled={isDeleting}
              >
                취소
              </button>
              <button
                type="button"
                className="rounded bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-50"
                onClick={() => {
                  void deletePhoto(deleteTarget);
                }}
                disabled={isDeleting}
              >
                {isDeleting ? "삭제 중..." : "삭제"}
              </button>
            </div>
          </div>
        </div>
      ) : null}
    </div >
  );
}
