import { useEffect, useState } from "react";
import { Link, useLocation, useNavigate, useParams } from "react-router";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { dumpThumbnailUrl, dumpUrl } from "../utils/urls.ts";
import { can } from "../utils/permissions.ts";
import { AddToPlaylistModal } from "../components/AddToPlaylistModal.tsx";

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

import type {
  Comment,
  Dump,
  PublicUser,
  RawComment,
  RawDump,
  UpdateDumpRequest,
} from "../model.ts";
import {
  deserializeComment,
  deserializeDump,
  deserializePublicUser,
  parseAPIResponse,
} from "../model.ts";

import { useAuth } from "../hooks/useAuth.ts";
import { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
import { relativeTime } from "../utils/relativeTime.ts";
import { useWS } from "../hooks/useWS.ts";
import { Avatar } from "../components/Avatar.tsx";
import RichContentCard from "../components/RichContentCard.tsx";
import FilePreview from "../components/FilePreview.tsx";
import { VoteButton } from "../components/VoteButton.tsx";
import { DumpCard } from "../components/DumpCard.tsx";
import { PageShell } from "../components/PageShell.tsx";
import { PageError } from "../components/PageError.tsx";
import { Markdown } from "../components/Markdown.tsx";
import { CommentThread } from "../components/CommentThread.tsx";
import { Tooltip } from "../components/Tooltip.tsx";
import { friendlyFetchError } from "../utils/apiError.ts";

type DumpState =
  | { status: "loading" }
  | { status: "error"; error: string }
  | { status: "loaded"; dump: Dump };

export function Dump() {
  const { selectedDump } = useParams();
  const location = useLocation();
  const navigate = useNavigate();
  const preloaded = (location.state as { dump?: Dump } | null)?.dump ?? null;

  const [dumpState, setDumpState] = useState<DumpState>(
    preloaded ? { status: "loaded", dump: preloaded } : { status: "loading" },
  );
  const [op, setOp] = useState<PublicUser | null>(null);
  const [playlistModalOpen, setPlaylistModalOpen] = useState(false);

  useDocumentTitle(
    dumpState.status === "loaded"
      ? dumpState.dump.title
      : dumpState.status === "error"
      ? null
      : undefined,
  );

  const [comments, setComments] = useState<Comment[]>([]);
  const [relatedDumps, setRelatedDumps] = useState<Dump[]>([]);

  const [titleEditing, setTitleEditing] = useState(false);
  const [titleDraft, setTitleDraft] = useState("");
  const [titleSaving, setTitleSaving] = useState(false);
  const [titleError, setTitleError] = useState<string | null>(null);

  const { user, token, authFetch } = useAuth();
  const {
    voteCounts,
    myVotes,
    castVote,
    removeVote,
    lastDumpEvent,
    lastCommentEvent,
  } = useWS();

  useEffect(() => {
    if (!selectedDump) return;
    const controller = new AbortController();

    if (preloaded) {
      fetch(`${API_URL}/api/users/by-id/${preloaded.userId}`, {
        signal: controller.signal,
      })
        .then((r) => r.json())
        .then((r) => r.success && setOp(deserializePublicUser(r.data)))
        .catch(() => {});
      return () => controller.abort();
    }

    (async () => {
      setDumpState({ status: "loading" });
      setOp(null);
      try {
        const res = await fetch(`${API_URL}/api/dumps/${selectedDump}`, {
          cache: "no-store",
          signal: controller.signal,
          headers: token ? { Authorization: `Bearer ${token}` } : {},
        });
        const apiResponse = parseAPIResponse<RawDump>(await res.json());
        if (!apiResponse.success) {
          throw new Error(apiResponse.error.message);
        }
        const dump: Dump = deserializeDump(apiResponse.data);
        setDumpState({ status: "loaded", dump });

        fetch(`${API_URL}/api/users/by-id/${dump.userId}`, {
          signal: controller.signal,
        })
          .then((r) => r.json())
          .then((r) => r.success && setOp(deserializePublicUser(r.data)))
          .catch(() => {});
      } catch (err) {
        if ((err as Error).name === "AbortError") return;
        setDumpState({ status: "error", error: friendlyFetchError(err) });
      }
    })();
    return () => controller.abort();
  }, [selectedDump, preloaded, token]);

  useEffect(() => {
    if (!lastDumpEvent) return;
    // Subscribing to the WS-pushed dump-update stream — setState here mirrors
    // external state into local state, no synchronous alternative.
    // eslint-disable-next-line react-hooks/set-state-in-effect
    setDumpState((prev) => {
      if (prev.status !== "loaded" || prev.dump.id !== lastDumpEvent.id) {
        return prev;
      }
      return { status: "loaded", dump: lastDumpEvent };
    });
  }, [lastDumpEvent]);

  // Fetch comments when dump loads
  useEffect(() => {
    if (!selectedDump) return;
    const controller = new AbortController();
    fetch(`${API_URL}/api/dumps/${selectedDump}/comments`, {
      signal: controller.signal,
      headers: token ? { Authorization: `Bearer ${token}` } : {},
    })
      .then((r) => r.json())
      .then((body) => {
        if (body.success) {
          setComments((body.data as RawComment[]).map(deserializeComment));
        }
      })
      .catch(() => {});
    return () => controller.abort();
  }, [selectedDump, token]);

  // Fetch related dumps (backlinks) when dump loads
  useEffect(() => {
    if (!selectedDump) return;
    const controller = new AbortController();
    fetch(`${API_URL}/api/dumps/${selectedDump}/related`, {
      signal: controller.signal,
      headers: token ? { Authorization: `Bearer ${token}` } : {},
    })
      .then((r) => r.json())
      .then((body) => {
        if (body.success) {
          setRelatedDumps((body.data as RawDump[]).map(deserializeDump));
        }
      })
      .catch(() => {});
    return () => controller.abort();
  }, [selectedDump, token]);

  // Scroll to and highlight a comment when navigating to #comment-{id}
  useEffect(() => {
    if (!location.hash.startsWith("#comment-")) return;
    const id = location.hash.slice(1);
    const el = document.getElementById(id);
    if (!el) return;
    el.scrollIntoView({ behavior: "smooth", block: "start" });
    el.classList.add("comment-node--highlight");
    const timer = setTimeout(
      () => el.classList.remove("comment-node--highlight"),
      2000,
    );
    return () => clearTimeout(timer);
  }, [comments, location.hash]);

  // React to WS comment events
  const loadedDumpId = dumpState.status === "loaded" ? dumpState.dump.id : null;
  useEffect(() => {
    if (
      !lastCommentEvent || !loadedDumpId ||
      lastCommentEvent.dumpId !== loadedDumpId
    ) return;
    // Subscribing to the WS-pushed comment-event stream — setState here
    // mirrors external state into local state, no synchronous alternative.
    if (lastCommentEvent.type === "created" && lastCommentEvent.comment) {
      // eslint-disable-next-line react-hooks/set-state-in-effect
      setComments((prev) => {
        if (prev.some((c) => c.id === lastCommentEvent.comment!.id)) {
          return prev;
        }
        return [...prev, lastCommentEvent.comment!];
      });
    } else if (
      lastCommentEvent.type === "deleted" && lastCommentEvent.commentId
    ) {
      setComments((prev) =>
        prev.map((c) =>
          c.id === lastCommentEvent.commentId
            ? { ...c, deleted: true, body: "" }
            : c
        )
      );
    } else if (
      lastCommentEvent.type === "updated" && lastCommentEvent.comment
    ) {
      setComments((prev) =>
        prev.map((c) =>
          c.id === lastCommentEvent.comment!.id ? lastCommentEvent.comment! : c
        )
      );
    }
  }, [lastCommentEvent, loadedDumpId]);

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

  if (dumpState.status === "error") {
    return (
      <PageError
        message={dumpState.error}
        actions={
          <>
            <button
              className="btn-border"
              type="button"
              onClick={() => globalThis.location.reload()}
            >
              <Trans>Retry</Trans>
            </button>
            <button
              className="btn-border"
              type="button"
              onClick={() => navigate("/")}
            >
              <Trans>← Back to all dumps</Trans>
            </button>
          </>
        }
      />
    );
  }

  const { dump } = dumpState;
  const canEdit = !!user &&
    (dump.userId === user.id || can(user, "dump:moderate"));

  const handleTitleSave = async () => {
    const trimmed = titleDraft.trim();
    if (!trimmed || trimmed.length > VALIDATION.DUMP_TITLE_MAX) return;

    setTitleSaving(true);
    setTitleError(null);
    try {
      const res = await authFetch(`${API_URL}/api/dumps/${dump.id}`, {
        method: "PUT",
        body: JSON.stringify(
          { title: trimmed } satisfies UpdateDumpRequest,
        ),
      });
      const apiResponse = parseAPIResponse<RawDump>(await res.json());
      if (!apiResponse.success) {
        setTitleError(apiResponse.error.message);
        return;
      }
      const updatedDump = deserializeDump(apiResponse.data);
      setTitleEditing(false);
      if (updatedDump.slug !== dump.slug) {
        navigate(dumpUrl(updatedDump), {
          replace: true,
          state: { dump: updatedDump },
        });
      } else {
        setDumpState({ status: "loaded", dump: updatedDump });
      }
    } catch (err) {
      setTitleError(friendlyFetchError(err));
    } finally {
      setTitleSaving(false);
    }
  };

  return (
    <PageShell>
      <div className="dump-detail">
        {/* Post header */}
        <div className="dump-post-header">
          <div className="dump-header-block">
            <div className="dump-header-left">
              <VoteButton
                dumpId={dump.id}
                count={voteCounts[dump.id] ?? dump.voteCount}
                voted={myVotes.has(dump.id)}
                disabled={!user}
                onCast={castVote}
                onRemove={removeVote}
              />
              {user && (
                <button
                  type="button"
                  className="btn-add-playlist"
                  onClick={() => setPlaylistModalOpen(true)}
                >
                  <Trans>+ Playlist</Trans>
                </button>
              )}
            </div>
            <div className="dump-header-right">
              {titleEditing
                ? (
                  <div className="dump-title-editor">
                    <input
                      type="text"
                      className="dump-title-input"
                      value={titleDraft}
                      onChange={(e) => setTitleDraft(e.target.value)}
                      onKeyDown={(e) => {
                        if (e.key === "Enter") {
                          e.preventDefault();
                          handleTitleSave();
                        } else if (e.key === "Escape") {
                          setTitleEditing(false);
                        }
                      }}
                      maxLength={VALIDATION.DUMP_TITLE_MAX}
                      disabled={titleSaving}
                      autoFocus
                    />
                    <button
                      type="button"
                      className="btn-primary"
                      onClick={handleTitleSave}
                      disabled={titleSaving || !titleDraft.trim()}
                    >
                      {titleSaving
                        ? <Trans>Saving…</Trans>
                        : <Trans>Save</Trans>}
                    </button>
                    <button
                      type="button"
                      className="form-cancel"
                      onClick={() => setTitleEditing(false)}
                      disabled={titleSaving}
                    >
                      <Trans>Cancel</Trans>
                    </button>
                    {titleError && (
                      <p className="dump-title-error">{titleError}</p>
                    )}
                  </div>
                )
                : (
                  <div
                    className={`dump-title-view${
                      canEdit ? " dump-title-view--editable" : ""
                    }`}
                    onClick={canEdit
                      ? () => {
                        setTitleDraft(dump.title);
                        setTitleError(null);
                        setTitleEditing(true);
                      }
                      : undefined}
                  >
                    <h1 className="dump-title">{dump.title}</h1>
                    {canEdit && (
                      <span className="dump-title-edit-btn" aria-hidden>
                        ✎
                      </span>
                    )}
                  </div>
                )}
              <div className="dump-op">
                <Avatar
                  userId={dump.userId}
                  username={op?.username ?? "?"}
                  hasAvatar={!!op?.avatarMime}
                  size={22}
                />
                {op
                  ? (
                    <Link to={`/users/${op.username}`} className="dump-op-link">
                      {op.username}
                    </Link>
                  )
                  : <span className="dump-op-link">…</span>}
                <Tooltip text={dump.createdAt.toLocaleString()}>
                  <time
                    className="dump-card-date"
                    dateTime={dump.createdAt.toISOString()}
                  >
                    {relativeTime(dump.createdAt)}
                  </time>
                </Tooltip>
                {dump.updatedAt && (
                  <Tooltip text={t`Edited ${dump.updatedAt.toLocaleString()}`}>
                    <span className="dump-edited-label">
                      <Trans>edited {relativeTime(dump.updatedAt)}</Trans>
                    </span>
                  </Tooltip>
                )}
                {dump.isPrivate && (
                  <span className="dump-card-private-badge">
                    <Trans>private</Trans>
                  </span>
                )}
              </div>
            </div>
          </div>

          {dump.comment && (
            <Markdown className="dump-comment">{dump.comment}</Markdown>
          )}
        </div>

        {/* Main content */}
        <div className="dump-rich-content">
          {dump.kind === "file"
            ? <FilePreview dump={dump} global />
            : dump.richContent
            ? (
              <RichContentCard
                richContent={dump.richContent}
                thumbnailOverrideUrl={dump.thumbnailMime
                  ? dumpThumbnailUrl(dump, token)
                  : undefined}
              />
            )
            : (
              <a
                href={dump.url}
                target="_blank"
                rel="noopener noreferrer"
                className="dump-url-link"
              >
                {dump.url}
              </a>
            )}
        </div>

        {/* Actions */}
        <div className="dump-actions">
          {canEdit && (
            <Link to={`${dumpUrl(dump)}/edit`}>
              <Trans>Edit</Trans>
            </Link>
          )}
          <Link to="/">
            <Trans>← Back to all dumps</Trans>
          </Link>
        </div>

        {/* Related (backlinks) */}
        {relatedDumps.length > 0 && (
          <section className="related-section">
            <h2 className="related-section-title">
              <Trans>Related</Trans>
            </h2>
            <ul className="dump-feed">
              {relatedDumps.map((related) => (
                <DumpCard
                  key={related.id}
                  dump={related}
                  voteCount={voteCounts[related.id] ?? related.voteCount}
                  voted={myVotes.has(related.id)}
                  canVote={!!user}
                  castVote={castVote}
                  removeVote={removeVote}
                />
              ))}
            </ul>
          </section>
        )}

        {/* Comments */}
        <CommentThread
          dumpId={dump.id}
          comments={comments}
          currentUser={user}
          token={token}
          onCommentCreated={(c) =>
            setComments((prev) =>
              prev.some((x) => x.id === c.id) ? prev : [...prev, c]
            )}
          onCommentDeleted={(id) =>
            setComments((prev) =>
              prev.map((c) =>
                c.id === id ? { ...c, deleted: true, body: "" } : c
              )
            )}
          onCommentUpdated={(updated) =>
            setComments((prev) =>
              prev.map((c) => (c.id === updated.id ? updated : c))
            )}
        />
      </div>
      {playlistModalOpen && (
        <AddToPlaylistModal
          dumpId={dump.id}
          onClose={() => setPlaylistModalOpen(false)}
        />
      )}
    </PageShell>
  );
}
