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

import { API_URL } from "../config/api.ts";
import type { Dump, PaginatedData, PublicUser } from "../model.ts";
import {
  deserializeAuthResponse,
  deserializeDump,
  deserializePublicUser,
  deserializeUser,
  type RawDump,
  type RawUser,
} from "../model.ts";
import { Avatar } from "../components/Avatar.tsx";
import { DumpCard } from "../components/DumpCard.tsx";
import { PlaylistCard } from "../components/PlaylistCard.tsx";
import { NewPlaylistForm } from "../components/NewPlaylistForm.tsx";
import { PageShell } from "../components/PageShell.tsx";
import { PageError } from "../components/PageError.tsx";
import { useAuth } from "../hooks/useAuth.ts";
import { useWS } from "../hooks/useWS.ts";
import type { Playlist, RawPlaylist } from "../model.ts";
import { deserializePlaylist } from "../model.ts";
import { useFeedCache } from "../hooks/useFeedCache.ts";
import { DumpCreateModal } from "../components/DumpCreateModal.tsx";
import { FollowUserButton } from "../components/FollowButton.tsx";

const PAGE_SIZE = 20;

function InviteButton() {
  const { authFetch } = useAuth();
  const [inviteUrl, setInviteUrl] = useState<string | null>(null);
  const [copied, setCopied] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function generate() {
    try {
      const res = await authFetch(`${API_URL}/api/invites`, { method: "POST" });
      const body = await res.json();
      if (body.success) {
        const url =
          `${globalThis.location.origin}/register?token=${body.data.token}`;
        setInviteUrl(url);
      } else {
        setError("Failed to generate invite");
      }
    } catch {
      setError("Failed to generate invite");
    }
  }

  async function copy() {
    if (!inviteUrl) return;
    await navigator.clipboard.writeText(inviteUrl);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  }

  if (inviteUrl) {
    return (
      <div className="invite-result">
        <span className="invite-url">{inviteUrl}</span>
        <button type="button" className="invite-copy-btn" onClick={copy}>
          {copied ? "Copied!" : "Copy"}
        </button>
      </div>
    );
  }

  return (
    <div className="invite-generate">
      <button type="button" className="invite-btn" onClick={generate}>
        + Invite someone
      </button>
      {error && <p className="form-error">{error}</p>}
    </div>
  );
}

const hydrateDump = (raw: Dump): Dump =>
  deserializeDump(raw as unknown as RawDump);
const hydratePlaylist = (raw: Playlist): Playlist =>
  deserializePlaylist(raw as unknown as RawPlaylist);

interface PaginatedList<T> {
  items: T[];
  hasMore: boolean;
  page: number;
  loadingMore: boolean;
}

function initialList<T>(items: T[], hasMore: boolean): PaginatedList<T> {
  return { items, hasMore, page: 1, loadingMore: false };
}

type ProfileState =
  | { status: "loading" }
  | { status: "error"; error: string }
  | {
    status: "loaded";
    user: PublicUser;
    dumps: PaginatedList<Dump>;
    votes: PaginatedList<Dump>;
    playlists: PaginatedList<Playlist>;
  };

