import { useRef, useState, type ChangeEvent, type DragEvent, type FormEvent } from 'react'
import { Alert } from '@/components/ui/Alert'
import { Button } from '@/components/ui/Button'
import { Dialog } from '@/components/ui/Dialog'
import { SelectField, TextField } from '@/components/ui/Field'
import { Icon } from '@/components/ui/Icon'
import { toast } from '@/components/ui/toast'
import { errorMessage } from '@/lib/api'
import { cn } from '@/lib/cn'
import { CATEGORY_LABELS, PLANT_CATEGORIES, isBuiltin, type PlantCategory, type PlantInput } from '@/lib/plants'
import { lotDefaults, newPlantDefaults, useCreateFromPacket, useScanPacket, type PacketProposal } from '@/lib/seedPacket'
import { LOT_UNITS, type LotUnit } from '@/lib/seedLots'
import { cmFromSpacing, spacingFromCm, spacingUnitLabel, type UnitPref } from '@/lib/units'

const categoryOptions = PLANT_CATEGORIES.map((c) => ({ value: c, label: CATEGORY_LABELS[c] }))
const unitOptions = LOT_UNITS.map((u) => ({ value: u.value, label: u.label }))

// 'new' is "create a new plant"; a number selects that existing candidate.
type Selection = number | 'new'

/**
 * Photograph a seed packet → the vision model reads it into fields → the user
 * matches it to the catalog and confirms (#81/#102). Two steps in one dialog.
 * The read never writes; a misread can't add anything on its own, and a wrong
 * auto-match would split a variety's history across duplicate rows — so the
 * match is always a human choice. Only offered where `capabilities.vision` is
 * on; a 503 is still handled in case the model is torn down in between.
 */
