import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type PointerEvent as ReactPointerEvent,
} from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "wouter";
import { Reply, Check, ExternalLink, X, GripVertical } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
  colorBg,
  useMarkNoteRead,
  userDisplayName,
  type UserSummary,
} from "@/lib/notes-api";
import {
  useIncomingNotePopup,
  type PopupPayload,
} from "@/contexts/IncomingNotePopupContext";
import { useAuth } from "@/contexts/AuthContext";

function avatarInitial(u: UserSummary | null, lang: string): string {
  const name = userDisplayName(u, lang);
  return name.trim().charAt(0).toUpperCase() || "?";
}

interface StoredPos {
  x: number;
  y: number;
}

const CARD_WIDTH = 384;
const CARD_HEIGHT_ESTIMATE = 280;
const VIEWPORT_MARGIN = 8;
const TOP_INSET = 80;
const SIDE_INSET = 24;

function storageKey(userId: number | null | undefined): string {
  return `txos:floating-note-popup:pos:${userId ?? "anon"}`;
}

function loadStoredPos(userId: number | null | undefined): StoredPos | null {
  if (typeof window === "undefined") return null;
  try {
    const raw = window.sessionStorage.getItem(storageKey(userId));
    if (!raw) return null;
    const parsed = JSON.parse(raw) as Partial<StoredPos>;
    if (typeof parsed.x !== "number" || typeof parsed.y !== "number") return null;
    return { x: parsed.x, y: parsed.y };
  } catch {
    return null;
  }
}

function saveStoredPos(
  userId: number | null | undefined,
  pos: StoredPos,
): void {
  if (typeof window === "undefined") return;
  try {
    window.sessionStorage.setItem(storageKey(userId), JSON.stringify(pos));
  } catch {
    /* ignore quota / disabled storage */
  }
}

/**
 * Default anchor when the user hasn't dragged the card yet.
 * RTL → top-left edge of the viewport; LTR → top-right edge. Keeps
 * the card clear of typical header bars (top inset of 80px).
 */
function defaultAnchor(isRtl: boolean): StoredPos {
  if (typeof window === "undefined") return { x: SIDE_INSET, y: TOP_INSET };
  const x = isRtl
    ? SIDE_INSET
    : Math.max(SIDE_INSET, window.innerWidth - CARD_WIDTH - SIDE_INSET);
  return { x, y: TOP_INSET };
}

function clampToViewport(pos: StoredPos): StoredPos {
  if (typeof window === "undefined") return pos;
  const maxX = Math.max(
    VIEWPORT_MARGIN,
    window.innerWidth - CARD_WIDTH - VIEWPORT_MARGIN,
  );
  const maxY = Math.max(
    VIEWPORT_MARGIN,
    window.innerHeight - CARD_HEIGHT_ESTIMATE - VIEWPORT_MARGIN,
  );
  return {
    x: Math.min(Math.max(VIEWPORT_MARGIN, pos.x), maxX),
    y: Math.min(Math.max(VIEWPORT_MARGIN, pos.y), maxY),
  };
}