export function UserPublicProfile() {
  const { username } = useParams();
  const navigate = useNavigate();
  const { user: me, authFetch, login, logout, token } = useAuth();
  const {
    voteCounts,
    myVotes,
    lastVoteEvent,
    castVote,
    removeVote,
    lastPlaylistEvent,
    deletedPlaylistIds,
  } = useWS();

  const { cached: cachedDumps, saveState: saveDumps } = useFeedCache<Dump>(
    `feed:profile-dumps:${username ?? ""}`,
    hydrateDump,
  );
  const { cached: cachedVotes, saveState: saveVotes } = useFeedCache<Dump>(
    `feed:profile-votes:${username ?? ""}`,
    hydrateDump,
  );
  const { cached: cachedPlaylists, saveState: savePlaylists } = useFeedCache<
    Playlist
  >(
    `feed:profile-playlists:${username ?? ""}`,
    hydratePlaylist,
  );

  const [state, setState] = useState<ProfileState>({ status: "loading" });
  const [uploading, setUploading] = useState(false);
  const [avatarError, setAvatarError] = useState<string | null>(null);
  const [profileVotedIds, setProfileVotedIds] = useState<Set<string>>(
    new Set(),
  );
  const fileInputRef = useRef<HTMLInputElement>(null);
  const prevMyVotesRef = useRef<Set<string> | null>(null);

  useEffect(() => {
    if (!username) return;
    setState({ status: "loading" });

    const allCached = cachedDumps && cachedVotes && cachedPlaylists;

    if (allCached) {
      // Only fetch the user object (lightweight, always fresh)
      fetch(`${API_URL}/api/users/${username}`)
        .then((r) => r.json())
        .then((body) => {
          if (!body.success) throw new Error("User not found");
          setState({
            status: "loaded",
            user: deserializePublicUser(body.data),
            dumps: {
              items: cachedDumps.items,
              hasMore: cachedDumps.hasMore,
              page: cachedDumps.page,
              loadingMore: false,
            },
            votes: {
              items: cachedVotes.items,
              hasMore: cachedVotes.hasMore,
              page: cachedVotes.page,
              loadingMore: false,
            },
            playlists: {
              items: cachedPlaylists.items,
              hasMore: cachedPlaylists.hasMore,
              page: cachedPlaylists.page,
              loadingMore: false,
            },
          });
          setProfileVotedIds(new Set(cachedVotes.items.map((d) => d.id)));
        })
        .catch((err) =>
          setState({
            status: "error",
            error: err instanceof Error
              ? err.message
              : "Failed to load profile",
          })
        );
      return;
    }

    (async () => {
      try {
        const authHeaders: HeadersInit = token
          ? { Authorization: `Bearer ${token}` }
          : {};
        const [userRes, dumpsRes, votesRes, playlistsRes] = await Promise.all([
          fetch(`${API_URL}/api/users/${username}`),
          fetch(
            `${API_URL}/api/users/${username}/dumps?page=1&limit=${PAGE_SIZE}`,
            { headers: authHeaders },
          ),
          fetch(
            `${API_URL}/api/users/${username}/votes?page=1&limit=${PAGE_SIZE}`,
            { headers: authHeaders },
          ),
          fetch(
            `${API_URL}/api/users/${username}/playlists?page=1&limit=${PAGE_SIZE}`,
            { headers: authHeaders },
          ),
        ]);

        if (!userRes.ok) {
          throw new Error(
            userRes.status === 404
              ? "User not found"
              : `HTTP ${userRes.status}`,
          );
        }

        const [userBody, dumpsBody, votesBody, playlistsBody] = await Promise
          .all([
            userRes.json(),
            dumpsRes.json(),
            votesRes.json(),
            playlistsRes.json(),
          ]);

        const votesData: PaginatedData<RawDump> = votesBody.success
          ? votesBody.data
          : { items: [], total: 0, hasMore: false };
        const playlistsData: PaginatedData<RawPlaylist> = playlistsBody.success
          ? playlistsBody.data
          : { items: [], total: 0, hasMore: false };
        const dumpsData: PaginatedData<RawDump> = dumpsBody.success
          ? dumpsBody.data
          : { items: [], total: 0, hasMore: false };

        const voteItems = votesData.items.map(deserializeDump);
        setState({
          status: "loaded",
          user: deserializePublicUser(userBody.data),
          dumps: initialList(
            dumpsData.items.map(deserializeDump),
            dumpsData.hasMore,
          ),
          votes: initialList(voteItems, votesData.hasMore),
          playlists: initialList(
            playlistsData.items.map(deserializePlaylist),
            playlistsData.hasMore,
          ),
        });
        setProfileVotedIds(new Set(voteItems.map((d) => d.id)));
      } catch (err) {
        setState({
          status: "error",
          error: err instanceof Error ? err.message : "Failed to load profile",
        });
      }
    })();
  }, [username]);

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

  // Own profile: keep profileVotedIds in sync with myVotes
  useEffect(() => {
    if (!profileUserId || me?.id !== profileUserId) return;
    setProfileVotedIds(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 voteIds = new Set(s.votes.items.map((d) => d.id));
      const toAdd = s.dumps.items.filter((d) =>
        myVotes.has(d.id) && !prev.has(d.id) && !voteIds.has(d.id)
      );
      if (toAdd.length === 0) return s;
      return {
        ...s,
        votes: { ...s.votes, items: [...toAdd, ...s.votes.items] },
      };
    });
    prevMyVotesRef.current = new Set(myVotes);
  }, [myVotes, me, profileUserId]);

  // Real-time upvoted list sync via WS vote events
  useEffect(() => {
    if (!lastVoteEvent || !profileUserId) return;
    const { dumpId, voterId, action } = lastVoteEvent;
    if (voterId !== profileUserId) return;
    const isOwnProfile = me?.id === profileUserId;

    if (action === "remove") {
      if (!isOwnProfile) {
        setProfileVotedIds((prev) => {
          const n = new Set(prev);
          n.delete(dumpId);
          return n;
        });
      }
    } else {
      if (!isOwnProfile) {
        setProfileVotedIds((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.items.some((d) => d.id === dumpId)
            ) {
              return s;
            }
            return {
              ...s,
              votes: { ...s.votes, items: [dump, ...s.votes.items] },
            };
          });
        })
        .catch(() => {});
    }
  }, [lastVoteEvent, me, profileUserId]);

  // Real-time playlist updates
  useEffect(() => {
    if (!lastPlaylistEvent || state.status !== "loaded") return;
    const profileUserId = state.user.id;
    const isOwnProfile = me?.id === profileUserId;
    const ev = lastPlaylistEvent;

    if (ev.type === "created" && ev.playlist?.userId === profileUserId) {
      if (ev.playlist.isPublic || isOwnProfile) {
        setState((s) => {
          if (s.status !== "loaded") return s;
          if (s.playlists.items.some((p) => p.id === ev.playlist!.id)) return s;
          return {
            ...s,
            playlists: {
              ...s.playlists,
              items: [ev.playlist!, ...s.playlists.items],
            },
          };
        });
      }
    } else if (ev.type === "updated" && ev.playlist?.userId === profileUserId) {
      setState((s) => {
        if (s.status !== "loaded") return s;
        return {
          ...s,
          playlists: {
            ...s.playlists,
            items: s.playlists.items
              .map((p) => p.id === ev.playlist!.id ? ev.playlist! : p)
              .filter((p) => p.isPublic || isOwnProfile),
          },
        };
      });
    } else if (ev.type === "deleted") {
      setState((s) => {
        if (s.status !== "loaded") return s;
        return {
          ...s,
          playlists: {
            ...s.playlists,
            items: s.playlists.items.filter((p) => p.id !== ev.playlistId),
          },
        };
      });
    }
  }, [lastPlaylistEvent, me]);

  useEffect(() => {
    if (deletedPlaylistIds.size === 0 || state.status !== "loaded") return;
    setState((s) => {
      if (s.status !== "loaded") return s;
      const filtered = s.playlists.items.filter((p) =>
        !deletedPlaylistIds.has(p.id)
      );
      if (filtered.length === s.playlists.items.length) return s;
      return { ...s, playlists: { ...s.playlists, items: filtered } };
    });
  }, [deletedPlaylistIds]);

  // Save scroll position + loaded state to sessionStorage on scroll
  useEffect(() => {
    if (state.status !== "loaded") return;
    let timer: ReturnType<typeof setTimeout>;
    const onScroll = () => {
      clearTimeout(timer);
      timer = setTimeout(() => {
        if (state.status !== "loaded") return;
        const y = globalThis.scrollY;
        saveDumps(state.dumps.items, state.dumps.page, state.dumps.hasMore, y);
        saveVotes(state.votes.items, state.votes.page, state.votes.hasMore, y);
        savePlaylists(
          state.playlists.items,
          state.playlists.page,
          state.playlists.hasMore,
          y,
        );
      }, 100);
    };
    globalThis.addEventListener("scroll", onScroll, { passive: true });
    return () => {
      globalThis.removeEventListener("scroll", onScroll);
      clearTimeout(timer);
    };
  }, [state, saveDumps, saveVotes, savePlaylists]);

  // Restore scroll position after cache restoration
  const scrollRestored = useRef(false);
  useLayoutEffect(() => {
    if (cachedDumps?.scrollY == null || scrollRestored.current) return;
    if (state.status === "loaded") {
      globalThis.scrollTo(0, cachedDumps.scrollY);
      scrollRestored.current = true;
    }
  }, [state.status, cachedDumps]);

  const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file || state.status !== "loaded") return;

    setAvatarError(null);
    setUploading(true);

    try {
      const formData = new FormData();
      formData.append("file", file);

      const res = await authFetch(`${API_URL}/api/avatars/me`, {
        method: "POST",
        body: formData,
      });
      const body = await res.json() as {
        success: boolean;
        data?: RawUser;
        error?: { message: string };
      };

      if (!res.ok || !body.success) {
        setAvatarError(body.error?.message ?? "Upload failed");
        return;
      }

      const storedRaw = localStorage.getItem("authResponse");
      if (storedRaw && body.data) {
        login({
          ...deserializeAuthResponse(JSON.parse(storedRaw)),
          user: deserializeUser(body.data),
        });
      }

      setState((prev) =>
        prev.status === "loaded"
          ? {
            ...prev,
            user: { ...prev.user, avatarMime: body.data?.avatarMime },
          }
          : prev
      );
    } catch {
      setAvatarError("Upload failed");
    } finally {
      setUploading(false);
      if (fileInputRef.current) fileInputRef.current.value = "";
    }
  };

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

  if (state.status === "error") {
    return (
      <PageError
        message={state.error}
        actions={
          <>
            <button
              className="logout-btn"
              type="button"
              onClick={() => navigate("/")}
            >
              ← Back
            </button>
            {me && (
              <button className="logout-btn" type="button" onClick={logout}>
                Log out
              </button>
            )}
          </>
        }
      />
    );
  }

  const { user: profileUser, dumps, votes, playlists } = state;
  const isOwnProfile = me?.username === profileUser.username;

  return (
    <PageShell>
      <div className="profile-header">
        <div className="profile-avatar-wrapper">
          <Avatar
            userId={profileUser.id}
            username={profileUser.username}
            hasAvatar={!!profileUser.avatarMime}
            size={72}
          />
          {isOwnProfile && (
            <label className="avatar-change-overlay" title="Change avatar">
              {uploading ? "…" : "✎"}
              <input
                ref={fileInputRef}
                type="file"
                accept="image/jpeg,image/png,image/gif,image/webp"
                onChange={handleAvatarUpload}
                disabled={uploading}
                style={{ display: "none" }}
              />
            </label>
          )}
        </div>
        <div>
          <h1 className="profile-username">{profileUser.username}</h1>
          {profileUser.invitedByUsername
            ? (
              <p className="profile-invited-by">
                invited by{" "}
                <Link
                  to={`/users/${profileUser.invitedByUsername}`}
                  className="profile-invited-by-link"
                >
                  @{profileUser.invitedByUsername}
                </Link>
              </p>
            )
            : (
              <p className="profile-invited-by profile-invited-by--founding">
                O.G.
              </p>
            )}
          {avatarError && <p className="form-error">{avatarError}</p>}
          {!isOwnProfile && (
            <FollowUserButton
              targetUserId={profileUser.id}
              targetUsername={profileUser.username}
            />
          )}
          {isOwnProfile && (
            <div className="profile-own-actions">
              <InviteButton />
              <button type="button" className="logout-btn" onClick={logout}>
                Log out
              </button>
            </div>
          )}
        </div>
      </div>

      <div className="profile-columns">
        <DumpList
          title={`Dumps (${dumps.items.length}${dumps.hasMore ? "+" : ""})`}
          dumps={dumps.items}
          voteCounts={voteCounts}
          myVotes={myVotes}
          canVote={!!me}
          castVote={castVote}
          removeVote={removeVote}
          isOwnProfile={isOwnProfile}
          viewAllHref={`/users/${profileUser.username}/dumps`}
        />

        <UpvotedDumpList
          title={`Upvoted (${profileVotedIds.size}${votes.hasMore ? "+" : ""})`}
          dumps={votes.items}
          votedIds={profileVotedIds}
          voteCounts={voteCounts}
          myVotes={myVotes}
          canVote={!!me}
          castVote={castVote}
          removeVote={removeVote}
          viewAllHref={`/users/${profileUser.username}/upvoted`}
        />
      </div>

      <section className="profile-section" id="playlists">
        <div className="profile-section-header">
          <h2 className="profile-section-title">
            Playlists ({playlists.items.length}
            {playlists.hasMore ? "+" : ""})
          </h2>
          {isOwnProfile && (
            <NewPlaylistForm
              onCreated={(p) =>
                setState((s) => {
                  if (s.status !== "loaded") return s;
                  if (s.playlists.items.some((pl) => pl.id === p.id)) return s;
                  return {
                    ...s,
                    playlists: {
                      ...s.playlists,
                      items: [p, ...s.playlists.items],
                    },
                  };
                })}
            />
          )}
        </div>
        {playlists.items.length === 0
          ? <p className="empty-state">No playlists yet.</p>
          : (
            <ul className="dump-feed">
              {playlists.items.map((p) => (
                <PlaylistCard key={p.id} playlist={p} isOwner={isOwnProfile} />
              ))}
            </ul>
          )}
        {playlists.items.length > 0 && (
          <Link
            to={`/users/${profileUser.username}/playlists`}
            className="profile-view-all"
          >
            View all →
          </Link>
        )}
      </section>
    </PageShell>
  );
}

