import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert, Button, FormActions, Modal } from '@nstart/ui'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
import 'leaflet-draw'
import 'leaflet-draw/dist/leaflet.draw.css'

/**
 * Результат AOI picker'а:
 * - bbox: `west,south,east,north` (CSV для ?bbox= query)
 * - polygon: GeoJSON Polygon (для ?polygon= query)
 *
 * Match'ит API ordinis-rest-api: `BoundingBox.parse` для bbox CSV,
 * `GeoJsonPolygon.parse` для polygon JSON. Server validates lon/lat
 * range, anti-meridian, polygon ring closure.
 */
export type AoiResult =
  | { kind: 'bbox'; bboxCsv: string; geojson: GeoJSON.Polygon }
  | { kind: 'polygon'; bboxCsv: string; geojson: GeoJSON.Polygon }

type Props = {
  isOpen: boolean
  onClose: () => void
  onApply: (result: AoiResult) => void
  /** Optional initial geometry to render and edit. */
  initial?: GeoJSON.Polygon | null
}

/**
 * AOI picker на Leaflet с draw controls (rectangle + polygon).
 *
 * Admin рисует bbox или произвольный полигон на OSM tile'ах →
 * onApply получает CSV bbox и GeoJSON polygon одновременно. Caller
 * выбирает что использовать для filter (bbox дешевле для server,
 * polygon точнее).
 *
 * <p>Map монтируется только когда isOpen=true чтобы Leaflet
 * корректно посчитал размеры контейнера (иначе серый экран до
 * первого resize).
 */
export const AoiPickerDialog = ({ isOpen, onClose, onApply, initial }: Props) => {
  const { t } = useTranslation()
  const mapDivRef = useRef<HTMLDivElement | null>(null)
  const mapRef = useRef<L.Map | null>(null)
  const drawnRef = useRef<L.FeatureGroup | null>(null)
  const [shape, setShape] = useState<L.Layer | null>(null)
  const [error, setError] = useState<string | null>(null)

  // Init map после mount + isOpen=true. Defer'им init до того как div
  // получит non-zero clientWidth/Height — иначе Leaflet вычисляет tile
  // grid от 0×0 и карта остаётся пустой даже после `invalidateSize`
  // (некоторые internal collections не recompute'ятся after init).
  //
  // Modal от @nstart/ui использует display animation, RAF poll'им до
  // первого размера. После init — ResizeObserver на window resize.
  useEffect(() => {
    if (!isOpen) return
    let cancelled = false
    let ro: ResizeObserver | null = null
    let rafId = 0

    const tryInit = () => {
      if (cancelled) return
      if (mapRef.current) return // уже init'нут
      const div = mapDivRef.current

      // Modal от @nstart/ui рендерит children только после useAnimatedPresence
      // флипает isMounted=true (на след. рендер после isOpen=true). Наш useEffect
      // фаерится РАНЬШЕ — div ещё не в DOM. Plus modal transform animation
      // изначально даёт 0×0 размер. Poll'им RAF до тех пор пока:
      //  - div существует (ref populated на render когда modal mounted'нул его)
      //  - clientWidth и clientHeight > 0 (после layout & transform settled)
      // Cap на 100 кадров (~1.6s) — fallback если что-то совсем странное.
      if (!div || div.clientWidth === 0 || div.clientHeight === 0) {
        if (rafId < 100) {
          rafId += 1
          requestAnimationFrame(tryInit)
        }
        return
      }

      // Россия по умолчанию: центр ~Урал, zoom видит всю страну.
      const map = L.map(div, { zoomControl: true }).setView([60, 90], 3)
      L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '© OpenStreetMap contributors',
        maxZoom: 18,
      }).addTo(map)

      const drawn = new L.FeatureGroup().addTo(map)
      drawnRef.current = drawn

      if (initial && initial.coordinates?.[0]?.length >= 4) {
        const ring = initial.coordinates[0].map(([lng, lat]) => L.latLng(lat, lng))
        const poly = L.polygon(ring, { color: '#1d4ed8', weight: 2 })
        drawn.addLayer(poly)
        setShape(poly)
        map.fitBounds(poly.getBounds(), { padding: [20, 20] })
      }

      const drawControl = new L.Control.Draw({
        draw: {
          rectangle: { shapeOptions: { color: '#1d4ed8', weight: 2 } },
          polygon: {
            shapeOptions: { color: '#1d4ed8', weight: 2 },
            allowIntersection: false,
            showArea: false,
          },
          marker: false,
          circle: false,
          circlemarker: false,
          polyline: false,
        },
        edit: {
          featureGroup: drawn,
          edit: { selectedPathOptions: { color: '#dc2626' } },
          remove: true,
        },
      })
      map.addControl(drawControl)

      map.on(L.Draw.Event.CREATED, (e) => {
        const event = e as L.LeafletEvent & { layerType: string; layer: L.Layer }
        drawn.clearLayers()
        drawn.addLayer(event.layer)
        setShape(event.layer)
        setError(null)
      })
      map.on(L.Draw.Event.DELETED, () => setShape(null))
      map.on(L.Draw.Event.EDITED, () => {
        const layers = drawn.getLayers()
        setShape(layers[0] ?? null)
      })

      mapRef.current = map

      // Window resize / dev tools / responsive layout — invalidate.
      ro = new ResizeObserver(() => {
        if (mapRef.current) mapRef.current.invalidateSize()
      })
      ro.observe(div)
    }

    tryInit()
    return () => {
      cancelled = true
      if (ro) ro.disconnect()
    }
  }, [isOpen, initial])

  // Cleanup при close: уничтожить map чтобы re-open был чистый старт.
  useEffect(() => {
    if (isOpen) return
    if (mapRef.current) {
      mapRef.current.remove()
      mapRef.current = null
      drawnRef.current = null
      setShape(null)
      setError(null)
    }
  }, [isOpen])

  const handleApply = () => {
    if (!shape) {
      setError(t('aoi.error.noShape'))
      return
    }
    const result = layerToResult(shape)
    if (!result) {
      setError(t('aoi.error.invalid'))
      return
    }
    onApply(result)
    onClose()
  }

  return (
    <Modal
      isOpen={isOpen}
      onClose={onClose}
      title={t('aoi.dialog.title')}
      maxWidth="max-w-5xl"
      panelClassName="max-h-[calc(100vh-2rem)] my-4 flex flex-col"
      bodyClassName="flex-1 flex flex-col gap-3 overflow-hidden"
    >
      <p className="text-sm text-carbon/70">{t('aoi.dialog.help')}</p>

      <div
        ref={mapDivRef}
        // shrink-0 защищает от flex-shrink в overflow-hidden parent body Modal'а:
        // на узком viewport (height < 700px) panel max-h сжимал body, flex-shrink
        // обрезал map с 520px до ~250px (DevTools confirmed). shrink-0 + min-h
        // фиксируют 520 даже когда panel переполнен.
        className="rounded-sm border border-regolith shrink-0"
        style={{ display: 'block', width: '100%', height: 520, minHeight: 520 }}
        aria-label={t('aoi.dialog.mapAriaLabel')}
      />

      {error && (
        <Alert variant="error" title={t('error.failed')}>
          {error}
        </Alert>
      )}

      <FormActions>
        <Button type="button" variant="ghost" onClick={onClose}>
          {t('form.cancel')}
        </Button>
        <Button type="button" variant="primary" onClick={handleApply} disabled={!shape}>
          {t('aoi.dialog.apply')}
        </Button>
      </FormActions>
    </Modal>
  )
}

