import { Link, useForm, usePage } from '@inertiajs/react'
import {
  CircleXIcon,
  PencilIcon,
  RefreshCwIcon,
  ScanSearchIcon,
  TriangleAlertIcon,
} from 'lucide-react'
import { useState } from 'react'
import type { FormEvent } from 'react'

import { EmptyState } from '@/components/EmptyState'
import { SectionCard } from '@/components/SectionCard'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'
import { faultText } from '@/lib/errors'
import { locale, t } from '@/lib/i18n'
import { route } from '@/lib/routes'

import { areaColor, areaLabel, analysisErrorText, analysisStateLabel } from './labels'
import { ANALYSIS_STEPS, LoaderPanel } from './Loader'
import { PhotoDropzone } from './PhotoDropzone'
import { PhotoMarkup } from './PhotoMarkup'
import type { Analysis, MarkupConfig, SavedMarkup } from './types'

/**
 * Las áreas reconocidas, nombradas y unidas en el idioma activo.
 *
 * Una cifra se abre donde se la lee: «3 áreas reconocidas» sin decir cuáles
 * obliga a deducirlas mirando lo que se dibujó abajo. Son tres a lo sumo seis
 * nombres cortos, así que entran en la misma oración y no hacen falta ni una
 * lista ni un modal.
 */
function areaList(areas: string[]): string {
  const names = areas.map(areaLabel)
  return new Intl.ListFormat(locale(), { style: 'long', type: 'conjunction' }).format(names)
}

/**
 * La marca de un área sobre la foto: un punto de su color, con su nombre.
 *
 * Es un punto y no una sombra a propósito. El modelo ubica bien el centro de
 * una superficie grande, pero no dibuja su contorno: una caja sobre un sendero
 * en diagonal se come media parcela, y se vería mal justo donde se mira. Un
 * punto un poco corrido se sigue leyendo como «esto de acá».
 *
 * Va en porcentaje del contenedor, que es la caja de la imagen sin recortar,
 * así que no hay nada que recalcular al cambiar el tamaño de la ventana.
 */
function AreaMark({ area, point }: { area: string; point: [number, number] }) {
  const color = areaColor(area)
  return (
    <span
      className="absolute flex -translate-x-1/2 -translate-y-1/2 items-center gap-1.5"
      style={{ left: `${point[0] * 100}%`, top: `${point[1] * 100}%` }}
    >
      <span
        aria-hidden
        className="size-3 shrink-0 rounded-full ring-2 ring-white/90"
        style={{ backgroundColor: color }}
      />
      <span
        className="rounded-full px-1.5 py-0.5 text-xs font-medium text-white shadow-sm"
        style={{ backgroundColor: color }}
      >
        {areaLabel(area)}
      </span>
    </span>
  )
}

/** Lo que hace falta para dibujar sobre la foto. Sin esto, la foto sólo se mira. */
export interface PhotoMarkupProps {
  config: MarkupConfig
  saved: SavedMarkup | null
  onSave: (markup: SavedMarkup) => void
  onRemove: () => void
  error?: string
}

interface Props {
  /** La foto ya analizada, o analizándose, o nada: entonces se pide una. */
  analysis: Analysis | null
  analyzeUrl: string
  /** Hasta cuántos bytes acepta el servidor para la foto. Lo dice él, no la pantalla. */
  maxSourceBytes: number
  /**
   * El editor de marcas, cuando ya se puede pedir sobre la foto. Lo decide la
   * página: es la que sabe si el recorrido sigue abierto.
   */
  markup?: PhotoMarkupProps
}

/**
 * La foto: el primer paso, obligatorio, y la entrada de todo lo demás (R1).
 *
 * Sin análisis es la zona de arrastre y el botón de analizar. Con uno, es la
 * miniatura y en qué quedó: la espera con sus frases, el aviso de que el
 * modelo falló y se reintenta, o el motivo si no salió. El análisis va atado a
 * la foto y se hace una vez: reemplazarla es volver a la dirección sin
 * `?analysis=`, lo que reinicia todo lo de abajo (R47).
 *
 * Con `MALFORMED_RESPONSE` el análisis falló pero el recorrido sigue: la
 * sección de áreas se dibuja vacía y la persona las agrega a mano (R46). Por
 * eso ese fallo no ofrece «otra foto» como única salida.
 */
