import { useEffect, useRef, useState } from "react";
import { Link, useNavigate, useParams } from "react-router";
import { API_URL } from "../config/api.ts";
import type { PlaylistWithDumps, RawPlaylistWithDumps } from "../model.ts";
import { deserializePlaylistWithDumps } from "../model.ts";
import { useAuth } from "../hooks/useAuth.ts";
import { useWS } from "../hooks/useWS.ts";
import { relativeTime } from "../utils/relativeTime.ts";
import { DumpCard } from "../components/DumpCard.tsx";
import { PageShell } from "../components/PageShell.tsx";
import { PageError } from "../components/PageError.tsx";
import { ConfirmModal } from "../components/ConfirmModal.tsx";
import { ImagePicker } from "../components/ImagePicker.tsx";
import { Markdown } from "../components/Markdown.tsx";
import { FollowPlaylistButton } from "../components/FollowButton.tsx";
import { ErrorCard } from "../components/ErrorCard.tsx";
import { friendlyFetchError } from "../utils/apiError.ts";

type LoadState =
  | { status: "loading" }
  | { status: "error"; error: string }
  | { status: "loaded"; playlist: PlaylistWithDumps };

export function PlaylistDetail() {
  const { playlistId } = useParams<{ playlistId: string }>();
  const navigate = useNavigate();
  const { user, authFetch, token } = useAuth();
  const {
    voteCounts,
    myVotes,
    castVote,
    removeVote,
    deletedDumpIds,
    lastPlaylistEvent,
  } = useWS();

  const [state, setState] = useState<LoadState>({ status: "loading" });

  // activeDumpIds: which dumps are currently in the playlist (the canonical set)
  const [activeDumpIds, setActiveDumpIds] = useState<Set<string>>(new Set());

  // fading: dumps whose removal is being animated — same cooldown→dismissing pattern as upvoted list
  const [fading, setFading] = useState<
    Record<string, "cooldown" | "dismissing">
  >({});
  const cancels = useRef<Map<string, () => void>>(new Map());

  const [dragSrcIndex, setDragSrcIndex] = useState<number | null>(null);
  const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);

  const [editOpen, setEditOpen] = useState(false);
  const [editTitle, setEditTitle] = useState("");
  const [editDescription, setEditDescription] = useState("");
  const [editIsPublic, setEditIsPublic] = useState(true);
  const [editSaving, setEditSaving] = useState(false);
  const [editError, setEditError] = useState<string | null>(null);
  const [confirmDelete, setConfirmDelete] = useState(false);
  const [imageFile, setImageFile] = useState<File | null>(null);
  const [imagePreview, setImagePreview] = useState<string | null>(null);

  // prevActiveDumpIds: used by the WS effect to diff incoming dumpIds
  const prevActiveDumpIdsRef = useRef<Set<string> | null>(null);
  const descriptionRef = useRef<HTMLTextAreaElement>(null);

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

  const fetchPlaylist = () => {
    if (!playlistId) return;
    setState({ status: "loading" });
    fetch(`${API_URL}/api/playlists/${playlistId}`, {
      headers: token ? { Authorization: `Bearer ${token}` } : {},
    })
      .then((r) => {
        if (!r.ok) {
          throw new Error(
            r.status === 404 ? "Playlist not found" : `HTTP ${r.status}`,
          );
        }
        return r.json();
      })
      .then((body) => {
        if (!body.success) throw new Error("Failed to load playlist");
        const pl = deserializePlaylistWithDumps(
          body.data as RawPlaylistWithDumps,
        );
        setState({ status: "loaded", playlist: pl });
        const ids = new Set(pl.dumps.map((d) => d.id));
        setActiveDumpIds(ids);
        prevActiveDumpIdsRef.current = ids;
        setFading({});
        cancels.current.forEach((c) => c());
        cancels.current.clear();
      })
      .catch((err) => {
        setState({
          status: "error",
          error: friendlyFetchError(err),
        });
      });
  };

  useEffect(() => {
    fetchPlaylist();
  }, [playlistId]);

  // Start the cooldown→dismissing→gone sequence for a dump being removed.
  // After the sequence completes, the dump is removed from state.playlist.dumps.
  function startFade(id: string) {
    if (cancels.current.has(id)) return; // already fading
    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) return;
        // Remove from the playlist.dumps array now that animation is done
        setState((prev) => {
          if (prev.status !== "loaded") return prev;
          return {
            ...prev,
            playlist: {
              ...prev.playlist,
              dumps: prev.playlist.dumps.filter((d) => d.id !== id),
            },
          };
        });
        kill();
      }, 350);

      kill = () => {
        dead = true;
        clearTimeout(t2);
        setFading((f) => {
          const n = { ...f };
          delete n[id];
          return n;
        });
        cancels.current.delete(id);
      };
      cancels.current.set(id, () => kill());
    }, 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());
  }

  // WS: playlist metadata updated or deleted
  useEffect(() => {
    if (!lastPlaylistEvent || !playlistId) return;
    const ev = lastPlaylistEvent;
    if (ev.playlistId !== playlistId) return;

    if (ev.type === "dumps_updated" && ev.dumpIds) {
      const newIds = new Set(ev.dumpIds);
      const prev = prevActiveDumpIdsRef.current ?? new Set<string>();

      // Removed: were active, not in new set → fade out
      for (const id of prev) {
        if (!newIds.has(id)) {
          setActiveDumpIds((s) => {
            const n = new Set(s);
            n.delete(id);
            return n;
          });
          startFade(id);
        }
      }

      // Re-added while fading → cancel fade, restore to active
      for (const id of newIds) {
        if (!prev.has(id)) {
          if (cancels.current.has(id)) {
            cancels.current.get(id)!();
          }
          // If this is a brand-new dump we haven't seen, re-fetch
          setState((s) => {
            if (s.status !== "loaded") return s;
            const known = s.playlist.dumps.some((d) => d.id === id);
            if (!known) {
              // Trigger a re-fetch asynchronously
              setTimeout(fetchPlaylist, 0);
            }
            return s;
          });
          setActiveDumpIds((s) => new Set([...s, id]));
        }
      }

      // Reorder active dumps to match the new server order,
      // keeping fading dumps at their current visual positions.
      setState((prev) => {
        if (prev.status !== "loaded") return prev;
        const dumpMap = new Map(prev.playlist.dumps.map((d) => [d.id, d]));
        const activeQueue = ev.dumpIds!
          .filter((id) => dumpMap.has(id))
          .map((id) => dumpMap.get(id)!);
        let qi = 0;
        const result = prev.playlist.dumps
          .filter((d) => dumpMap.has(d.id))
          .map((d) => newIds.has(d.id) ? activeQueue[qi++] : d);
        while (qi < activeQueue.length) result.push(activeQueue[qi++]);
        return {
          ...prev,
          playlist: { ...prev.playlist, dumps: result },
        };
      });

      prevActiveDumpIdsRef.current = newIds;
    } else if (ev.type === "updated" && ev.playlist) {
      setState((prev) => {
        if (prev.status !== "loaded") return prev;
        return {
          ...prev,
          playlist: {
            ...prev.playlist,
            title: ev.playlist!.title,
            description: ev.playlist!.description,
            isPublic: ev.playlist!.isPublic,
            imageMime: ev.playlist!.imageMime,
          },
        };
      });
    } else if (ev.type === "deleted") {
      navigate("/");
    }
  }, [lastPlaylistEvent, playlistId]);

  // Filter out globally deleted dumps (dump was deleted entirely, not just removed from playlist)
  useEffect(() => {
    if (deletedDumpIds.size === 0) return;
    setState((prev) => {
      if (prev.status !== "loaded") return prev;
      const filtered = prev.playlist.dumps.filter((d) =>
        !deletedDumpIds.has(d.id)
      );
      if (filtered.length === prev.playlist.dumps.length) return prev;
      return { ...prev, playlist: { ...prev.playlist, dumps: filtered } };
    });
    setActiveDumpIds((prev) => {
      const n = new Set(prev);
      for (const id of deletedDumpIds) n.delete(id);
      return n;
    });
  }, [deletedDumpIds]);

  const handleDragStart = (index: number) => setDragSrcIndex(index);

  const handleDragOver = (e: React.DragEvent, index: number) => {
    e.preventDefault();
    if (dragSrcIndex === null || dragSrcIndex === index) return;
    setState((prev) => {
      if (prev.status !== "loaded") return prev;
      // Only reorder among active dumps
      const activeDumps = prev.playlist.dumps.filter((d) =>
        activeDumpIds.has(d.id)
      );
      const fadingDumps = prev.playlist.dumps.filter((d) =>
        !activeDumpIds.has(d.id)
      );
      const reordered = [...activeDumps];
      const [moved] = reordered.splice(dragSrcIndex, 1);
      reordered.splice(index, 0, moved);
      setDragSrcIndex(index);
      setDragOverIndex(index);
      return {
        ...prev,
        playlist: { ...prev.playlist, dumps: [...reordered, ...fadingDumps] },
      };
    });
  };

  const handleDragEnd = async () => {
    if (state.status !== "loaded" || !playlistId) return;
    setDragSrcIndex(null);
    setDragOverIndex(null);
    const activeDumps = state.playlist.dumps.filter((d) =>
      activeDumpIds.has(d.id)
    );
    try {
      await authFetch(`${API_URL}/api/playlists/${playlistId}/order`, {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ dumpIds: activeDumps.map((d) => d.id) }),
      });
    } catch {
      fetchPlaylist();
    }
  };

  const handleRemoveDump = (dumpId: string) => {
    if (!playlistId) return;
    // Fire-and-forget the API call; animate immediately
    authFetch(`${API_URL}/api/playlists/${playlistId}/dumps/${dumpId}`, {
      method: "DELETE",
    }).catch(() => {
      // On failure, cancel the fade and restore the item
      cancels.current.get(dumpId)?.();
      setActiveDumpIds((prev) => new Set([...prev, dumpId]));
    });
    setActiveDumpIds((prev) => {
      const n = new Set(prev);
      n.delete(dumpId);
      return n;
    });
    startFade(dumpId);
  };

  const handleCancelRemove = (dumpId: string) => {
    if (!playlistId) return;
    cancels.current.get(dumpId)?.();
    setActiveDumpIds((prev) => new Set([...prev, dumpId]));
    // Re-add server-side since DELETE already fired
    authFetch(`${API_URL}/api/playlists/${playlistId}/dumps/${dumpId}`, {
      method: "POST",
    }).catch(() => {});
  };

  useEffect(() => {
    const el = descriptionRef.current;
    if (!el) return;
    el.style.height = "auto";
    el.style.height = `${el.scrollHeight}px`;
  }, [editDescription, editOpen]);

  const openEdit = () => {
    if (state.status !== "loaded") return;
    setEditTitle(state.playlist.title);
    setEditDescription(state.playlist.description ?? "");
    setEditIsPublic(state.playlist.isPublic);
    setImageFile(null);
    setImagePreview(null);
    setEditError(null);
    setEditOpen(true);
  };

  const handleEditSave = async () => {
    if (!playlistId || state.status !== "loaded") return;
    setEditSaving(true);
    setEditError(null);
    try {
      await authFetch(`${API_URL}/api/playlists/${playlistId}`, {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          title: editTitle,
          description: editDescription || undefined,
          isPublic: editIsPublic,
        }),
      });

      if (imageFile) {
        const fd = new FormData();
        fd.append("file", imageFile);
        await authFetch(`${API_URL}/api/playlists/${playlistId}/image`, {
          method: "POST",
          body: fd,
        });
      }

      setEditOpen(false);
      fetchPlaylist();
    } catch (err) {
      setEditError(friendlyFetchError(err));
    } finally {
      setEditSaving(false);
    }
  };

  const handleDelete = async () => {
    if (!playlistId) return;
    await authFetch(`${API_URL}/api/playlists/${playlistId}`, {
      method: "DELETE",
    });
    navigate("/");
  };

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

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

  const { playlist } = state;
  const isOwner = !!user && user.id === playlist.userId;

  // Active dumps in playlist order; fading dumps appended so they stay visible
  const activeDumps = playlist.dumps.filter((d) => activeDumpIds.has(d.id));
  const visibleDumps = playlist.dumps.filter((d) =>
    activeDumpIds.has(d.id) || d.id in fading
  );

  return (
    <PageShell>
      <div className="playlist-detail-header">
        <div className="playlist-detail-header-top">
          {editOpen
            ? (
              <ImagePicker
                src={imagePreview ??
                  (playlist.imageMime
                    ? `${API_URL}/api/playlists/${playlist.id}/image`
                    : null)}
                alt="Cover"
                size={72}
                onChange={(file) => {
                  setImageFile(file);
                  setImagePreview(URL.createObjectURL(file));
                }}
              />
            )
            : playlist.imageMime && (
              <img
                src={`${API_URL}/api/playlists/${playlist.id}/image`}
                alt=""
                className="playlist-detail-img"
              />
            )}

          <div className="playlist-detail-content">
            {editOpen
              ? (
                <div className="playlist-detail-title-row">
                  <input
                    type="text"
                    className="playlist-edit-input"
                    value={editTitle}
                    onChange={(e) => setEditTitle(e.target.value)}
                    autoFocus
                  />
                  <button
                    type="button"
                    className="btn-primary"
                    disabled={editSaving}
                    onClick={handleEditSave}
                  >
                    {editSaving ? "Saving…" : "Save"}
                  </button>
                  <button
                    type="button"
                    className="form-cancel"
                    onClick={() => setEditOpen(false)}
                  >
                    Cancel
                  </button>
                  <button
                    type="button"
                    className="btn-danger"
                    onClick={() => setConfirmDelete(true)}
                  >
                    Delete
                  </button>
                </div>
              )
              : (
                <div className="playlist-detail-title-row">
                  <h1 className="playlist-detail-title">{playlist.title}</h1>
                  {!isOwner && (
                    <FollowPlaylistButton
                      targetPlaylistId={playlist.id}
                      isPublic={playlist.isPublic}
                    />
                  )}
                  {isOwner && (
                    <button
                      type="button"
                      className="playlist-edit-btn"
                      onClick={openEdit}
                    >
                      Edit
                    </button>
                  )}
                </div>
              )}

            {editOpen
              ? (
                <textarea
                  ref={descriptionRef}
                  className="playlist-edit-textarea"
                  value={editDescription}
                  onChange={(e) => setEditDescription(e.target.value)}
                  placeholder="Description (optional)"
                  rows={1}
                />
              )
              : playlist.description && (
                <Markdown className="playlist-detail-description">
                  {playlist.description}
                </Markdown>
              )}

            <div className="playlist-detail-meta">
              {editOpen
                ? (
                  <div className="visibility-toggle playlist-edit-toggle">
                    <button
                      type="button"
                      className={editIsPublic ? "active" : ""}
                      onClick={() => setEditIsPublic(true)}
                    >
                      Public
                    </button>
                    <button
                      type="button"
                      className={!editIsPublic ? "active" : ""}
                      onClick={() => setEditIsPublic(false)}
                    >
                      Private
                    </button>
                  </div>
                )
                : (
                  <>
                    <span
                      className={`playlist-badge${
                        playlist.isPublic ? "" : " playlist-badge--private"
                      }`}
                    >
                      {playlist.isPublic ? "public" : "private"}
                    </span>
                    {playlist.ownerUsername && (
                      <Link
                        to={`/users/${playlist.ownerUsername}`}
                        className="playlist-detail-owner"
                      >
                        @{playlist.ownerUsername}
                      </Link>
                    )}
                    <time
                      dateTime={playlist.createdAt.toISOString()}
                      title={playlist.createdAt.toLocaleString()}
                    >
                      {relativeTime(playlist.createdAt)}
                    </time>
                  </>
                )}
            </div>
            {editError && (
              <ErrorCard title="Failed to save" message={editError} />
            )}
          </div>
        </div>
      </div>

      {visibleDumps.length === 0
        ? <p className="empty-state">No dumps in this playlist yet.</p>
        : (
          <div className="playlist-dump-list">
            {visibleDumps.map((dump) => {
              const isActive = activeDumpIds.has(dump.id);
              const phase = fading[dump.id];
              // drag index is within the active-only list
              const activeIndex = isActive ? activeDumps.indexOf(dump) : -1;
              const cardCls = phase === "cooldown"
                ? "dump-card--fading"
                : phase === "dismissing"
                ? "dump-card--dismissing"
                : undefined;

              return (
                <div
                  key={dump.id}
                  className={`playlist-dump-item${
                    activeIndex === dragOverIndex && isActive
                      ? " playlist-dump-item--drag-over"
                      : ""
                  }`}
                  draggable={isOwner && isActive}
                  onDragStart={isOwner && isActive
                    ? () => handleDragStart(activeIndex)
                    : undefined}
                  onDragOver={isOwner && isActive
                    ? (e) => handleDragOver(e, activeIndex)
                    : undefined}
                  onDragEnd={isOwner && isActive ? handleDragEnd : undefined}
                >
                  {isOwner && isActive && (
                    <span className="drag-handle" aria-hidden>⠿</span>
                  )}
                  <DumpCard
                    dump={dump}
                    voteCount={voteCounts[dump.id] ?? dump.voteCount}
                    voted={myVotes.has(dump.id)}
                    canVote={!!user}
                    castVote={castVote}
                    removeVote={removeVote}
                    className={cardCls}
                    isOwner={!!user && user.id === dump.userId}
                  />
                  {isOwner && (isActive
                    ? (
                      <button
                        type="button"
                        className="playlist-remove-btn"
                        onClick={() => handleRemoveDump(dump.id)}
                        aria-label="Remove from playlist"
                      >
                        ✕
                      </button>
                    )
                    : phase === "cooldown" && (
                      <button
                        type="button"
                        className="playlist-cancel-btn"
                        onClick={() => handleCancelRemove(dump.id)}
                        aria-label="Cancel removal"
                      >
                        Undo
                      </button>
                    ))}
                </div>
              );
            })}
          </div>
        )}
      {confirmDelete && (
        <ConfirmModal
          message="Delete this playlist? This cannot be undone."
          confirmLabel="Delete playlist"
          onConfirm={handleDelete}
          onCancel={() => setConfirmDelete(false)}
        />
      )}
    </PageShell>
  );
}
