import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useEffect, useRef, useState } from "react";
import { Platform, StyleSheet, View } from "react-native";
import MapView, {
  AnimatedRegion,
  Marker,
  MarkerAnimated,
  PROVIDER_DEFAULT,
} from "react-native-maps";
import MapViewDirections from "react-native-maps-directions";

import { icons } from "@/constants";
import { SERVICES } from "@/constants/services";
import { tr } from "@/lib/i18n";
import {
  calculateDriverTimes,
  calculateRegion,
  generateMarkersFromData,
} from "@/lib/map";
import { useTheme } from "@/lib/theme";
import { useNearbyDrivers } from "@/lib/use-nearby-drivers";
import { useDriverStore, useLocationStore, useServiceStore } from "@/store";
import type { MarkerData } from "@/types/type";

// react-native-maps sizes itself from a real style object, so give it explicit
// dimensions rather than relying on percentage classNames resolving to 0.
const styles = StyleSheet.create({
  map: { ...StyleSheet.absoluteFillObject, borderRadius: 16 },
  markerBubble: {
    width: 34,
    height: 34,
    borderRadius: 17,
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "#111827",
    borderWidth: 2,
    borderColor: "#ffffff",
    // A flat dot on a light map is hard to pick out; a soft shadow lifts it.
    shadowColor: "#000",
    shadowOpacity: 0.3,
    shadowRadius: 3,
    shadowOffset: { width: 0, height: 1 },
    elevation: 4,
  },
  markerBubbleSelected: {
    backgroundColor: "#0286ff",
  },
  // Wraps bubble + arrow so the arrow can orbit the bubble by rotating the
  // whole frame, while the vehicle glyph inside stays upright and readable.
  markerFrame: {
    width: 54,
    height: 54,
    alignItems: "center",
    justifyContent: "center",
  },
  headingArrow: {
    position: "absolute",
    top: 0,
  },
});

// How long a marker takes to slide to its new position.
//
// Deliberately the poll interval, not less: each update is the car's position
// as of that moment, so spreading the movement across the whole gap until the
// next one is what makes a series of samples read as continuous travel. A
// shorter duration would animate quickly and then sit frozen, which looks
// worse than not animating at all.
const MARKER_GLIDE_MS = 5000;

// Below this the GPS heading is mostly noise — a stationary phone reports
// wildly varying directions — so the arrow is hidden and the car is simply
// drawn as parked.
const MOVING_KPH = 5;

// A driver pin drawn as the vehicle they actually drive.
//
// Every driver used to get the same car marker, so a moto rider watching a
// motorbike approach saw a car on their map — and the four services were
// indistinguishable at a glance. The glyphs come from the same SERVICES table
// the service picker uses, so a pin and its tile always agree.
const glyphFor = (service?: string | null) =>
  (SERVICES.find((s) => s.id === service) ?? SERVICES[0]).icon;

const ServiceMarker = ({
  marker,
  selected,
}: {
  marker: MarkerData;
  selected: boolean;
}) => {
  // Android renders a custom marker view by snapshotting it, and a snapshot
  // taken before layout is blank. Track changes briefly so the first real
  // frame is captured, then stop — leaving it on re-snapshots every marker on
  // every frame, which makes a map full of drivers crawl.
  const [tracksViewChanges, setTracksViewChanges] = useState(true);

  const heading = marker.heading ?? null;
  const moving = (marker.speed_kph ?? 0) >= MOVING_KPH;
  const showArrow = moving && heading !== null;

  // The marker's own coordinate, animated rather than assigned.
  //
  // Positions arrive every few seconds; setting them directly teleports each
  // car across the gap it covered since the last update. Holding the
  // coordinate in an AnimatedRegion and easing to each new fix turns the same
  // samples into visible travel — which is the whole point of showing other
  // drivers at all.
  const coordinate = useRef(
    new AnimatedRegion({
      latitude: marker.latitude,
      longitude: marker.longitude,
      latitudeDelta: 0,
      longitudeDelta: 0,
    }),
  ).current;

  useEffect(() => {
    // `timing` is not on the public typings for AnimatedRegion in this
    // version, though it exists at runtime; the cast keeps the call honest
    // without loosening the rest of the component.
    (
      coordinate as unknown as {
        timing: (config: Record<string, unknown>) => {
          start: () => void;
        };
      }
    )
      .timing({
        latitude: marker.latitude,
        longitude: marker.longitude,
        latitudeDelta: 0,
        longitudeDelta: 0,
        duration: MARKER_GLIDE_MS,
        // AnimatedRegion drives a native prop that the native driver can't
        // handle, so this animation runs on the JS thread by necessity.
        useNativeDriver: false,
      })
      .start();
  }, [coordinate, marker.latitude, marker.longitude]);

  useEffect(() => {
    setTracksViewChanges(true);
    const timer = setTimeout(() => setTracksViewChanges(false), 800);
    return () => clearTimeout(timer);
  }, [selected, marker.service, showArrow, heading]);

  // react-native-maps accepts an AnimatedRegion here at runtime — it is what
  // every animated-marker example passes — but types the prop as an animated
  // LatLng, so the two don't line up. Cast at the boundary rather than
  // loosening the component's own types.
  const animatedCoordinate = coordinate as unknown as React.ComponentProps<
    typeof MarkerAnimated
  >["coordinate"];

  return (
    <MarkerAnimated
      coordinate={animatedCoordinate}
      title={marker.title}
      anchor={{ x: 0.5, y: 0.5 }}
      tracksViewChanges={tracksViewChanges}
    >
      <View style={styles.markerFrame}>
        {/* Rotating the frame swings the arrow around the bubble to point the
            way the car is travelling, while the bubble itself — and the
            vehicle glyph in it — stays upright and legible. */}
        {showArrow ? (
          <View
            style={[
              StyleSheet.absoluteFill,
              { transform: [{ rotate: `${heading}deg` }] },
              styles.markerFrame,
            ]}
          >
            <MaterialCommunityIcons
              name="navigation"
              size={14}
              color={selected ? "#0286ff" : "#111827"}
              style={styles.headingArrow}
            />
          </View>
        ) : null}

        <View
          style={[
            styles.markerBubble,
            selected ? styles.markerBubbleSelected : null,
          ]}
        >
          <MaterialCommunityIcons
            name={glyphFor(marker.service)}
            size={18}
            color="#ffffff"
          />
        </View>
      </View>
    </MarkerAnimated>
  );
};

