"use client";

import { useCallback, useEffect, useMemo, useState } from "react";

import {
  api,
  downloadUrl,
  type AdminEvent,
  type GuestPage,
  type GuestRead,
  type InvitationStatus,
} from "@/lib/api/browser";

import BulkInviteModal from "./BulkInviteModal";
import GuestDrawer from "./GuestDrawer";
import GuestForm from "./GuestForm";
import ImportView from "./ImportView";
import { Button, Card, Empty, ErrorNote, Loading, StatusBadge, inputClass } from "./ui";

/**
 * Guest table with search, filters and pagination (tasks 3.6, 3.7, 3.11).
 *
 * Every filter is a server query against an indexed column — the table never loads all
 * 800 guests and filters in the browser. At wedding scale that would work by accident and
 * then stop working on the one night it matters.
 */
const PAGE_SIZE = 50;
const DEBOUNCE_MS = 300;

const STATUSES: InvitationStatus[] = [
  "pending",
  "opened",
  "accepted",
  "declined",
  "cancelled",
  "expired",
];

export default function GuestsView({
  initialEventId = "",
  canEdit,
  canDelete,
  canExport,
  canSend,
  canImport,
}: {
  /** Pre-selection from the address, e.g. arriving from an event's Guests button (design D6). */
  initialEventId?: string;
  canEdit: boolean;
  canDelete: boolean;
  canExport: boolean;
  canSend: boolean;
  canImport: boolean;
}) {
  const [search, setSearch] = useState("");
  const [debounced, setDebounced] = useState("");
  const [event, setEvent] = useState(initialEventId);
  const [status, setStatus] = useState("");
  const [side, setSide] = useState("");
  const [tag, setTag] = useState("");
  const [page, setPage] = useState(1);
  const [unknownEvent, setUnknownEvent] = useState(false);

  const [events, setEvents] = useState<AdminEvent[] | null>(null);
  const [data, setData] = useState<GuestPage | null>(null);
  const [error, setError] = useState<unknown>(null);
  const [loading, setLoading] = useState(true);
  const [editing, setEditing] = useState<GuestRead | "new" | null>(null);
  const [viewing, setViewing] = useState<string | null>(null);
  const [importing, setImporting] = useState(false);

  /**
   * Which guests a batched send would go to (add-bulk-invitation-send D12).
   *
   * Browser state, keyed by id. It survives paging — moving through pages to tick more names
   * is the point — and is emptied by any filter change, because a selection whose rows are
   * no longer on screen cannot be checked before it is sent. Nothing is persisted
   * server-side: there is no state to expire, and a reload starting clean is the safe
   * direction for a control that mails people.
   */
  const [selected, setSelected] = useState<Set<string>>(new Set());
  const [selectingAll, setSelectingAll] = useState(false);
  const [inviting, setInviting] = useState<string[] | null>(null);

  // Typing a name should not fire a query per keystroke against a table this size.
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebounced(search);
      setPage(1);
    }, DEBOUNCE_MS);
    return () => clearTimeout(timer);
  }, [search]);

  const query = useMemo(() => {
    const params = new URLSearchParams({ page: String(page), page_size: String(PAGE_SIZE) });
    if (debounced) params.set("search", debounced);
    // `event_id`, not `event`: the filter used to send an event *type*, which the API
    // never read — so choosing "Walima" silently listed everyone. With any number of
    // events of a type possible now (design D12), a type could not identify a list anyway.
    if (event) params.set("event_id", event);
    if (status) params.set("invitation_status", status);
    if (side) params.set("side", side);
    if (tag) params.set("tag", tag);
    return params;
  }, [debounced, event, status, side, tag, page]);

  const load = useCallback(async () => {
    setLoading(true);
    try {
      setData(await api.guests(query));
      setError(null);
    } catch (err) {
      setError(err);
    } finally {
      setLoading(false);
    }
  }, [query]);

  useEffect(() => {
    void load();
  }, [load]);

  const pages = data ? Math.max(1, Math.ceil(data.total / data.page_size)) : 1;

  // Loaded once: the dropdown needs real events now that there can be several of a type.
  useEffect(() => {
    void api
      .events()
      .then(setEvents)
      .catch(() => setEvents([]));
  }, []);

  /**
   * Reconcile an event seeded from the address against the real list (design D7).
   *
   * A malformed identifier and a well-formed one naming a deleted event fail differently at
   * the API — 422 versus an empty page — but identically to the admin reading the screen. One
   * check against a list that is being fetched anyway covers both, and also covers the event
   * being deleted between the grid rendering and the link being followed.
   *
   * Deliberately no early-out on an empty `event`: clearing it below re-runs this effect, and
   * resetting the flag there would erase the notice in the same tick it was raised.
   */
  useEffect(() => {
    if (!events || !event) return;
    if (events.some((e) => e.id === event)) return;
    setEvent("");
    setUnknownEvent(true);
  }, [events, event]);

  /**
   * The import modal names the event it will write to, and that name is this filter. If the
   * filter moves — by hand, or because the reconciliation above cleared a deleted event — a
   * preview computed against the old list would still be on screen while the destination
   * underneath it had changed. Closing is the only honest response; nothing has been written
   * at that point, and a re-opened modal states its destination afresh.
   */
  useEffect(() => {
    setImporting(false);
  }, [event]);

  /**
   * Any filter change empties the selection (D12).
   *
   * Not paging — `page` is deliberately absent from the dependencies. Ticking names across
   * several pages is the ordinary way to build a selection, while a selection describing
   * rows the filters no longer match is one the admin cannot verify before it mails people.
   */
  useEffect(() => {
    setSelected(new Set());
  }, [debounced, event, status, side, tag]);

  /**
   * Reflect the filters in the address so the view can be linked to and bookmarked (design D6).
   *
   * `replaceState`, not `router.replace`: this page is `force-dynamic`, so a router navigation
   * would re-render the server component on every filter change — including each debounced
   * keystroke. This updates the address bar and nothing else.
   *
   * It also must not push: arriving here from the events grid is a push from that link, so
   * Back belongs to the grid. Pushing per filter change would bury it under a dozen entries.
   */
  useEffect(() => {
    const params = new URLSearchParams();
    if (debounced) params.set("search", debounced);
    if (event) params.set("event_id", event);
    if (status) params.set("invitation_status", status);
    if (side) params.set("side", side);
    if (tag) params.set("tag", tag);
    const qs = params.toString();
    window.history.replaceState(null, "", qs ? `?${qs}` : window.location.pathname);
  }, [debounced, event, status, side, tag]);

  /** Any filter change resets to page 1 — otherwise narrowing from 800 guests to 12 while
   * sitting on page 4 shows an empty table that looks like a bug. */
  const filter = (set: (v: string) => void) => (value: string) => {
    set(value);
    setPage(1);
    // The notice describes the address this screen was opened at; touching a filter by hand
    // moves past it.
    setUnknownEvent(false);
  };

  const pageIds = useMemo(() => (data?.items ?? []).map((g) => g.id), [data]);
  const allOnPageSelected = pageIds.length > 0 && pageIds.every((id) => selected.has(id));

  function toggle(id: string) {
    setSelected((current) => {
      const next = new Set(current);
      if (!next.delete(id)) next.add(id);
      return next;
    });
  }

  function togglePage() {
    setSelected((current) => {
      const next = new Set(current);
      for (const id of pageIds) {
        if (allOnPageSelected) next.delete(id);
        else next.add(id);
      }
      return next;
    });
  }

  /**
   * Extend the selection to every guest the filters match (D2).
   *
   * Resolved to real ids through the API rather than kept as "whatever the filters mean",
   * because a filter re-evaluated at send time can grow between the count on the button and
   * the messages going out — an import finishing in another tab is enough.
   */
  async function selectAllMatching() {
    if (!event) return;
    setSelectingAll(true);
    try {
      const { guest_ids } = await api.eventGuestIds(event, matchingParams(debounced, status, side, tag));
      setSelected(new Set(guest_ids));
      setError(null);
    } catch (err) {
      setError(err);
    } finally {
      setSelectingAll(false);
    }
  }

  async function remove(guest: GuestRead) {
    // Soft delete on the API side, but it still removes them from every headcount, so it
    // gets a confirmation rather than a one-tap mistake in a scrolling list.
    if (!confirm(`Remove ${guest.full_name}? They will drop out of all headcounts.`)) return;
    try {
      await api.deleteGuest(guest.id);
      await load();
    } catch (err) {
      setError(err);
    }
  }

  return (
    <div className="space-y-4">
      <Card>
        <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
          <div className="lg:col-span-2">
            <label htmlFor="guest-search" className="sr-only">
              Search guests
            </label>
            <input
              id="guest-search"
              type="search"
              value={search}
              onChange={(e) => setSearch(e.target.value)}
              placeholder="Search name, phone or email"
              className={inputClass}
            />
          </div>
          <Select label="Event" value={event} onChange={filter(setEvent)}>
            <option value="">All events</option>
            {(events ?? []).map((e) => (
              <option key={e.id} value={e.id}>
                {e.title_en}
              </option>
            ))}
          </Select>
          <Select label="Status" value={status} onChange={filter(setStatus)}>
            <option value="">Any status</option>
            {STATUSES.map((s) => (
              <option key={s} value={s}>
                {s}
              </option>
            ))}
          </Select>
          <Select label="Side" value={side} onChange={filter(setSide)}>
            <option value="">Either side</option>
            <option value="bride">Bride</option>
            <option value="groom">Groom</option>
            <option value="common">Common</option>
          </Select>
        </div>

        <div className="mt-3 flex flex-wrap items-center gap-2">
          <input
            type="text"
            value={tag}
            onChange={(e) => {
              setTag(e.target.value);
              setPage(1);
            }}
            placeholder="Filter by tag"
            aria-label="Filter by tag"
            className={`${inputClass} max-w-[12rem]`}
          />
          {/* This list spans events unless one is selected, so the total counts records:
              the same person on two lists is two rows here (design D11). It only becomes a
              count of people once an event filter narrows it to a single list. */}
          <span className="text-sm text-stone-500">
            {data
              ? `${data.total} ${event ? "guest" : "guest record"}${data.total === 1 ? "" : "s"}`
              : "…"}
          </span>
          {/* Announced rather than merely shown: the count is what the Send button acts on,
              and it changes without the button itself changing. */}
          {selected.size > 0 && (
            <span role="status" className="text-sm font-medium text-stone-800">
              {selected.size} selected
            </span>
          )}
          <div className="ml-auto flex gap-2">
            {/* The reason this screen has checkboxes. Disabled with the reason on it until
                both preconditions hold — an event, because the message text belongs to one
                (design D1), and a selection, because there is otherwise nobody to send to. */}
            {canSend && (
              <Button
                onClick={() => setInviting([...selected])}
                disabled={!event || selected.size === 0}
                title={
                  !event
                    ? "Choose an event first — the message text belongs to one"
                    : selected.size === 0
                      ? "Select the guests to invite first"
                      : undefined
                }
              >
                {selected.size > 0 ? `Send invitations (${selected.size})` : "Send invitations"}
              </Button>
            )}
            {/* Said out loud, not left to a tooltip. This screen is used one-handed on a
                phone where there is no hover, and a disabled button is not focusable — so
                `title` alone reaches neither a touch user nor a screen reader. Shown only
                for the case the admin cannot work out for themselves: they have picked
                people and it still refuses. "Nothing selected" needs no explaining. */}
            {canSend && selected.size > 0 && !event && (
              <p role="status" className="w-full text-right text-xs text-amber-800">
                Choose an event above to send — the message text belongs to one.
              </p>
            )}
            {canExport && (
              // Exports the *filtered* set, not everything — the caterer list is a filter,
              // and exporting all 800 when the screen shows 40 would be a data leak by
              // accident.
              <a
                href={downloadUrl.export(exportParams(event, status))}
                className="inline-flex min-h-11 items-center rounded-md border border-stone-300 bg-white px-4 text-sm font-medium text-stone-800 hover:bg-stone-50"
              >
                Export CSV
              </a>
            )}
            {/* Import sits beside Add guest because it is the same act in bulk: rows joining
                one event's list. It carries the same precondition and therefore the same
                treatment — disabled with the reason on it, rather than a modal that has to
                ask the question this screen already answers (design D1). */}
            {canImport && (
              <Button
                variant="secondary"
                onClick={() => setImporting(true)}
                disabled={!event}
                title={event ? undefined : "Choose an event first — imported guests join one"}
              >
                Import CSV
              </Button>
            )}
            {/* A guest joins one event's list, so there is nothing to add them to until
                an event is chosen. Disabled with the reason stated beats a form that
                fails on submit. */}
            {canEdit && (
              <Button
                onClick={() => setEditing("new")}
                disabled={!event}
                title={event ? undefined : "Choose an event first — a guest belongs to one"}
              >
                Add guest
              </Button>
            )}
          </div>
        </div>
      </Card>

      {/* Offered only when it means something — when the filters match more than is on
          screen. Requires an event, because that is what the send needs anyway (D2). */}
      {canSend && event && data && data.total > data.items.length && (
        <div className="flex flex-wrap items-center gap-2 rounded-lg bg-stone-100 px-3 py-2 text-sm">
          <span className="text-stone-700">
            {allOnPageSelected
              ? `All ${data.items.length} on this page are selected.`
              : `${data.total} guests match these filters.`}
          </span>
          <Button variant="secondary" onClick={() => void selectAllMatching()} disabled={selectingAll}>
            {selectingAll ? "Selecting…" : `Select all ${data.total} matching`}
          </Button>
          {selected.size > 0 && (
            <Button variant="ghost" onClick={() => setSelected(new Set())}>
              Clear selection
            </Button>
          )}
        </div>
      )}

      {unknownEvent && (
        <p role="status" className="rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-900">
          That event no longer exists, so this is every guest record instead. Pick an event
          above to narrow it.
        </p>
      )}

      <ErrorNote error={error} />

      <Card>
        {loading && !data ? (
          <Loading label="Loading guests…" />
        ) : !data || data.items.length === 0 ? (
          <Empty>No guests match these filters.</Empty>
        ) : (
          <div className="overflow-x-auto">
            <table className="w-full min-w-[46rem] text-left text-sm">
              <thead>
                <tr className="border-b border-stone-200 text-xs uppercase tracking-wide text-stone-500">
                  {canSend && (
                    <th scope="col" className="w-8 py-2 pr-2 font-medium">
                      <input
                        type="checkbox"
                        checked={allOnPageSelected}
                        onChange={togglePage}
                        aria-label="Select every guest on this page"
                        className="h-4 w-4 accent-stone-900"
                      />
                    </th>
                  )}
                  <th scope="col" className="py-2 pr-3 font-medium">
                    Name
                  </th>
                  <th scope="col" className="py-2 pr-3 font-medium">
                    Contact
                  </th>
                  <th scope="col" className="py-2 pr-3 font-medium">
                    Side
                  </th>
                  <th scope="col" className="py-2 pr-3 font-medium">
                    Invitations
                  </th>
                  <th scope="col" className="py-2 pr-3 font-medium">
                    <span className="sr-only">Actions</span>
                  </th>
                </tr>
              </thead>
              <tbody>
                {data.items.map((guest) => (
                  <tr key={guest.id} className="border-b border-stone-100 last:border-0">
                    {canSend && (
                      <td className="py-2.5 pr-2">
                        <input
                          type="checkbox"
                          checked={selected.has(guest.id)}
                          onChange={() => toggle(guest.id)}
                          // Named for the guest: a screen reader in a 50-row table has to
                          // hear which one this is, not "Select guest" fifty times.
                          aria-label={`Select ${guest.full_name}`}
                          className="h-4 w-4 accent-stone-900"
                        />
                      </td>
                    )}
                    <td className="py-2.5 pr-3">
                      {/* Text, not a button. The detail view has one named affordance —
                          the Send invitation action — rather than two controls that do the
                          same thing, one of which does not look like a control at all. */}
                      <span className="font-medium text-stone-900">{guest.full_name}</span>
                      {guest.group_tag.length > 0 && (
                        <span className="ml-2 text-xs text-stone-400">
                          {guest.group_tag.join(", ")}
                        </span>
                      )}
                    </td>
                    <td className="py-2.5 pr-3 text-stone-600">
                      <div className="whitespace-nowrap">{guest.phone_e164 ?? "—"}</div>
                      <div className="flex items-center gap-1 text-xs text-stone-500">
                        {guest.email ?? <span className="text-amber-700">no email</span>}
                        {guest.email_invalid && (
                          <span title="Hard-bounced; excluded from every send">⚠</span>
                        )}
                      </div>
                    </td>
                    <td className="py-2.5 pr-3 text-stone-600">{guest.side}</td>
                    <td className="py-2.5 pr-3">
                      <div className="flex flex-wrap gap-1">
                        {guest.invitations.map((inv) => (
                          <span key={inv.id} className="whitespace-nowrap">
                            <span className="text-xs text-stone-500">
                              {inv.event_type.slice(0, 3)}
                            </span>{" "}
                            <StatusBadge status={inv.status} />
                          </span>
                        ))}
                        {guest.invitations.length === 0 && (
                          <span className="text-xs text-stone-400">none</span>
                        )}
                      </div>
                    </td>
                    <td className="py-2.5 pr-3 text-right">
                      <div className="flex justify-end gap-1">
                        {canSend && (
                          // The row's primary action, and the reason this screen exists.
                          // Named for the guest so a screen reader in a 40-row table hears
                          // which one it is about, not "Send invitation" forty times.
                          <Button
                            variant="secondary"
                            onClick={() => setViewing(guest.id)}
                            aria-label={`Send invitation to ${guest.full_name}`}
                          >
                            Send invitation
                          </Button>
                        )}
                        {canEdit && (
                          <Button
                            variant="ghost"
                            onClick={() => setEditing(guest)}
                            aria-label={`Edit ${guest.full_name}`}
                          >
                            Edit
                          </Button>
                        )}
                        {canDelete && (
                          <Button
                            variant="ghost"
                            onClick={() => void remove(guest)}
                            aria-label={`Remove ${guest.full_name}`}
                          >
                            Remove
                          </Button>
                        )}
                      </div>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}

        {data && pages > 1 && (
          <nav
            aria-label="Pagination"
            className="mt-4 flex items-center justify-between border-t border-stone-100 pt-3 text-sm"
          >
            <Button
              variant="secondary"
              disabled={page <= 1}
              onClick={() => setPage((p) => p - 1)}
            >
              Previous
            </Button>
            <span className="text-stone-500">
              Page {data.page} of {pages}
            </span>
            <Button
              variant="secondary"
              disabled={page >= pages}
              onClick={() => setPage((p) => p + 1)}
            >
              Next
            </Button>
          </nav>
        )}
      </Card>

      {editing && (
        <GuestForm
          guest={editing === "new" ? null : editing}
          eventId={editing === "new" ? event : editing.event_id}
          eventName={
            (events ?? []).find(
              (e) => e.id === (editing === "new" ? event : editing.event_id),
            )?.title_en
          }
          onClose={() => setEditing(null)}
          onSaved={(saved) => {
            // A newly added guest exists to be sent an invitation, so land on their detail
            // view rather than a table row: the link and QR are right there, while the
            // person is still on the phone. Editing an existing guest is a correction and
            // needs no such follow-up.
            const wasNew = editing === "new";
            setEditing(null);
            if (wasNew) setViewing(saved.id);
            void load();
          }}
        />
      )}

      {/* `importing && event` rather than `importing` alone: the destination is required, so
          the modal never mounts without one and `ImportView` never has to render a state it
          cannot act in. */}
      {importing && event && (
        <ImportView
          eventId={event}
          eventName={(events ?? []).find((e) => e.id === event)?.title_en}
          onClose={() => setImporting(false)}
          // Reload while the modal is still open, so closing it reveals a table that already
          // includes the imported rows (design D3).
          onImported={() => void load()}
        />
      )}

      {/* `inviting && event` rather than `inviting` alone: the event is required, so the
          modal never mounts without one and never has to render a state it cannot act in. */}
      {inviting && event && (
        <BulkInviteModal
          eventId={event}
          eventTitle={(events ?? []).find((e) => e.id === event)?.title_en ?? "this event"}
          guestIds={inviting}
          onClose={() => setInviting(null)}
          // Refresh behind the modal so closing it reveals a table whose invitation
          // statuses already reflect the send.
          onSent={() => void load()}
        />
      )}

      {viewing && (
        <GuestDrawer
          guestId={viewing}
          canEdit={canEdit}
          canSend={canSend}
          onClose={() => setViewing(null)}
          onChanged={() => void load()}
        />
      )}
    </div>
  );
}

/**
 * The filters as the guest-ids endpoint takes them (D2).
 *
 * No `event_id`: that endpoint is addressed by event, so the event travels in the path and
 * cannot be contradicted here. Everything else must match what the table was queried with,
 * or "select all 412 matching" would select a different 412.
 */
function matchingParams(search: string, status: string, side: string, tag: string) {
  const params = new URLSearchParams();
  if (search) params.set("search", search);
  if (status) params.set("invitation_status", status);
  if (side) params.set("side", side);
  if (tag) params.set("tag", tag);
  return params;
}

function exportParams(event: string, status: string) {
  const params = new URLSearchParams();
  if (event) params.set("event_id", event);
  if (status) params.set("invitation_status", status);
  return params;
}

function Select({
  label,
  value,
  onChange,
  children,
}: {
  label: string;
  value: string;
  onChange: (v: string) => void;
  children: React.ReactNode;
}) {
  return (
    <label className="block">
      <span className="sr-only">{label}</span>
      <select value={value} onChange={(e) => onChange(e.target.value)} className={inputClass}>
        {children}
      </select>
    </label>
  );
}