// ── Plain dump list ──────────────────────────────────────────────────────────

function DumpList(
  {
    title,
    dumps,
    voteCounts,
    myVotes,
    canVote,
    castVote,
    removeVote,
    isOwnProfile,
    viewAllHref,
  }: {
    title: string;
    dumps: Dump[];
    voteCounts: Record<string, number>;
    myVotes: Set<string>;
    canVote: boolean;
    castVote: (id: string) => void;
    removeVote: (id: string) => void;
    isOwnProfile?: boolean;
    viewAllHref: string;
  },
) {
  const [createModalOpen, setCreateModalOpen] = useState(false);
  return (
    <section className="profile-section">
      <div className="profile-section-header">
        <h2 className="profile-section-title">{title}</h2>
        {isOwnProfile && (
          <button
            type="button"
            className="new-playlist-toggle"
            onClick={() => setCreateModalOpen(true)}
          >
            + New dump
          </button>
        )}
      </div>
      {createModalOpen && (
        <DumpCreateModal onClose={() => setCreateModalOpen(false)} />
      )}
      {dumps.length === 0
        ? <p className="empty-state">Nothing here yet.</p>
        : (
          <ul className="dump-feed">
            {dumps.map((dump) => (
              <DumpCard
                key={dump.id}
                dump={dump}
                voteCount={voteCounts[dump.id] ?? dump.voteCount}
                voted={myVotes.has(dump.id)}
                canVote={canVote}
                castVote={castVote}
                removeVote={removeVote}
                isOwner={isOwnProfile}
              />
            ))}
          </ul>
        )}
      {dumps.length > 0 && (
        <Link to={viewAllHref} className="profile-view-all">View all →</Link>
      )}
    </section>
  );
}

