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

import { Avatar } from "../components/Avatar.tsx";
import { DumpCard } from "../components/DumpCard.tsx";
import { AppHeader } from "../components/AppHeader.tsx";

import { API_URL } from "../config/api.ts";

import {
  deserializeDump,
  type Dump,
  type PaginatedData,
  type RawDump,
  type User,
} from "../model.ts";

import { useFeedCache } from "../hooks/useFeedCache.ts";
import { useAuth } from "../hooks/useAuth.ts";
import { useWS } from "../hooks/useWS.ts";
import { useInfiniteScroll } from "../hooks/useInfiniteScroll.ts";

const PAGE_SIZE = 20;

// After JSON roundtrip, createdAt is a string — re-parse it
const hydrateDump = (raw: Dump): Dump =>
  deserializeDump(raw as unknown as RawDump);

type DumpsState =
  | { status: "loading" }
  | { status: "error"; error: string }
  | {
    status: "loaded";
    dumps: Dump[];
    hasMore: boolean;
    page: number;
    loadingMore: boolean;
  };

type FeedTab = "hot" | "new" | "followed";
type FollowedSection = "users" | "playlists";

function hotScore(dump: Dump): number {
  const ageHours = (Date.now() - dump.createdAt.getTime()) / 3_600_000;
  return (dump.voteCount + 1) / Math.pow(ageHours + 2, 1.5);
}

// ── FollowedSubFeed ──────────────────────────────────────────────────────────

interface FollowedSubFeedProps {
  state: DumpsState;
  voteCounts: Record<string, number>;
  myVotes: Set<string>;
  user: User | null;
  castVote: (id: string) => void;
  removeVote: (id: string) => void;
  deletedDumpIds: Set<string>;
  emptyMessage: string;
  onLoadMore: () => void;
}

function FollowedSubFeed({
  state,
  voteCounts,
  myVotes,
  user,
  castVote,
  removeVote,
  deletedDumpIds,
  emptyMessage,
  onLoadMore,
}: FollowedSubFeedProps) {
  const hasMore = state.status === "loaded" && state.hasMore &&
    !state.loadingMore;
  const sentinelRef = useInfiniteScroll(onLoadMore, hasMore);

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

  const visible = state.dumps.filter((d) => !deletedDumpIds.has(d.id));

  if (visible.length === 0) {
    return <p className="index-status">{emptyMessage}</p>;
  }

  return (
    <>
      <ul className="dump-feed">
        {visible.map((dump) => (
          <DumpCard
            key={dump.id}
            dump={dump}
            voteCount={voteCounts[dump.id] ?? dump.voteCount}
            voted={myVotes.has(dump.id)}
            canVote={!!user}
            castVote={castVote}
            removeVote={removeVote}
            isOwner={user?.id === dump.userId}
          />
        ))}
      </ul>
      <div ref={sentinelRef} />
      {state.loadingMore && <p className="feed-loading-more">Loading more…</p>}
    </>
  );
}

// ── Index ────────────────────────────────────────────────────────────────────