/**
 * Конвертирует Leaflet layer (rectangle / polygon) в bbox CSV +
 * GeoJSON Polygon. Возвращает null если geometry invalid.
 *
 * Для rectangle — bbox-режим (server использует ST_MakeEnvelope,
 * быстрее polygon ST_Intersects), но GeoJSON тоже даём для
 * обратной совместимости / preview.
 */
const layerToResult = (layer: L.Layer): AoiResult | null => {
  if (layer instanceof L.Rectangle) {
    const b = layer.getBounds()
    const bbox = `${b.getWest()},${b.getSouth()},${b.getEast()},${b.getNorth()}`
    return { kind: 'bbox', bboxCsv: bbox, geojson: rectangleToGeoJSON(b) }
  }
  if (layer instanceof L.Polygon) {
    const latlngsRaw = layer.getLatLngs()
    // Polygon.getLatLngs() returns LatLng[][] для simple polygon.
    const ring = (Array.isArray(latlngsRaw[0]) ? latlngsRaw[0] : latlngsRaw) as L.LatLng[]
    if (!ring || ring.length < 3) return null
    const closed = [...ring, ring[0]]
    const coords = closed.map((p) => [p.lng, p.lat] as [number, number])
    const b = layer.getBounds()
    const bbox = `${b.getWest()},${b.getSouth()},${b.getEast()},${b.getNorth()}`
    return {
      kind: 'polygon',
      bboxCsv: bbox,
      geojson: { type: 'Polygon', coordinates: [coords] },
    }
  }
  return null
}

const rectangleToGeoJSON = (b: L.LatLngBounds): GeoJSON.Polygon => {
  const w = b.getWest()
  const s = b.getSouth()
  const e = b.getEast()
  const n = b.getNorth()
  return {
    type: 'Polygon',
    coordinates: [
      [
        [w, s],
        [e, s],
        [e, n],
        [w, n],
        [w, s],
      ],
    ],
  }
}
