import {
  Children, isValidElement, useEffect, useLayoutEffect, useMemo, useRef, useState,
  type InputHTMLAttributes, type SelectHTMLAttributes, type ReactNode, type TextareaHTMLAttributes,
} from 'react'
import { createPortal } from 'react-dom'
import { useQuery } from '@tanstack/react-query'
import { ChevronDown } from 'lucide-react'
import { cn } from '@/lib/utils'
import { api } from '@/lib/api'
import { useOrgSettings } from '@/lib/useOrgSettings'
import { localizeNativeValidation } from '@/lib/validation'

interface FieldProps {
  label: string
  error?: string
  children: ReactNode
  className?: string
}

export function Field({ label, error, children, className }: FieldProps) {
  return (
    <label className={cn('block space-y-1.5', className)}>
      <span className="text-sm font-medium text-slate-700 dark:text-slate-200">{label}</span>
      {children}
      {error && <span className="text-xs text-red-600">{error}</span>}
    </label>
  )
}

const inputClass = 'w-full h-10 rounded-md border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 px-3 text-sm leading-none focus:outline-none focus:ring-2 focus:ring-[var(--color-brand)] disabled:opacity-60 disabled:bg-slate-50 dark:disabled:bg-slate-800/60'

export function TextInput(props: InputHTMLAttributes<HTMLInputElement>) {
  // Подключаем локализацию нативных подсказок валидации (required, type=email,
  // pattern и т.д.) — чтобы юзер не видел английских "Please fill out this field".
  const ref = useRef<HTMLInputElement>(null)
  useEffect(() => {
    if (ref.current) localizeNativeValidation(ref.current)
  }, [])
  // Email type без явного pattern — добавляем строгий pattern (требует TLD ≥2),
  // чтобы name@domain без точки/зоны не проходил. title задаёт текст подсказки;
  // localizeNativeValidation подставит его при patternMismatch.
  const extraProps: InputHTMLAttributes<HTMLInputElement> = {}
  if (props.type === 'email' && !props.pattern) {
    extraProps.pattern = '[^@\\s]+@[^@\\s]+\\.[A-Za-z]{2,}'
    extraProps.title = props.title ?? 'Введите корректный email с доменной зоной (например, name@example.kz)'
  }
  return <input ref={ref} {...props} {...extraProps} className={cn(inputClass, props.className)} />
}

export function TextArea(props: TextareaHTMLAttributes<HTMLTextAreaElement>) {
  // TextArea — multi-line, высоту не фиксируем.
  return <textarea {...props} className={cn(inputClass, 'h-auto py-2 font-[inherit] leading-normal', props.className)} />
}

type SelectOption = { value: string; label: string; disabled?: boolean }

function nodeText(node: ReactNode): string {
  if (node == null || node === false) return ''
  if (typeof node === 'string' || typeof node === 'number') return String(node)
  if (Array.isArray(node)) return node.map(nodeText).join('')
  if (isValidElement(node)) return nodeText((node.props as { children?: ReactNode }).children)
  return ''
}

function extractOptions(children: ReactNode): SelectOption[] {
  const out: SelectOption[] = []
  Children.forEach(children, (child) => {
    if (!isValidElement(child)) return
    const t = child.type as unknown as string
    if (t === 'option') {
      const props = child.props as { value?: string | number; children?: ReactNode; disabled?: boolean }
      out.push({
        value: String(props.value ?? ''),
        label: nodeText(props.children),
        disabled: props.disabled,
      })
    } else if (t === 'optgroup') {
      const props = child.props as { children?: ReactNode }
      out.push(...extractOptions(props.children))
    }
  })
  return out
}

/** Drop-in замена нативного <select>: визуально похож на TextInput, но при клике
 * раскрывает выпадающий список с поиском по подстроке. API совместим со старым
 * Select — onChange получает synthetic event с e.target.value. Дочерние <option>
 * парсятся в options array (label = текст внутри <option>).
 *
 * Опциональный onCreate(label) — если задан, в выпадающем списке появляется пункт
 * «Создать «query»» когда введённый текст не совпадает ни с одним label.
 * Колбэк должен создать сущность на сервере и вернуть value (id) нового элемента,
 * после чего Select автоматически подставит его как выбранный. */
