import { useEffect, useId, useMemo, useRef, useState } from 'react'
import { Link } from 'react-router-dom'
import { useQuery } from '@apollo/client/react'
import { AnimatePresence, m } from 'framer-motion'
import {
  CircleMarker,
  MapContainer,
  Marker,
  Polyline,
  TileLayer,
  useMap,
  useMapEvents,
} from 'react-leaflet'
import L from 'leaflet'
import { Battery, Clock3, Layers, MapPin, Thermometer, X } from 'lucide-react'

import {
  GET_PHONE_GPS_TRACK_QUERY,
  GET_PHONES_QUERY,
} from '../../entities/device/api/device.graphql'
import type {
  Device,
  DeviceGpsTrackPoint,
  DeviceNetworkStatus,
  GetPhoneGpsTrackData,
  GetPhoneGpsTrackVariables,
  GetPhonesData,
  GetPhonesVariables,
} from '../../entities/device/model/types'

import { FullscreenControl } from '../../widgets/FullscreenControlLeaflet/FullscreenControl'
import {
  MapTrackPeriodControl,
  type MapTrackPeriodValue,
} from './components/MapTrackPeriodControl/MapTrackPeriodControl'
import { popoverVariants, uiFadeTransition } from '../../shared/lib/motion'

import 'leaflet/dist/leaflet.css'
import './MapPage.scss'

const MAP_PHONES_SORT_DIRECTION = 'DESC'
const MAP_PHONES_SORT_FIELD = 'Date'
const routeColor = 'var(--color-primary)'

type MapInfoPoint = {
  key: string
  title: string
  subtitle?: string
  date?: number
  lat: number
  lng: number
}

function getNetworkStatusClass(status?: DeviceNetworkStatus) {
  if (status === 'Online') return 'is-online'
  if (status === 'Lost') return 'is-lost'

  return 'is-offline'
}

function getNetworkStatusLabel(status?: DeviceNetworkStatus) {
  if (status === 'Online') return 'В сети'
  if (status === 'Lost') return 'Потерян'

  return 'Не в сети'
}

function createMapInfoPointKey(lat: number, lng: number, date?: number) {
  return `${date ?? 'no-date'}-${lat}-${lng}`
}

const markerIconCache = new Map<string, L.DivIcon>()

function createMarkerIcon(status?: DeviceNetworkStatus) {
  const iconKey = status ?? 'default'
  const cachedIcon = markerIconCache.get(iconKey)

  if (cachedIcon) return cachedIcon

  const icon = L.divIcon({
    className: `device-map-marker ${getNetworkStatusClass(status)}`,
    html: `
      <div class="device-map-marker__pin">
        <div class="device-map-marker__inner"></div>
      </div>
    `,
    iconSize: [34, 34],
    iconAnchor: [17, 17],
    popupAnchor: [0, -18],
  })

  markerIconCache.set(iconKey, icon)

  return icon
}

function formatLocationDate(timestamp?: number) {
  if (!timestamp) return 'Нет данных'

  return new Intl.DateTimeFormat('ru-RU', {
    day: '2-digit',
    month: '2-digit',
    year: 'numeric',
    hour: '2-digit',
    minute: '2-digit',
  }).format(new Date(timestamp))
}

function formatCoordinate(value: number) {
  return value.toFixed(6)
}

function formatMetric(value: number | null | undefined, suffix: string) {
  if (typeof value !== 'number') return '-'

  return `${value}${suffix}`
}

function sortGpsTrack(track: DeviceGpsTrackPoint[]) {
  return [...track].sort((a, b) => a.date - b.date)
}

function isDateInPeriod(date: number, period: MapTrackPeriodValue | null) {
  if (!period) return false

  return (
    date >= period.periodStart.getTime() &&
    date <= period.periodEnd.getTime()
  )
}

type MapResetHandlerProps = {
  onReset: () => void
}

function MapResetHandler({ onReset }: MapResetHandlerProps) {
  useMapEvents({
    click: () => {
      onReset()
    },
  })

  return null
}

