import { useEffect, useMemo, useRef, useState } from 'react'
import { useQuery } from '@apollo/client/react'
import {
  CircleMarker,
  MapContainer,
  Marker,
  Polyline,
  TileLayer,
  useMap,
} from 'react-leaflet'
import L from 'leaflet'
import { Battery, Clock3, Map as MapIcon, MapPin, Thermometer, X } from 'lucide-react'

import type { Device } from '../../types'
import { GET_PHONE_GPS_TRACK_QUERY } from '../../../../entities/device/api/device.graphql'
import type {
  DeviceGpsTrackPoint,
  DeviceTelemetryItem,
  GetPhoneGpsTrackData,
  GetPhoneGpsTrackVariables,
} from '../../../../entities/device/model/types'
import { FullscreenControl } from '../../../../widgets/FullscreenControlLeaflet/FullscreenControl'
import {
  MapTrackPeriodControl,
  type MapTrackPeriodValue,
} from '../../../MapPage/components/MapTrackPeriodControl/MapTrackPeriodControl'

import './DeviceMapCard.scss'

type DeviceMapCardProps = {
  device: Device
  variant?: 'card' | 'modal'
}

type TelemetryLine = Array<[number, number]>

type DeviceMapPoint = {
  key: string
  kind: 'current' | 'route'
  title: string
  subtitle?: string
  date?: number
  lat: number
  lng: number
}

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

const routeColor = 'var(--color-primary)'

const defaultLocation = {
  lat: 54.7558,
  lng: 87.1099,
}

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

function getConnectionStatusClass(connection: Device['connection']) {
  if (connection === 'online') return 'is-online'
  if (connection === 'offlineDanger') return 'is-lost'

  return 'is-offline'
}

