import { useId, useMemo, useState, type ReactNode } from 'react';

import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';

import { enumLabel, fieldLabel, hours } from './format';
import { MAX_TURNS, PRICES, play } from './game';
import { DOC_MINUTES, LOOKUP_MINUTES, TURN_MINUTES } from './score';
import { CLOSE_REASONS, TYPOLOGIES, type CloseReason, type TriageState, type Typology } from './types';

/**
 * The analyst's controls: four lookups with their prices on the button, and
 * a disposition with a citation checklist.
 *
 * Every control produces exactly the JSON object a model would have to
 * produce, and hands it to the same engine. The visitor never sees a control
 * the model does not have, and the reply the engine received is shown under
 * the controls so the action set is learned by using it. The controls own no
 * state the board does not: the next board comes back from `play()` and goes
 * to `onChange`, and the only local state here is what is half-typed.
 */

type Panel = 'none' | 'close' | 'escalate';

const price = (minutes: number) => `+${hours(minutes)} h`;

/** A price or a "done" beside a verb: mono, muted, the way a cost is printed. */
function Price({ children }: { children: ReactNode }) {
  return <span className="font-mono text-caption font-normal text-muted">{children}</span>;
}

function Section({ title, children }: { title: string; children: ReactNode }) {
  return (
    <fieldset className="min-w-0 rounded-control border border-border p-3">
      <legend className="px-1 font-mono text-label uppercase text-muted">{title}</legend>
      {children}
    </fieldset>
  );
}

/**
 * A native select. The border is the only affordance a select has on this
 * ground, so it is `border-strong` (≥3:1 in both themes) rather than the
 * decorative hairline; the focus ring is the site's.
 */
const SELECT =
  'min-h-11 min-w-0 rounded-control border border-border-strong bg-surface px-2.5 font-sans text-ui text-fg ' +
  'transition-colors duration-1 hover:bg-surface-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg ' +
  'disabled:cursor-not-allowed disabled:opacity-50';

/** The citable ids, grouped the way the screen groups them. */
function groupShown(shown: readonly string[]) {
  const groups: { label: string; ids: string[] }[] = [
    { label: 'KYC file', ids: [] },
    { label: 'Transactions', ids: [] },
    { label: 'Counterparties', ids: [] },
    { label: 'Prior alerts', ids: [] },
    { label: 'Documents', ids: [] },
    { label: 'Other', ids: [] },
  ];
  const byId = (a: string, b: string) => {
    const na = Number(a.replace(/^\D+/, ''));
    const nb = Number(b.replace(/^\D+/, ''));
    return Number.isFinite(na) && Number.isFinite(nb) && na !== nb ? na - nb : a.localeCompare(b);
  };
  for (const id of shown) {
    const index = id.startsWith('kyc.')
      ? 0
      : id.startsWith('T-')
        ? 1
        : id.startsWith('CP-')
          ? 2
          : id.startsWith('A-')
            ? 3
            : id.startsWith('doc.')
              ? 4
              : 5;
    groups[index]!.ids.push(id);
  }
  for (const group of groups) group.ids.sort(byId);
  return groups.filter((group) => group.ids.length > 0);
}

