/**
 * scaffold-ui-primitives/generate.ts — Build EntityLookup.tsx + i18n locale stubs.
 *
 * Pure function — no I/O. Caller in index.ts writes the returned files honoring
 * each file's `strategy` (overwrite vs deep-merge-json for locales).
 *
 * Theme-compliance contract: EntityLookup.tsx MUST use only SmartStack tokens
 * (var(--bg-card), var(--color-accent-*), …) — zero hardcoded Tailwind colors
 * (bg-blue-500, text-gray-700, dark:bg-…). The companion test asserts this
 * with a regex sweep so a future hand-edit can't silently regress it.
 */
import type { GeneratedFile, ScaffoldUiPrimitivesInput } from './types.js';

const HEADER = `/* AUTO-GENERATED — re-run scaffold-ui-primitives to refresh. Add @customised at the top to opt out. */`;

const LOCALES = ['fr', 'en', 'it', 'de'] as const;

const I18N_STRINGS: Record<typeof LOCALES[number], Record<string, string>> = {
  fr: {
    placeholder: 'Rechercher…',
    loading: 'Chargement…',
    empty: 'Aucun résultat',
    error: 'Erreur de chargement',
    clear: 'Effacer la sélection',
    search: 'Rechercher',
  },
  en: {
    placeholder: 'Search…',
    loading: 'Loading…',
    empty: 'No results',
    error: 'Failed to load',
    clear: 'Clear selection',
    search: 'Search',
  },
  it: {
    placeholder: 'Cerca…',
    loading: 'Caricamento…',
    empty: 'Nessun risultato',
    error: 'Errore di caricamento',
    clear: 'Cancella selezione',
    search: 'Cerca',
  },
  de: {
    placeholder: 'Suchen…',
    loading: 'Wird geladen…',
    empty: 'Keine Ergebnisse',
    error: 'Ladefehler',
    clear: 'Auswahl löschen',
    search: 'Suchen',
  },
};

