import { Link, router, useForm, usePage } from '@inertiajs/react'
import { RotateCcwIcon } from 'lucide-react'
import type { FormEvent } from 'react'
import { useEffect, useMemo, useState } from 'react'

import { DataTimestamp } from '@/components/DataTimestamp'
import { SectionCard } from '@/components/SectionCard'
import { Button } from '@/components/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { useBatchPolling } from '@/hooks/useBatchPolling'
import { AppLayout } from '@/layouts/AppLayout'
import { faultText } from '@/lib/errors'
import { formatNumber } from '@/lib/format'
import { t } from '@/lib/i18n'
import { route } from '@/lib/routes'

import { GenerationsTable } from '../GenerationsTable'
import { AnswerSummary } from './AnswerSummary'
import { AreaContainer, fieldControlId, serviceControlId } from './AreaContainer'
import { AreasSection } from './AreasSection'
import { servicesForAll } from './conditions'
import { DebugSection } from './DebugSection'
import { GuardsSection } from './GuardsSection'
import { unitLabel } from './labels'
import {
  containerErrors,
  draftFromAnalysis,
  draftFromGeneration,
  groupableAreas,
  orderedContainers,
  pendingOf,
  toggleArea,
  toggleGrouped,
  unitKey,
  unitsOf,
  withAnswer,
  withGuard,
  withMarkup,
  withNotes,
  withService,
  withWholeArea,
} from './draft'
import type { Draft, Unit } from './draft'
import { NotesSection } from './NotesSection'
import { PhotoSection } from './PhotoSection'
import { composePrompt } from './prompt'
import { ResultSection } from './ResultSection'
import { generationHref } from './rows'
import type {
  Analysis,
  AnswerValue,
  Catalog,
  Generation,
  GenerationOrigin,
  GenerationState,
  MarkupConfig,
  PromptRecipe,
  SavedMarkup,
} from './types'

interface Props {
  catalog: Catalog
  /** Si el rol puede leer el prompt (R53). Sin esto no viaja ni la receta ni el prompt del pedido. */
  can_see_prompt: boolean
  /** Las piezas fijas del prompt, para armarlo a la vista con paridad con el servidor. */
  prompt: PromptRecipe | null
  /** La foto ya analizada que nombra la dirección, o nada: entonces la página arranca en la foto. */
  analysis: Analysis | null
  /** El pedido que nombra la dirección, o nada. */
  generation: Generation | null
  recent: Generation[]
  recent_total: number
  recent_limit: number
  /** Estados no terminales de toda la cola. De acá sale el sondeo de la pestaña de recientes. */
  active_states: string[]
  /** El estado del análisis que se mira, sólo si no terminó. De acá sale su sondeo. */
  analysis_active_states: string[]
  analyze_url: string
  generate_url: string
  max_source_bytes: number
  /** El tope de las notas libres, el mismo que valida el servidor. */
  max_notes_length: number
  /** El editor de marcas sobre la foto: la paleta cerrada y el tope de su nota. */
  markup: MarkupConfig
  options: { origin: GenerationOrigin[]; state: GenerationState[]; service: string[] }
}

/**
 * La página única del generador: un formulario que se extiende hacia abajo a
 * medida que se completa —foto, análisis, áreas, contenedores, prompt,
 * resultado— (R47), no un asistente de pasos. Lo que hay en pantalla lo dicen
 * los dos identificadores de la dirección: `?analysis=` es la foto sobre la que
 * se trabaja y `?generation=` el pedido que se mira.
 *
 * Dos pestañas, como la v1: «Design» es la herramienta y «Recent» es el
 * registro, la misma tabla que la cola. La pestaña es estado de la pantalla y
 * no de la dirección.
 */