export function ScanPacketDialog({ unit, onClose }: { unit: UnitPref; onClose: () => void }) {
  const scan = useScanPacket()
  const create = useCreateFromPacket()
  const fileInput = useRef<HTMLInputElement>(null)
  const scanAbort = useRef<AbortController | null>(null)

  const [proposal, setProposal] = useState<PacketProposal | null>(null)
  const [error, setError] = useState<string | null>(null)
  const [dragOver, setDragOver] = useState(false)

  const [selection, setSelection] = useState<Selection>('new')
  const [name, setName] = useState('')
  const [category, setCategory] = useState<PlantCategory>('vegetable')
  const [spacing, setSpacing] = useState('')
  const [days, setDays] = useState('')
  const [vendor, setVendor] = useState('')
  const [quantity, setQuantity] = useState('')
  const [lotUnit, setLotUnit] = useState<LotUnit>('packets')
  const [packedForYear, setPackedForYear] = useState('')

  const unitLabel = spacingUnitLabel(unit)
  const busy = scan.isPending || create.isPending

  function readFile(file: File | undefined) {
    if (!file) return
    setError(null)
    const controller = new AbortController()
    scanAbort.current = controller
    scan.mutate(
      { file, signal: controller.signal },
      {
        onSuccess: (p) => {
          const plant = newPlantDefaults(p)
          const lot = lotDefaults(p.packet)
          setProposal(p)
          setSelection(p.candidates[0]?.plant.id ?? 'new')
          setName(plant.name)
          setCategory(plant.category)
          setSpacing(String(spacingFromCm(plant.spacingCm, unit)))
          setDays(plant.daysToMaturity != null ? String(plant.daysToMaturity) : '')
          setVendor(lot.vendor)
          setQuantity(String(lot.quantity))
          setLotUnit(lot.unit)
          setPackedForYear(lot.packedForYear != null ? String(lot.packedForYear) : '')
        },
        onError: (err) => {
          if ((err as Error)?.name === 'AbortError') return
          setError(errorMessage(err, "Couldn't read that photo. Try a clearer, well-lit shot of the packet."))
        },
      },
    )
  }

  function onFile(e: ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0]
    e.target.value = '' // so re-picking the same file fires change again
    readFile(file)
  }

  function onDrop(e: DragEvent) {
    e.preventDefault()
    setDragOver(false)
    readFile(e.dataTransfer.files?.[0])
  }

  async function onConfirm(e: FormEvent) {
    e.preventDefault()
    if (!proposal) return
    setError(null)
    const qty = quantity.trim() === '' ? 0 : Number(quantity)
    if (!Number.isFinite(qty) || qty < 0) return setError('Quantity must be a number, or blank.')
    let year: number | null = null
    if (packedForYear.trim()) {
      const y = Number(packedForYear)
      if (!Number.isInteger(y) || y < 1900 || y > 2200) return setError('Packed-for should be a four-digit year.')
      year = y
    }
    const lot = {
      vendor: vendor.trim(),
      sourceUrl: '',
      sku: proposal.packet.sku,
      lotCode: proposal.packet.lotCode,
      purchasedAt: null,
      packedForYear: year,
      quantity: qty,
      unit: lotUnit,
      costCents: null,
      germinationPct: null,
      notes: '',
    }
    let newPlant: PlantInput | undefined
    let plantId: number | undefined
    if (selection === 'new') {
      if (!name.trim()) return setError('Name the new plant, or pick an existing one above.')
      const spacingCm = cmFromSpacing(parseFloat(spacing), unit)
      if (!Number.isFinite(spacingCm) || spacingCm < 1) return setError(`Spacing must be at least 1 ${unitLabel}.`)
      let daysToMaturity: number | null = null
      if (days.trim()) {
        const d = Number(days)
        if (!Number.isInteger(d) || d < 1) return setError('Days to maturity is a whole number of days, or blank.')
        daysToMaturity = d
      }
      newPlant = { ...newPlantDefaults(proposal), name: name.trim(), category, spacingCm, daysToMaturity, vendor: vendor.trim() }
    } else {
      plantId = selection
    }
    try {
      const res = await create.mutateAsync({ plantId, newPlant, lot })
      toast.info(res.plantIsNew ? `Added ${res.plant.name} and its seed lot.` : `Recorded a seed lot for ${res.plant.name}.`)
      onClose()
    } catch (err) {
      setError(errorMessage(err, 'Could not save the packet.'))
    }
  }

  const variety = proposal ? [proposal.packet.species, proposal.packet.variety].filter(Boolean).join(' — ') : ''

  return (
    <Dialog title="Scan a seed packet" onClose={onClose} busy={create.isPending} width={560}>
      {!proposal ? (
        <>
          <input
            ref={fileInput}
            type="file"
            accept="image/*"
            capture="environment"
            onChange={onFile}
            className="hidden"
            aria-hidden
            tabIndex={-1}
          />
          <div
            onDragOver={(e) => {
              e.preventDefault()
              setDragOver(true)
            }}
            onDragLeave={() => setDragOver(false)}
            onDrop={onDrop}
            className={cn(
              'flex flex-col items-center gap-2.5 rounded-lg border-2 border-dashed px-5 py-9 text-center',
              dragOver ? 'border-accent-400 bg-accent-100' : 'border-neutral-400',
            )}
          >
            <Icon name="camera" size={30} className="text-accent-2-600" />
            <div className="text-sm font-semibold">Photograph the packet — front is enough</div>
            <div className="max-w-[36ch] text-[12.5px] text-ink-mute">
              The vision model reads it into fields. It only reads; nothing is saved until you confirm.
            </div>
            {scan.isPending ? (
              <p className="mt-1 flex items-center gap-2 text-[13px] font-semibold text-ink-soft">
                <span className="inline-block h-2 w-2 animate-pulse rounded-full bg-accent" />
                Reading the packet… this can take a few seconds.
              </p>
            ) : (
              <Button variant="primary" icon="camera" className="mt-1" onClick={() => fileInput.current?.click()}>
                Take or choose a photo
              </Button>
            )}
          </div>
          {error && <Alert>{error}</Alert>}
          <div className="flex justify-end">
            {/* Never disabled — this is the way out of a slow scan. */}
            <Button
              onClick={() => {
                scanAbort.current?.abort()
                onClose()
              }}
            >
              Cancel
            </Button>
          </div>
        </>
      ) : (
        <form onSubmit={onConfirm} className="flex flex-col gap-3.5">
          <div className="grid grid-cols-2 gap-2.5">
            <TextField label="Variety" name="variety" value={selection === 'new' ? name : variety} readOnly={selection !== 'new'} onChange={(e) => setName(e.target.value)} />
            <TextField label="Vendor" name="vendor" value={vendor} onChange={(e) => setVendor(e.target.value)} />
            <TextField label="Packed for" name="packedForYear" type="number" inputMode="numeric" value={packedForYear} onChange={(e) => setPackedForYear(e.target.value)} />
            <div className="field">
              <label htmlFor="scan-quantity">Quantity</label>
              <div className="flex gap-1.5">
                <input id="scan-quantity" className="input min-w-0" type="number" inputMode="decimal" step="any" min="0" value={quantity} onChange={(e) => setQuantity(e.target.value)} />
                <select className="input w-auto flex-none" aria-label="Unit" value={lotUnit} onChange={(e) => setLotUnit(e.target.value as LotUnit)}>
                  {unitOptions.map((o) => (
                    <option key={o.value} value={o.value}>
                      {o.label}
                    </option>
                  ))}
                </select>
              </div>
            </div>
          </div>

          <div className="mt-0.5 text-[12.5px] font-bold text-ink-soft">Match it to your catalog — nothing is auto-created:</div>
          <div role="radiogroup" aria-label="Which plant this packet is" className="flex flex-col gap-2">
            {proposal.candidates.map((c, i) => (
              <MatchOption
                key={c.plant.id}
                selected={selection === c.plant.id}
                onSelect={() => setSelection(c.plant.id)}
                label={`${c.plant.name} (${isBuiltin(c.plant) ? 'built-in' : 'yours'})`}
                sub={i === 0 ? `best match · ${c.reason}` : c.reason}
              />
            ))}
            <MatchOption
              selected={selection === 'new'}
              onSelect={() => setSelection('new')}
              label="Create a new plant"
              sub="from the extracted fields"
            />
          </div>

          {selection === 'new' && (
            <div className="grid grid-cols-2 gap-2.5 rounded-md border border-divider bg-bg p-3.5">
              <SelectField label="Category" name="category" value={category} onChange={(e) => setCategory(e.target.value as PlantCategory)} options={categoryOptions} />
              <TextField label={`Spacing (${unitLabel})`} name="spacing" type="number" inputMode="decimal" step="any" min="1" required value={spacing} onChange={(e) => setSpacing(e.target.value)} />
              <TextField label="Days to maturity" name="days" type="number" inputMode="numeric" step="1" min="1" value={days} onChange={(e) => setDays(e.target.value)} wrapperClassName="col-span-2" />
            </div>
          )}

          {error && <Alert>{error}</Alert>}
          <div className="mt-0.5 flex justify-between gap-2">
            <Button
              variant="ghost"
              onClick={() => {
                setError(null)
                setProposal(null)
              }}
              disabled={busy}
            >
              Rescan
            </Button>
            <span className="flex gap-2">
              <Button onClick={onClose} disabled={busy}>
                Cancel
              </Button>
              <Button type="submit" variant="primary" disabled={busy}>
                {create.isPending ? 'Saving…' : selection === 'new' ? 'Create plant + lot' : 'Add the lot'}
              </Button>
            </span>
          </div>
        </form>
      )}
    </Dialog>
  )
}

function MatchOption({ selected, onSelect, label, sub }: { selected: boolean; onSelect: () => void; label: string; sub: string }) {
  return (
    <button
      type="button"
      role="radio"
      aria-checked={selected}
      onClick={onSelect}
      className={cn(
        'flex cursor-pointer items-center gap-2 rounded-full border px-4 py-2.5 text-left',
        selected ? 'border-accent-400 bg-accent-200' : 'border-divider bg-bg',
      )}
    >
      <span className="text-[13.5px] font-bold">{label}</span>
      <span className="ml-auto text-right text-xs text-ink-mute">{sub}</span>
    </button>
  )
}