export function Select({
  value, onChange, disabled, className, children, placeholder, onCreate, createLabel = 'Создать', ...rest
}: SelectHTMLAttributes<HTMLSelectElement> & {
  placeholder?: string
  onCreate?: (label: string) => Promise<string>
  createLabel?: string
}) {
  const options = useMemo(() => extractOptions(children), [children])
  const [open, setOpen] = useState(false)
  const [query, setQuery] = useState('')
  const [highlight, setHighlight] = useState(0)
  const [creating, setCreating] = useState(false)
  const [createError, setCreateError] = useState<string | null>(null)
  const wrapRef = useRef<HTMLDivElement>(null)
  const searchRef = useRef<HTMLInputElement>(null)
  const dropdownRef = useRef<HTMLDivElement>(null)
  const [pos, setPos] = useState<{ top: number; left: number; width: number; openUp: boolean } | null>(null)

  // Считаем позицию dropdown'а: предпочтительно вниз, но если внизу < 240px,
  // а вверху больше — открываем вверх (auto-flip). Через portal в body
  // — overflow-hidden родителей не режет.
  const recomputePos = () => {
    const el = wrapRef.current
    if (!el) return
    const r = el.getBoundingClientRect()
    const spaceBelow = window.innerHeight - r.bottom
    const spaceAbove = r.top
    const openUp = spaceBelow < 240 && spaceAbove > spaceBelow
    setPos({
      top: openUp ? r.top - 4 : r.bottom + 4,
      left: r.left,
      width: r.width,
      openUp,
    })
  }
  useLayoutEffect(() => {
    if (!open) return
    recomputePos()
    const onScrollOrResize = () => setOpen(false)
    window.addEventListener('scroll', onScrollOrResize, true)
    window.addEventListener('resize', onScrollOrResize)
    return () => {
      window.removeEventListener('scroll', onScrollOrResize, true)
      window.removeEventListener('resize', onScrollOrResize)
    }
  }, [open])

  const current = String(value ?? '')
  const selected = options.find((o) => o.value === current)
  const filtered = useMemo(() => {
    if (!query.trim()) return options
    const q = query.trim().toLowerCase()
    return options.filter((o) => o.label.toLowerCase().includes(q))
  }, [options, query])

  useEffect(() => { setHighlight(0) }, [query, open])

  useEffect(() => {
    if (!open) return
    const onDoc = (e: MouseEvent) => {
      const t = e.target as Node
      const inWrap = wrapRef.current?.contains(t)
      const inDropdown = dropdownRef.current?.contains(t)
      if (!inWrap && !inDropdown) { setOpen(false); setQuery('') }
    }
    document.addEventListener('mousedown', onDoc)
    return () => document.removeEventListener('mousedown', onDoc)
  }, [open])

  useEffect(() => {
    if (open) requestAnimationFrame(() => searchRef.current?.focus())
  }, [open])

  const choose = (v: string) => {
    setOpen(false); setQuery(''); setCreateError(null)
    if (onChange) {
      const fake = { target: { value: v }, currentTarget: { value: v } } as unknown as React.ChangeEvent<HTMLSelectElement>
      onChange(fake)
    }
  }

  const trimmed = query.trim()
  const exactMatch = options.some((o) => o.label.toLowerCase() === trimmed.toLowerCase())
  const canCreate = !!onCreate && trimmed.length > 0 && !exactMatch
  const handleCreate = async () => {
    if (!onCreate || !trimmed) return
    setCreating(true); setCreateError(null)
    try {
      const newId = await onCreate(trimmed)
      choose(newId)
    } catch (e) {
      setCreateError((e as Error).message || 'Не удалось создать')
    } finally {
      setCreating(false)
    }
  }

  const triggerLabel = selected
    ? selected.label
    : (placeholder ?? (options.find((o) => o.value === '')?.label ?? '—'))

  return (
    <div ref={wrapRef} className="relative">
      <button
        type="button"
        disabled={disabled}
        onClick={() => !disabled && setOpen((o) => !o)}
        aria-haspopup="listbox"
        aria-expanded={open}
        data-name={rest.name}
        className={cn(inputClass, 'flex items-center justify-between text-left pr-9', className)}
      >
        <span className={cn('truncate', !selected && 'text-slate-400')}>{triggerLabel}</span>
        <ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
      </button>
      {open && pos && createPortal(
        <div
          ref={dropdownRef}
          style={{
            position: 'fixed',
            left: pos.left,
            width: pos.width,
            ...(pos.openUp ? { bottom: window.innerHeight - pos.top } : { top: pos.top }),
          }}
          className="z-[100] max-h-72 overflow-hidden rounded-md border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-lg flex flex-col"
        >
          <div className="p-2 border-b border-slate-100 dark:border-slate-800">
            <input
              ref={searchRef}
              value={query}
              onChange={(e) => setQuery(e.target.value)}
              placeholder="Поиск…"
              className="w-full h-8 rounded border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-2 text-sm focus:outline-none focus:ring-1 focus:ring-[var(--color-brand)]"
              onKeyDown={(e) => {
                if (e.key === 'Escape') { e.preventDefault(); setOpen(false); setQuery('') }
                else if (e.key === 'ArrowDown') {
                  e.preventDefault()
                  setHighlight((h) => Math.min(h + 1, Math.max(0, filtered.length - 1)))
                } else if (e.key === 'ArrowUp') {
                  e.preventDefault()
                  setHighlight((h) => Math.max(0, h - 1))
                } else if (e.key === 'Enter') {
                  e.preventDefault()
                  if (filtered.length === 0 && canCreate) { handleCreate(); return }
                  const opt = filtered[highlight]
                  if (opt && !opt.disabled) choose(opt.value)
                }
              }}
            />
          </div>
          <ul className="py-1 overflow-auto" role="listbox">
            {filtered.length === 0 && !canCreate ? (
              <li className="px-3 py-2 text-sm text-slate-400">Ничего не найдено</li>
            ) : filtered.map((opt, i) => (
              <li key={`${opt.value}-${i}`}>
                <button
                  type="button"
                  disabled={opt.disabled}
                  onMouseEnter={() => setHighlight(i)}
                  onClick={() => !opt.disabled && choose(opt.value)}
                  className={cn(
                    'w-full text-left px-3 py-1.5 text-sm flex items-center',
                    i === highlight && !opt.disabled && 'bg-slate-100 dark:bg-slate-800',
                    opt.value === current && 'font-medium text-[var(--color-brand)]',
                    opt.disabled && 'opacity-50 cursor-not-allowed',
                  )}
                >
                  <span className="truncate">{opt.label || '—'}</span>
                </button>
              </li>
            ))}
            {canCreate && (
              <li className="border-t border-slate-100 dark:border-slate-800 mt-1 pt-1">
                <button
                  type="button"
                  disabled={creating}
                  onClick={handleCreate}
                  className="w-full text-left px-3 py-1.5 text-sm hover:bg-slate-100 dark:hover:bg-slate-800 text-[var(--color-brand)] disabled:opacity-60"
                >
                  {creating ? 'Создаю…' : `${createLabel} «${trimmed}»`}
                </button>
                {createError && (
                  <div className="px-3 py-1 text-xs text-red-600">{createError}</div>
                )}
              </li>
            )}
          </ul>
        </div>,
        document.body,
      )}
    </div>
  )
}

