import type { LatLngBoundsExpression, LatLngExpression } from 'leaflet';
import { useEffect, useMemo } from 'react';
import {
  CircleMarker,
  MapContainer,
  Polyline,
  TileLayer,
  Tooltip,
  useMap,
} from 'react-leaflet';

import type { NetworkSnapshot } from '../../services/api';

import 'leaflet/dist/leaflet.css';

interface NetworkMapProps {
  readonly snapshot: NetworkSnapshot;
  readonly focusedStationId?: string | null;
  readonly startStationId?: string | null;
  readonly endStationId?: string | null;
  readonly routeSegments?: LatLngExpression[][];
  readonly onStationClick?: (stationId: string) => void;
}

interface StationPosition {
  readonly id: string;
  readonly name: string;
  readonly position: LatLngExpression;
}

const DEFAULT_CENTER: LatLngExpression = [51.505, -0.09];

export function NetworkMap({
  snapshot,
  focusedStationId,
  startStationId,
  endStationId,
  routeSegments = [],
  onStationClick,
}: NetworkMapProps): JSX.Element {
  const stationPositions = useMemo<StationPosition[]>(() => {
    return snapshot.stations.map((station) => ({
      id: station.id,
      name: station.name,
      position: [station.latitude, station.longitude] as LatLngExpression,
    }));
  }, [snapshot.stations]);

  const stationLookup = useMemo(() => {
    const lookup = new Map<string, LatLngExpression>();
    for (const station of stationPositions) {
      lookup.set(station.id, station.position);
    }
    return lookup;
  }, [stationPositions]);

  const trackSegments = useMemo(() => {
    return snapshot.tracks
      .map((track) => {
        if (track.coordinates && track.coordinates.length >= 2) {
          return track.coordinates.map(
            (pair) => [pair[0], pair[1]] as LatLngExpression
          );
        }

        const start = stationLookup.get(track.startStationId);
        const end = stationLookup.get(track.endStationId);
        if (!start || !end) {
          return null;
        }
        return [start, end] as LatLngExpression[];
      })
      .filter((segment): segment is LatLngExpression[] => segment !== null);
  }, [snapshot.tracks, stationLookup]);

  const bounds = useMemo(() => {
    if (stationPositions.length === 0) {
      return null;
    }

    let minLat = Infinity;
    let maxLat = -Infinity;
    let minLng = Infinity;
    let maxLng = -Infinity;

    for (const { position } of stationPositions) {
      const [lat, lng] = position as [number, number];
      minLat = Math.min(minLat, lat);
      maxLat = Math.max(maxLat, lat);
      minLng = Math.min(minLng, lng);
      maxLng = Math.max(maxLng, lng);
    }

    const padding = 0.02;
    return [
      [minLat - padding, minLng - padding],
      [maxLat + padding, maxLng + padding],
    ] as LatLngBoundsExpression;
  }, [stationPositions]);

  const focusedPosition = useMemo(() => {
    if (!focusedStationId) {
      return null;
    }
    return stationLookup.get(focusedStationId) ?? null;
  }, [focusedStationId, stationLookup]);

  return (
    <MapContainer
      className="network-map"
      center={bounds ? undefined : DEFAULT_CENTER}
      bounds={bounds ?? undefined}
      scrollWheelZoom
    >
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      {focusedPosition ? <StationFocus position={focusedPosition} /> : null}
      {trackSegments.map((segment, index) => (
        <Polyline
          key={`track-${index}`}
          positions={segment}
          pathOptions={{ color: '#334155', weight: 3, opacity: 0.8 }}
        />
      ))}
      {routeSegments.map((segment, index) => (
        <Polyline
          key={`route-${index}`}
          positions={segment}
          pathOptions={{ color: '#facc15', weight: 6, opacity: 0.9 }}
        />
      ))}
      {stationPositions.map((station) => (
        <CircleMarker
          key={station.id}
          center={station.position}
          radius={station.id === focusedStationId ? 9 : 6}
          pathOptions={{
            color: resolveMarkerStroke(
              station.id,
              startStationId,
              endStationId,
              focusedStationId
            ),
            fillColor: resolveMarkerFill(
              station.id,
              startStationId,
              endStationId,
              focusedStationId
            ),
            fillOpacity: 0.96,
            weight: station.id === focusedStationId ? 3 : 1,
          }}
          eventHandlers={{
            click: () => {
              onStationClick?.(station.id);
            },
          }}
        >
          <Tooltip
            direction="top"
            offset={[0, -8]}
            permanent={station.id === focusedStationId}
            sticky
          >
            {station.name}
          </Tooltip>
        </CircleMarker>
      ))}
    </MapContainer>
  );
}

function StationFocus({ position }: { position: LatLngExpression }): null {
  const map = useMap();

  useEffect(() => {
    map.panTo(position, { animate: true, duration: 0.6 });
  }, [map, position]);

  return null;
}

function resolveMarkerStroke(
  stationId: string,
  startStationId?: string | null,
  endStationId?: string | null,
  focusedStationId?: string | null
): string {
  if (stationId === startStationId) {
    return '#38bdf8';
  }
  if (stationId === endStationId) {
    return '#fb923c';
  }
  if (stationId === focusedStationId) {
    return '#22c55e';
  }
  return '#f97316';
}

function resolveMarkerFill(
  stationId: string,
  startStationId?: string | null,
  endStationId?: string | null,
  focusedStationId?: string | null
): string {
  if (stationId === startStationId) {
    return '#bae6fd';
  }
  if (stationId === endStationId) {
    return '#fed7aa';
  }
  if (stationId === focusedStationId) {
    return '#bbf7d0';
  }
  return '#ffe4c7';
}