export function Controls({
  state,
  onChange,
}: {
  state: TriageState;
  onChange: (next: TriageState) => void;
}) {
  const ids = useId();
  const [month, setMonth] = useState('');
  const [cp, setCp] = useState('');
  const [panel, setPanel] = useState<Panel>('none');
  const [reason, setReason] = useState<CloseReason>('CONSISTENT_WITH_PROFILE');
  const [typology, setTypology] = useState<Typology>('STRUCTURING');
  const [cites, setCites] = useState<string[]>([]);
  const [prose, setProse] = useState('');
  const [lastReply, setLastReply] = useState<string | null>(null);

  const done = state.status !== 'playing';
  const left = MAX_TURNS - state.turnsSpent;

  const send = (object: Record<string, unknown>) => {
    const reply = JSON.stringify(object);
    setLastReply(reply);
    onChange(play(state, reply));
  };

  const monthsLeft = useMemo(
    () => state.screen.months.filter((m) => !state.lookups.some((l) => l.what === 'history' && l.key === m)),
    [state.screen.months, state.lookups],
  );
  const cpsLeft = useMemo(
    () =>
      state.shown
        .filter((id) => id.startsWith('CP-'))
        .filter((id) => !state.lookups.some((l) => l.what === 'counterparty' && l.key === id))
        .sort((a, b) => Number(a.slice(3)) - Number(b.slice(3))),
    [state.shown, state.lookups],
  );
  const priorsMade = state.lookups.some((l) => l.what === 'prior_alerts');
  const docsMade = state.lookups.some((l) => l.what === 'documents');
  const groups = useMemo(() => groupShown(state.shown), [state.shown]);

  const toggleCite = (id: string) =>
    setCites((current) => (current.includes(id) ? current.filter((c) => c !== id) : [...current, id]));

  const submitDisposition = () => {
    if (cites.length === 0 || done) return;
    if (panel === 'close') send({ action: 'close', reason, cites, note: prose });
    else if (panel === 'escalate') send({ action: 'escalate', typology, cites, narrative: prose });
    setPanel('none');
    setCites([]);
    setProse('');
  };

  const status = done
    ? state.status === 'aborted'
      ? `No disposition in ${MAX_TURNS} replies. The alert stays in the queue.`
      : `Disposition recorded after ${hours(state.minutesSpent)} hours. The board shows what the generator planted.`
    : state.lastRejection
      ? `That reply was not accepted: ${state.lastRejection}. It still cost a turn. ${left} ${left === 1 ? 'reply' : 'replies'} left.`
      : `Hours spent so far: ${hours(state.minutesSpent)}. ${left} ${left === 1 ? 'reply' : 'replies'} left.`;

  return (
    <div className="flex flex-col gap-3 pb-[max(1rem,var(--safe-bottom))]">
      {/* The board is a group of tables; the outcome of every click is said
          here in words, where it announces itself. */}
      <p aria-live="polite" className="font-sans text-ui text-fg-2">
        {status}
      </p>

      <Section title={`Lookups — each reply also costs ${price(TURN_MINUTES)}`}>
        <div className="grid grid-cols-1 gap-2">
          <div className="flex min-w-0 gap-2">
            <label className="sr-only" htmlFor={`${ids}-month`}>
              Month to itemise
            </label>
            <select
              id={`${ids}-month`}
              className={cn(SELECT, 'min-w-[7rem] flex-1 font-mono text-mono-data')}
              value={month}
              disabled={done || monthsLeft.length === 0}
              onChange={(event) => setMonth(event.target.value)}
            >
              <option value="">{monthsLeft.length === 0 ? 'every month itemised' : 'month…'}</option>
              {monthsLeft.map((m) => (
                <option key={m} value={m}>
                  {m}
                </option>
              ))}
            </select>
            <Button
              type="button"
              variant="outline"
              size="touch"
              className="shrink-0"
              disabled={done || month === ''}
              onClick={() => {
                send({ action: 'lookup', what: 'history', month });
                setMonth('');
              }}
            >
              History <Price>{price(PRICES.history)}</Price>
            </Button>
          </div>

          <div className="flex min-w-0 gap-2">
            <label className="sr-only" htmlFor={`${ids}-cp`}>
              Counterparty to look up
            </label>
            <select
              id={`${ids}-cp`}
              className={cn(SELECT, 'min-w-[7rem] flex-1 font-mono text-mono-data')}
              value={cp}
              disabled={done || cpsLeft.length === 0}
              onChange={(event) => setCp(event.target.value)}
            >
              <option value="">{cpsLeft.length === 0 ? 'no counterparty left' : 'counterparty…'}</option>
              {cpsLeft.map((id) => (
                <option key={id} value={id}>
                  {id}
                </option>
              ))}
            </select>
            <Button
              type="button"
              variant="outline"
              size="touch"
              className="shrink-0"
              disabled={done || cp === ''}
              onClick={() => {
                send({ action: 'lookup', what: 'counterparty', id: cp });
                setCp('');
              }}
            >
              Counterparty <Price>{price(PRICES.counterparty)}</Price>
            </Button>
          </div>

          {/* Two per row on a phone, so the pair reads as the pair it is. */}
          <div className="grid grid-cols-2 gap-2">
            <Button
              type="button"
              variant="outline"
              size="touch"
              disabled={done || priorsMade}
              onClick={() => send({ action: 'lookup', what: 'prior_alerts' })}
            >
              Prior alerts <Price>{priorsMade ? 'done' : price(LOOKUP_MINUTES)}</Price>
            </Button>
            <Button
              type="button"
              variant="outline"
              size="touch"
              disabled={done || docsMade}
              onClick={() => send({ action: 'lookup', what: 'documents' })}
            >
              Documents <Price>{docsMade ? 'done' : price(DOC_MINUTES)}</Price>
            </Button>
          </div>
        </div>
      </Section>

      <Section title="Disposition — ends the episode">
        {/* A toggle pair: the pressed half is the one green fill in the
            controls, with the accent edge that keeps it a control on paper. */}
        <div className="grid grid-cols-2 gap-2" role="group" aria-label="Choose a disposition">
          <Button
            type="button"
            size="touch"
            variant={panel === 'close' ? 'default' : 'outline'}
            className={cn(panel === 'close' && 'border border-accent-edge shadow-none')}
            aria-pressed={panel === 'close'}
            disabled={done}
            onClick={() => setPanel(panel === 'close' ? 'none' : 'close')}
          >
            Close
          </Button>
          <Button
            type="button"
            size="touch"
            variant={panel === 'escalate' ? 'default' : 'outline'}
            className={cn(panel === 'escalate' && 'border border-accent-edge shadow-none')}
            aria-pressed={panel === 'escalate'}
            disabled={done}
            onClick={() => setPanel(panel === 'escalate' ? 'none' : 'escalate')}
          >
            Escalate
          </Button>
        </div>

        {panel !== 'none' && !done ? (
          <form
            className="mt-3 flex flex-col gap-3"
            onSubmit={(event) => {
              event.preventDefault();
              submitDisposition();
            }}
          >
            <label className="flex flex-col gap-1">
              <span className="font-mono text-label uppercase text-muted">{panel === 'close' ? 'Reason' : 'Typology'}</span>
              {panel === 'close' ? (
                <select className={SELECT} value={reason} onChange={(event) => setReason(event.target.value as CloseReason)}>
                  {CLOSE_REASONS.map((r) => (
                    <option key={r} value={r}>
                      {enumLabel(r)}
                    </option>
                  ))}
                </select>
              ) : (
                <select className={SELECT} value={typology} onChange={(event) => setTypology(event.target.value as Typology)}>
                  {TYPOLOGIES.map((t) => (
                    <option key={t} value={t}>
                      {enumLabel(t)}
                    </option>
                  ))}
                </select>
              )}
            </label>

            <fieldset className="min-w-0">
              <legend className="mb-1 font-sans text-small text-fg-2">
                Cite the facts that decide it — only ids you have been shown
              </legend>
              <div className="flex max-h-56 flex-col gap-2.5 overflow-y-auto overscroll-contain rounded-control border border-border-strong p-2">
                {groups.map((group) => (
                  <div key={group.label}>
                    <p className="mb-1 font-mono text-label uppercase text-muted">{group.label}</p>
                    <div className="flex flex-wrap gap-1">
                      {group.ids.map((id) => {
                        const on = cites.includes(id);
                        return (
                          <label
                            key={id}
                            className={cn(
                              'inline-flex cursor-pointer select-none items-center gap-1 rounded-tile border px-1.5 py-1.5 font-mono text-label tracking-normal transition-colors duration-1',
                              'has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 has-[:focus-visible]:ring-offset-bg',
                              on
                                ? 'border-accent-edge bg-accent-subtle text-fg'
                                : 'border-border-strong text-fg-2 hover:bg-surface-2',
                            )}
                            title={id.startsWith('kyc.') ? fieldLabel(id) : id}
                          >
                            <input type="checkbox" className="sr-only" checked={on} onChange={() => toggleCite(id)} />
                            {on ? <span aria-hidden="true">✓</span> : null}
                            {id}
                          </label>
                        );
                      })}
                    </div>
                  </div>
                ))}
              </div>
            </fieldset>

            <label className="flex flex-col gap-1">
              <span className="flex flex-wrap items-center gap-2">
                <span className="font-mono text-label uppercase text-muted">{panel === 'close' ? 'Note' : 'Narrative'}</span>
                <span className="rounded-tile border border-border-strong px-1.5 py-px font-mono text-label uppercase text-muted">
                  displayed, never scored
                </span>
              </span>
              <textarea
                className="min-h-16 rounded-control border border-border-strong bg-surface px-2.5 py-1.5 font-sans text-ui text-fg placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg"
                value={prose}
                onChange={(event) => setProse(event.target.value)}
                placeholder={panel === 'close' ? 'Why this is consistent with the file…' : 'What the pattern is and why it is reportable…'}
              />
            </label>

            <div className="flex flex-wrap items-center gap-3">
              <Button
                type="submit"
                size="touch"
                className="w-full border border-accent-edge shadow-none sm:w-auto"
                disabled={cites.length === 0}
              >
                {panel === 'close' ? 'Close the alert' : 'Escalate for filing'}
              </Button>
              <span className="font-mono text-caption text-muted">
                {cites.length} cited · {price(TURN_MINUTES)}
              </span>
            </div>
          </form>
        ) : null}
      </Section>

      {lastReply ? (
        <details className="group font-mono text-label uppercase text-muted">
          <summary className="min-h-11 cursor-pointer list-none py-2 [&::-webkit-details-marker]:hidden">
            <span aria-hidden="true" className="mr-1.5 inline-block transition-transform duration-1 group-open:rotate-90">
              ›
            </span>
            The reply the engine received
          </summary>
          <pre className="overflow-x-auto rounded-control border border-border bg-surface-3 p-2 font-mono text-code normal-case tracking-normal text-fg [contain:inline-size]">
            {lastReply}
          </pre>
        </details>
      ) : null}
    </div>
  );
}

export default Controls;