function MapResizeWatcher() {
  const map = useMap()

  useEffect(() => {
    function invalidateMapSize() {
      map.invalidateSize()
    }

    const animationFrameId = window.requestAnimationFrame(invalidateMapSize)
    const timeoutId = window.setTimeout(invalidateMapSize, 220)

    window.addEventListener('resize', invalidateMapSize)
    document.addEventListener('fullscreenchange', invalidateMapSize)

    return () => {
      window.cancelAnimationFrame(animationFrameId)
      window.clearTimeout(timeoutId)
      window.removeEventListener('resize', invalidateMapSize)
      document.removeEventListener('fullscreenchange', invalidateMapSize)
    }
  }, [map])

  return null
}

type MapFitBoundsProps = {
  fitKey: string
  positions: [number, number][]
}

function MapFitBounds({ fitKey, positions }: MapFitBoundsProps) {
  const map = useMap()
  const lastFitKeyRef = useRef<string | null>(null)

  useEffect(() => {
    if (!positions.length) return
    if (lastFitKeyRef.current === fitKey) return

    if (positions.length === 1) {
      map.setView(positions[0], 14)
      lastFitKeyRef.current = fitKey
      return
    }

    const bounds = L.latLngBounds(positions)

    map.fitBounds(bounds, {
      padding: [60, 60],
      maxZoom: 15,
      animate: true,
    })

    lastFitKeyRef.current = fitKey
  }, [fitKey, map, positions])

  return null
}