export function Index() {
  const location = useLocation();
  const justDeletedId = (location.state as { deletedDumpId?: string } | null)
    ?.deletedDumpId;

  const { user, token } = useAuth();
  const {
    onlineUsers,
    voteCounts,
    myVotes,
    recentDumps,
    deletedDumpIds,
    castVote,
    removeVote,
  } = useWS();

  // Main feed
  const { cached, saveState } = useFeedCache<Dump>(
    `feed:index:${user?.id ?? "guest"}`,
    hydrateDump,
  );
  const [dumpsState, setDumpsState] = useState<DumpsState>(() =>
    cached
      ? {
        status: "loaded",
        dumps: cached.items,
        hasMore: cached.hasMore,
        page: cached.page,
        loadingMore: false,
      }
      : { status: "loading" }
  );
  const mainFetchDone = useRef(false);

  // Followed feeds
  const { cached: cachedFollowedUsers, saveState: saveFollowedUsers } =
    useFeedCache<Dump>(
      `feed:followed-users:${user?.id ?? "guest"}`,
      hydrateDump,
    );
  const { cached: cachedFollowedPlaylists, saveState: saveFollowedPlaylists } =
    useFeedCache<Dump>(
      `feed:followed-playlists:${user?.id ?? "guest"}`,
      hydrateDump,
    );

  const [followedUsersDumps, setFollowedUsersDumps] = useState<DumpsState>({
    status: "loading",
  });
  const [followedPlaylistsDumps, setFollowedPlaylistsDumps] = useState<
    DumpsState
  >({ status: "loading" });

  const [tab, setTab] = useState<FeedTab>("hot");
  const [followedSection, setFollowedSection] = useState<FollowedSection>(
    "users",
  );

  // When the logo is clicked it navigates to / with state { tab: "hot" }, producing
  // a new location.key even if already on /. React to that to reset the active tab.
  useEffect(() => {
    const st = location.state as { tab?: string } | null;
    if (st?.tab === "hot" || st?.tab === "new" || st?.tab === "followed") {
      setTab(st.tab as FeedTab);
    }
  }, [location]);

  // ── Main feed fetch ──

  useEffect(() => {
    if (mainFetchDone.current || cached) return;
    mainFetchDone.current = true;
    (async () => {
      try {
        const res = await fetch(
          `${API_URL}/api/dumps/?page=1&limit=${PAGE_SIZE}`,
          {
            headers: token ? { Authorization: `Bearer ${token}` } : {},
          },
        );
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const body = await res.json();
        const { items, hasMore } = body.data as PaginatedData<RawDump>;
        setDumpsState({
          status: "loaded",
          dumps: items.map(deserializeDump),
          hasMore,
          page: 1,
          loadingMore: false,
        });
      } catch (err) {
        setDumpsState({
          status: "error",
          error: err instanceof Error ? err.message : "Failed to load",
        });
      }
    })();
  }, [cached, token]);

  // ── Followed feeds fetch (lazy, on first tab open) ──

  useEffect(() => {
    if (tab !== "followed" || !user || !token) return;

    if (followedUsersDumps.status === "loading") {
      if (cachedFollowedUsers) {
        setFollowedUsersDumps({
          status: "loaded",
          dumps: cachedFollowedUsers.items,
          hasMore: cachedFollowedUsers.hasMore,
          page: cachedFollowedUsers.page,
          loadingMore: false,
        });
      } else {
        fetch(`${API_URL}/api/follows/feed/users?page=1&limit=${PAGE_SIZE}`, {
          headers: { Authorization: `Bearer ${token}` },
        })
          .then((r) => r.json())
          .then((body) => {
            const { items, hasMore } = body.data as PaginatedData<RawDump>;
            setFollowedUsersDumps({
              status: "loaded",
              dumps: items.map(deserializeDump),
              hasMore,
              page: 1,
              loadingMore: false,
            });
          })
          .catch((err) =>
            setFollowedUsersDumps({
              status: "error",
              error: err instanceof Error ? err.message : "Failed to load",
            })
          );
      }
    }

    if (followedPlaylistsDumps.status === "loading") {
      if (cachedFollowedPlaylists) {
        setFollowedPlaylistsDumps({
          status: "loaded",
          dumps: cachedFollowedPlaylists.items,
          hasMore: cachedFollowedPlaylists.hasMore,
          page: cachedFollowedPlaylists.page,
          loadingMore: false,
        });
      } else {
        fetch(
          `${API_URL}/api/follows/feed/playlists?page=1&limit=${PAGE_SIZE}`,
          {
            headers: { Authorization: `Bearer ${token}` },
          },
        )
          .then((r) => r.json())
          .then((body) => {
            const { items, hasMore } = body.data as PaginatedData<RawDump>;
            setFollowedPlaylistsDumps({
              status: "loaded",
              dumps: items.map(deserializeDump),
              hasMore,
              page: 1,
              loadingMore: false,
            });
          })
          .catch((err) =>
            setFollowedPlaylistsDumps({
              status: "error",
              error: err instanceof Error ? err.message : "Failed to load",
            })
          );
      }
    }
  }, [
    tab,
    user?.id,
    token,
    cachedFollowedUsers,
    cachedFollowedPlaylists,
    followedUsersDumps.status,
    followedPlaylistsDumps.status,
  ]);

  // ── Load-more callbacks ──

  const loadMore = useCallback(() => {
    if (
      dumpsState.status !== "loaded" || !dumpsState.hasMore ||
      dumpsState.loadingMore
    ) return;
    const nextPage = dumpsState.page + 1;
    setDumpsState((s) =>
      s.status === "loaded" ? { ...s, loadingMore: true } : s
    );
    fetch(`${API_URL}/api/dumps/?page=${nextPage}&limit=${PAGE_SIZE}`, {
      headers: token ? { Authorization: `Bearer ${token}` } : {},
    })
      .then((r) => r.json())
      .then((body) => {
        const { items, hasMore } = body.data as PaginatedData<RawDump>;
        setDumpsState((s) =>
          s.status === "loaded"
            ? {
              ...s,
              dumps: [...s.dumps, ...items.map(deserializeDump)],
              hasMore,
              page: nextPage,
              loadingMore: false,
            }
            : s
        );
      })
      .catch(() =>
        setDumpsState((s) =>
          s.status === "loaded" ? { ...s, loadingMore: false } : s
        )
      );
  }, [dumpsState, token]);

  const loadMoreFollowedUsers = useCallback(() => {
    if (
      followedUsersDumps.status !== "loaded" ||
      !followedUsersDumps.hasMore ||
      followedUsersDumps.loadingMore ||
      !token
    ) return;
    const nextPage = followedUsersDumps.page + 1;
    setFollowedUsersDumps((s) =>
      s.status === "loaded" ? { ...s, loadingMore: true } : s
    );
    fetch(
      `${API_URL}/api/follows/feed/users?page=${nextPage}&limit=${PAGE_SIZE}`,
      {
        headers: { Authorization: `Bearer ${token}` },
      },
    )
      .then((r) => r.json())
      .then((body) => {
        const { items, hasMore } = body.data as PaginatedData<RawDump>;
        setFollowedUsersDumps((s) =>
          s.status === "loaded"
            ? {
              ...s,
              dumps: [...s.dumps, ...items.map(deserializeDump)],
              hasMore,
              page: nextPage,
              loadingMore: false,
            }
            : s
        );
      })
      .catch(() =>
        setFollowedUsersDumps((s) =>
          s.status === "loaded" ? { ...s, loadingMore: false } : s
        )
      );
  }, [followedUsersDumps, token]);

  const loadMoreFollowedPlaylists = useCallback(() => {
    if (
      followedPlaylistsDumps.status !== "loaded" ||
      !followedPlaylistsDumps.hasMore ||
      followedPlaylistsDumps.loadingMore ||
      !token
    ) return;
    const nextPage = followedPlaylistsDumps.page + 1;
    setFollowedPlaylistsDumps((s) =>
      s.status === "loaded" ? { ...s, loadingMore: true } : s
    );
    fetch(
      `${API_URL}/api/follows/feed/playlists?page=${nextPage}&limit=${PAGE_SIZE}`,
      {
        headers: { Authorization: `Bearer ${token}` },
      },
    )
      .then((r) => r.json())
      .then((body) => {
        const { items, hasMore } = body.data as PaginatedData<RawDump>;
        setFollowedPlaylistsDumps((s) =>
          s.status === "loaded"
            ? {
              ...s,
              dumps: [...s.dumps, ...items.map(deserializeDump)],
              hasMore,
              page: nextPage,
              loadingMore: false,
            }
            : s
        );
      })
      .catch(() =>
        setFollowedPlaylistsDumps((s) =>
          s.status === "loaded" ? { ...s, loadingMore: false } : s
        )
      );
  }, [followedPlaylistsDumps, token]);

  // ── Scroll save effects ──

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

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

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

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

  // ── Scroll restoration ──

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

  // ── Derived values ──

  const loading = dumpsState.status === "loading";
  const error = dumpsState.status === "error" ? dumpsState.error : null;
  const dumps = dumpsState.status === "loaded" ? dumpsState.dumps : [];
  const loadingMore = dumpsState.status === "loaded" && dumpsState.loadingMore;

  const restIds = new Set(dumps.map((d) => d.id));
  const combined = [...recentDumps.filter((d) => !restIds.has(d.id)), ...dumps]
    .filter((d) => !deletedDumpIds.has(d.id) && d.id !== justDeletedId);

  const sortedDumps = [...combined].sort(
    tab === "new"
      ? (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
      : (a, b) => hotScore(b) - hotScore(a),
  );

  // ── Render ──

  const presenceRow = (
    <div className="index-presence">
      {onlineUsers.map((u) => (
        <Link
          key={u.userId}
          to={`/users/${u.username}`}
          title={u.username}
          className="index-presence-avatar"
        >
          <Avatar
            userId={u.userId}
            username={u.username}
            hasAvatar={u.hasAvatar}
            size={32}
          />
        </Link>
      ))}
    </div>
  );

  const tabBar = (
    <div className="feed-sort">
      <button
        type="button"
        className={`feed-sort-btn${tab === "hot" ? " active" : ""}`}
        onClick={() => setTab("hot")}
      >
        Hot
      </button>
      <button
        type="button"
        className={`feed-sort-btn${tab === "new" ? " active" : ""}`}
        onClick={() => setTab("new")}
      >
        New
      </button>
      {user && (
        <button
          type="button"
          className={`feed-sort-btn${tab === "followed" ? " active" : ""}`}
          onClick={() => setTab("followed")}
        >
          Followed
        </button>
      )}
    </div>
  );

  return (
    <div className="index-page">
      <AppHeader
        centerSlot={
          <div className="header-center-slot">
            {presenceRow}
            {tabBar}
          </div>
        }
      />

      {/* Shown only on narrow viewports */}
      <div className="index-below-header">
        {tabBar}
        {presenceRow}
      </div>

      {/* Hot / New feed */}
      {tab !== "followed" && (
        <>
          {loading && <p className="index-status">Loading…</p>}
          {error && <p className="index-status index-status--error">{error}</p>}

          {!loading && !error && combined.length === 0 && (
            <p className="index-status">No dumps yet. Be the first!</p>
          )}

          {!loading && !error && combined.length > 0 && (
            <ul className="dump-feed">
              {sortedDumps.map((dump) => (
                <DumpCard
                  key={dump.id}
                  dump={dump}
                  voteCount={voteCounts[dump.id] ?? dump.voteCount}
                  voted={myVotes.has(dump.id)}
                  canVote={!!user}
                  castVote={castVote}
                  removeVote={removeVote}
                  isOwner={user?.id === dump.userId}
                />
              ))}
            </ul>
          )}

          <div ref={sentinelRef} />
          {loadingMore && <p className="feed-loading-more">Loading more…</p>}
        </>
      )}

      {/* Followed feed */}
      {tab === "followed" && user && (
        <div className="followed-feed">
          <div className="feed-sort followed-sub-nav">
            <button
              type="button"
              className={`feed-sort-btn${
                followedSection === "users" ? " active" : ""
              }`}
              onClick={() => setFollowedSection("users")}
            >
              From people
            </button>
            <button
              type="button"
              className={`feed-sort-btn${
                followedSection === "playlists" ? " active" : ""
              }`}
              onClick={() => setFollowedSection("playlists")}
            >
              From playlists
            </button>
          </div>

          {followedSection === "users" && (
            <FollowedSubFeed
              state={followedUsersDumps}
              voteCounts={voteCounts}
              myVotes={myVotes}
              user={user}
              castVote={castVote}
              removeVote={removeVote}
              deletedDumpIds={deletedDumpIds}
              emptyMessage="Follow some users to see their dumps here."
              onLoadMore={loadMoreFollowedUsers}
            />
          )}

          {followedSection === "playlists" && (
            <FollowedSubFeed
              state={followedPlaylistsDumps}
              voteCounts={voteCounts}
              myVotes={myVotes}
              user={user}
              castVote={castVote}
              removeVote={removeVote}
              deletedDumpIds={deletedDumpIds}
              emptyMessage="Follow some public playlists to see their dumps here."
              onLoadMore={loadMoreFollowedPlaylists}
            />
          )}
        </div>
      )}
    </div>
  );
}