export function IncomingNotePopup() {
  const { t, i18n } = useTranslation();
  const isRtl = i18n.language === "ar";
  const [, setLocation] = useLocation();
  const { current, queueLength, dismiss } = useIncomingNotePopup();
  const markRead = useMarkNoteRead();
  const { user } = useAuth();
  const userId = user?.id ?? null;

  const [position, setPosition] = useState<StoredPos>(() =>
    clampToViewport(loadStoredPos(userId) ?? defaultAnchor(isRtl)),
  );
  const [enterAnim, setEnterAnim] = useState(false);
  // Reactive flag mirrored from `dragRef`. The ref drives per-pixel
  // pointermove updates without re-rendering, but we still need a
  // reactive bit so React turns OFF the transform transition during
  // a drag — otherwise pointermove writes race the CSS transition
  // and the card stutters.
  const [isDragging, setIsDragging] = useState(false);

  // Pointer-event drag state. Kept in a ref so dragging doesn't churn
  // through React renders on every pointermove — only the inline style
  // is mutated until pointerup, when we commit the final position.
  const dragRef = useRef<{
    pointerId: number;
    startClientX: number;
    startClientY: number;
    startPosX: number;
    startPosY: number;
  } | null>(null);
  const cardRef = useRef<HTMLDivElement | null>(null);

  // Re-anchor when identity / language direction changes so the previous
  // user's stored position doesn't leak into a new session and the
  // default corner flips correctly on LTR↔RTL switch.
  useEffect(() => {
    setPosition(clampToViewport(loadStoredPos(userId) ?? defaultAnchor(isRtl)));
    // Intentional: only re-anchor on identity / direction change.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [userId, isRtl]);

  // Trigger the "scale-in + fade" enter animation on mount of each new
  // popup entry. We toggle from `false → true` on the next paint so the
  // CSS transition has a starting state to interpolate from.
  useEffect(() => {
    if (!current) return;
    setEnterAnim(false);
    const id = requestAnimationFrame(() => setEnterAnim(true));
    return () => cancelAnimationFrame(id);
  }, [current]);

  // Keep the card on-screen if the viewport shrinks (rotate / resize).
  useEffect(() => {
    if (typeof window === "undefined") return;
    const onResize = () => setPosition((p) => clampToViewport(p));
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);

  const handleDismissCurrent = useCallback(() => {
    if (!current) return;
    if (current.kind === "reply") {
      dismiss({ replyId: current.replyId });
    } else {
      dismiss(current.noteId);
    }
  }, [current, dismiss]);

  // ESC dismisses the current entry (parity with the old modal).
  useEffect(() => {
    if (!current) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        e.preventDefault();
        handleDismissCurrent();
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [current, handleDismissCurrent]);

  const isReply = current?.kind === "reply";
  const replyPayload = isReply
    ? (current as Extract<PopupPayload, { kind: "reply" }>)
    : null;

  const senderForHeader: UserSummary | null = useMemo(() => {
    if (!current) return null;
    if (replyPayload) return replyPayload.replier;
    return current.sender;
  }, [current, replyPayload]);

  const senderName = senderForHeader
    ? userDisplayName(senderForHeader, i18n.language)
    : null;

  const heading = useMemo(() => {
    if (!current) return "";
    if (replyPayload) {
      return senderName
        ? t("notes.popup.replyHeading", { name: senderName })
        : t("notes.popup.replyHeadingNoSender");
    }
    return senderName
      ? t("notes.popup.heading", { name: senderName })
      : t("notes.popup.headingNoSender");
  }, [current, replyPayload, senderName, t]);

  if (!current) return null;

  const acknowledge = () => {
    // Only the recipient (note variant) marks-as-read. The reply
    // variant goes to the *original sender* (note owner), who can't
    // mark their own outbound note read.
    if (!replyPayload) {
      markRead.mutate(current.noteId);
    }
  };

  const handleDismiss = () => handleDismissCurrent();
  const handleMarkRead = () => {
    acknowledge();
    handleDismiss();
  };
  const handleReply = () => {
    acknowledge();
    const noteId = current.noteId;
    handleDismiss();
    setLocation(`/notes?thread=${noteId}&reply=1`);
  };
  const handleOpen = () => {
    acknowledge();
    const noteId = current.noteId;
    handleDismiss();
    setLocation(`/notes?thread=${noteId}`);
  };

  // ---- Drag handlers ----
  // Only the header (`.drag-handle`) starts a drag. The card itself
  // is positioned `fixed` and updated via inline style during move; on
  // pointerup the final position is committed to React state +
  // sessionStorage. Buttons inside the body get their own clicks
  // because we never call preventDefault on them.
  const onHandlePointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
    if (e.button !== 0 && e.pointerType === "mouse") return;
    const target = e.currentTarget;
    target.setPointerCapture(e.pointerId);
    dragRef.current = {
      pointerId: e.pointerId,
      startClientX: e.clientX,
      startClientY: e.clientY,
      startPosX: position.x,
      startPosY: position.y,
    };
    setIsDragging(true);
  };

  const onHandlePointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {
    const drag = dragRef.current;
    if (!drag || drag.pointerId !== e.pointerId) return;
    const next = clampToViewport({
      x: drag.startPosX + (e.clientX - drag.startClientX),
      y: drag.startPosY + (e.clientY - drag.startClientY),
    });
    if (cardRef.current) {
      cardRef.current.style.transform = `translate3d(${next.x}px, ${next.y}px, 0)`;
    }
  };

  const onHandlePointerEnd = (e: ReactPointerEvent<HTMLDivElement>) => {
    const drag = dragRef.current;
    if (!drag || drag.pointerId !== e.pointerId) return;
    const target = e.currentTarget;
    if (target.hasPointerCapture(e.pointerId)) {
      target.releasePointerCapture(e.pointerId);
    }
    const next = clampToViewport({
      x: drag.startPosX + (e.clientX - drag.startClientX),
      y: drag.startPosY + (e.clientY - drag.startClientY),
    });
    dragRef.current = null;
    setIsDragging(false);
    setPosition(next);
    saveStoredPos(userId, next);
  };

  const bodyTitle = replyPayload
    ? replyPayload.noteTitle?.trim() || t("notes.popup.untitled")
    : current.title?.trim() || t("notes.popup.untitled");
  const bodyContent = replyPayload
    ? replyPayload.replyContent
    : current.content;

  return (
    // Full-viewport invisible layer. `pointer-events-none` so clicks
    // fall through to the app — the popup is "floating", not modal.
    <div
      className="fixed inset-0 z-[110] pointer-events-none"
      aria-hidden="false"
    >
      <div
        ref={cardRef}
        key={replyPayload ? `reply-${replyPayload.replyId}` : `note-${current.noteId}`}
        role="dialog"
        aria-label={heading}
        data-testid="incoming-note-popup"
        data-popup-kind={replyPayload ? "reply" : "note"}
        dir={isRtl ? "rtl" : "ltr"}
        className="pointer-events-auto absolute top-0 left-0"
        style={{
          width: CARD_WIDTH,
          maxWidth: "calc(100vw - 16px)",
          transform: `translate3d(${position.x}px, ${position.y}px, 0)`,
          transition: isDragging
            ? "none"
            : "transform 220ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 200ms ease-out",
          opacity: enterAnim ? 1 : 0,
          willChange: "transform",
        }}
      >
        <div
          className={`rounded-2xl bg-background ring-4 ring-amber-400/70 shadow-2xl shadow-amber-500/30 overflow-hidden transition-transform duration-200 ${
            enterAnim ? "scale-100" : "scale-95"
          }`}
        >
          {/* Drag handle / header */}
          <div
            onPointerDown={onHandlePointerDown}
            onPointerMove={onHandlePointerMove}
            onPointerUp={onHandlePointerEnd}
            onPointerCancel={onHandlePointerEnd}
            className="flex items-center gap-3 px-4 py-3 cursor-grab active:cursor-grabbing select-none border-b bg-muted/40 touch-none"
            data-testid="incoming-note-popup-drag-handle"
            title={t("notes.popup.dragHint")}
          >
            <GripVertical
              size={16}
              className="text-muted-foreground shrink-0"
              aria-hidden="true"
            />
            <span className="relative shrink-0">
              <span
                className="absolute inset-0 rounded-full bg-amber-400/60 animate-ping"
                aria-hidden="true"
              />
              {senderForHeader?.avatarUrl ? (
                <img
                  src={senderForHeader.avatarUrl}
                  alt=""
                  className="relative h-8 w-8 rounded-full object-cover ring-2 ring-amber-400"
                />
              ) : (
                <div className="relative h-8 w-8 rounded-full bg-amber-200 text-amber-900 text-sm font-semibold flex items-center justify-center ring-2 ring-amber-400">
                  {avatarInitial(senderForHeader, i18n.language)}
                </div>
              )}
            </span>
            <div className="flex-1 min-w-0">
              <div className="text-sm font-semibold truncate">{heading}</div>
            </div>
            <button
              type="button"
              onClick={handleDismiss}
              onPointerDown={(e) => e.stopPropagation()}
              className="text-muted-foreground hover:text-foreground rounded-md p-1"
              aria-label={t("notes.popup.dismiss")}
              data-testid="incoming-note-popup-close"
            >
              <X size={16} />
            </button>
          </div>

          {/* Body */}
          <div className="p-4 space-y-3">
            <div
              className={`rounded-xl border border-black/5 shadow-sm p-3 ${colorBg(
                current.color,
              )}`}
              data-testid="incoming-note-popup-body"
            >
              <div className="font-semibold text-sm text-foreground break-words">
                {bodyTitle}
              </div>
              {bodyContent && (
                <div className="text-sm text-foreground/80 mt-2 whitespace-pre-wrap break-words max-h-[40vh] overflow-y-auto">
                  {bodyContent}
                </div>
              )}
            </div>

            {queueLength > 1 && (
              <div
                className="text-xs text-muted-foreground"
                data-testid="incoming-note-popup-queue"
              >
                {t("notes.popup.queueMore", { count: queueLength - 1 })}
              </div>
            )}

            <div className="flex flex-wrap gap-2 sm:flex-nowrap justify-end">
              <Button
                type="button"
                variant="ghost"
                size="sm"
                onClick={handleDismiss}
                data-testid="incoming-note-popup-dismiss"
              >
                <X size={14} className="me-1" />
                {t("notes.popup.dismiss")}
              </Button>
              <Button
                type="button"
                variant="outline"
                size="sm"
                onClick={handleOpen}
                data-testid="incoming-note-popup-open"
              >
                <ExternalLink size={14} className="me-1" />
                {replyPayload
                  ? t("notes.popup.openThread")
                  : t("notes.popup.openInNotes")}
              </Button>
              {!replyPayload && (
                <Button
                  type="button"
                  variant="outline"
                  size="sm"
                  onClick={handleMarkRead}
                  data-testid="incoming-note-popup-mark-read"
                >
                  <Check size={14} className="me-1" />
                  {t("notes.popup.markRead")}
                </Button>
              )}
              <Button
                type="button"
                size="sm"
                autoFocus
                onClick={handleReply}
                data-testid="incoming-note-popup-reply"
              >
                <Reply size={14} className="me-1" />
                {replyPayload ? t("notes.popup.replyBack") : t("notes.popup.reply")}
              </Button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