export function MapPage() {
  const fullscreenTargetId = useId()
  const [selectedDevice, setSelectedDevice] = useState<Device | null>(null)
  const [selectedPeriod, setSelectedPeriod] =
    useState<MapTrackPeriodValue | null>(null)
  const [phonesPaginationError, setPhonesPaginationError] =
    useState<Error | null>(null)
  const [isFetchingNextPhonesPage, setIsFetchingNextPhonesPage] =
    useState(false)
  const [gpsFitDataKey, setGpsFitDataKey] = useState('no-period')
  const [selectedPointKey, setSelectedPointKey] = useState<string | null>(null)
  const [isInfoCardVisible, setIsInfoCardVisible] = useState(false)
  const isFetchingPhonesPageRef = useRef(false)
  const requestedPhonesPagesRef = useRef(new Set<number>([0]))
  const fittedGpsPeriodRef = useRef<string | null>(null)

  const { data, loading, error, fetchMore } = useQuery<
    GetPhonesData,
    GetPhonesVariables
  >(GET_PHONES_QUERY, {
    variables: {
      page: 0,
      sortDirection: MAP_PHONES_SORT_DIRECTION,
      sortField: MAP_PHONES_SORT_FIELD,
    },
    fetchPolicy: 'network-only',
    notifyOnNetworkStatusChange: true,
  })

  const {
    data: gpsTrackData,
    loading: isGpsTrackLoading,
    error: gpsTrackError,
  } = useQuery<GetPhoneGpsTrackData, GetPhoneGpsTrackVariables>(
    GET_PHONE_GPS_TRACK_QUERY,
    {
      variables: {
        phoneId: String(selectedDevice?.id),
        startDate: selectedPeriod?.periodStart.getTime() ?? 0,
        endDate: selectedPeriod?.periodEnd.getTime(),
      },
      skip: !selectedDevice || !selectedPeriod,
      fetchPolicy: 'network-only',
    },
  )

  const phonesTotalPages = data?.getPhones.totalPages ?? 0

  useEffect(() => {
    if (
      phonesPaginationError ||
      phonesTotalPages <= 1 ||
      isFetchingPhonesPageRef.current
    ) {
      return
    }

    const nextPhonesPage = Array.from(
      { length: phonesTotalPages - 1 },
      (_, index) => index + 1,
    ).find((page) => !requestedPhonesPagesRef.current.has(page))

    if (nextPhonesPage === undefined) return

    isFetchingPhonesPageRef.current = true
    requestedPhonesPagesRef.current.add(nextPhonesPage)
    setIsFetchingNextPhonesPage(true)
    setPhonesPaginationError(null)

    fetchMore({
      variables: {
        page: nextPhonesPage,
        sortDirection: MAP_PHONES_SORT_DIRECTION,
        sortField: MAP_PHONES_SORT_FIELD,
      },
      updateQuery: (previousData, { fetchMoreResult }) => {
        const nextPageData = fetchMoreResult?.getPhones

        if (!nextPageData) return previousData

        const previousDevices = previousData.getPhones.page
        const previousDeviceIds = new Set(
          previousDevices.map((device) => device.id),
        )
        const nextDevices = nextPageData.page.filter((device) => {
          return !previousDeviceIds.has(device.id)
        })

        return {
          getPhones: {
            ...nextPageData,
            page: [...previousDevices, ...nextDevices],
          },
        }
      },
    })
      .catch((requestError: unknown) => {
        setPhonesPaginationError(
          requestError instanceof Error
            ? requestError
            : new Error(
                'Не удалось загрузить следующую страницу устройств',
              ),
        )
      })
      .finally(() => {
        isFetchingPhonesPageRef.current = false
        setIsFetchingNextPhonesPage(false)
      })
  }, [
    fetchMore,
    isFetchingNextPhonesPage,
    phonesPaginationError,
    phonesTotalPages,
  ])

  const phonesError = error ?? phonesPaginationError
  const isPhonesLoading = loading || isFetchingNextPhonesPage
  const selectedPeriodKey = selectedPeriod
    ? `${selectedPeriod.periodStart.getTime()}-${selectedPeriod.periodEnd.getTime()}`
    : 'no-period'

  useEffect(() => {
    if (!selectedPeriod) {
      fittedGpsPeriodRef.current = null
      setGpsFitDataKey('no-period')
      return
    }

    if (
      isGpsTrackLoading ||
      !gpsTrackData ||
      fittedGpsPeriodRef.current === selectedPeriodKey
    ) {
      return
    }

    fittedGpsPeriodRef.current = selectedPeriodKey
    setGpsFitDataKey(`ready:${selectedPeriodKey}`)
  }, [gpsTrackData, isGpsTrackLoading, selectedPeriod, selectedPeriodKey])

  const devices = useMemo(() => {
    return data?.getPhones.page ?? []
  }, [data?.getPhones.page])

  const devicesWithLocation = useMemo(() => {
    return devices.filter(
      (device): device is Device & {
        lastLocation: NonNullable<Device['lastLocation']>
      } => Boolean(device.lastLocation),
    )
  }, [devices])
  const shouldShowPhonesLoadState =
    !selectedDevice &&
    (Boolean(phonesError) ||
      (isPhonesLoading && devicesWithLocation.length === 0))

  const sortedGpsTrack = useMemo(() => {
    return sortGpsTrack(gpsTrackData?.getPhoneGpsTrack ?? [])
  }, [gpsTrackData])

  const selectedLastLocation = selectedDevice?.lastLocation ?? null

  const shouldAddLastLocationToRoute = Boolean(
    selectedLastLocation &&
      isDateInPeriod(selectedLastLocation.date, selectedPeriod),
  )

  const selectedCurrentLocation =
    shouldAddLastLocationToRoute && selectedLastLocation
      ? selectedLastLocation
      : null

  const selectedRoutePoints = useMemo(() => {
    if (!selectedCurrentLocation) {
      return sortedGpsTrack
    }

    return sortedGpsTrack.filter((point) => {
      return point.date !== selectedCurrentLocation.date
    })
  }, [sortedGpsTrack, selectedCurrentLocation])

  const selectedRoutePositions = useMemo<[number, number][]>(() => {
    const routePositions = selectedRoutePoints.map(
      (point) => [point.lat, point.lng] as [number, number],
    )

    if (!selectedCurrentLocation) {
      return routePositions
    }

    return [
      ...routePositions,
      [selectedCurrentLocation.lat, selectedCurrentLocation.lng],
    ]
  }, [selectedRoutePoints, selectedCurrentLocation])

  const selectedMapPositions = useMemo<[number, number][]>(() => {
    const positions = [...selectedRoutePositions]

    if (!selectedLastLocation) return positions

    const hasLastLocation = positions.some(([lat, lng]) => {
      return lat === selectedLastLocation.lat && lng === selectedLastLocation.lng
    })

    if (hasLastLocation) return positions

    return [...positions, [selectedLastLocation.lat, selectedLastLocation.lng]]
  }, [selectedLastLocation, selectedRoutePositions])

  const isSelectedMode = Boolean(selectedDevice)

  const routeMapPoints = useMemo<MapInfoPoint[]>(() => {
    return selectedRoutePoints.map((point) => ({
      key: createMapInfoPointKey(point.lat, point.lng, point.date),
      title: 'Точка маршрута',
      subtitle: 'Точка маршрута',
      date: point.date,
      lat: point.lat,
      lng: point.lng,
    }))
  }, [selectedDevice, selectedRoutePoints])

  const currentMapPoint = useMemo<MapInfoPoint | null>(() => {
    if (!selectedDevice || !selectedLastLocation) return null

    return {
      key: createMapInfoPointKey(
        selectedLastLocation.lat,
        selectedLastLocation.lng,
        selectedLastLocation.date,
      ),
      title: selectedDevice.serial || `Устройство #${selectedDevice.id}`,
      subtitle: selectedDevice.org?.name,
      date: selectedLastLocation.date,
      lat: selectedLastLocation.lat,
      lng: selectedLastLocation.lng,
    }
  }, [selectedDevice, selectedLastLocation])

  const selectedInfoPoint = useMemo(() => {
    if (!selectedPointKey) return currentMapPoint

    return (
      routeMapPoints.find((point) => point.key === selectedPointKey) ??
      currentMapPoint
    )
  }, [currentMapPoint, routeMapPoints, selectedPointKey])

  useEffect(() => {
    if (selectedPointKey && selectedInfoPoint?.key !== selectedPointKey) {
      setSelectedPointKey(null)
    }
  }, [selectedInfoPoint, selectedPointKey])

  const visibleMapPositions = useMemo<[number, number][]>(() => {
    if (isSelectedMode) {
      return selectedMapPositions
    }

    return devicesWithLocation.map(
      (device) =>
        [
          device.lastLocation.lat,
          device.lastLocation.lng,
        ] as [number, number],
    )
  }, [isSelectedMode, selectedMapPositions, devicesWithLocation])
  const visibleMapFitKey = isSelectedMode
    ? `selected:${selectedDevice?.id ?? 'none'}:${selectedPeriodKey}:${gpsFitDataKey}`
    : 'all-devices'

  const firstLocation = devicesWithLocation[0]?.lastLocation

  const mapCenter: [number, number] = firstLocation
    ? [firstLocation.lat, firstLocation.lng]
    : [55.397243, 86.117034]

  function handleSelectDevice(device: Device) {
    setSelectedDevice(device)
    setSelectedPeriod(null)
    setSelectedPointKey(null)
    setIsInfoCardVisible(true)
  }

  function handleResetSelection() {
    setSelectedDevice(null)
    setSelectedPeriod(null)
    setSelectedPointKey(null)
    setIsInfoCardVisible(false)
  }

  return (
    <section className="map-page">
      <div className="map-page__map" id={fullscreenTargetId}>
        {shouldShowPhonesLoadState && (
          <div
            className={`map-page__load-state${
              phonesError ? ' is-error' : ''
            }`}
          >
            {phonesError
              ? 'Не удалось загрузить устройства'
              : 'Загружаем устройства'}
          </div>
        )}

        <AnimatePresence initial={false}>
          {selectedDevice && selectedInfoPoint && isInfoCardVisible && (
            <m.div
              className="device-map-info-card device-map-info-card--map-page"
              variants={popoverVariants}
              initial="initial"
              animate="animate"
              exit="exit"
              transition={uiFadeTransition}
            >
              <button
                className="device-map-info-card__close"
                type="button"
                aria-label="Закрыть информацию о точке"
                onClick={handleResetSelection}
              >
                <X size={15} />
              </button>

              <div className="device-map-info-card__header">
                <Link
                  className="device-map-info-card__title"
                  to={`/devices/${selectedDevice.id}`}
                >
                  {selectedInfoPoint.title}
                </Link>
                <span>
                  {selectedInfoPoint.subtitle ??
                    selectedDevice.org?.name ??
                    selectedDevice.serial}
                </span>
              </div>

              <div className="device-map-info-card__rows">
                <div className="device-map-info-card__row">
                  <span
                    className={`device-map-info-card__status ${getNetworkStatusClass(
                      selectedDevice.networkStatus,
                    )}`}
                  >
                    {getNetworkStatusLabel(selectedDevice.networkStatus)}
                  </span>
                </div>

                <div className="device-map-info-card__row">
                  <Clock3 size={15} />
                  <span>{formatLocationDate(selectedInfoPoint.date)}</span>
                </div>

                <div className="device-map-info-card__row">
                  <Battery size={15} />
                  <span>
                    Заряд АКБ:{' '}
                    {formatMetric(selectedDevice.techState?.batteryLevel, '%')}
                  </span>
                </div>

                <div className="device-map-info-card__row">
                  <Thermometer size={15} />
                  <span>
                    Температура:{' '}
                    {formatMetric(selectedDevice.techState?.temperature, '°C')}
                  </span>
                </div>

                <div className="device-map-info-card__row">
                  <MapPin size={15} />
                  <span>
                    {formatCoordinate(selectedInfoPoint.lat)},{' '}
                    {formatCoordinate(selectedInfoPoint.lng)}
                  </span>
                </div>
              </div>
            </m.div>
          )}
        </AnimatePresence>

        <AnimatePresence initial={false}>
          {selectedDevice && (
            <m.div
              className="map-selection-panel"
              variants={popoverVariants}
              initial="initial"
              animate="animate"
              exit="exit"
              transition={uiFadeTransition}
            >
            <div className="map-track-filter">
              <MapTrackPeriodControl onChange={setSelectedPeriod} />

              <button
                className="map-track-filter__reset"
                type="button"
                onClick={handleResetSelection}
              >
                <Layers size={17} />
                Все устройства
              </button>

              {(isGpsTrackLoading || gpsTrackError) && (
                <div
                  className={`map-track-filter__state${
                    gpsTrackError ? ' is-error' : ''
                  }`}
                >
                  {gpsTrackError
                    ? 'Не удалось загрузить маршрут'
                    : 'Загружаем маршрут'}
                </div>
              )}
            </div>
          </m.div>
          )}
        </AnimatePresence>

        <MapContainer
          center={mapCenter}
          zoom={12}
          scrollWheelZoom
          attributionControl={false}
          className="map-page__leaflet"
        >
          <MapResizeWatcher />
          <MapResetHandler onReset={handleResetSelection} />
          <MapFitBounds
            fitKey={visibleMapFitKey}
            positions={visibleMapPositions}
          />

          <TileLayer url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}" />

          {!isSelectedMode &&
            devicesWithLocation.map((device) => {
              if (!device.lastLocation) return null

              return (
                <Marker
                  key={device.id}
                  position={[device.lastLocation.lat, device.lastLocation.lng]}
                  icon={createMarkerIcon(device.networkStatus)}
                  eventHandlers={{
                    click: (event) => {
                      event.originalEvent.stopPropagation()
                      handleSelectDevice(device)
                    },
                  }}
                />
              )
            })}

          {isSelectedMode && (
            <>
              {selectedRoutePositions.length > 1 && (
                <Polyline
                  positions={selectedRoutePositions}
                  pathOptions={{
                    color: routeColor,
                    weight: 4,
                    opacity: 0.85,
                  }}
                />
              )}

              {routeMapPoints.map((point) => (
                <CircleMarker
                  key={point.key}
                  center={[point.lat, point.lng]}
                  radius={4}
                  pathOptions={{
                    color: routeColor,
                    fillColor: routeColor,
                    fillOpacity: 1,
                    weight: 2,
                  }}
                  eventHandlers={{
                    click: (event) => {
                      event.originalEvent.stopPropagation()
                      setSelectedPointKey(point.key)
                      setIsInfoCardVisible(true)
                    },
                  }}
                />
              ))}

              {selectedLastLocation && currentMapPoint && (
                <Marker
                  position={[
                    selectedLastLocation.lat,
                    selectedLastLocation.lng,
                  ]}
                  icon={createMarkerIcon(selectedDevice?.networkStatus)}
                  eventHandlers={{
                    click: (event) => {
                      event.originalEvent.stopPropagation()
                      setSelectedPointKey(currentMapPoint.key)
                      setIsInfoCardVisible(true)
                    },
                  }}
                />
              )}
            </>
          )}

          <FullscreenControl targetId={fullscreenTargetId} />
        </MapContainer>
      </div>
    </section>
  )
}