/** Select с серверным поиском: при каждом вводе дёргает endpoint с ?search=&pageSize=20.
 * Не требует заранее загруженных опций — подходит для справочников с неограниченным ростом
 * (контрагенты, группы товаров). API: value/onChange работают с ID строкой.
 * getLabel — как извлечь отображаемый текст из пришедшего объекта (дефолт: item.name). */
export function AsyncSelect({
  url, value, onChange, placeholder, disabled, className, name: htmlName,
  getLabel = (item: Record<string, unknown>) => String(item['name'] ?? ''),
  onCreate, createLabel = 'Создать',
}: {
  url: string
  value?: string | null
  onChange: (value: string) => void
  placeholder?: string
  disabled?: boolean
  className?: string
  name?: string
  getLabel?: (item: Record<string, unknown>) => string
  onCreate?: (label: string) => Promise<string>
  createLabel?: string
}) {
  const [open, setOpen] = useState(false)
  const [query, setQuery] = useState('')
  const [debouncedQuery, setDebouncedQuery] = useState('')
  const [highlight, setHighlight] = useState(0)
  const [selectedLabel, setSelectedLabel] = useState('')
  const [creating, setCreating] = useState(false)
  const [createError, setCreateError] = useState<string | null>(null)
  const wrapRef = useRef<HTMLDivElement>(null)
  const searchRef = useRef<HTMLInputElement>(null)
  const dropdownRef = useRef<HTMLDivElement>(null)
  const [pos, setPos] = useState<{ top: number; left: number; width: number; openUp: boolean } | null>(null)

  useEffect(() => {
    const t = setTimeout(() => setDebouncedQuery(query), 300)
    return () => clearTimeout(t)
  }, [query])

  const { data: options = [], isFetching } = useQuery<Record<string, unknown>[]>({
    queryKey: ['async-select', url, debouncedQuery],
    queryFn: async () => {
      const sep = url.includes('?') ? '&' : '?'
      const res = await api.get<{ items: Record<string, unknown>[] }>(
        `${url}${sep}search=${encodeURIComponent(debouncedQuery.trim())}&pageSize=20`,
      )
      return res.data.items
    },
    enabled: open,
    staleTime: 30_000,
    placeholderData: (prev) => prev,
  })

  useEffect(() => {
    if (!value) { setSelectedLabel(''); return }
    const found = options.find((o) => o['id'] === value)
    if (found) setSelectedLabel(getLabel(found))
  }, [options, value, getLabel])

  const recomputePos = () => {
    const el = wrapRef.current
    if (!el) return
    const r = el.getBoundingClientRect()
    const spaceBelow = window.innerHeight - r.bottom
    const openUp = spaceBelow < 240 && r.top > spaceBelow
    setPos({ top: openUp ? r.top - 4 : r.bottom + 4, left: r.left, width: r.width, openUp })
  }
  useLayoutEffect(() => {
    if (!open) return
    recomputePos()
    const fn = () => setOpen(false)
    window.addEventListener('scroll', fn, true)
    window.addEventListener('resize', fn)
    return () => { window.removeEventListener('scroll', fn, true); window.removeEventListener('resize', fn) }
  }, [open])

  useEffect(() => {
    if (!open) return
    const onDoc = (e: MouseEvent) => {
      const t = e.target as Node
      if (!wrapRef.current?.contains(t) && !dropdownRef.current?.contains(t)) { setOpen(false); setQuery('') }
    }
    document.addEventListener('mousedown', onDoc)
    return () => document.removeEventListener('mousedown', onDoc)
  }, [open])

  useEffect(() => { if (open) requestAnimationFrame(() => searchRef.current?.focus()) }, [open])
  useEffect(() => { setHighlight(0) }, [query, open])

  const choose = (id: string, label: string) => {
    setOpen(false); setQuery(''); setCreateError(null)
    setSelectedLabel(label); onChange(id)
  }

  const trimmed = query.trim()
  const exactMatch = options.some((o) => getLabel(o).toLowerCase() === trimmed.toLowerCase())
  const canCreate = !!onCreate && trimmed.length > 0 && !exactMatch

  const handleCreate = async () => {
    if (!onCreate || !trimmed) return
    setCreating(true); setCreateError(null)
    try { choose(await onCreate(trimmed), trimmed) }
    catch (e) { setCreateError((e as Error).message || 'Не удалось создать') }
    finally { setCreating(false) }
  }

  const triggerLabel = selectedLabel || (value ? '…' : (placeholder ?? '—'))

  return (
    <div ref={wrapRef} className="relative">
      <button type="button" disabled={disabled} onClick={() => !disabled && setOpen((o) => !o)}
        data-name={htmlName}
        className={cn(inputClass, 'flex items-center justify-between text-left pr-9', className)}
      >
        <span className={cn('truncate', !selectedLabel && !value && 'text-slate-400')}>{triggerLabel}</span>
        <ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
      </button>
      {open && pos && createPortal(
        <div ref={dropdownRef}
          style={{ position: 'fixed', left: pos.left, width: pos.width, ...(pos.openUp ? { bottom: window.innerHeight - pos.top } : { top: pos.top }) }}
          className="z-[100] max-h-72 overflow-hidden rounded-md border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-lg flex flex-col"
        >
          <div className="p-2 border-b border-slate-100 dark:border-slate-800">
            <input ref={searchRef} value={query} onChange={(e) => setQuery(e.target.value)}
              placeholder="Поиск…"
              className="w-full h-8 rounded border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-2 text-sm focus:outline-none focus:ring-1 focus:ring-[var(--color-brand)]"
              onKeyDown={(e) => {
                if (e.key === 'Escape') { e.preventDefault(); setOpen(false); setQuery('') }
                else if (e.key === 'ArrowDown') { e.preventDefault(); setHighlight((h) => Math.min(h + 1, Math.max(0, options.length - 1))) }
                else if (e.key === 'ArrowUp') { e.preventDefault(); setHighlight((h) => Math.max(0, h - 1)) }
                else if (e.key === 'Enter') {
                  e.preventDefault()
                  if (options.length === 0 && canCreate) { handleCreate(); return }
                  const opt = options[highlight]
                  if (opt) choose(String(opt['id'] ?? ''), getLabel(opt))
                }
              }}
            />
          </div>
          <ul className="py-1 overflow-auto" role="listbox">
            {isFetching ? (
              <li className="px-3 py-2 text-sm text-slate-400">Загрузка…</li>
            ) : options.length === 0 && !canCreate ? (
              <li className="px-3 py-2 text-sm text-slate-400">Ничего не найдено</li>
            ) : options.map((opt, i) => {
              const id = String(opt['id'] ?? ''); const label = getLabel(opt)
              return (
                <li key={id}>
                  <button type="button" onMouseEnter={() => setHighlight(i)} onClick={() => choose(id, label)}
                    className={cn('w-full text-left px-3 py-1.5 text-sm block',
                      i === highlight && 'bg-slate-100 dark:bg-slate-800',
                      id === value && 'font-medium text-[var(--color-brand)]',
                    )}
                  >
                    <span className="truncate block">{label}</span>
                  </button>
                </li>
              )
            })}
            {canCreate && (
              <li className="border-t border-slate-100 dark:border-slate-800 mt-1 pt-1">
                <button type="button" disabled={creating} onClick={handleCreate}
                  className="w-full text-left px-3 py-1.5 text-sm hover:bg-slate-100 dark:hover:bg-slate-800 text-[var(--color-brand)] disabled:opacity-60"
                >
                  {creating ? 'Создаю…' : `${createLabel} «${trimmed}»`}
                </button>
                {createError && <div className="px-3 py-1 text-xs text-red-600">{createError}</div>}
              </li>
            )}
          </ul>
        </div>,
        document.body,
      )}
    </div>
  )
}