function createDeviceMarkerIcon(connection: Device['connection']) {
  const className = getConnectionStatusClass(connection)
  const cachedIcon = markerIconCache.get(className)

  if (cachedIcon) return cachedIcon

  const icon = L.divIcon({
    className: `device-map-marker ${className}`,
    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(className, 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, suffix: string) {
  if (value === null) return '-'

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

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

function sortTelemetry(telemetry: DeviceTelemetryItem[]) {
  return [...telemetry].sort((a, b) => a.date - b.date)
}

function createTelemetryLine(
  telemetry: DeviceTelemetryItem[],
  getValue: (item: DeviceTelemetryItem) => number | null,
): TelemetryLine {
  return telemetry
    .map((item) => [item.date, getValue(item)] as const)
    .filter((item): item is [number, number] => typeof item[1] === 'number')
}

function getLineValueAtTime(data: TelemetryLine, timestamp?: number) {
  if (!timestamp || !data.length) return null

  const firstPoint = data[0]
  const lastPoint = data[data.length - 1]

  if (timestamp <= firstPoint[0]) return firstPoint[1]
  if (timestamp >= lastPoint[0]) return lastPoint[1]

  let left = 0
  let right = data.length - 1

  while (right - left > 1) {
    const middle = Math.floor((left + right) / 2)

    if (data[middle][0] <= timestamp) {
      left = middle
    } else {
      right = middle
    }
  }

  const currentPoint = data[left]
  const nextPoint = data[right]
  const currentTime = currentPoint[0]
  const currentValue = currentPoint[1]
  const nextTime = nextPoint[0]
  const nextValue = nextPoint[1]

  if (nextTime === currentTime) return Math.round(currentValue)

  const progress = (timestamp - currentTime) / (nextTime - currentTime)
  const interpolatedValue =
    currentValue + (nextValue - currentValue) * progress

  return Math.round(interpolatedValue)
}

function createPointKey(kind: DeviceMapPoint['kind'], lat: number, lng: number, date?: number) {
  return `${kind}-${date ?? 'no-date'}-${lat}-${lng}`
}

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

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

function MapResizeWatcher() {
  const map = useMap()

  useEffect(() => {
    const timeoutId = window.setTimeout(() => {
      map.invalidateSize()
    }, 100)

    return () => window.clearTimeout(timeoutId)
  }, [map])

  return null
}

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

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

    lastFitKeyRef.current = fitKey

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

    const bounds = L.latLngBounds(positions)

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

  return null
}

export function DeviceMapCard({
  device,
  variant = 'card',
}: DeviceMapCardProps) {
  const [selectedPeriod, setSelectedPeriod] =
    useState<MapTrackPeriodValue | null>(null)
  const [selectedPointKey, setSelectedPointKey] = useState<string | null>(null)
  const networkStatusClass = getConnectionStatusClass(device.connection)
  const deviceMarkerIcon = useMemo(
    () => createDeviceMarkerIcon(device.connection),
    [device.connection],
  )

  const { data: gpsTrackData } = useQuery<
    GetPhoneGpsTrackData,
    GetPhoneGpsTrackVariables
  >(GET_PHONE_GPS_TRACK_QUERY, {
    variables: {
      phoneId: String(device.id),
      startDate: selectedPeriod?.periodStart.getTime() ?? 0,
      endDate: selectedPeriod?.periodEnd.getTime(),
    },
    skip: !device.id || !selectedPeriod,
    fetchPolicy: 'network-only',
    pollInterval: selectedPeriod?.preset === 'today' ? 15000 : 0,
  })

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

  const telemetry = useMemo(() => {
    return sortTelemetry(device.telemetry ?? [])
  }, [device.telemetry])

  const telemetryLines = useMemo(() => {
    return {
      battery: createTelemetryLine(telemetry, (item) => item.batteryLevel),
      temperature: createTelemetryLine(telemetry, (item) => item.temperature),
      capacity: createTelemetryLine(telemetry, (item) => item.batteryCapacity),
      cycles: createTelemetryLine(telemetry, (item) => item.batteryCycles),
    }
  }, [telemetry])

  const shouldShowCurrentLocation = isDateInPeriod(
    device.lastLocationDate,
    selectedPeriod,
  )

  const selectedCurrentLocation =
    shouldShowCurrentLocation && device.location
      ? {
          lat: device.location.lat,
          lng: device.location.lng,
          date: device.lastLocationDate,
        }
      : null

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

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

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

    if (!selectedCurrentLocation) {
      return positions
    }

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

  const location = selectedCurrentLocation ?? device.location ?? defaultLocation

  const currentPosition = useMemo<[number, number]>(() => {
    return [location.lat, location.lng]
  }, [location.lat, location.lng])

  const fitPositions =
    routePositions.length > 0 ? routePositions : [currentPosition]

  const fitKey = useMemo(() => {
    if (!selectedPeriod) {
      return `${device.id}-${device.location ? 'located' : 'fallback'}`
    }

    return [
      device.id,
      selectedPeriod.periodStart.getTime(),
      selectedPeriod.periodEnd.getTime(),
      routePositions.length > 0 ? 'route' : 'point',
    ].join('-')
  }, [device.id, device.location, routePositions.length, selectedPeriod])

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

  const currentMapPoint = useMemo<DeviceMapPoint | null>(() => {
    if (!selectedCurrentLocation) return null

    return {
      key: createPointKey(
        'current',
        selectedCurrentLocation.lat,
        selectedCurrentLocation.lng,
        selectedCurrentLocation.date,
      ),
      kind: 'current',
      title: device.model ?? 'Устройство',
      subtitle: device.factoryNumber,
      date: selectedCurrentLocation.date,
      lat: selectedCurrentLocation.lat,
      lng: selectedCurrentLocation.lng,
    }
  }, [device.factoryNumber, device.model, selectedCurrentLocation])

  const selectedMapPoint = useMemo(() => {
    if (!selectedPointKey) return null

    if (currentMapPoint?.key === selectedPointKey) {
      return currentMapPoint
    }

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

  useEffect(() => {
    if (selectedPointKey && !selectedMapPoint) {
      setSelectedPointKey(null)
    }
  }, [selectedMapPoint, selectedPointKey])

  const selectedTelemetry = useMemo(() => {
    return {
      battery: getLineValueAtTime(telemetryLines.battery, selectedMapPoint?.date),
      temperature: getLineValueAtTime(
        telemetryLines.temperature,
        selectedMapPoint?.date,
      ),
      capacity: getLineValueAtTime(telemetryLines.capacity, selectedMapPoint?.date),
      cycles: getLineValueAtTime(telemetryLines.cycles, selectedMapPoint?.date),
    }
  }, [selectedMapPoint, telemetryLines])

  return (
    <div
      className={
        variant === 'modal'
          ? 'device-card device-map device-map--modal'
          : 'device-card device-map'
      }
    >
      <div className="device-card__header">
        <div className="device-card__title">
          <MapIcon size={18} />
          <h3>Устройство на карте</h3>
        </div>

        <p>
          Последнее местоположение:{' '}
          {device.lastLocationAt ?? 'Нет данных'}
        </p>
      </div>

      <div className="device-map__container">
        <div className="device-map__period-control">
          <MapTrackPeriodControl onChange={setSelectedPeriod} />
        </div>

        {selectedMapPoint && (
          <div className="device-map-info-card">
            <button
              className="device-map-info-card__close"
              type="button"
              aria-label="Закрыть информацию о точке"
              onClick={() => setSelectedPointKey(null)}
            >
              <X size={15} />
            </button>

            <div className="device-map-info-card__header">
                <b>{selectedMapPoint.title}</b>
                <span>
                  {selectedMapPoint.subtitle ?? device.factoryNumber}
                </span>
            </div>

            <div className="device-map-info-card__rows">
              <div className="device-map-info-card__row">
                <span
                  className={`device-map-info-card__status ${networkStatusClass}`}
                >
                  {device.connectionText}
                </span>
              </div>

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

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

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

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

        <MapContainer
          center={currentPosition}
          zoom={14}
          scrollWheelZoom
          attributionControl={false}
          className="device-map__leaflet"
          id="device-map"
        >
          <MapResizeWatcher />
          <MapFitBounds positions={fitPositions} fitKey={fitKey} />

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

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

          {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)
                },
              }}
            />
          ))}

          {currentMapPoint && (
            <Marker
              position={currentPosition}
              icon={deviceMarkerIcon}
              eventHandlers={{
                click: (event) => {
                  event.originalEvent.stopPropagation()
                  setSelectedPointKey(currentMapPoint.key)
                },
              }}
            />
          )}

          <FullscreenControl targetId="device-map" />
        </MapContainer>
      </div>
    </div>
  )
}
