/**
 * What Piggy is allowed to do, chosen before the question is asked.
 *
 * This is the only control in PIG that decides whether a language model may
 * write to the company's book, so it is written to be read rather than to be
 * clever. Four things follow from that and are deliberate:
 *
 *   names — the segments say "Read only", "Ask first" and "Auto", not
 *           `read_only` / `confirm` / `auto`. The enum is the wire's language
 *           and nobody choosing a permission should have to learn it.
 *   consequence — the sentence under the segments describes the mode that is
 *           selected NOW, and changes as the selection does. A toggle whose
 *           meaning lives in documentation is a toggle people set once and then
 *           misremember.
 *   the keyboard — the arrows move focus and do NOT select. The ARIA pattern
 *           says they should, and for a preference it would be right; for a
 *           permission it meant that arrowing across to read what Auto does
 *           turned Auto on. See `keyboardAt`.
 *   the exceptions — `auto` still stops at a contract, a commitment, an
 *           allocation and anything compliance-shaped. That is `requiresApproval`'s
 *           rule, and if the control does not say so, the first person to choose
 *           auto will reasonably assume nothing stops, and will either be
 *           frightened of the mode or trust it further than it deserves.
 *
 * The mode is NOT enforced here. `requiresApproval` in @pig/core is the single
 * source of truth and the agent applies it server-side; this control only tells
 * the relay what the user picked. Treating it as a guard would put the
 * authorisation in the browser, where the user can edit it.
 */
import { useCallback, useEffect, useId, useRef, useState, type JSX } from 'react';
import { Eye, ListChecks, TriangleAlert, Zap, type LucideIcon } from 'lucide-react';
import {
  PIGGY_ALWAYS_CONFIRM_KINDS,
  type PiggyGuardedKind,
  type PiggyMode,
} from '@pig/core';
import { PIGGY_DEFAULT_MODE } from '@/lib/piggy-chat';
import { useOptionalIdentity } from '@/lib/identity';
import { Label, cn } from '@/components/ui';

// ------------------------------------------------------------------- copy

interface ModeOption {
  value: PiggyMode;
  /** The user's word for it. */
  label: string;
  icon: LucideIcon;
  /** What choosing this mode means, in one sentence, present tense. */
  sentence: string;
  /** True when picking it hands an agent the ability to write unattended. */
  consequential?: boolean;
}

/**
 * The four kinds `requiresApproval` refuses to automate, spelled for a person.
 *
 * Derived from `PIGGY_ALWAYS_CONFIRM_KINDS` rather than typed out, because the
 * sentence is a promise about policy: if a fifth guarded kind is added upstream
 * and this copy were a literal, the control would quietly go on promising four.
 * The map is exhaustive by type, so adding one there fails the build here.
 */
const GUARDED_KIND_LABELS: Record<PiggyGuardedKind, string> = {
  contract: 'contracts',
  commitment: 'commitments',
  allocation: 'allocations',
  compliance: 'compliance',
};

const GUARDED_SENTENCE = (() => {
  const names = PIGGY_ALWAYS_CONFIRM_KINDS.map((kind) => GUARDED_KIND_LABELS[kind]);
  // en-GB: "contracts, commitments, allocations and compliance".
  const list = new Intl.ListFormat('en-GB', { style: 'long', type: 'conjunction' }).format(names);
  return `${list.charAt(0).toUpperCase()}${list.slice(1)} still stop for your approval.`;
})();

const READ_ONLY_OPTION: ModeOption = {
  value: 'read_only',
  label: 'Read only',
  icon: Eye,
  sentence: 'Piggy answers from your CRM and is offered no tool that could change it.',
};

const MODE_OPTIONS: readonly ModeOption[] = [
  READ_ONLY_OPTION,
  {
    value: 'confirm',
    label: 'Ask first',
    icon: ListChecks,
    sentence: 'Piggy proposes each change and nothing is saved until you press Apply.',
  },
  {
    value: 'auto',
    label: 'Auto',
    icon: Zap,
    sentence: 'Piggy makes changes to your CRM itself, without asking first.',
    consequential: true,
  },
];

/** Why the write modes are unavailable. Shown, never merely implied. */
const NO_WRITE_REASON =
  'Your access does not allow changing records, so Piggy can only read.';

/**
 * The user's word for a mode, and its icon, for a control that summarises this
 * one rather than replacing it — the workspace header's trigger.
 *
 * Exported rather than restated at the call site: the trigger says what the
 * segments say, and a second copy of "Ask first" is a second opinion waiting to
 * disagree with this file the first time the copy is edited.
 */
export function piggyModeSummary(mode: PiggyMode): { label: string; icon: LucideIcon } {
  const option = optionFor(mode);
  return { label: option.label, icon: option.icon };
}

function optionFor(mode: PiggyMode): ModeOption {
  // The union is closed and the array covers it; the fallback exists so a mode
  // read back from storage on a future build cannot render an empty control.
  return MODE_OPTIONS.find((option) => option.value === mode) ?? READ_ONLY_OPTION;
}