export default function Index({
  catalog,
  can_see_prompt,
  prompt,
  analysis,
  generation,
  recent,
  recent_total,
  recent_limit,
  active_states,
  analysis_active_states,
  analyze_url,
  generate_url,
  max_source_bytes,
  max_notes_length,
  markup,
  options,
}: Props) {
  const [tab, setTab] = useState<'design' | 'recent'>('design')

  // Tres sondeos con el único mecanismo del producto (RT-13), cada uno con lo
  // suyo: el análisis mientras no termina, el pedido que se mira mientras
  // corre, y la pestaña de recientes mientras haya algo corriendo en la cola.
  useBatchPolling({
    states: analysis_active_states,
    only: ['analysis', 'analysis_active_states'],
  })
  useBatchPolling({ states: [generation?.state], only: ['generation', 'active_states'] })
  useBatchPolling({ states: active_states, only: ['recent', 'recent_total', 'active_states'] })

  const recentDescription =
    recent_total > recent.length
      ? t('ai.recent.truncated', {
          shown: formatNumber(recent.length),
          total: formatNumber(recent_total),
          limit: formatNumber(recent_limit),
        })
      : t('ai.recent.description')

  return (
    <AppLayout
      title={t('nav.imageGenerator')}
      description={t('ai.imageGenerator.description')}
      // Reiniciar es ir a la dirección pelada del generador: sin `?analysis=`
      // ni `?generation=` no hay foto, no hay pedido y el recorrido arranca en
      // cero. Antes había que ir a buscar el ítem de la barra lateral, que es
      // el mismo enlace escondido más lejos.
      //
      // Sólo aparece cuando hay algo que reiniciar. En la pantalla en blanco
      // no lleva a ningún lado: es la que ya se está mirando.
      actions={
        analysis || generation ? (
          <Button asChild variant="outline">
            <Link href={route('ai.image_generator')}>
              <RotateCcwIcon aria-hidden />
              {t('ai.generator.restart')}
            </Link>
          </Button>
        ) : undefined
      }
    >
      <Tabs value={tab} onValueChange={(value) => setTab(value as 'design' | 'recent')}>
        <TabsList>
          <TabsTrigger value="design">{t('ai.generator.tab.design')}</TabsTrigger>
          <TabsTrigger value="recent">{t('ai.generator.tab.recent')}</TabsTrigger>
        </TabsList>

        <TabsContent value="design" className="flex flex-col gap-6">
          {/*
            La clave es el análisis: cambiar de foto es empezar de nuevo, y todo
            lo que la persona contestó pertenece a esa foto. Un pedido nuevo bajo
            el mismo análisis no borra nada: sólo cambia qué se muestra.
          */}
          <Designer
            key={analysis?.id ?? 'no-analysis'}
            catalog={catalog}
            recipe={prompt}
            canSeePrompt={can_see_prompt}
            analysis={analysis}
            generation={generation}
            analyzeUrl={analyze_url}
            generateUrl={generate_url}
            maxSourceBytes={max_source_bytes}
            maxNotesLength={max_notes_length}
            markupConfig={markup}
          />
        </TabsContent>

        <TabsContent value="recent" className="flex flex-col gap-4">
          <p className="text-muted-foreground">{recentDescription}</p>
          <GenerationsTable
            generations={recent}
            options={options}
            caption={t('ai.recent.caption')}
            empty={{
              title: t('ai.recent.empty.title'),
              description: t('ai.recent.empty.description'),
            }}
            onOpen={(row) => {
              // Abrir un pedido es cambiar la dirección y volver a «Design».
              setTab('design')
              router.visit(generationHref(row))
            }}
          />
        </TabsContent>
      </Tabs>
    </AppLayout>
  )
}

/**
 * Los errores de un contenedor: los que el servidor devolvió para cualquiera de
 * sus áreas, más lo que la pantalla ya sabe que falta.
 *
 * Se juntan porque la caja es una sola: un campo sin contestar en el grupo está
 * sin contestar en las tres áreas, y decirlo tres veces no agrega nada.
 */
function unitErrors(
  unit: Unit,
  byArea: Record<string, Record<string, string>>,
  missing: string[]
): Record<string, string> {
  const own: Record<string, string> = {}
  for (const area of unit.areas) {
    Object.assign(own, byArea[area])
    for (const item of missing) {
      const [named, field] = item.split('.')
      if (named === area) own[field] = t('errors.ANSWER_MISSING')
    }
  }
  return own
}

// ---------------------------------------------------------------------------
// El recorrido
// ---------------------------------------------------------------------------

interface DesignerProps {
  catalog: Catalog
  /** Nada cuando el rol no puede leer el prompt: sin receta no hay con qué armarlo. */
  recipe: PromptRecipe | null
  canSeePrompt: boolean
  analysis: Analysis | null
  generation: Generation | null
  analyzeUrl: string
  generateUrl: string
  maxSourceBytes: number
  maxNotesLength: number
  markupConfig: MarkupConfig
}

/**
 * La capa de marcas como archivo, para subirla con el pedido.
 *
 * Sirve igual para una capa recién dibujada (`data:`) que para la de un pedido
 * que se ajusta (la ruta con sesión): las dos se leen con `fetch`.
 */
