import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Send } from "lucide-react";
import { api, HttpError, type Message, type MessageUser } from "../lib/api";
import { useLiveQuery } from "../lib/useLiveQuery";
import { relativeTime } from "../lib/format";
import { notify } from "../lib/notifications";
import { partnerPub, POLL_MS, useKeyState } from "../lib/messaging";
import * as e2ee from "../lib/e2ee";
import KeyGate from "./KeyGate";

// The message list + composer for one conversation, shared by the full Messages page and the
// floating dock window. Handles client-side decryption and encrypt-on-send; the surrounding
// chrome (back button / minimize / close) is the caller's. Self-contained key gating: prompts
// setup/unlock (server-truth) for a user conversation until messaging is ready.
export default function ChatThread({
  meId,
  partnerId,
  isSystem,
}: {
  meId: number;
  partnerId: number;
  isSystem: boolean;
}) {
  const { t } = useTranslation();
  const qc = useQueryClient();
  const [draft, setDraft] = useState("");
  const [plain, setPlain] = useState<Record<number, string>>({});
  const bottomRef = useRef<HTMLDivElement>(null);
  const ready = useKeyState() === "ready";
  const needsKey = !isSystem && !ready;

  const q = useLiveQuery<{ partner: MessageUser; items: Message[] }>(
    ["thread", partnerId],
    () => api.thread(partnerId),
    { intervalMs: POLL_MS, enabled: !needsKey }
  );
  const items = q.data?.items ?? [];

  // Decrypt the thread when fresh server data arrives. Keyed on dataUpdatedAt (not `items`): the
  // `?? []` fallback is a new array reference every render, which would otherwise re-run this
  // effect (and setPlain a new object) on every render while data is loading — a render storm.
  useEffect(() => {
    if (isSystem || !ready) return;
    let cancelled = false;
    (async () => {
      const out: Record<number, string> = {};
      for (const m of q.data?.items ?? []) {
        if (m.ciphertext && m.iv) {
          try {
            const pub = await partnerPub(partnerId);
            out[m.id] = await e2ee.decryptFrom(partnerId, pub, m.ciphertext, m.iv);
          } catch {
            out[m.id] = "⚠ " + t("messages.cantDecrypt");
          }
        }
      }
      if (!cancelled) setPlain(out);
    })();
    return () => {
      cancelled = true;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [q.dataUpdatedAt, ready, isSystem, partnerId, t]);

  // Opening the thread (or a new incoming message arriving) marks messages read server-side, so
  // refresh the badges — but ONLY when the incoming-message count actually changed, not on every
  // 20s poll (which would needlessly refetch /conversations + /unread_count each tick).
  const lastIncoming = useRef(-1);
  const lastPartner = useRef(partnerId);
  useEffect(() => {
    if (!q.dataUpdatedAt) return;
    // The Messages page reuses one ChatThread instance across conversation switches (not keyed by
    // partnerId), so reset the counter when the partner changes — otherwise switching to a thread
    // with the same incoming-count would skip the open-marks-read refresh.
    if (lastPartner.current !== partnerId) {
      lastPartner.current = partnerId;
      lastIncoming.current = -1;
    }
    const incoming = (q.data?.items ?? []).filter((m) => m.recipient_id === meId).length;
    if (incoming !== lastIncoming.current) {
      lastIncoming.current = incoming;
      qc.invalidateQueries({ queryKey: ["conversations"] });
      qc.invalidateQueries({ queryKey: ["message-unread"] });
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [q.dataUpdatedAt, partnerId, meId, qc]);

  // Scroll to the newest message when the count changes (open / new message) — NOT on `plain`,
  // which gets a fresh object on every poll's decrypt pass and would yank a scrolled-up reader
  // back to the bottom even when nothing new arrived.
  useEffect(() => {
    bottomRef.current?.scrollIntoView({ block: "end" });
  }, [items.length]);

  const send = useMutation({
    mutationFn: async (text: string) => {
      const pub = await partnerPub(partnerId);
      const { ciphertext, iv } = await e2ee.encryptFor(partnerId, pub, text);
      return api.sendMessage(partnerId, ciphertext, iv);
    },
    onSuccess: () => {
      setDraft("");
      qc.invalidateQueries({ queryKey: ["thread", partnerId] });
      qc.invalidateQueries({ queryKey: ["conversations"] });
    },
    onError: (e) => {
      // The draft is kept (only cleared on success) so the user can retry. 400/409/422/500 already
      // raised the global error dialog; 404 (recipient gone) and 429 (rate-limited) are "caller-
      // handled" in api.req (no modal), so surface those here or the send fails silently.
      if (e instanceof HttpError && (e.status === 404 || e.status === 429)) {
        notify({ level: "error", message: e.detail || t("messages.sendFailed") });
      }
    },
  });

  const submit = () => {
    const text = draft.trim();
    if (text && !send.isPending) send.mutate(text);
  };

  if (needsKey) {
    return (
      <div className="flex-1 grid place-items-center p-3 overflow-y-auto">
        <div className="w-full max-w-sm">
          <KeyGate meId={meId} compact />
        </div>
      </div>
    );
  }

  return (
    <>
      <div className="flex-1 overflow-y-auto p-3 flex flex-col gap-2">
        {items.length === 0 ? (
          <div className="m-auto text-sm text-muted">{t("messages.threadEmpty")}</div>
        ) : (
          items.map((m) => {
            const mine = m.sender_id === meId;
            const text = m.kind === "system" ? m.body ?? "" : plain[m.id] ?? "…";
            return (
              <div key={m.id} className={`flex ${mine ? "justify-end" : "justify-start"}`}>
                <div
                  className={`max-w-[80%] rounded-2xl px-3 py-2 text-sm whitespace-pre-wrap break-words ${
                    mine ? "bg-accent text-accent-fg rounded-br-sm" : "bg-card rounded-bl-sm"
                  }`}
                >
                  {text}
                  <div className={`text-[10px] mt-1 ${mine ? "text-accent-fg/70" : "text-muted"}`}>
                    {relativeTime(m.created_at)}
                  </div>
                </div>
              </div>
            );
          })
        )}
        <div ref={bottomRef} />
      </div>

      {isSystem ? (
        <div className="p-2 border-t border-border text-center text-xs text-muted">{t("messages.systemReadOnly")}</div>
      ) : (
        <div className="flex items-end gap-2 p-2 border-t border-border">
          <textarea
            value={draft}
            onChange={(e) => setDraft(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === "Enter" && !e.shiftKey) {
                e.preventDefault();
                submit();
              }
            }}
            rows={1}
            placeholder={t("messages.writePlaceholder")}
            className="flex-1 resize-none max-h-28 bg-card border border-border rounded-xl px-3 py-2 text-sm outline-none focus:border-accent"
          />
          <button
            onClick={submit}
            disabled={!draft.trim() || send.isPending}
            className="shrink-0 p-2.5 rounded-xl bg-accent text-accent-fg hover:opacity-90 transition disabled:opacity-40"
            aria-label={t("messages.send")}
            title={t("messages.send")}
          >
            <Send className="w-4 h-4" />
          </button>
        </div>
      )}
    </>
  );
}