export function PhotoSection({ analysis, analyzeUrl, maxSourceBytes, markup }: Props) {
  const { errors } = usePage().props
  const form = useForm<{ image: File | null }>({ image: null })
  const blank = route('ai.image_generator')
  // El modo de marcas vive acá y no en el editor: el botón que lo abre está en
  // el encabezado de la tarjeta, al lado de «Reemplazar».
  const [editing, setEditing] = useState(false)

  const submit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault()
    if (form.data.image === null) return
    form.post(analyzeUrl, { forceFormData: true, preserveScroll: true })
  }

  if (analysis === null) {
    return (
      <form onSubmit={submit} aria-busy={form.processing}>
        <SectionCard
          title={{ text: t('ai.photo.title') }}
          description={t('ai.photo.description')}
          id="photo"
          footer={
            <Button type="submit" disabled={form.processing || form.data.image === null}>
              {form.processing ? <Spinner /> : <ScanSearchIcon aria-hidden />}
              {form.processing ? t('ai.photo.analyzing') : t('ai.photo.analyze')}
            </Button>
          }
        >
          <PhotoDropzone
            file={form.data.image}
            maxBytes={maxSourceBytes}
            error={faultText(errors.image)}
            onChange={(file) => form.setData('image', file)}
          />
        </SectionCard>
      </form>
    )
  }

  const waiting =
    analysis.state === 'QUEUED' || analysis.state === 'RUNNING' || analysis.state === 'RETRYING'
  const points =
    analysis.state === 'COMPLETED'
      ? analysis.areas.map((area) => {
          const point = analysis.points[area]
          if (!point) return null
          return <AreaMark key={area} area={area} point={point} />
        })
      : null

  return (
    <SectionCard
      title={{ text: t('ai.photo.title') }}
      description={
        analysis.state === 'COMPLETED' ? t('ai.photo.analyzed') : analysisStateLabel(analysis.state)
      }
      id="photo"
      actions={
        <div className="flex flex-wrap justify-end gap-2">
          {markup ? (
            <Button
              type="button"
              variant="outline"
              onClick={() => setEditing(true)}
              disabled={editing}
            >
              <PencilIcon aria-hidden />
              {markup.saved ? t('ai.markup.editAgain') : t('ai.markup.edit')}
            </Button>
          ) : null}
          <Button asChild variant="outline">
            <Link href={blank}>
              <RefreshCwIcon aria-hidden />
              {t('ai.photo.replace')}
            </Link>
          </Button>
        </div>
      }
    >
      <div className="flex flex-col gap-4">
        {/*
          El contenedor se ajusta a la caja de la imagen —`w-fit`—, y la imagen
          entra entera: sin recorte, un punto en coordenadas relativas cae
          siempre en el mismo lugar del paisaje, y al cambiar el ancho la foto y
          sus marcas se mueven juntas. Con `object-cover` el navegador tapaba
          parte de la foto y el punto quedaba corrido.

          El techo es de alto, no de ancho, porque el problema es una foto
          vertical: a todo el ancho de la tarjeta se vuelve una pared. Como no
          hay `object-fit`, ese techo arrastra el ancho solo y la proporción se
          mantiene, que es justamente lo que los puntos necesitan.

          Con el recorrido abierto, la foto es además el editor de marcas: la
          misma caja, con el lienzo encima y los controles escondidos detrás.
        */}
        {markup ? (
          <PhotoMarkup
            photoUrl={analysis.source_url}
            photoAlt={t('ai.photo.sourceAlt')}
            marks={points}
            editing={editing}
            onEditingChange={setEditing}
            config={markup.config}
            saved={markup.saved}
            onSave={markup.onSave}
            onRemove={markup.onRemove}
            error={markup.error}
          />
        ) : (
          <div className="relative mx-auto w-fit max-w-full">
            <img
              src={analysis.source_url}
              alt={t('ai.photo.sourceAlt')}
              className="bg-muted block max-h-128 w-auto max-w-full rounded-md border"
            />
            {points}
          </div>
        )}
        <div className="flex min-w-0 flex-col gap-3">
          {analysis.state === 'RETRYING' ? (
            <Alert variant="attention">
              <TriangleAlertIcon />
              <AlertDescription>{t('ai.analysis.retrying')}</AlertDescription>
            </Alert>
          ) : null}
          {waiting ? (
            <LoaderPanel steps={ANALYSIS_STEPS} active={waiting} eta={t('ai.analysis.eta')} />
          ) : null}
          {analysis.state === 'FAILED' ? (
            <EmptyState
              icon={CircleXIcon}
              title={t('ai.analysis.failed')}
              description={analysisErrorText(analysis.error_code)}
              action={
                <Button asChild variant={analysis.error_code === 'MALFORMED_RESPONSE' ? 'outline' : 'default'}>
                  <Link href={blank}>
                    <RefreshCwIcon aria-hidden />
                    {t('ai.analysis.tryAnotherPhoto')}
                  </Link>
                </Button>
              }
            />
          ) : null}
          {analysis.state === 'COMPLETED' ? (
            <p className="text-muted-foreground">
              {analysis.areas.length > 0
                ? t('ai.analysis.detectedCount', {
                    count: analysis.areas.length,
                    areas: areaList(analysis.areas),
                  })
                : t('ai.analysis.detectedNone')}
            </p>
          ) : null}
        </div>
      </div>
    </SectionCard>
  )
}