async function markupFile(markup: SavedMarkup): Promise<File> {
  const blob = await (await fetch(markup.image)).blob()
  return new File([blob], 'markup.png', { type: 'image/png' })
}

/**
 * Las secciones del recorrido, de arriba hacia abajo, cada una a la vista
 * cuando la anterior está resuelta.
 *
 * Con un pedido en pantalla, las secciones de áreas, contenedores y prompt se
 * pliegan en un resumen de lo que se pidió, hasta que se pulsa «Adjust»: ahí
 * vuelven con las respuestas del pedido puestas y se puede cambiar algo y
 * pedir otra vez sin recargar (R26). «Generate another» va a la dirección del
 * análisis solo: misma foto, contenedores en blanco.
 */
function Designer({
  catalog,
  recipe,
  canSeePrompt,
  analysis,
  generation,
  analyzeUrl,
  generateUrl,
  maxSourceBytes,
  maxNotesLength,
  markupConfig,
}: DesignerProps) {
  const { errors } = usePage().props
  const [draft, setDraft] = useState<Draft>(() => draftFromAnalysis(analysis))
  const [editing, setEditing] = useState(false)
  const [missing, setMissing] = useState<string[]>([])
  const [activeArea, setActiveArea] = useState('')
  /**
   * El control que hay que enfocar en cuanto se dibuje.
   *
   * Con las áreas en pestañas, el campo que falta contestar puede estar en una
   * que no está abierta, y un control que no está en el documento no se puede
   * enfocar. Se abre su pestaña y el foco va después, cuando React ya montó lo
   * de adentro.
   */
  const [focusTarget, setFocusTarget] = useState<string | null>(null)
  const form = useForm({ analysis: analysis?.id ?? '', areas: '[]', notes: '', guards: '{}' })

  const generationId = generation?.id ?? null
  useEffect(() => {
    // Un pedido nuevo en la dirección —recién mandado, o abierto desde la
    // cola— se mira en modo lectura hasta que alguien decida ajustarlo.
    setEditing(false)
  }, [generationId])

  // La preselección llega tarde, y por eso no alcanza con el estado inicial.
  // Quien sube una foto ve la página con el análisis todavía en cola: ahí no
  // hay áreas que marcar, y el sondeo que después las trae no remonta nada
  // —la clave es el análisis y su id no cambia—, así que el borrador se
  // quedaba vacío y lo detectado no aparecía marcado. Se siembra una sola vez,
  // cuando el análisis termina, y nunca encima de lo que la persona ya tocó.
  const [seeded, setSeeded] = useState(() => analysis?.state === 'COMPLETED')
  useEffect(() => {
    if (seeded || analysis?.state !== 'COMPLETED') return
    setDraft(draftFromAnalysis(analysis))
    setSeeded(true)
  }, [analysis, seeded])

  useEffect(() => {
    if (focusTarget === null) return
    document.getElementById(focusTarget)?.focus()
    setFocusTarget(null)
  }, [focusTarget])

  const poolPresent = analysis?.pool_present ?? false
  const units = useMemo(() => unitsOf(catalog, draft), [catalog, draft])
  const containers = useMemo(() => orderedContainers(catalog, draft), [catalog, draft])
  // Con una sola de las áreas que comparten diseño no hay nada que juntar, y el
  // interruptor no se dibuja en ninguna tarjeta.
  const groupable = groupableAreas(catalog, draft).length > 1
  // Sin receta no se arma: es lo que ve un rol que no puede leer el prompt.
  const promptText = useMemo(
    () =>
      recipe
        ? composePrompt(recipe, catalog, containers, poolPresent, draft.notes, draft.guards)
        : '',
    [recipe, catalog, containers, poolPresent, draft.notes, draft.guards]
  )
  const pending = useMemo(() => pendingOf(catalog, containers), [catalog, containers])

  const onToggleArea = (area: string, checked: boolean) => {
    setDraft((current) => toggleArea(current, area, checked))
  }

  const onService = (areas: string[], service: string) => {
    setDraft((current) => withService(current, areas, service))
    setMissing((current) => current.filter((item) => !areas.includes(item.split('.')[0])))
  }

  const onWholeArea = (areas: string[], wholeArea: boolean) => {
    setDraft((current) => withWholeArea(current, areas, wholeArea))
  }

  const onNotes = (notes: string) => {
    setDraft((current) => withNotes(current, notes))
  }

  const onGuard = (guard: string, raised: boolean) => {
    setDraft((current) => withGuard(current, guard, raised))
  }

  const onMarkup = (markup: SavedMarkup | null) => {
    setDraft((current) => withMarkup(current, markup))
  }

  const onGrouped = (grouped: boolean) => {
    setDraft((current) => toggleGrouped(catalog, current, grouped, poolPresent))
    setMissing([])
  }

  const onAnswer = (areas: string[], field: string, value: AnswerValue) => {
    setDraft((current) => withAnswer(catalog, current, areas, field, value))
    setMissing((current) =>
      current.filter((item) => !areas.some((area) => item === `${area}.${field}`))
    )
  }

  const submit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault()
    if (!analysis) return
    if (pending.length > 0) {
      setMissing(pending)
      const [area, field] = pending[0].split('.')
      const unit = units.find((candidate) => candidate.areas.includes(area))
      const key = unit ? unitKey(unit) : area
      setActiveArea(area)
      setFocusTarget(field === 'service' ? serviceControlId(key) : fieldControlId(key, field))
      return
    }
    // Las marcas son un extra: sin ellas el envío es exactamente el de siempre.
    const markup = draft.markup
    const layer = markup ? await markupFile(markup) : null
    form.transform((data) => ({
      ...data,
      areas: JSON.stringify(containers),
      notes: draft.notes,
      guards: JSON.stringify(draft.guards),
      ...(markup && layer
        ? { markup: layer, markup_note: markup.note }
        : {}),
    }))
    form.post(generateUrl, { preserveScroll: true, forceFormData: layer !== null })
  }

  const adjust = () => {
    if (!generation) return
    setDraft(draftFromGeneration(catalog, generation))
    setMissing([])
    setEditing(true)
  }

  const blank = route('ai.image_generator')
  const anotherHref = analysis ? `${blank}?analysis=${analysis.id}` : blank
  const timestamp = generation ? (
    <DataTimestamp
      value={generation.finished_at ?? generation.created_at}
      className="text-muted-foreground text-xs"
    />
  ) : null

  // Un pedido sin análisis en la dirección —una fila v1, o un enlace viejo— se
  // mira solo: no hay foto analizada sobre la que seguir trabajando.
  if (!analysis) {
    if (generation) {
      return <ResultSection generation={generation} anotherHref={blank} timestamp={timestamp} />
    }
    return <PhotoSection analysis={null} analyzeUrl={analyzeUrl} maxSourceBytes={maxSourceBytes} />
  }

  const canConfirmAreas = analysis.can_generate
  const reading = generation !== null && generation.is_v2 && !editing
  // Se dibuja sobre la foto mientras el recorrido está abierto: con el pedido
  // en lectura, lo que se pidió ya está dicho y no se marca encima.
  const photoMarkup =
    canConfirmAreas && !reading
      ? {
          config: markupConfig,
          saved: draft.markup,
          onSave: onMarkup,
          onRemove: () => onMarkup(null),
          error: faultText(errors.markup),
        }
      : undefined
  const byArea = containerErrors(errors, containers)
  // El contenedor que se mira, por el área que se abrió. Si esa área se sacó de
  // la sección de arriba —o se fue adentro del grupo—, la pestaña cae sola en
  // el primer contenedor que quede.
  const current = units.find((unit) => unit.areas.includes(activeArea)) ?? units[0]
  const troubled = new Set([...missing.map((item) => item.split('.')[0]), ...Object.keys(byArea)])
  const requestError =
    faultText(errors.areas) ?? faultText(errors.analysis) ?? faultText(errors.form)

  const result = generation ? (
    <ResultSection
      generation={generation}
      onAdjust={generation.is_v2 && !editing ? adjust : undefined}
      anotherHref={anotherHref}
      timestamp={timestamp}
    />
  ) : null
  const photo = (
    <PhotoSection
      analysis={analysis}
      analyzeUrl={analyzeUrl}
      maxSourceBytes={maxSourceBytes}
      markup={photoMarkup}
    />
  )

  return (
    <>
      {/*
        Desde que hay un pedido en pantalla, «Tu diseño» ocupa el lugar de la
        tarjeta de la foto, arriba de todo: la imagen de esta pantalla pasa a
        ser el diseño, con el original al lado. Vale también mientras se
        genera, y ahí lo que está arriba es la espera, que es justo lo que se
        quiere mirar. El sitio no cambia cuando el resultado llega: no hay
        salto.

        Al ajustar, la tarjeta de la foto vuelve debajo del diseño, porque es
        el único lugar donde se editan las marcas. Sin ella, «Adjust» reenviaba
        las marcas del pedido tal cual y no había forma de cambiarlas.
      */}
      {result}
      {!result || editing ? photo : null}

      {reading && generation ? (
        <SectionCard
          title={{ text: t('ai.summary.title') }}
          description={t('ai.summary.description')}
          id="request"
        >
          <AnswerSummary
            areas={generation.areas}
            catalog={catalog}
            notes={generation.notes}
            guards={generation.guards}
            markup={
              generation.markup_url
                ? { image: generation.markup_url, note: generation.markup_note }
                : null
            }
            photoUrl={generation.source_url}
            prompt={generation.prompt}
            canSeePrompt={canSeePrompt}
          />
        </SectionCard>
      ) : null}

      {canConfirmAreas && !reading ? (
        <form onSubmit={submit} className="flex flex-col gap-6" aria-busy={form.processing}>
          <AreasSection
            areas={catalog.areas}
            detected={analysis.areas}
            confidence={analysis.confidence}
            poolPresent={poolPresent}
            selected={draft.selected}
            onToggle={onToggleArea}
            groupable={groupable}
            grouped={draft.grouped}
            onGrouped={onGrouped}
          />

          {/*
            Un área a la vez, en pestañas. En dos columnas, cuatro o cinco áreas
            dejaban una tira que no se termina de leer, y cada servicio pide su
            propia atención: se contesta uno y se pasa al siguiente. La pestaña
            marca la que todavía tiene algo mal, para que nada quede escondido
            detrás de una solapa sin abrir.
          */}
          {units.length > 0 && current ? (
            <Tabs
              value={unitKey(current)}
              onValueChange={(key) => {
                const unit = units.find((candidate) => unitKey(candidate) === key)
                if (unit) setActiveArea(unit.areas[0])
              }}
            >
              <TabsList>
                {units.map((unit) => (
                  <TabsTrigger key={unitKey(unit)} value={unitKey(unit)}>
                    {unitLabel(unit.areas)}
                    {unit.areas.some((area) => troubled.has(area)) ? (
                      <>
                        <span className="bg-destructive size-1.5 rounded-full" aria-hidden />
                        <span className="sr-only">{t('ai.containers.needsAttention')}</span>
                      </>
                    ) : null}
                  </TabsTrigger>
                ))}
              </TabsList>

              {units.map((unit) => (
                <TabsContent key={unitKey(unit)} value={unitKey(unit)}>
                  <AreaContainer
                    areas={unit.areas}
                    selected={draft.selected}
                    services={servicesForAll(catalog, unit.areas, poolPresent)}
                    service={unit.service}
                    answers={unit.answers}
                    wholeArea={unit.wholeArea}
                    onService={(service) => onService(unit.areas, service)}
                    onAnswer={(field, value) => onAnswer(unit.areas, field, value)}
                    onWholeArea={(wholeArea) => onWholeArea(unit.areas, wholeArea)}
                    errors={unitErrors(unit, byArea, missing)}
                  />
                </TabsContent>
              ))}
            </Tabs>
          ) : null}

          {/*
            Los guards antes de las notas: primero hasta dónde se puede llegar,
            después qué se pide. Al revés, la nota se escribe creyendo que puede
            pedir cualquier cosa y la regla aparece cuando ya está escrita.
          */}
          {containers.length > 0 ? (
            <GuardsSection guards={catalog.guards} lowered={draft.guards} onGuard={onGuard} />
          ) : null}

          {/*
            Las notas después de las áreas y antes del prompt: es el orden en
            que entran al texto, y el orden en que se decide. Primero lo que el
            catálogo sabe preguntar, después lo que sólo se ve en esta foto.
          */}
          {containers.length > 0 ? (
            <NotesSection
              notes={draft.notes}
              maxLength={maxNotesLength}
              onChange={onNotes}
              error={faultText(errors.notes)}
              complete={pending.length === 0}
              submitting={form.processing}
              requestError={requestError}
            />
          ) : null}

          {/*
            Después del botón y sin tarjeta: el prompt no se aprueba, se mira
            cuando algo salió raro. Sin el rol no se dibuja **ni el rótulo**: un
            «Debug» que no abre nada es peor que no estar (R53).
          */}
          {containers.length > 0 && canSeePrompt ? <DebugSection prompt={promptText} /> : null}
        </form>
      ) : null}

    </>
  )
}
