/**
 * Reusable ticket-selection popup (ticket #18, G3).
 *
 * A modal picker consumed by any feature that needs the user to choose a ticket
 * (sprint composition #19, ticket linking #17, …). It reuses the shared
 * {@link TicketFacetsBar} (search + status/priority facets) and the slim
 * {@link useTicketSearch} hook, so filtering/search behave exactly like the main
 * listing without dragging in its sprint grouping or event subscriptions.
 *
 * Integration rules (sprint scoping note `ui-rework-sprint-scoping-contracts`):
 * - G5: mount at the **chrome level** (ProjectsView/App), never inside
 *   LayoutGrid/LeafView. A focus-trap is mandatory because xterm captures
 *   keyboard focus in the workspace.
 * - z-index `floatingWindowNested` (60): the picker is opened from within other
 *   floating windows, so it must sit above them.
 *
 * ⚠️ Location note: this lives under `features/tickets` (not `shared/ui`) because
 * it depends on the ticket domain + DI gateways, whereas `shared/ui` is the pure
 * design system (Button/Input/…). This matches every other feature overlay
 * (SprintManager, MemoryEditor, …). Only the design-system-level `zIndex.ts`
 * went into `shared/ui`.
 *
 * Single-select: a row click resolves and closes, handing back one result.
 * Multi-select (#41): a row click toggles a local selection set (the picker
 * stays open); a footer button confirms and hands back the resolved array.
 */

import { useEffect, useRef, useState } from "react";

import type { TicketRef } from "@/domain";
import type { TicketListQuery } from "@/ports";
import { Button, Spinner, cn, zIndex } from "@/shared";
import { TicketFacetsBar } from "./TicketFacetsBar";
import { pickerFiltersKey } from "./ticketFilterPersistence";
import { PriorityBadge, StatusBadge, TicketRef as TicketRefBadge } from "./ticketMeta";
import {
  useTicketSearch,
  type TicketPickerResult,
} from "./useTicketSearch";

export type { TicketPickerResult } from "./useTicketSearch";

export type TicketPickerSelectionMode = "single" | "multi";

export interface TicketPickerProps {
  /** Whether the popup is shown. Rendered as `null` when closed. */
  open: boolean;
  projectId: string;
  /** Dialog heading (default "Sélectionner un ticket"). */
  title?: string;
  /** Confirm-button label (multi-select only; unused in single-select V1). */
  confirmLabel?: string;
  /** Selection mode (default "single"). */
  selectionMode?: TicketPickerSelectionMode;
  /** Refs to hide from the results (e.g. the current ticket, already-linked). */
  excludeRefs?: TicketRef[];
  /** Seed filter/search query. */
  initialQuery?: TicketListQuery;
  /**
   * Whether the picker persists its filters across openings (ticket #29,
   * default `true`). `initialQuery` still wins per field, so a caller imposing a
   * business filter keeps priority; a caller that must never inherit stored
   * preferences can opt out entirely with `false`.
   */
  persistFilters?: boolean;
  /**
   * Selection callback. In `"single"` mode it receives one
   * {@link TicketPickerResult}; in `"multi"` it receives the array.
   */
  onSelect: (result: TicketPickerResult | TicketPickerResult[]) => void;
  /** Called on Escape, backdrop click, or the Close button. */
  onClose: () => void;
}

/** Selector for tabbable elements, used by the focus-trap. */
const FOCUSABLE =
  'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';

export function TicketPicker({
  open,
  projectId,
  title = "Sélectionner un ticket",
  confirmLabel = "Sélectionner",
  selectionMode = "single",
  excludeRefs,
  initialQuery,
  persistFilters = true,
  onSelect,
  onClose,
}: TicketPickerProps) {
  if (!open) return null;
  return (
    <TicketPickerBody
      projectId={projectId}
      title={title}
      confirmLabel={confirmLabel}
      selectionMode={selectionMode}
      excludeRefs={excludeRefs}
      initialQuery={initialQuery}
      persistFilters={persistFilters}
      onSelect={onSelect}
      onClose={onClose}
    />
  );
}

/**
 * The mounted picker. Split from {@link TicketPicker} so the hook (and its
 * fetch) only run while the popup is actually open — mounting/unmounting on
 * `open` resets state cleanly between openings.
 */