// "mutedStandard" is an Apple Maps type. Android's MapManager looks the value
// up in a fixed table and unboxes the result into an int, so an unrecognised
// name is a null Integer -> NullPointerException, and the map never draws.
const MAP_TYPE = Platform.OS === "ios" ? "mutedStandard" : "standard";

// showsPointsOfInterest is iOS-only; on Android the same muting is done with a
// style array, so both platforms get the same clean base map.
const MUTED_POI_STYLE = [
  {
    featureType: "poi",
    elementType: "labels",
    stylers: [{ visibility: "off" }],
  },
  {
    featureType: "transit",
    elementType: "labels.icon",
    stylers: [{ visibility: "off" }],
  },
];

// The single driver assigned to a ride, as returned by GET /ride/:id. Used to
// show the rider a live marker for the driver who accepted, instead of the
// generic "nearby drivers of this service" search list.
type TrackedDriver = {
  id: number;
  latitude: number | null;
  longitude: number | null;
  first_name?: string | null;
  last_name?: string | null;
  profile_image_url?: string | null;
  car_image_url?: string | null;
  car_seats?: number | null;
  rating?: number | null;
  car_model?: string | null;
  // Drives the pin glyph, so the rider watching their assigned driver arrive
  // sees a motorbike when a motorbike is coming.
  service?: string | null;
};

type LatLng = { latitude: number; longitude: number };

export type MapProps = {
  trackedDriver?: TrackedDriver | null;
  /**
   * Show position and nearby drivers only — never a route line, and never zoom
   * out to fit a destination.
   *
   * The home map answers "where am I and what's around me". Drawing the
   * destination there meant a rider who had merely searched an address, or
   * finished a trip earlier, kept seeing a route to it every time they opened
   * the app.
   */
  routeless?: boolean;
  // Driver view: override the store-derived origin/destination so the map
  // centers on the driver's own live position and pins the rider's pickup,
  // without touching the rider-facing location store.
  originOverride?: LatLng | null;
  destinationOverride?: (LatLng & { label?: string }) | null;
};