interface MoneyInputProps {
  value: number | null | undefined
  onChange: (v: number | null) => void
  currencyCode?: string | null
  currencySymbol?: string | null
  /** Если задан — переопределяет настройку организации. Если не задан —
   * читается из useOrgSettings().allowFractionalPrices. */
  allowFractional?: boolean
  disabled?: boolean
  placeholder?: string
  className?: string
}

/** Денежное поле: только цифры (+ точка/запятая если allowFractional),
 * справа — символ валюты (₸/$/€). Хранит локальный draft string чтобы
 * пользователь мог посимвольно ввести "100.50" без того чтобы точка
 * пропадала на пути value→Number(...)→back. Снаружи прокидывается number
 * (или null если пусто); финальная нормализация — на onBlur. */
export function MoneyInput({
  value, onChange, currencyCode, currencySymbol, allowFractional,
  disabled, placeholder = '0', className,
}: MoneyInputProps) {
  const org = useOrgSettings()
  const settingKnown = allowFractional !== undefined || org.data !== undefined
  const fractional = allowFractional ?? org.data?.allowFractionalPrices ?? true

  const formatFromValue = (v: number | null | undefined, frac: boolean) => {
    if (v == null) return ''
    // При fractional=true всегда показываем ровно 2 знака после запятой
    // (1317 → «1317.00», 1317.5 → «1317.50»). Так юзеру визуально понятно,
    // что поле допускает копейки и текущее значение — это целое.
    if (frac) return v.toFixed(2)
    return String(Math.round(v))
  }

  // Локальный draft — то что юзер реально печатает. Может быть промежуточным
  // ("100.", "100.5", "0."). Sync с внешним value только когда поле не в фокусе.
  const [draft, setDraft] = useState<string>(() => formatFromValue(value, fractional))
  const [focused, setFocused] = useState(false)

  useEffect(() => {
    if (focused) return
    setDraft(formatFromValue(value, fractional))
  }, [value, fractional, focused])

  // Если настройка fractional=false, а в state лежит дробь — округляем наружу,
  // чтобы при сохранении ушло то, что пользователь видит. Не делаем пока юзер
  // активно печатает (focused) — иначе перебьём его ввод.
  useEffect(() => {
    if (!settingKnown || fractional || focused) return
    if (value == null) return
    const rounded = Math.round(value)
    if (rounded !== value) onChange(rounded)
  }, [settingKnown, fractional, focused, value, onChange])

  const suffix = currencySymbol || currencyCode || '₸'

  const commitDraft = (raw: string) => {
    const cleaned = fractional ? raw.replace(/[^\d.]/g, '') : raw.replace(/[^\d]/g, '')
    if (cleaned === '' || cleaned === '.') { onChange(null); setDraft(''); return }
    const parts = cleaned.split('.')
    const normalized = parts.length > 1 ? parts[0] + '.' + parts.slice(1).join('') : cleaned
    const n = Number(normalized)
    if (!Number.isFinite(n)) { onChange(null); setDraft(''); return }
    const final = fractional ? n : Math.round(n)
    onChange(final)
    setDraft(formatFromValue(final, fractional))
  }

  return (
    <div className={cn('relative', className)}>
      <input
        type="text"
        inputMode={fractional ? 'decimal' : 'numeric'}
        disabled={disabled}
        value={draft}
        placeholder={placeholder}
        onFocus={(e) => { setFocused(true); e.currentTarget.select() }}
        onBlur={() => { setFocused(false); commitDraft(draft) }}
        onChange={(e) => {
          let raw = e.target.value.replace(',', '.')
          raw = fractional ? raw.replace(/[^\d.]/g, '') : raw.replace(/[^\d]/g, '')
          if (fractional) {
            const parts = raw.split('.')
            if (parts.length > 2) raw = parts[0] + '.' + parts.slice(1).join('')
          }
          // Draft сохраняем как есть — даже промежуточное "100." с точкой на конце,
          // чтобы пользователь мог продолжать печатать дробную часть.
          setDraft(raw)
          if (raw === '' || raw === '.') { onChange(null); return }
          // Промежуточное «100.» — снаружи отдаём 100 как число, draft не трогаем.
          if (fractional && raw.endsWith('.')) {
            const n = Number(raw.slice(0, -1))
            if (Number.isFinite(n)) onChange(n)
            return
          }
          const n = Number(raw)
          if (Number.isFinite(n)) onChange(fractional ? n : Math.round(n))
        }}
        className={cn(inputClass, 'pr-10 text-right tabular-nums')}
      />
      <span className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-sm text-slate-500">
        {suffix}
      </span>
    </div>
  )
}

