// ---------------------------------------------------------------------------
// customer-grid / FieldSelect.tsx
// Wave-14 item 20 (contract C-FLDSEL) — the field picker that can show an icon.
//
// A native <select> cannot render a mark beside an option, so every surface that
// asks "which field?" was a bare list of labels while the column header, the
// Hide-fields panel and the create picker all wore the type vocabulary. This is
// that vocabulary's fourth painter: a button in the `.cg-select` footprint that
// opens a listbox of icon + label rows.
//
// THREE STATES, not two — the whole reason this replaces a <select>:
//   unset                  -> the PLACEHOLDER, never the first field
//   set and offered        -> that field's mark + label
//   set but NOT offered    -> the field's own label if the caller still passes it
//                             (Toolbar's `withCurrentField` does), else the raw key
//                             in a muted "missing" slot
// The third is why `<select>` was dangerous here: a `value` matching no <option>
// renders the FIRST one, so a saved view holding a dropped column displayed a
// different field than it held and the first edit wrote that displayed one back
// ([[cg-condition-builder-items]]).
//
// The rows are real <button>s under role="listbox"/role="option", exactly as
// ColumnMenu's TypePicker ships them: Enter/Space activate natively, Tab moves,
// and the arrow keys are the only thing this file has to add.
// ---------------------------------------------------------------------------

import { Fragment, useMemo, useRef, useState } from "react";
import type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from "react";
import { AnchoredOverlay } from "./OverlaySurface";
import { FieldTypeIcon } from "./icons";
import { TYPE_LABELS } from "./iconShapes";
import type { FieldType } from "./types";

/** The pseudo-kind rides along: a metric is picked from these lists like a column is. */
export type FieldSelectType = FieldType | "measure";

export interface FieldSelectItem {
  key: string;
  label: string;
  /** The type mark. Omit only when `icon` supplies a mark from another vocabulary. */
  type?: FieldSelectType;
  /** Overrides the type mark — the copy-configuration modal picks VIEWS, which wear their
   *  display-mode mark rather than a field type. */
  icon?: ReactNode;
  /** A quiet heading above this row. Rows are rendered in the order given; a heading is
   *  emitted whenever the group CHANGES, so the caller owns the grouping by ordering its
   *  own list. ⚠ Do not group the filter builder's field list — owner item 1 of the previous
   *  wave made that ONE FLAT LIST on purpose. */
  group?: string;
  /** A second line under the label (a metric's period, a view's mode). */
  hint?: string;
  disabled?: boolean;
}

/** Re-exported so a consumer needs one import for the pair (C-FLDSEL names both). */
export { FieldTypeIcon };

function ItemMark({ item }: { item: FieldSelectItem }) {
  if (item.icon) return <>{item.icon}</>;
  if (!item.type) return <span className="cg-fsel-nomark" aria-hidden />;
  return (
    <FieldTypeIcon
      type={item.type}
      title={item.type === "measure" ? "Metric" : TYPE_LABELS[item.type]}
    />
  );
}