export const Map = ({
  trackedDriver,
  originOverride,
  destinationOverride,
  routeless = false,
}: MapProps = {}) => {
  const {
    userLatitude,
    userLongitude,
    destinationLatitude,
    destinationLongitude,
  } = useLocationStore();
  const { service } = useServiceStore();
  const { selectedDriver, setDrivers } = useDriverStore();
  const { isDark } = useTheme();

  const trackingMode =
    Boolean(trackedDriver) ||
    originOverride !== undefined ||
    destinationOverride !== undefined;

  // Online drivers of the selected service near the rider. Falls back to a
  // Beirut center when the rider's position isn't resolved yet so the map
  // still populates instead of sitting empty.
  //
  // The search starts tight around the rider and widens in 5 km steps only
  // when it finds nobody, so a rider on a busy street sees the cars actually
  // near them rather than every car in the country.
  const lat = userLatitude ?? 33.8938;
  const lng = userLongitude ?? 35.5018;
  const { drivers } = useNearbyDrivers(service, lat, lng);

  const [markers, setMarkers] = useState<MarkerData[]>([]);
  const mapRef = useRef<MapView>(null);

  // Region: in tracking mode, center on the driver's own position (or the
  // pickup point if that isn't resolved yet) instead of the rider's location
  // store, which tracking mode never touches.
  const region = trackingMode
    ? calculateRegion({
        userLatitude:
          originOverride?.latitude ?? destinationOverride?.latitude ?? null,
        userLongitude:
          originOverride?.longitude ?? destinationOverride?.longitude ?? null,
        destinationLatitude: originOverride
          ? (destinationOverride?.latitude ?? null)
          : null,
        destinationLongitude: originOverride
          ? (destinationOverride?.longitude ?? null)
          : null,
      })
    : calculateRegion({
        userLatitude,
        userLongitude,
        destinationLatitude: routeless ? null : destinationLatitude,
        destinationLongitude: routeless ? null : destinationLongitude,
      });

  // `initialRegion` is read once, at mount. The map mounts before the location
  // fix arrives, so it would sit on the Beirut fallback forever and never zoom
  // out to fit a destination the rider picks later. Animate on every real
  // change instead. Keyed on the coordinates so the repeated setUserLocation
  // from reverse geocoding (same coords, new address) doesn't yank the camera
  // back while the rider is panning.
  const regionKey = `${region.latitude},${region.longitude},${region.latitudeDelta},${region.longitudeDelta}`;
  const lastRegionKey = useRef(regionKey);

  useEffect(() => {
    if (lastRegionKey.current === regionKey) return;

    lastRegionKey.current = regionKey;
    mapRef.current?.animateToRegion(region, 500);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [regionKey]);

  useEffect(() => {
    if (trackedDriver) {
      setMarkers(
        trackedDriver.latitude != null && trackedDriver.longitude != null
          ? [
              {
                id: trackedDriver.id,
                latitude: trackedDriver.latitude,
                longitude: trackedDriver.longitude,
                title:
                  `${trackedDriver.first_name ?? ""} ${trackedDriver.last_name ?? ""}`.trim(),
                profile_image_url: trackedDriver.profile_image_url ?? "",
                car_image_url: trackedDriver.car_image_url ?? "",
                car_seats: trackedDriver.car_seats ?? 0,
                rating: trackedDriver.rating ?? 0,
                first_name: trackedDriver.first_name ?? "",
                last_name: trackedDriver.last_name ?? "",
                car_model: trackedDriver.car_model ?? null,
                service: trackedDriver.service ?? undefined,
              },
            ]
          : [],
      );
      return;
    }

    if (trackingMode) {
      setMarkers([]);
      return;
    }

    if (Array.isArray(drivers)) {
      if (!userLatitude || !userLongitude) return;

      const newMarkers = generateMarkersFromData({
        data: drivers,
        userLatitude,
        userLongitude,
      });

      setMarkers(newMarkers);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [trackedDriver, trackingMode, drivers, userLatitude, userLongitude]);

  useEffect(() => {
    if (trackingMode) return;
    if (markers.length > 0 && destinationLatitude && destinationLongitude) {
      calculateDriverTimes({
        markers,
        userLatitude,
        userLongitude,
        destinationLatitude,
        destinationLongitude,
        service,
      }).then((driversWithTimes) => {
        setDrivers((driversWithTimes as MarkerData[]) ?? []);
      });
    }
  }, [
    trackingMode,
    markers,
    destinationLatitude,
    destinationLongitude,
    userLatitude,
    userLongitude,
    setDrivers,
    service,
  ]);

  // The map itself never waits on the driver list or the location fix: drivers
  // are an overlay, and calculateRegion falls back to Beirut without coords.
  // Previously either one failing replaced the whole map with a spinner or an
  // error line, which read as "the map didn't load".

  return (
    <MapView
      ref={mapRef}
      provider={PROVIDER_DEFAULT}
      style={styles.map}
      tintColor={isDark ? "white" : "black"}
      mapType={MAP_TYPE}
      customMapStyle={MUTED_POI_STYLE}
      showsPointsOfInterest={false}
      initialRegion={region}
      showsUserLocation
      userInterfaceStyle={isDark ? "dark" : "light"}
    >
      {markers.map((marker) => (
        <ServiceMarker
          key={marker.id}
          marker={marker}
          selected={Boolean(trackedDriver) || selectedDriver === marker.id}
        />
      ))}

      {destinationOverride ? (
        <Marker
          key="pickup"
          coordinate={{
            latitude: destinationOverride.latitude,
            longitude: destinationOverride.longitude,
          }}
          title={destinationOverride.label ?? tr("components.map.destination")}
          image={icons.pin}
        />
      ) : (
        !routeless &&
        userLatitude &&
        userLongitude &&
        destinationLatitude &&
        destinationLongitude && (
          <>
            <Marker
              key="destination"
              coordinate={{
                latitude: destinationLatitude,
                longitude: destinationLongitude,
              }}
              title={tr("components.map.destination")}
              image={icons.pin}
            />

            <MapViewDirections
              origin={{
                latitude: userLatitude,
                longitude: userLongitude,
              }}
              destination={{
                latitude: destinationLatitude,
                longitude: destinationLongitude,
              }}
              apikey={process.env.EXPO_PUBLIC_GOOGLE_API_KEY!}
              strokeColor="#0286FF"
              strokeWidth={3}
            />
          </>
        )
      )}
    </MapView>
  );
};
