import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Check, Search, Send } from "lucide-react";
import { useVisualViewport } from "@/hooks/use-visual-viewport";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { toast } from "@/hooks/use-toast";
import { useAuth } from "@/contexts/AuthContext";
import {
  useSendNote,
  useUserDirectory,
  userDisplayName,
} from "@/lib/notes-api";

export function SendNoteDialog({
  noteId,
  onClose,
}: {
  noteId: number;
  onClose: () => void;
}) {
  const { t, i18n } = useTranslation();
  const lang = i18n.language;
  const { user } = useAuth();
  const { data: directory = [], isLoading } = useUserDirectory();
  const send = useSendNote();
  const [search, setSearch] = useState("");
  const [picked, setPicked] = useState<Set<number>>(new Set());
  // #630: Keep the recipients list and the Send button visible above
  // the iOS soft keyboard.
  //
  // Background: the previous fix relied on `useVisualViewport` to
  // shrink `listMaxHeight` while the keyboard was up. That works on
  // desktop Safari and on iPad Safari (regular tab), but iPad PWAs
  // (Add to Home Screen) do NOT reliably resize visualViewport when
  // the soft keyboard opens — see the comment block in
  // `components/ui/dialog.tsx` and the reverted attempts #622–#624.
  // So the dialog stayed full height, kept its Radix center anchor
  // (top: 50%; translate(-50%, -50%)), and the keyboard covered the
  // entire bottom half — list and Send button included.
  //
  // The targeted, per-dialog fix here:
  //  • Track focus on the search input directly. Focus is the most
  //    reliable signal that the keyboard is (or is about to be) up,
  //    and it doesn't depend on visualViewport reporting anything.
  //  • While focused, anchor the dialog to the TOP of the viewport
  //    instead of the center, and cap its height to ~50dvh. That
  //    guarantees the entire dialog (title → input → list → buttons)
  //    fits in the upper half of the screen, leaving the lower half
  //    free for the keyboard regardless of what visualViewport says.
  //  • When focus leaves, drop back to Radix's normal centered layout
  //    so desktop and not-typing iPad keep the original look.
  //
  // `useVisualViewport` is still consulted as a refinement on devices
  // where it DOES update (desktop touch laptops, Android Chrome, iPad
  // Safari tab) so the list cap matches the actual usable height.
  const vv = useVisualViewport();
  const [searchFocused, setSearchFocused] = useState(false);
  // Device guard — only the focus-driven top-anchored override should
  // fire on touch devices where a soft keyboard actually appears.
  // Desktops with a hardware keyboard have no overlay to dodge, and
  // architect review flagged that applying the override on every
  // focus regressed their layout. `(pointer: coarse)` matches iPad,
  // iPhone, and Android touch primary inputs while excluding mouse-
  // driven desktop and touch laptops where the primary pointer is a
  // trackpad/mouse.
  const [isTouchPrimary, setIsTouchPrimary] = useState(false);
  useEffect(() => {
    if (typeof window === "undefined" || !window.matchMedia) return;
    const mq = window.matchMedia("(pointer: coarse)");
    setIsTouchPrimary(mq.matches);
    const handler = (e: MediaQueryListEvent) => setIsTouchPrimary(e.matches);
    mq.addEventListener?.("change", handler);
    return () => mq.removeEventListener?.("change", handler);
  }, []);
  const keyboardLikely = searchFocused && isTouchPrimary;
  // Slight delay on blur so the user can tap a list row without the
  // dialog jumping back to center mid-tap (a focus/blur race that
  // would shift the row out from under the finger).
  const blurTimeoutRef = useRef<number | null>(null);
  // Lifecycle cleanup — clear a pending blur timeout on unmount so we
  // don't schedule a setState on an unmounted component (and to satisfy
  // the architect's nitpick about the missing teardown).
  useEffect(() => {
    return () => {
      if (blurTimeoutRef.current !== null) {
        window.clearTimeout(blurTimeoutRef.current);
        blurTimeoutRef.current = null;
      }
    };
  }, []);
  const onSearchFocus = () => {
    if (blurTimeoutRef.current !== null) {
      window.clearTimeout(blurTimeoutRef.current);
      blurTimeoutRef.current = null;
    }
    setSearchFocused(true);
  };
  const onSearchBlur = () => {
    blurTimeoutRef.current = window.setTimeout(() => {
      setSearchFocused(false);
      blurTimeoutRef.current = null;
    }, 150);
  };

  // Primary signal: visualViewport actually reports a keyboard inset.
  // Used on desktop touch laptops, iPad Safari (regular tab), and
  // Android Chrome where vv updates correctly.
  const keyboardActive = vv.keyboardInset > 0;
  // Fallback signal: iPad PWA (Add to Home Screen) doesn't reliably
  // update visualViewport when the keyboard opens (see comment in
  // components/ui/dialog.tsx). On touch-primary devices, treat focus
  // on the search input as evidence the keyboard is up so the dialog
  // still gets repositioned. This is a fallback only — when vv DOES
  // update, the primary branch above takes over.
  const keyboardFallback = !keyboardActive && searchFocused && isTouchPrimary;
  const KEYBOARD_PREDICTION_INSET =
    keyboardActive || keyboardFallback ? 48 : 0;
  const listMaxHeight = (() => {
    const baseHeight = vv.height || window.innerHeight;
    const assumedUsable = keyboardFallback
      ? Math.min(baseHeight, window.innerHeight * 0.5)
      : baseHeight;
    const usable = assumedUsable - KEYBOARD_PREDICTION_INSET;
    // Reserve ~220px for the dialog chrome (title + search input +
    // footer + paddings) so the list never pushes the buttons off
    // screen. Clamp to a sensible min/max so desktop keeps the old
    // 288px (max-h-72) feel.
    const forList = Math.max(120, Math.min(288, usable - 220));
    return forList;
  })();

  // Dialog positioning override:
  //   • Primary path (`keyboardActive`): compute the new vertical
  //     centre from the *visual* viewport — anchored at
  //     `offsetTop + height/2`, shifted up by the QuickType strip
  //     allowance so the Send button isn't hidden under predictions.
  //     Keep Radix's `translate(-50%, -50%)` so the dialog stays
  //     centered around the new anchor.
  //   • Fallback path (`keyboardFallback`): top-anchor at 6vh with
  //     `translate(-50%, 0)` and cap `maxHeight: 50dvh` so the dialog
  //     never crosses the screen midpoint where the keyboard sits.
  //   • Otherwise: pass NO style so Radix's default centered layout
  //     is fully restored (no leftover transition, no leftover top).
  const dialogStyle: React.CSSProperties | undefined = keyboardActive
    ? {
        top: vv.offsetTop + vv.height / 2 - KEYBOARD_PREDICTION_INSET,
        left: "50%",
        transform: "translate(-50%, -50%)",
        transition: "top 120ms ease",
      }
    : keyboardFallback
      ? {
          // #634: bind `top` to `visualViewport.offsetTop` so when iPad
          // PWA Safari scrolls the layout viewport down behind the
          // soft keyboard, our top anchor follows the visible area
          // instead of landing above it (which previously hid the
          // dialog title + search input).
          top: `calc(${vv.offsetTop || 0}px + 6vh)`,
          // #634: pin `left: 50%` explicitly. Radix already sets it
          // via Tailwind, but the inline `transform` below replaces
          // Tailwind's `translate-x-[-50%] translate-y-[-50%]` so we
          // also restate the horizontal anchor to defend against any
          // animation/RTL interplay that was shifting the box left.
          left: "50%",
          transform: "translate(-50%, 0)",
          maxHeight: "50dvh",
          // #634: allow the dialog itself to scroll when its content
          // is taller than the cap — otherwise the header could be
          // clipped while only the bottom of the list/buttons stayed
          // visible.
          overflowY: "auto",
          transition: "top 120ms ease, transform 120ms ease",
        }
      : undefined;

  const candidates = useMemo(() => {
    const me = user?.id;
    return directory.filter((u) => u.id !== me);
  }, [directory, user]);

  const filtered = useMemo(() => {
    const q = search.trim().toLowerCase();
    if (!q) return candidates;
    return candidates.filter((u) =>
      [u.username, u.displayNameAr, u.displayNameEn]
        .filter(Boolean)
        .some((s) => (s as string).toLowerCase().includes(q)),
    );
  }, [candidates, search]);

  const toggle = (id: number) => {
    setPicked((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  };

  const [confirmOpen, setConfirmOpen] = useState(false);

  const performSend = () => {
    send.mutate(
      { id: noteId, recipientUserIds: Array.from(picked) },
      {
        onSuccess: () => {
          toast({ title: t("notes.sendSuccess", "Note sent") });
          setConfirmOpen(false);
          onClose();
        },
        onError: () => {
          setConfirmOpen(false);
          toast({
            title: t("notes.sendFailed", "Could not send note"),
            variant: "destructive",
          });
        },
      },
    );
  };

  const requestSubmit = () => {
    if (picked.size === 0 || send.isPending) return;
    if (picked.size === 1) {
      performSend();
      return;
    }
    setConfirmOpen(true);
  };

  const confirmSubmit = () => {
    performSend();
  };

  return (
    <Dialog open onOpenChange={(o) => !o && onClose()}>
      <DialogContent
        className="max-w-md"
        {...(dialogStyle ? { style: dialogStyle } : {})}
        data-testid="send-note-dialog"
      >
        <DialogHeader className="pr-8">
          <DialogTitle>{t("notes.sendNote", "Send note")}</DialogTitle>
        </DialogHeader>
        <div className="relative">
          <Search
            size={14}
            className="absolute top-1/2 -translate-y-1/2 text-muted-foreground start-2.5"
          />
          <Input
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            onFocus={onSearchFocus}
            onBlur={onSearchBlur}
            placeholder={t("notes.recipientsPlaceholder", "Search people…")}
            className="ps-9"
            data-testid="send-note-search"
          />
        </div>
        <div
          className="overflow-y-auto border rounded-lg divide-y"
          style={{ maxHeight: listMaxHeight }}
        >
          {isLoading && (
            <div className="p-3 text-sm text-muted-foreground">
              {t("common.loading", "Loading...")}
            </div>
          )}
          {!isLoading && filtered.length === 0 && (
            <div className="p-3 text-sm text-muted-foreground text-center">
              —
            </div>
          )}
          {filtered.map((u) => {
            const checked = picked.has(u.id);
            return (
              <button
                key={u.id}
                onClick={() => toggle(u.id)}
                data-testid={`send-recipient-option-${u.id}`}
                className={`w-full flex items-center justify-between gap-2 px-3 py-2 text-sm hover:bg-slate-100 ${
                  checked ? "bg-primary/5" : ""
                }`}
              >
                <span className="truncate">{userDisplayName(u, lang)}</span>
                {checked && <Check size={14} className="text-primary" />}
              </button>
            );
          })}
        </div>
        {picked.size > 0 && (
          <div className="text-xs text-muted-foreground">
            {t("notes.recipientsCount", { count: picked.size })}
          </div>
        )}
        <div className="flex items-center justify-end gap-2">
          <Button variant="ghost" onClick={onClose}>
            {t("common.cancel", "Cancel")}
          </Button>
          <Button
            onClick={requestSubmit}
            disabled={picked.size === 0 || send.isPending}
            data-testid="send-note-submit"
          >
            <Send size={14} className="me-1" />
            {t("notes.send", "Send")}
          </Button>
        </div>
      </DialogContent>
      <AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
        <AlertDialogContent data-testid="send-note-confirm">
          <AlertDialogHeader>
            <AlertDialogTitle>
              {t("notes.confirmSendTitle", "Send this note?")}
            </AlertDialogTitle>
            <AlertDialogDescription>
              {t("notes.confirmSendBody", { count: picked.size })}
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>{t("common.cancel", "Cancel")}</AlertDialogCancel>
            <AlertDialogAction
              onClick={confirmSubmit}
              disabled={send.isPending}
              data-testid="send-note-confirm-submit"
            >
              {t("notes.confirmSendCta", "Send")}
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </Dialog>
  );
}