interface PercentInputProps {
  value: number | null | undefined
  onChange: (v: number | null) => void
  disabled?: boolean
  placeholder?: string
  className?: string
}

/** Поле для процента: только цифры + запятая/точка, до 2 знаков. Справа суффикс «%».
 * onBlur нормализует значение до 2 знаков (Math.round * 100 / 100). null если пусто. */
export function PercentInput({ value, onChange, disabled, placeholder = '—', className }: PercentInputProps) {
  const [draft, setDraft] = useState<string>(value == null ? '' : value.toFixed(2))
  const [focused, setFocused] = useState(false)
  useEffect(() => {
    if (focused) return
    setDraft(value == null ? '' : value.toFixed(2))
  }, [value, focused])

  const commit = (raw: string) => {
    const cleaned = raw.replace(',', '.').replace(/[^\d.]/g, '')
    if (cleaned === '' || cleaned === '.') { onChange(null); setDraft(''); return }
    const parts = cleaned.split('.')
    const normalized = parts.length > 1 ? parts[0] + '.' + parts.slice(1).join('') : cleaned
    const n = Number(normalized)
    if (!Number.isFinite(n)) { onChange(null); setDraft(''); return }
    const rounded = Math.round(n * 100) / 100
    onChange(rounded)
    setDraft(rounded.toFixed(2))
  }

  return (
    <div className={cn('relative', className)}>
      <input
        type="text"
        inputMode="decimal"
        disabled={disabled}
        value={draft}
        placeholder={placeholder}
        onFocus={(e) => { setFocused(true); e.currentTarget.select() }}
        onBlur={() => { setFocused(false); commit(draft) }}
        onChange={(e) => {
          let raw = e.target.value.replace(',', '.').replace(/[^\d.]/g, '')
          const parts = raw.split('.')
          if (parts.length > 2) raw = parts[0] + '.' + parts.slice(1).join('')
          setDraft(raw)
          if (raw === '' || raw === '.') { onChange(null); return }
          if (raw.endsWith('.')) {
            const n = Number(raw.slice(0, -1))
            if (Number.isFinite(n)) onChange(n)
            return
          }
          const n = Number(raw)
          if (Number.isFinite(n)) onChange(n)
        }}
        className={cn(inputClass, 'pr-8 text-right tabular-nums')}
      />
      <span className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-sm text-slate-500">%</span>
    </div>
  )
}