function TicketPickerBody({
  projectId,
  title,
  confirmLabel,
  selectionMode,
  excludeRefs,
  initialQuery,
  persistFilters,
  onSelect,
  onClose,
}: Required<
  Pick<
    TicketPickerProps,
    | "projectId"
    | "title"
    | "confirmLabel"
    | "selectionMode"
    | "persistFilters"
    | "onSelect"
    | "onClose"
  >
> &
  Pick<TicketPickerProps, "excludeRefs" | "initialQuery">) {
  const vm = useTicketSearch(projectId, {
    initialQuery,
    excludeRefs,
    persistenceKey: persistFilters ? pickerFiltersKey(projectId) : undefined,
  });
  const dialogRef = useRef<HTMLDivElement>(null);
  const [selecting, setSelecting] = useState(false);
  const [resolveError, setResolveError] = useState<string | null>(null);
  // Multi-select (#41): the locally-accumulated selection. Persists across
  // filter/search changes so the user can build a batch from several queries.
  const [selected, setSelected] = useState<Set<TicketRef>>(() => new Set());
  const isMulti = selectionMode === "multi";

  // Focus-trap (G5): remember the previously-focused element, move focus into
  // the dialog on mount, keep Tab cycling inside it, and restore focus on close.
  useEffect(() => {
    const previouslyFocused = document.activeElement as HTMLElement | null;
    const node = dialogRef.current;
    // Focus the first focusable control (the search input) once mounted.
    const first = node?.querySelector<HTMLElement>(FOCUSABLE);
    first?.focus();

    function onKeyDown(e: KeyboardEvent) {
      if (e.key === "Escape") {
        e.preventDefault();
        onClose();
        return;
      }
      if (e.key !== "Tab" || !node) return;
      const focusables = Array.from(
        node.querySelectorAll<HTMLElement>(FOCUSABLE),
      ).filter((el) => el.offsetParent !== null || el === document.activeElement);
      if (focusables.length === 0) return;
      const firstEl = focusables[0];
      const lastEl = focusables[focusables.length - 1];
      const active = document.activeElement;
      if (e.shiftKey && active === firstEl) {
        e.preventDefault();
        lastEl.focus();
      } else if (!e.shiftKey && active === lastEl) {
        e.preventDefault();
        firstEl.focus();
      }
    }

    document.addEventListener("keydown", onKeyDown);
    return () => {
      document.removeEventListener("keydown", onKeyDown);
      previouslyFocused?.focus?.();
    };
  }, [onClose]);

  function describeErr(e: unknown): string {
    return e && typeof e === "object" && "message" in e
      ? String((e as { message: unknown }).message)
      : String(e);
  }

  /** Multi-select: toggle a row in/out of the local selection (never closes). */
  function toggleSelected(ref: TicketRef) {
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(ref)) next.delete(ref);
      else next.add(ref);
      return next;
    });
  }

  /** Single-select: resolve the clicked row and hand it back, then close. */
  async function handlePick(ref: TicketRef) {
    if (selecting) return;
    setSelecting(true);
    setResolveError(null);
    try {
      const result = await vm.resolve(ref);
      onSelect(result);
      onClose();
    } catch (e) {
      setResolveError(describeErr(e));
      setSelecting(false);
    }
  }

  /** Multi-select: resolve every selected ref and hand back the array. */
  async function handleConfirm() {
    if (selecting || selected.size === 0) return;
    setSelecting(true);
    setResolveError(null);
    try {
      const results = await Promise.all(
        Array.from(selected).map((ref) => vm.resolve(ref)),
      );
      onSelect(results);
      onClose();
    } catch (e) {
      setResolveError(describeErr(e));
      setSelecting(false);
    }
  }

  return (
    <div
      className="fixed inset-0 flex items-center justify-center bg-black/55 p-4"
      style={{ zIndex: zIndex.floatingWindowNested }}
      onMouseDown={(e) => {
        // Backdrop click closes; clicks inside the panel don't bubble here.
        if (e.target === e.currentTarget) onClose();
      }}
    >
      <div
        ref={dialogRef}
        role="dialog"
        aria-modal="true"
        aria-label={title}
        className="flex max-h-[80vh] w-full max-w-lg flex-col overflow-hidden rounded-lg border border-border bg-surface shadow-xl"
      >
        {/* ── Header ── */}
        <header className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3">
          <span className="text-sm font-medium text-content">{title}</span>
          <Button
            size="sm"
            variant="ghost"
            aria-label="close ticket picker"
            onClick={onClose}
          >
            Close
          </Button>
        </header>

        {/* ── Filters ── */}
        <div className="shrink-0 border-b border-border px-4 py-3">
          <TicketFacetsBar
            text={vm.text}
            onTextChange={vm.setText}
            statuses={vm.statuses}
            priorities={vm.priorities}
            onToggleStatus={vm.toggleStatus}
            onTogglePriority={vm.togglePriority}
            onClearFacets={vm.clearFacets}
            sort={vm.sort}
            onSortChange={vm.setSort}
          />
        </div>

        {(vm.error || resolveError) && (
          <p
            role="alert"
            className="shrink-0 border-b border-danger/40 bg-danger/10 px-4 py-2 text-sm text-danger"
          >
            {resolveError ?? vm.error}
          </p>
        )}

        {/* ── Results ── */}
        <div className="min-h-0 flex-1 overflow-auto px-4 py-3">
          {vm.busy && vm.rows.length === 0 ? (
            <div className="flex items-center gap-2 text-sm text-muted">
              <Spinner size={14} />
              <span>Loading tickets…</span>
            </div>
          ) : vm.rows.length === 0 ? (
            <p className="text-sm text-muted">No matching tickets.</p>
          ) : (
            <ul className="flex flex-col divide-y divide-border">
              {vm.rows.map((t) => {
                const checked = selected.has(t.ref);
                return (
                  <li key={t.ref} className="py-1.5 first:pt-0 last:pb-0">
                    <button
                      type="button"
                      aria-label={`select ticket ${t.ref}`}
                      {...(isMulti
                        ? { role: "checkbox", "aria-checked": checked }
                        : {})}
                      disabled={selecting}
                      onClick={() =>
                        isMulti ? toggleSelected(t.ref) : void handlePick(t.ref)
                      }
                      className={cn(
                        "flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left transition-colors",
                        "hover:bg-raised focus:bg-raised focus:outline-none",
                        "disabled:cursor-not-allowed disabled:opacity-60",
                        isMulti && checked && "bg-raised",
                      )}
                    >
                      {isMulti && (
                        <span
                          aria-hidden="true"
                          className={cn(
                            "mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded border text-[10px] font-bold",
                            checked
                              ? "border-primary bg-primary text-on-primary"
                              : "border-border text-transparent",
                          )}
                        >
                          ✓
                        </span>
                      )}
                      <TicketRefBadge ticketRef={t.ref} className="mt-0.5" />
                      <span className="min-w-0 flex-1 truncate text-sm font-medium text-content">
                        {t.title}
                      </span>
                      <span className="flex shrink-0 flex-col items-end gap-1">
                        <StatusBadge status={t.status} />
                        <PriorityBadge priority={t.priority} />
                      </span>
                    </button>
                  </li>
                );
              })}
            </ul>
          )}

          {vm.hasMore && (
            <div className="mt-3 flex justify-center">
              <Button
                size="sm"
                variant="ghost"
                loading={vm.busy}
                onClick={() => vm.loadMore()}
              >
                Load more
              </Button>
            </div>
          )}
        </div>

        {/* ── Multi-select confirm footer (#41) — count + confirm; disabled
            while empty or resolving. Single-select has no footer. ── */}
        {isMulti && (
          <footer className="flex shrink-0 items-center justify-between gap-2 border-t border-border px-4 py-3">
            <span className="text-xs text-muted" aria-live="polite">
              {selected.size} sélectionné{selected.size > 1 ? "s" : ""}
            </span>
            <div className="flex items-center gap-2">
              <Button
                size="sm"
                variant="ghost"
                aria-label="cancel ticket selection"
                onClick={onClose}
              >
                Annuler
              </Button>
              <Button
                size="sm"
                aria-label="confirm ticket selection"
                loading={selecting}
                disabled={selected.size === 0 || selecting}
                onClick={() => void handleConfirm()}
              >
                {selected.size > 0
                  ? `${confirmLabel} (${selected.size})`
                  : confirmLabel}
              </Button>
            </div>
          </footer>
        )}
      </div>
    </div>
  );
}