function entityLookupComponent(): string {
  return `${HEADER}
import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { api } from '@atlashub/smartstack';
import { Search, ChevronDown, X, Loader2 } from 'lucide-react';

/**
 * Paginated reference shape returned by GET /api/{module}/{plural}/lookup.
 * Matches \`{Entity}RefDto\` emitted by scaffold-business.
 */
interface RefItem {
  id: string;
  displayName: string;
  [k: string]: unknown;
}

interface PaginatedResult<T> {
  items: T[];
  // Matches SmartStack.app PaginatedResult<T>.TotalCount on the wire (JSON: "totalCount").
  totalCount: number;
  page: number;
  pageSize: number;
}

export interface EntityLookupProps<TItem extends RefItem = RefItem> {
  /** Endpoint hitting the lookup contract, e.g. \`/api/hr/departments/lookup\`. */
  apiEndpoint: string;
  /** Currently selected entity id (the FK Guid to persist). \`null\` = empty. */
  value: string | null;
  /** Receives the selected id (or null on clear) and, when available, the
   *  full item so the caller can store a denormalised label without a refetch. */
  onChange: (id: string | null, item?: TItem) => void;
  /** Visible field label rendered above the input. */
  label: string;
  /** Override for the search input placeholder. Defaults to
   *  \`t('entityLookup.placeholder')\`. */
  placeholder?: string;
  /** Optional remap when the backend returns more than \`{ id, displayName }\`.
   *  Defaults to \`(it) => ({ id: it.id, label: it.displayName })\`. */
  mapOption?: (item: TItem) => { id: string; label: string; sublabel?: string };
  required?: boolean;
  disabled?: boolean;
  /** Error string rendered under the input — typically a form validation
   *  message from react-hook-form / zod. */
  error?: string;
  /** Multi-select — not implemented yet; reserved so the API doesn't break
   *  callers when the feature lands. Throws at runtime if set to true. */
  multi?: boolean;
}

const PAGE_SIZE = 20;
const DEBOUNCE_MS = 300;

export function EntityLookup<TItem extends RefItem = RefItem>({
  apiEndpoint,
  value,
  onChange,
  label,
  placeholder,
  mapOption,
  required = false,
  disabled = false,
  error,
  multi = false,
}: EntityLookupProps<TItem>) {
  if (multi) {
    throw new Error('EntityLookup: multi-select is not implemented yet (PR3 scope).');
  }
  const { t } = useTranslation('common');
  const [search, setSearch] = useState('');
  const [debouncedSearch, setDebouncedSearch] = useState('');
  const [open, setOpen] = useState(false);
  const [focusedIdx, setFocusedIdx] = useState(0);
  const inputRef = useRef<HTMLInputElement>(null);
  const listRef = useRef<HTMLUListElement>(null);

  const [data, setData] = useState<PaginatedResult<TItem> | undefined>();
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);
  const [selectedItem, setSelectedItem] = useState<TItem | null>(null);

  // Debounce the search input so we don't fire a request per keystroke.
  useEffect(() => {
    const t = setTimeout(() => setDebouncedSearch(search), DEBOUNCE_MS);
    return () => clearTimeout(t);
  }, [search]);

  // Fetch lookup results when dropdown is open.
  const fetchResults = useCallback(async (signal: AbortSignal) => {
    setIsLoading(true);
    setIsError(false);
    try {
      const result = await api.get<PaginatedResult<TItem>>(apiEndpoint, {
        params: { search: debouncedSearch || undefined, page: 1, pageSize: PAGE_SIZE },
        signal,
      });
      setData(result);
    } catch (err) {
      if (!(err instanceof DOMException && err.name === 'AbortError')) setIsError(true);
    } finally {
      setIsLoading(false);
    }
  }, [apiEndpoint, debouncedSearch]);

  useEffect(() => {
    if (!open) return;
    const ac = new AbortController();
    void fetchResults(ac.signal);
    return () => ac.abort();
  }, [open, fetchResults]);

  // Resolve the currently selected item label so the input shows a human
  // name instead of the raw Guid.
  useEffect(() => {
    if (!value) { setSelectedItem(null); return; }
    let cancelled = false;
    api.get<PaginatedResult<TItem>>(apiEndpoint, { params: { search: value, pageSize: 1 } })
      .then((result) => { if (!cancelled) setSelectedItem(result.items.find((it) => it.id === value) ?? null); })
      .catch(() => { if (!cancelled) setSelectedItem(null); });
    return () => { cancelled = true; };
  }, [apiEndpoint, value]);

  const options = useMemo(() => {
    const items = data?.items ?? [];
    return items.map((it) => {
      const mapped = mapOption ? mapOption(it) : { id: it.id, label: it.displayName };
      return { ...mapped, raw: it };
    });
  }, [data, mapOption]);

  const selectedLabel = useMemo(() => {
    if (!value) return '';
    if (!selectedItem) return value;
    return mapOption ? mapOption(selectedItem).label : selectedItem.displayName;
  }, [value, selectedItem, mapOption]);

  function pick(idx: number): void {
    const opt = options[idx];
    if (!opt) return;
    onChange(opt.id, opt.raw);
    setOpen(false);
    setSearch('');
    inputRef.current?.blur();
  }

  function onKeyDown(e: KeyboardEvent<HTMLInputElement>): void {
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      if (!open) setOpen(true);
      setFocusedIdx((i) => Math.min(i + 1, options.length - 1));
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      setFocusedIdx((i) => Math.max(i - 1, 0));
    } else if (e.key === 'Enter') {
      e.preventDefault();
      pick(focusedIdx);
    } else if (e.key === 'Escape') {
      e.preventDefault();
      setOpen(false);
      inputRef.current?.blur();
    }
  }

  const inputId = useMemo(() => \`entity-lookup-\${Math.random().toString(36).slice(2, 9)}\`, []);
  const listId = \`\${inputId}-list\`;

  return (
    <div className="flex flex-col gap-1">
      <label htmlFor={inputId} className="text-sm font-medium text-[var(--text-primary)]">
        {label}
        {required && <span className="ml-0.5 text-[var(--color-danger-500)]" aria-label="required">*</span>}
      </label>
      <div className="relative">
        <div className="absolute inset-y-0 left-2.5 flex items-center pointer-events-none">
          {isLoading && open ? (
            <Loader2 className="w-4 h-4 animate-spin text-[var(--text-muted)]" />
          ) : (
            <Search className="w-4 h-4 text-[var(--text-muted)]" />
          )}
        </div>
        <input
          id={inputId}
          ref={inputRef}
          type="text"
          role="combobox"
          aria-expanded={open}
          aria-controls={listId}
          aria-autocomplete="list"
          aria-required={required}
          aria-invalid={Boolean(error)}
          value={open ? search : selectedLabel}
          onChange={(e) => {
            setSearch(e.target.value);
            setFocusedIdx(0);
            if (!open) setOpen(true);
          }}
          onFocus={() => setOpen(true)}
          onBlur={() => setTimeout(() => setOpen(false), 150)}
          onKeyDown={onKeyDown}
          placeholder={placeholder ?? t('entityLookup.placeholder', { defaultValue: 'Search…' })}
          disabled={disabled}
          className="w-full pl-9 pr-16 py-2 rounded-[var(--radius-input,0.375rem)] border border-[var(--border-color)] bg-[var(--bg-card)] text-[var(--text-primary)] placeholder-[var(--text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-500)] focus:border-transparent disabled:opacity-60 disabled:cursor-not-allowed"
        />
        <div className="absolute inset-y-0 right-2 flex items-center gap-1">
          {value && !disabled && (
            <button
              type="button"
              onMouseDown={(e) => {
                e.preventDefault();
                onChange(null);
                setSearch('');
              }}
              aria-label={t('entityLookup.clear', { defaultValue: 'Clear selection' })}
              className="p-1 rounded hover:bg-[var(--bg-hover)] text-[var(--text-muted)]"
            >
              <X className="w-3.5 h-3.5" />
            </button>
          )}
          <ChevronDown className="w-4 h-4 text-[var(--text-muted)]" />
        </div>
        {open && (
          <ul
            id={listId}
            ref={listRef}
            role="listbox"
            className="absolute z-10 mt-1 w-full max-h-64 overflow-y-auto rounded-[var(--radius-input,0.375rem)] border border-[var(--border-color)] bg-[var(--bg-card)] shadow-lg"
          >
            {isError && (
              <li className="px-3 py-2 text-sm text-[var(--text-muted)]">
                {t('entityLookup.error', { defaultValue: 'Failed to load' })}
              </li>
            )}
            {!isError && isLoading && options.length === 0 && (
              <li className="px-3 py-2 text-sm text-[var(--text-muted)] flex items-center gap-2">
                <Loader2 className="w-3.5 h-3.5 animate-spin" />
                {t('entityLookup.loading', { defaultValue: 'Loading…' })}
              </li>
            )}
            {!isError && !isLoading && options.length === 0 && (
              <li className="px-3 py-2 text-sm text-[var(--text-muted)]">
                {t('entityLookup.empty', { defaultValue: 'No results' })}
              </li>
            )}
            {options.map((opt, idx) => (
              <li
                key={opt.id}
                role="option"
                aria-selected={value === opt.id}
                onMouseEnter={() => setFocusedIdx(idx)}
                onMouseDown={(e) => {
                  e.preventDefault();
                  pick(idx);
                }}
                className={\`px-3 py-2 text-sm cursor-pointer flex flex-col text-[var(--text-primary)] \${
                  idx === focusedIdx ? 'bg-[var(--bg-hover)]' : ''
                } \${value === opt.id ? 'font-medium' : ''}\`}
              >
                <span>{opt.label}</span>
                {opt.sublabel && (
                  <span className="text-xs text-[var(--text-muted)]">{opt.sublabel}</span>
                )}
              </li>
            ))}
          </ul>
        )}
      </div>
      {error && (
        <p role="alert" className="text-xs text-[var(--color-danger-500)]">
          {error}
        </p>
      )}
    </div>
  );
}
`;
}

function localeFile(locale: typeof LOCALES[number]): GeneratedFile {
  const payload = { entityLookup: I18N_STRINGS[locale] };
  return {
    path: `src/i18n/locales/${locale}/common.json`,
    content: JSON.stringify(payload, null, 2) + '\n',
    strategy: 'deep-merge-json',
  };
}

export function generate(_spec: ScaffoldUiPrimitivesInput): GeneratedFile[] {
  return [
    { path: 'src/components/ui/EntityLookup.tsx', content: entityLookupComponent(), strategy: 'overwrite' },
    ...LOCALES.map(localeFile),
  ];
}
