/**
 * Sprint-selection popup (ticket #38) — the sprint analogue of
 * {@link TicketPicker}. A modal that lists the project's sprints (via
 * {@link TicketGateway.listSprints}) and lets the caller pick one, or the
 * explicit **« Sans sprint »** entry to clear the assignment.
 *
 * Used by the ticket-creation form to attach a sprint at creation time, but it
 * is a generic read-only picker: it never mutates: it just hands the chosen
 * `Sprint` (or `null` for "no sprint") back through `onSelect`.
 *
 * Integration rules (mirrors {@link TicketPicker}, note
 * `ui-rework-sprint-scoping-contracts`):
 * - G5: mount at the **chrome/panel level**, never inside LayoutGrid/LeafView. A
 *   focus-trap is mandatory because xterm captures keyboard focus.
 * - z-index `floatingWindowNested` (60): it opens from within other floating
 *   surfaces (the create form), so it must sit above them.
 */

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

import type { GatewayError, Sprint } from "@/domain";
import { Button, Spinner, cn, zIndex } from "@/shared";
import { useGateways } from "@/app/di";

export interface SprintPickerProps {
  /** Whether the popup is shown. Rendered as `null` when closed. */
  open: boolean;
  projectId: string;
  /** The currently-selected sprint id (highlighted); `null`/absent ⇒ none. */
  selectedSprintId?: string | null;
  /** Dialog heading (default "Sélectionner un sprint"). */
  title?: string;
  /** Selection callback — a `Sprint`, or `null` for the "Sans sprint" entry. */
  onSelect: (sprint: Sprint | null) => 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 SprintPicker({
  open,
  projectId,
  selectedSprintId,
  title = "Sélectionner un sprint",
  onSelect,
  onClose,
}: SprintPickerProps) {
  if (!open) return null;
  return (
    <SprintPickerBody
      projectId={projectId}
      selectedSprintId={selectedSprintId ?? null}
      title={title}
      onSelect={onSelect}
      onClose={onClose}
    />
  );
}

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

/**
 * The mounted picker. Split from {@link SprintPicker} so the sprint fetch only
 * runs while the popup is actually open — mounting/unmounting on `open` resets
 * state cleanly between openings.
 */
function SprintPickerBody({
  projectId,
  selectedSprintId,
  title,
  onSelect,
  onClose,
}: Required<Omit<SprintPickerProps, "open">>) {
  const { ticket } = useGateways();
  const dialogRef = useRef<HTMLDivElement>(null);
  const [sprints, setSprints] = useState<Sprint[] | null>(null);
  const [error, setError] = useState<string | null>(null);

  // Load the project's sprints once on mount.
  useEffect(() => {
    let cancelled = false;
    setError(null);
    void ticket
      .listSprints(projectId)
      .then((list) => {
        if (!cancelled) setSprints(list);
      })
      .catch((e) => {
        if (!cancelled) {
          setError(describe(e));
          setSprints([]);
        }
      });
    return () => {
      cancelled = true;
    };
  }, [ticket, projectId]);

  // Focus-trap (G5): mirror TicketPicker — move focus in on mount, keep Tab
  // cycling inside the dialog, restore focus on close, and close on Escape.
  useEffect(() => {
    const previouslyFocused = document.activeElement as HTMLElement | null;
    const node = dialogRef.current;
    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]);

  const ordered = sprints
    ? [...sprints].sort((a, b) => a.order - b.order)
    : [];

  function pick(sprint: Sprint | null) {
    onSelect(sprint);
    onClose();
  }

  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-sm 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 sprint picker"
            onClick={onClose}
          >
            Close
          </Button>
        </header>

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

        {/* ── Options ── */}
        <div className="min-h-0 flex-1 overflow-auto px-4 py-3">
          <ul className="flex flex-col divide-y divide-border">
            {/* "Sans sprint" always available (clears the assignment). */}
            <li className="py-1.5 first:pt-0">
              <SprintOption
                label="Sans sprint"
                ariaLabel="select no sprint"
                selected={selectedSprintId === null}
                onClick={() => pick(null)}
              />
            </li>

            {sprints === null ? (
              <li className="flex items-center gap-2 py-2 text-sm text-muted">
                <Spinner size={14} />
                <span>Loading sprints…</span>
              </li>
            ) : (
              ordered.map((s) => (
                <li key={s.id} className="py-1.5 last:pb-0">
                  <SprintOption
                    label={s.name}
                    ariaLabel={`select sprint ${s.name}`}
                    count={s.ticketCount}
                    selected={selectedSprintId === s.id}
                    onClick={() => pick(s)}
                  />
                </li>
              ))
            )}
          </ul>

          {sprints !== null && ordered.length === 0 && !error && (
            <p className="mt-2 text-sm text-muted">No sprints yet.</p>
          )}
        </div>
      </div>
    </div>
  );
}

/** One selectable row (checkmark on the active entry), shared shape/styling. */
function SprintOption({
  label,
  ariaLabel,
  count,
  selected,
  onClick,
}: {
  label: string;
  ariaLabel: string;
  count?: number;
  selected: boolean;
  onClick: () => void;
}) {
  return (
    <button
      type="button"
      role="option"
      aria-selected={selected}
      aria-label={ariaLabel}
      onClick={onClick}
      className={cn(
        "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors",
        "hover:bg-raised focus:bg-raised focus:outline-none",
        selected && "bg-raised",
      )}
    >
      <span
        aria-hidden="true"
        className={cn(
          "flex h-4 w-4 shrink-0 items-center justify-center text-[10px] font-bold",
          selected ? "text-primary" : "text-transparent",
        )}
      >
        ✓
      </span>
      <span className="min-w-0 flex-1 truncate text-sm font-medium text-content">
        {label}
      </span>
      {count !== undefined && (
        <span className="shrink-0 rounded-full bg-raised px-1.5 text-[10px] font-medium text-muted">
          {count}
        </span>
      )}
    </button>
  );
}