// ── Upvoted list: fades items out when votes are removed ─────────────────────

function UpvotedDumpList(
  {
    title,
    dumps,
    votedIds,
    voteCounts,
    myVotes,
    canVote,
    castVote,
    removeVote,
    viewAllHref,
  }: {
    title: string;
    dumps: Dump[];
    votedIds: Set<string>;
    voteCounts: Record<string, number>;
    myVotes: Set<string>;
    canVote: boolean;
    castVote: (id: string) => void;
    removeVote: (id: string) => void;
    viewAllHref: string;
  },
) {
  const [fading, setFading] = useState<
    Record<string, "cooldown" | "dismissing">
  >({});
  const cancels = useRef<Map<string, () => void>>(new Map());
  const prevVotedIds = useRef<Set<string> | null>(null);

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

  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 visibleDumps = dumps.filter((d) =>
    votedIds.has(d.id) || d.id in fading
  );

  return (
    <section className="profile-section">
      <div className="profile-section-header">
        <h2 className="profile-section-title">{title}</h2>
      </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={canVote}
                  castVote={castVote}
                  removeVote={removeVote}
                  className={extraCls}
                />
              );
            })}
          </ul>
        )}
      {visibleDumps.length > 0 && (
        <Link to={viewAllHref} className="profile-view-all">View all →</Link>
      )}
    </section>
  );
}