// ---------------------------------------------------------------- control

export function PiggyModeControl({
  value,
  onChange,
  compact = false,
  canWrite,
}: {
  value: PiggyMode;
  onChange: (mode: PiggyMode) => void;
  compact?: boolean;
  canWrite: boolean;
}): JSX.Element {
  const describedBy = useId();
  const buttons = useRef(new Map<PiggyMode, HTMLButtonElement>());

  /**
   * Where the keyboard is, which is not the same as what is chosen.
   *
   * The ARIA radiogroup pattern normally selects whatever the arrow keys land
   * on. That is right for a preference and wrong for a permission: arrowing
   * across to read what Auto does was granting an agent unattended write access
   * to the book, and the sentence explaining the consequence appeared *because*
   * the consequence had already been accepted. Here the arrows move focus, the
   * sentence updates to describe what is under the cursor, and Space or Enter
   * is what commits. Null means the keyboard is elsewhere and the tabstop
   * belongs to the selected segment, so tabbing back in returns to the choice
   * in force rather than to wherever the last arrow press stopped.
   */
  const [keyboardAt, setKeyboardAt] = useState<PiggyMode | null>(null);

  /**
   * What is drawn as selected. Not necessarily what the parent holds: a stored
   * `auto` outlives the capability that justified it, so someone whose write
   * grant was removed would otherwise open the composer being told Piggy is
   * about to edit records it will now be refused.
   */
  const selected: PiggyMode = canWrite ? value : 'read_only';

  useEffect(() => {
    // The correction is pushed up rather than kept local, because the parent is
    // what puts `mode` on the wire. Showing read-only while sending `auto`
    // would be the one disagreement this control must never have. It cannot
    // loop: the parent's next value satisfies the condition.
    if (!canWrite && value !== 'read_only') onChange('read_only');
  }, [canWrite, value, onChange]);

  const choices = MODE_OPTIONS.filter((option) => canWrite || option.value === 'read_only');

  /** Roving tabstop: the keyboard's position if it has one, else the choice. */
  const roving: PiggyMode = keyboardAt ?? selected;

  const moveTo = useCallback((next: PiggyMode | undefined) => {
    if (!next) return;
    setKeyboardAt(next);
    buttons.current.get(next)?.focus();
  }, []);

  const step = useCallback(
    (direction: 1 | -1) => {
      const index = choices.findIndex((option) => option.value === roving);
      moveTo(choices[(index + direction + choices.length) % choices.length]?.value);
    },
    [choices, moveTo, roving],
  );

  /**
   * The sentence describes what the keyboard is on, not what is chosen — so
   * someone arrowing across Auto reads its consequence before deciding, which
   * is the whole point of no longer selecting on focus.
   */
  const active = optionFor(roving);
  /** True while the keyboard is reading a mode that has not been chosen. */
  const previewing = roving !== selected;

  return (
    <div className={cn('flex min-w-0 flex-col', compact ? 'gap-1.5' : 'gap-2')}>
      {compact ? null : <Label>What Piggy may do</Label>}

      <div
        role="radiogroup"
        aria-label="What Piggy may do"
        aria-describedby={describedBy}
        className="grid grid-cols-3 gap-1 rounded-lg border border-border bg-surface-2 p-1"
        onBlur={(event) => {
          // Leaving the group hands the tabstop back to the chosen segment, so
          // the next Tab in lands on the mode in force rather than on whichever
          // one the reader stopped over last time.
          if (!event.currentTarget.contains(event.relatedTarget)) setKeyboardAt(null);
        }}
        onKeyDown={(event) => {
          if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
            event.preventDefault();
            step(1);
          } else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
            event.preventDefault();
            step(-1);
          } else if (event.key === 'Home') {
            event.preventDefault();
            moveTo(choices[0]?.value);
          } else if (event.key === 'End') {
            event.preventDefault();
            moveTo(choices[choices.length - 1]?.value);
          }
        }}
      >
        {MODE_OPTIONS.map((option) => {
          const isSelected = option.value === selected;
          const disabled = !canWrite && option.value !== 'read_only';
          const Icon = option.icon;
          return (
            <button
              key={option.value}
              ref={(node) => {
                if (node) buttons.current.set(option.value, node);
                else buttons.current.delete(option.value);
              }}
              type="button"
              role="radio"
              aria-checked={isSelected}
              // Roving tabstop: a radio group is one stop in the tab order, and
              // the arrow keys move within it.
              tabIndex={option.value === roving ? 0 : -1}
              disabled={disabled}
              title={disabled ? NO_WRITE_REASON : option.sentence}
              onClick={() => {
                setKeyboardAt(option.value);
                onChange(option.value);
              }}
              className={cn(
                'flex min-h-[44px] min-w-0 items-center justify-center rounded-md',
                'font-medium transition-colors duration-1 ease-enter touch-manipulation select-none',
                // Tight enough that "Read only" survives whole in a dock
                // narrower than the 22rem one; the label is what makes this
                // control legible, so it is the last thing allowed to truncate.
                compact ? 'gap-1 px-1 text-xs' : 'gap-1.5 px-2 text-xs sm:text-sm',
                isSelected
                  ? 'bg-surface text-fg shadow-sm'
                  : 'text-muted hover:text-fg disabled:hover:text-muted',
                disabled && 'cursor-not-allowed opacity-50',
              )}
            >
              <Icon
                aria-hidden
                className={cn(
                  'shrink-0',
                  compact ? 'h-3 w-3' : 'h-4 w-4',
                  isSelected && option.consequential ? 'text-warning' : undefined,
                )}
              />
              <span className="truncate">{option.label}</span>
            </button>
          );
        })}
      </div>

      {/*
        Announced on change, because the consequence arrives a beat after the
        press and a screen-reader user gets no colour to tell them the tone of
        the panel changed. It follows the keyboard rather than the choice, so
        arrowing across Auto reads its consequence — which is the only way to
        find out, now that arrowing no longer turns it on.
      */}
      <div id={describedBy} aria-live="polite" className="min-w-0 text-xs leading-snug">
        {active.consequential ? (
          <p
            className={cn(
              'flex items-start gap-2 rounded-lg border border-warning/40 bg-warning/10 text-fg',
              compact ? 'px-2 py-1.5' : 'px-2.5 py-2',
            )}
          >
            <TriangleAlert aria-hidden className="mt-px h-3.5 w-3.5 shrink-0 text-warning" />
            <span>
              {previewing ? <span className="font-medium">{active.label}: </span> : null}
              {active.sentence} <span className="font-medium">{GUARDED_SENTENCE}</span>
              {previewing ? ' Press Enter to choose it.' : null}
            </span>
          </p>
        ) : (
          <p className="text-muted">
            {previewing ? <span className="font-medium text-fg">{active.label}: </span> : null}
            {active.sentence}
            {previewing ? ' Press Enter to choose it.' : null}
          </p>
        )}
        {canWrite ? null : <p className="mt-1 text-muted">{NO_WRITE_REASON}</p>}
      </div>
    </div>
  );
}

