import {
  useCallback,
  useEffect,
  useLayoutEffect,
  useRef,
  useState,
} from "react";
import { Link, useParams } from "react-router";

import { API_URL } from "../config/api.ts";
import type { Dump, PaginatedData, PublicUser, RawDump } from "../model.ts";
import { deserializeDump, deserializePublicUser } from "../model.ts";
import { useAuth } from "../hooks/useAuth.ts";
import { useWS } from "../hooks/useWS.ts";
import { useInfiniteScroll } from "../hooks/useInfiniteScroll.ts";
import { useFeedCache } from "../hooks/useFeedCache.ts";
import { Avatar } from "../components/Avatar.tsx";
import { DumpCard } from "../components/DumpCard.tsx";
import { PageShell } from "../components/PageShell.tsx";
import { PageError } from "../components/PageError.tsx";

const PAGE_SIZE = 20;
const hydrateDump = (raw: Dump): Dump =>
  deserializeDump(raw as unknown as RawDump);

type State =
  | { status: "loading" }
  | { status: "error"; error: string }
  | {
    status: "loaded";
    profileUser: PublicUser;
    votes: Dump[];
    hasMore: boolean;
    page: number;
    loadingMore: boolean;
  };

export function UserUpvoted() {
  const { username } = useParams();
  const { user: me, token } = useAuth();
  const { voteCounts, myVotes, lastVoteEvent, castVote, removeVote } = useWS();
  const { cached, saveState } = useFeedCache<Dump>(
    `feed:user-upvoted-full:${username ?? ""}`,
    hydrateDump,
  );

  const [state, setState] = useState<State>({ status: "loading" });
  const [votedIds, setVotedIds] = useState<Set<string>>(new Set());
  const [fading, setFading] = useState<
    Record<string, "cooldown" | "dismissing">
  >({});
  const cancels = useRef<Map<string, () => void>>(new Map());
  const prevVotedIds = useRef<Set<string> | null>(null);
  const prevMyVotesRef = useRef<Set<string> | null>(null);

  useEffect(() => () => {
    cancels.current.forEach((c) => c());
  }, []);

  useEffect(() => {
    if (!username) return;
    setState({ status: "loading" });
    setVotedIds(new Set());
    prevVotedIds.current = null;
    prevMyVotesRef.current = null;

    if (cached) {
      fetch(`${API_URL}/api/users/${username}`)
        .then((r) => r.json())
        .then((body) => {
          if (!body.success) throw new Error("User not found");
          const voteIds = new Set(cached.items.map((d) => d.id));
          setState({
            status: "loaded",
            profileUser: deserializePublicUser(body.data),
            votes: cached.items,
            hasMore: cached.hasMore,
            page: cached.page,
            loadingMore: false,
          });
          setVotedIds(voteIds);
        })
        .catch((err) =>
          setState({
            status: "error",
            error: err instanceof Error ? err.message : "Failed to load",
          })
        );
      return;
    }

    const authHeaders: HeadersInit = token
      ? { Authorization: `Bearer ${token}` }
      : {};
    Promise.all([
      fetch(`${API_URL}/api/users/${username}`),
      fetch(
        `${API_URL}/api/users/${username}/votes?page=1&limit=${PAGE_SIZE}`,
        { headers: authHeaders },
      ),
    ])
      .then(([userRes, votesRes]) =>
        Promise.all([userRes.json(), votesRes.json()])
      )
      .then(([userBody, votesBody]) => {
        if (!userBody.success) throw new Error("User not found");
        const { items, hasMore } = votesBody.success
          ? votesBody.data as PaginatedData<RawDump>
          : { items: [], hasMore: false };
        const voteItems = items.map(deserializeDump);
        setState({
          status: "loaded",
          profileUser: deserializePublicUser(userBody.data),
          votes: voteItems,
          hasMore,
          page: 1,
          loadingMore: false,
        });
        setVotedIds(new Set(voteItems.map((d) => d.id)));
      })
      .catch((err) =>
        setState({
          status: "error",
          error: err instanceof Error ? err.message : "Failed to load",
        })
      );
  }, [username]);

  const profileUserId = state.status === "loaded" ? state.profileUser.id : null;

  // Own profile: keep votedIds in sync with myVotes
  useEffect(() => {
    if (!profileUserId || me?.id !== profileUserId) return;
    setVotedIds(new Set(myVotes));
    if (prevMyVotesRef.current === null) {
      prevMyVotesRef.current = new Set(myVotes);
      return;
    }
    const prev = prevMyVotesRef.current;
    setState((s) => {
      if (s.status !== "loaded") return s;
      const voteIdSet = new Set(s.votes.map((d) => d.id));
      const toAdd = [...myVotes].filter((id) =>
        !prev.has(id) && !voteIdSet.has(id)
      );
      if (toAdd.length === 0) return s;
      // Newly voted items will arrive via lastVoteEvent fetch below
      return s;
    });
    prevMyVotesRef.current = new Set(myVotes);
  }, [myVotes, me, profileUserId]);

  // WS vote events
  useEffect(() => {
    if (!lastVoteEvent || !profileUserId) return;
    const { dumpId, voterId, action } = lastVoteEvent;
    if (voterId !== profileUserId) return;

    if (action === "remove") {
      setVotedIds((prev) => {
        const n = new Set(prev);
        n.delete(dumpId);
        return n;
      });
    } else {
      setVotedIds((prev) => new Set([...prev, dumpId]));
      fetch(`${API_URL}/api/dumps/${dumpId}`)
        .then((r) => r.json())
        .then((body) => {
          if (!body.success) return;
          const dump = deserializeDump(body.data);
          setState((s) => {
            if (s.status !== "loaded" || s.votes.some((d) => d.id === dumpId)) {
              return s;
            }
            return { ...s, votes: [dump, ...s.votes] };
          });
        })
        .catch(() => {});
    }
  }, [lastVoteEvent, profileUserId]);

  // Fade animation when items leave votedIds
  useEffect(() => {
    if (prevVotedIds.current === null) {
      prevVotedIds.current = new Set(votedIds);
      return;
    }

    const prev = prevVotedIds.current;

    for (const id of prev) {
      if (!votedIds.has(id) && !cancels.current.has(id)) {
        let dead = false;
        let kill = () => {};
        kill = () => {
          dead = true;
          setFading((f) => {
            const n = { ...f };
            delete n[id];
            return n;
          });
          cancels.current.delete(id);
        };
        cancels.current.set(id, () => kill());
        setFading((f) => ({ ...f, [id]: "cooldown" }));

        const t1 = setTimeout(() => {
          if (dead) return;
          setFading((f) => ({ ...f, [id]: "dismissing" }));
          const t2 = setTimeout(() => {
            if (!dead) kill();
          }, 350);
          kill = () => {
            dead = true;
            clearTimeout(t2);
            setFading((f) => {
              const n = { ...f };
              delete n[id];
              return n;
            });
            cancels.current.delete(id);
          };
        }, 2000);

        kill = () => {
          dead = true;
          clearTimeout(t1);
          setFading((f) => {
            const n = { ...f };
            delete n[id];
            return n;
          });
          cancels.current.delete(id);
        };
        cancels.current.set(id, () => kill());
      }
    }

    for (const id of votedIds) {
      if (!prev.has(id) && cancels.current.has(id)) {
        cancels.current.get(id)!();
      }
    }

    prevVotedIds.current = new Set(votedIds);
  }, [votedIds]);

  const loadMore = useCallback(() => {
    if (
      state.status !== "loaded" || !state.hasMore || state.loadingMore ||
      !username
    ) return;
    const nextPage = state.page + 1;
    setState((s) => s.status === "loaded" ? { ...s, loadingMore: true } : s);
    fetch(
      `${API_URL}/api/users/${username}/votes?page=${nextPage}&limit=${PAGE_SIZE}`,
      { headers: token ? { Authorization: `Bearer ${token}` } : {} },
    )
      .then((r) => r.json())
      .then((body) => {
        const { items, hasMore } = body.data as PaginatedData<RawDump>;
        const newItems = items.map(deserializeDump);
        setState((s) =>
          s.status === "loaded"
            ? {
              ...s,
              votes: [...s.votes, ...newItems],
              hasMore,
              page: nextPage,
              loadingMore: false,
            }
            : s
        );
        setVotedIds((prev) => new Set([...prev, ...newItems.map((d) => d.id)]));
      })
      .catch(() =>
        setState((s) =>
          s.status === "loaded" ? { ...s, loadingMore: false } : s
        )
      );
  }, [state, username, token]);

  const sentinelRef = useInfiniteScroll(
    loadMore,
    state.status === "loaded" && state.hasMore && !state.loadingMore,
  );

  useEffect(() => {
    if (state.status !== "loaded") return;
    let timer: ReturnType<typeof setTimeout>;
    const onScroll = () => {
      clearTimeout(timer);
      timer = setTimeout(() => {
        if (state.status !== "loaded") return;
        saveState(state.votes, state.page, state.hasMore, globalThis.scrollY);
      }, 100);
    };
    globalThis.addEventListener("scroll", onScroll, { passive: true });
    return () => {
      globalThis.removeEventListener("scroll", onScroll);
      clearTimeout(timer);
    };
  }, [state, saveState]);

  const scrollRestored = useRef(false);
  useLayoutEffect(() => {
    if (cached?.scrollY == null || scrollRestored.current) return;
    if (state.status === "loaded") {
      globalThis.scrollTo(0, cached.scrollY);
      scrollRestored.current = true;
    }
  }, [state.status, cached]);

  if (state.status === "loading") {
    return (
      <PageShell>
        <p className="page-loading">Loading…</p>
      </PageShell>
    );
  }

  if (state.status === "error") {
    return (
      <PageError
        message={state.error}
        actions={
          <Link to={`/users/${username}`} className="logout-btn">
            ← Back to profile
          </Link>
        }
      />
    );
  }

  const { profileUser, votes, hasMore, loadingMore } = state;
  const visibleDumps = votes.filter((d) =>
    votedIds.has(d.id) || d.id in fading
  );

  return (
    <PageShell>
      <div className="profile-subpage-header">
        <Link to={`/users/${username}`} className="profile-subpage-back">
          ← {profileUser.username}
        </Link>
        <div className="profile-subpage-title-row">
          <Avatar
            userId={profileUser.id}
            username={profileUser.username}
            hasAvatar={!!profileUser.avatarMime}
            size={36}
          />
          <h1 className="profile-subpage-title">Upvoted</h1>
        </div>
      </div>

      {visibleDumps.length === 0
        ? <p className="empty-state">Nothing here yet.</p>
        : (
          <ul className="dump-feed">
            {visibleDumps.map((dump) => {
              const phase = fading[dump.id];
              const extraCls = phase === "cooldown"
                ? "dump-card--fading"
                : phase === "dismissing"
                ? "dump-card--dismissing"
                : undefined;
              return (
                <DumpCard
                  key={dump.id}
                  dump={dump}
                  voteCount={voteCounts[dump.id] ?? dump.voteCount}
                  voted={myVotes.has(dump.id)}
                  canVote={!!me}
                  castVote={castVote}
                  removeVote={removeVote}
                  className={extraCls}
                />
              );
            })}
          </ul>
        )}

      <div ref={sentinelRef} />
      {loadingMore && <p className="feed-loading-more">Loading more…</p>}
      {!hasMore && visibleDumps.length > 0 && (
        <p className="index-status">All {votes.length} upvoted dumps loaded.</p>
      )}
    </PageShell>
  );
}