function CaretDown() {
  return (
    <svg
      className="cg-fsel-caret"
      width="10"
      height="10"
      viewBox="0 0 16 16"
      fill="none"
      aria-hidden
    >
      <path
        d="M4 6l4 4 4-4"
        stroke="currentColor"
        strokeWidth={1.5}
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function CheckMark() {
  return (
    <svg
      className="cg-type-check"
      width="14"
      height="14"
      viewBox="0 0 16 16"
      fill="none"
      aria-hidden
    >
      <path
        d="m3.5 8.5 3 3 6-6.5"
        stroke="currentColor"
        strokeWidth={1.6}
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

/** Above this many rows the find box earns its place; below it, it is one more thing to
 *  look past on a list you can already see all of. */
const SEARCH_FROM = 8;

export function FieldSelectButton({
  fields,
  value,
  onChange,
  placeholder = "Choose a field…",
  allow,
  ariaLabel,
  className,
  disabled,
  /** Rendered above the list — the surface's own sentence ("Group by", "Copy from"). */
  title,
  /** Keeps the picker open after a choice (the reference-inserter picks repeatedly). */
  keepOpen,
  /** Marks this control as its pane's initial focus target — the attribute `AnchoredOverlay`'s
   *  `initialFocus` selector looks for. A replaced `<select>` that carried it must keep it,
   *  or the pane opens with focus nowhere. */
  overlayAutofocus,
}: {
  fields: readonly FieldSelectItem[];
  value?: string;
  onChange: (key: string) => void;
  placeholder?: string;
  /** Restrict the OFFER by type. The current value is still resolved against the full list,
   *  so narrowing the offer never hides what the rule actually holds. */
  allow?: readonly FieldSelectType[];
  ariaLabel?: string;
  className?: string;
  disabled?: boolean;
  title?: string;
  keepOpen?: boolean;
  overlayAutofocus?: boolean;
}) {
  const [open, setOpen] = useState(false);
  const [q, setQ] = useState("");
  const triggerRef = useRef<HTMLButtonElement>(null);
  const listRef = useRef<HTMLDivElement>(null);

  const offered = useMemo(
    () =>
      allow
        ? fields.filter((f) => f.type != null && allow.includes(f.type))
        : fields,
    [fields, allow]
  );
  /**
   * ⚠ `""` is a VALUE here, not the absence of one — the map's encodings offer an explicit
   * "No colour" row whose key is the empty string, so the list can MARK the none state
   * instead of leaving every row unmarked. Which means "is anything set" cannot be `!!value`,
   * and (the bug this replaced) the label cannot be `current?.label ?? value ?? placeholder`:
   * `??` falls through on null/undefined only, so an empty-string value rendered an EMPTY
   * BUTTON where the placeholder belonged.
   */
  const isSet = value != null;
  // Resolved over the FULL list, never the offer — see the three states above.
  const current = isSet ? fields.find((f) => f.key === value) : undefined;
  const needle = q.trim().toLowerCase();
  const shown = needle
    ? offered.filter((f) => f.label.toLowerCase().includes(needle))
    : offered;

  // No explicit re-focus of the trigger: `AnchoredOverlay`'s `restoreFocus` (default) already
  // returns focus to whatever was focused when it mounted, which is this button. Doing it here
  // too would be a second answer to the same question, and the two would disagree the day the
  // overlay's rule changes.
  const close = () => {
    setOpen(false);
    setQ("");
  };

  /** Arrow keys move between the option buttons; the find box is part of the loop, so
   *  ArrowDown out of it lands on the first row rather than on nothing. */
  const onListKeyDown = (event: ReactKeyboardEvent) => {
    if (event.key !== "ArrowDown" && event.key !== "ArrowUp"
        && event.key !== "Home" && event.key !== "End") return;
    const rows = Array.from(
      listRef.current?.querySelectorAll<HTMLButtonElement>("button.cg-type-row") ?? []
    ).filter((el) => !el.disabled);
    if (!rows.length) return;
    event.preventDefault();
    const at = rows.indexOf(document.activeElement as HTMLButtonElement);
    if (event.key === "Home") return rows[0].focus();
    if (event.key === "End") return rows[rows.length - 1].focus();
    const step = event.key === "ArrowDown" ? 1 : -1;
    // From outside the rows (the find box), Down enters at the top and Up at the bottom.
    const next = at < 0 ? (step === 1 ? 0 : rows.length - 1)
                        : (at + step + rows.length) % rows.length;
    rows[next].focus();
  };

  let lastGroup: string | undefined;

  return (
    <>
      <button
        ref={triggerRef}
        type="button"
        disabled={disabled}
        className={
          "cg-select cg-fsel" +
          (current ? "" : isSet && value !== "" ? " is-missing" : " is-empty") +
          (className ? ` ${className}` : "")
        }
        aria-label={ariaLabel}
        aria-haspopup="listbox"
        aria-expanded={open}
        data-overlay-autofocus={overlayAutofocus ? true : undefined}
        onClick={() => setOpen((o) => !o)}
      >
        {current ? (
          <ItemMark item={current} />
        ) : (
          // No mark, but the SLOT stays: an unresolvable value and the placeholder both keep
          // the label in the same column as every resolved row above and below them.
          <span className="cg-fsel-nomark" aria-hidden />
        )}
        <span className="cg-fsel-label">
          {/* Set to something this table no longer offers AND the caller did not pass a row
              for it: the key is shown VERBATIM in the amber `is-missing` slot. Unreadable is
              the honest rendering of a broken reference, and it is the one thing a <select>
              could never do — it would show its first option instead. */}
          {current?.label ?? (isSet && value !== "" ? value : placeholder)}
        </span>
        <CaretDown />
      </button>
      {open && triggerRef.current && (
        <AnchoredOverlay
          anchor={triggerRef.current}
          className="cg-pop cg-fsel-pop"
          onDismiss={close}
          role="dialog"
          ariaLabel={ariaLabel ?? title ?? "Choose a field"}
          initialFocus={
            offered.length >= SEARCH_FROM ? ".cg-fsel-search input" : "button.cg-type-row"
          }
          dataKind="field-select"
        >
          <div className="cg-fsel-body" onKeyDown={onListKeyDown}>
            {title && <div className="cg-pop-title">{title}</div>}
            {offered.length >= SEARCH_FROM && (
              <div className="cg-type-search cg-fsel-search">
                <svg width="13" height="13" viewBox="0 0 16 16" fill="none" aria-hidden>
                  <circle cx="7" cy="7" r="4.4" stroke="currentColor" strokeWidth="1.35" />
                  <path
                    d="m10.4 10.4 3.1 3.1"
                    stroke="currentColor"
                    strokeWidth="1.35"
                    strokeLinecap="round"
                  />
                </svg>
                <input
                  type="text"
                  value={q}
                  placeholder="Find a field…"
                  aria-label="Find a field"
                  onChange={(event) => setQ(event.target.value)}
                />
              </div>
            )}
            <div className="cg-type-list" role="listbox" ref={listRef}
                 aria-label={ariaLabel ?? title ?? "Fields"}>
              {shown.map((item) => {
                const heading = item.group && item.group !== lastGroup ? item.group : null;
                lastGroup = item.group;
                return (
                  // A Fragment, not a wrapper: `.cg-type-list` is the flex column that lays
                  // the rows out, and a wrapper would need `display: contents` to stay out of
                  // the way — which drops the element from the accessibility tree in exactly
                  // the container that is claiming to be a listbox.
                  <Fragment key={item.key}>
                    {heading && (
                      <div className="cg-fsel-group" role="presentation">
                        {heading}
                      </div>
                    )}
                    <button
                      type="button"
                      role="option"
                      disabled={item.disabled}
                      aria-selected={item.key === value}
                      className={"cg-type-row" + (item.key === value ? " is-on" : "")}
                      onClick={() => {
                        onChange(item.key);
                        if (keepOpen) setQ("");
                        else close();
                      }}
                    >
                      <ItemMark item={item} />
                      <span className="cg-type-label">
                        {item.label}
                        {item.hint && <span className="cg-fsel-hint">{item.hint}</span>}
                      </span>
                      {item.key === value && <CheckMark />}
                    </button>
                  </Fragment>
                );
              })}
              {shown.length === 0 && (
                <div className="cg-pop-note">
                  {offered.length === 0 ? "No field to choose here." : "No field matches."}
                </div>
              )}
            </div>
          </div>
        </AnchoredOverlay>
      )}
    </>
  );
}