// -------------------------------------------------------------- preference

/**
 * Per user, not per browser.
 *
 * Two people share a laptop far more often than a CRM's security model likes to
 * admit, and a single `pig.piggy.mode` key would hand the second one an agent
 * already licensed to write by the first. The signed-in id is part of the key
 * for that reason alone.
 */
const MODE_STORAGE_PREFIX = 'pig.piggy.mode.';

function isMode(value: unknown): value is PiggyMode {
  return MODE_OPTIONS.some((option) => option.value === value);
}

function readStoredMode(key: string | null): PiggyMode | null {
  if (!key) return null;
  try {
    const raw = localStorage.getItem(key);
    // Validated rather than cast: a value written by an older build, or edited
    // by hand, would otherwise travel to the relay as a mode and collect a 400
    // on every turn until someone cleared their storage.
    return isMode(raw) ? raw : null;
  } catch {
    // Private browsing throws on access. The default is the safe one anyway.
    return null;
  }
}

/**
 * The stored answer to "what may Piggy do", defaulting to `read_only`.
 *
 * `PIGGY_DEFAULT_MODE` is imported rather than restated so this cannot become a
 * second opinion on what "safe" means; it is read-only, which is both the
 * safest mode and a useful one — Piggy still answers every question it can
 * answer, and the only thing withheld is the ability to change records, which
 * is exactly the thing a person should turn on knowingly. Defaulting to
 * `confirm` would be defensible on the grounds that it never writes unasked,
 * but it puts write tools in front of the model on first use for someone who
 * never asked for them, and the relay would then be told so on every turn.
 */
export function usePiggyMode(): { mode: PiggyMode; setMode: (mode: PiggyMode) => void } {
  const identity = useOptionalIdentity();
  const key = identity ? `${MODE_STORAGE_PREFIX}${identity.id}` : null;
  const [mode, setModeState] = useState<PiggyMode>(() => readStoredMode(key) ?? PIGGY_DEFAULT_MODE);

  useEffect(() => {
    // Re-read whenever the person changes. Falling back to the default rather
    // than keeping what is on screen matters here: a new signed-in user with no
    // stored preference must not inherit the last one's `auto`.
    setModeState(readStoredMode(key) ?? PIGGY_DEFAULT_MODE);
  }, [key]);

  const setMode = useCallback(
    (next: PiggyMode) => {
      setModeState(next);
      if (!key) return;
      try {
        localStorage.setItem(key, next);
      } catch {
        // Non-fatal: the choice simply does not survive the tab, and the next
        // one opens read-only, which is the harmless direction to fail in.
      }
    },
    [key],
  );

  return { mode, setMode };
}