interface NumberInputProps {
  value: number | null | undefined
  onChange: (v: number | null) => void
  step?: number
  disabled?: boolean
  placeholder?: string
  className?: string
}

/** Числовое поле без валюты: только цифры + точка/запятая. Пустое → null. */
export function NumberInput({
  value, onChange, disabled, placeholder = '0', className,
}: NumberInputProps) {
  const display = value == null ? '' : String(value)
  return (
    <input
      type="text"
      inputMode="decimal"
      disabled={disabled}
      value={display}
      placeholder={placeholder}
      onFocus={(e) => e.currentTarget.select()}
      onChange={(e) => {
        const raw = e.target.value.replace(',', '.').replace(/[^\d.]/g, '')
        if (raw === '') return onChange(null)
        const parts = raw.split('.')
        const cleaned = parts.length > 1 ? parts[0] + '.' + parts.slice(1).join('') : raw
        const n = Number(cleaned)
        if (!Number.isFinite(n)) return
        onChange(n)
      }}
      className={cn(inputClass, 'text-right tabular-nums', className)}
    />
  )
}

export function Checkbox({
  label,
  checked,
  onChange,
  disabled,
}: {
  label: string
  checked: boolean
  onChange: (v: boolean) => void
  disabled?: boolean
}) {
  return (
    <label className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-200 cursor-pointer">
      <input
        type="checkbox"
        checked={checked}
        disabled={disabled}
        onChange={(e) => onChange(e.target.checked)}
        className="w-4 h-4 rounded border-slate-300 text-[var(--color-brand)] focus:ring-[var(--color-brand)] accent-[var(--color-brand)]"
      />
      {label}
    </label>
  )
}
