import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ActivityIndicator, Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Constants from 'expo-constants';
import {
  MapView,
  Camera,
  ShapeSource,
  CircleLayer,
  FillLayer,
  LineLayer,
  UserLocation,
} from '@maplibre/maplibre-react-native';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '@/theme/ThemeContext';
import { ps } from '@/api/client';
import { useLocation, type Coords } from '@/features/location/useLocation';
import { useAuth } from '@/auth/AuthContext';
import { getMirrorZones, syncZones } from '@/api/zoneMirror';
import {
  areaRequiresAccount,
  getAdjustedAreas,
  refreshAreas,
  type ParkingArea,
} from '@/api/parkingAreas';
import { distanceToGeometry, type LonLat } from '@/features/citymap/geo';
import {
  getParkedPin,
  setParkedPin,
  type ParkedSpot,
} from '@/features/session/activeParkingStore';
import { pinParkedSpot } from '@/features/session/activeParking';
import type { RootStackParamList } from '@/navigation/RootNavigator';
import type { Zone } from 'parksmarter-client';

const MAP_STYLE_LIGHT =
  (Constants.expoConfig?.extra?.mapStyleUrl as string) ??
  'https://tiles.openfreemap.org/styles/liberty';
const MAP_STYLE_DARK =
  (Constants.expoConfig?.extra?.mapStyleUrlDark as string) ??
  'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json';

// Default view when we have no last-session lot: all of downtown Sandpoint, ID.
const DEFAULT_CENTER: Coords = { latitude: 48.2766, longitude: -116.5533 };
const DEFAULT_ZOOM = 14;

type Nav = NativeStackNavigationProp<RootStackParamList>;

/**
 * How far from a city-map area a parked pin can be and still be taken as "that's
 * where I am". A block is ~170 m, a street ~12 m wide; 40 m picks the right side
 * of the right street without silently matching a spot two blocks away.
 */
const AREA_SNAP_METERS = 40;

/** Nearest city-map area to a point, or null if nothing is close enough. */
function areaAt(point: LonLat, areas: ParkingArea[]): ParkingArea | null {
  let best: ParkingArea | null = null;
  let bestDist = AREA_SNAP_METERS;
  for (const a of areas) {
    const d = distanceToGeometry(point, a.geometry);
    if (d < bestDist) {
      best = a;
      bestDist = d;
    }
  }
  return best;
}

/** Coerce the server's zone color (hex string, color name, or numeric) into a usable color. */
function normalizeColor(v: unknown): string | null {
  if (typeof v === 'number') return '#' + (v & 0xffffff).toString(16).padStart(6, '0');
  if (typeof v === 'string') {
    const s = v.trim();
    if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(s)) return s;
    if (/^\d+$/.test(s)) return '#' + (Number(s) & 0xffffff).toString(16).padStart(6, '0');
    if (/^[a-z]+$/i.test(s)) return s; // named color
  }
  return null;
}

export function MapScreen() {
  const navigation = useNavigation<Nav>();
  const insets = useSafeAreaInsets();
  const { mode } = useTheme();
  const mapStyle = mode === 'dark' ? MAP_STYLE_DARK : MAP_STYLE_LIGHT;
  const { coords, refresh } = useLocation();
  const { isAnonymous } = useAuth();
  const [zones, setZones] = useState<Zone[]>([]);
  const [status, setStatus] = useState<string>('Pan to an area and tap “Search this area”.');
  const [loading, setLoading] = useState(false);
  const [initialCenter, setInitialCenter] = useState<Coords | null>(null);

  // The city's printed parking map, georeferenced. Local geography — loading and
  // tapping these never touches the ParkSmarter API.
  const [areas, setAreas] = useState<ParkingArea[]>([]);
  const [showAreas, setShowAreas] = useState(true);
  // Set while waiting for the user to tap where they parked (the GPS-less path).
  const [pinning, setPinning] = useState(false);
  const [spot, setSpot] = useState<ParkedSpot | null>(null);

  const mapRef = useRef<any>(null);
  const cameraRef = useRef<any>(null);
  const [mapReady, setMapReady] = useState(false);
  // We position the camera exactly once, then never auto-recenter — otherwise
  // re-renders (from searching) would snap the map back and fight the user's pan.
  const positioned = useRef(false);
  // The live viewport, tracked from region changes. MapLibre's native camera
  // re-applies its last setCamera() stop on any re-render, so before a search we
  // overwrite that stop with the current view (zero-duration) — otherwise the
  // results re-render snaps the map back to the previous programmatic target.
  const viewRef = useRef<{ center: [number, number]; zoom: number } | null>(null);
  // The native UserLocation dot has its own GPS feed — capture it so "My
  // location" works even when expo-location can't get a fix (e.g. indoors).
  const nativeFix = useRef<Coords | null>(null);

  // Look up the LAST session's parking-lot coordinate (the meter's own location
  // from history — never the user's GPS). Used to open the map and by "Last lot".
  const lastSessionLot = useCallback(async (): Promise<Coords | null> => {
    // No account, no session history — and asking anyway 401s, which the global
    // handler would turn into a bogus "session expired" bounce.
    if (isAnonymous) return null;
    try {
      const past = await ps.getPastParkingSessions({ currentPage: 1, pageSize: 1 });
      const s = past.Session?.[0] as Record<string, any> | undefined;
      const lat = Number(s?.Lat);
      const lng = Number(s?.Long);
      if (s && Number.isFinite(lat) && Number.isFinite(lng) && !(lat === 0 && lng === 0)) {
        return { latitude: lat, longitude: lng };
      }
    } catch {
      /* ignore */
    }
    return null;
  }, [isAnonymous]);

  // Open on your last parking lot — NOT your GPS. Your location is only ever sent
  // to the API when you explicitly tap "My location", so we never auto-center on it.
  useEffect(() => {
    (async () => {
      const lot = await lastSessionLot();
      setInitialCenter(lot ?? DEFAULT_CENTER);
    })();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // One-time initial positioning, once both the map and a target are ready.
  useEffect(() => {
    if (mapReady && initialCenter && !positioned.current) {
      positioned.current = true;
      cameraRef.current?.setCamera?.({
        centerCoordinate: [initialCenter.longitude, initialCenter.latitude],
        zoomLevel: initialCenter === DEFAULT_CENTER ? DEFAULT_ZOOM : 14,
        animationDuration: 0,
      });
    }
  }, [mapReady, initialCenter]);

  const searchAt = useCallback(
    async (c: Coords, label: string) => {
      setLoading(true);
      setStatus(`Searching ${label}…`);
      try {
        if (isAnonymous) {
          // No ParkSmarter login — read areas from our mirror (all of them; the
          // covered area is small). The device GPS is never sent.
          const all = await getMirrorZones();
          const found = all.filter((z) => z.Lat != null && z.Long != null);
          setZones(found);
          setStatus(found.length ? `${found.length} areas` : 'No areas mirrored yet — sign in to load them.');
          return;
        }
        const res = await ps.getMetersByLocation({ latitude: c.latitude, longitude: c.longitude });
        const found = (res.Zones ?? []).filter((z) => z.Lat != null && z.Long != null);
        setZones(found);
        setStatus(found.length ? `${found.length} meters near ${label}` : `No meters found ${label}`);
        void syncZones(found); // admin-only; no-ops otherwise
      } catch (e: any) {
        setZones([]);
        setStatus(
          e?.status === 401
            ? 'Session expired — sign in again.'
            : `Search failed: ${e?.serverMessage ?? e?.message ?? 'error'}`,
        );
      } finally {
        setLoading(false);
      }
    },
    [isAnonymous],
  );

  // Anonymous: load the mirrored areas once on open (no ParkSmarter needed).
  useEffect(() => {
    if (isAnonymous) void searchAt(DEFAULT_CENTER, 'Sandpoint');
  }, [isAnonymous, searchAt]);

  // City parking map: draw the cached/bundled copy immediately so the overlay is
  // there offline, then quietly refresh from the server behind it.
  useEffect(() => {
    let alive = true;
    (async () => {
      const local = await getAdjustedAreas();
      if (alive) setAreas(local.areas);
      try {
        await refreshAreas();
        const fresh = await getAdjustedAreas();
        if (alive) setAreas(fresh.areas);
      } catch {
        /* offline or unseeded — the local copy is already drawn */
      }
    })();
    return () => {
      alive = false;
    };
  }, []);

  // Re-read the pin on every focus: the session may have ended on another screen
  // (or from the notification), which clears it.
  useFocusEffect(
    useCallback(() => {
      void getParkedPin().then(setSpot);
    }, []),
  );

  // Search whatever the map is currently centered on. This only ever sends the
  // map's center point — never the device GPS. (If you want to search your own
  // location, tap "My location" to center there first, then Search this area.)
  const searchThisArea = async () => {
    let center: [number, number] | null = null;
    try {
      const c = await mapRef.current?.getCenter?.(); // [lng, lat]
      if (Array.isArray(c) && c.length === 2) center = [c[0], c[1]];
    } catch {
      /* fall through */
    }
    // Fallback is the tracked viewport center — deliberately NOT the GPS fix.
    if (!center && viewRef.current) center = viewRef.current.center;
    if (!center) {
      setStatus('Move the map, then tap “Search this area”.');
      return;
    }
    // Commit the current view as the camera's stop so the results re-render
    // doesn't revert to the last programmatic (e.g. "My location") target.
    cameraRef.current?.setCamera?.({ centerCoordinate: center, animationDuration: 0 });
    await searchAt({ latitude: center[1], longitude: center[0] }, 'this area');
  };

  // Recenter on the live GPS fix (if available) and search there. Prefer the
  // native map fix (the blue dot), then expo-location, then a forced refresh.
  const goToMyLocation = async () => {
    const c = nativeFix.current ?? coords ?? (await refresh());
    if (!c) {
      setStatus('No location fix yet — GPS may be unavailable (e.g. indoors).');
      return;
    }
    cameraRef.current?.setCamera?.({
      centerCoordinate: [c.longitude, c.latitude],
      zoomLevel: 15,
      animationDuration: 700,
    });
    await searchAt(c, 'you');
  };

  // Center on the LAST SESSION's parking lot (the meter's own coordinate from
  // history — never your GPS) and search around it with a ~couple-mile view.
  const searchLastSessionLot = async () => {
    if (isAnonymous) {
      setStatus('Sign in to use your last parking lot — it comes from your account history.');
      return;
    }
    setLoading(true);
    setStatus('Finding your last parking lot…');
    const lot = await lastSessionLot();
    if (!lot) {
      setStatus('No past parking session with a location yet.');
      setLoading(false);
      return;
    }
    cameraRef.current?.setCamera?.({
      centerCoordinate: [lot.longitude, lot.latitude],
      zoomLevel: 12.5, // ~a couple miles across
      animationDuration: 600,
    });
    await searchAt(lot, 'your last lot');
  };

  /* -------------------------------------------------- parking-map interaction */

  /**
   * The paid city lots are ParkSmarter-only, so they're hidden from someone
   * browsing without an account — showing parking you can't actually buy is worse
   * than not showing it.
   */
  const hidden = useCallback(
    (a: ParkingArea) => isAnonymous && areaRequiresAccount(a),
    [isAnonymous],
  );

  /** What actually gets drawn and tapped. */
  const visibleAreas = useMemo(() => areas.filter((a) => !hidden(a)), [areas, hidden]);

  /** Open an area, carrying the pin along if we have one. */
  const openArea = useCallback(
    (area: ParkingArea, at?: ParkedSpot) => {
      navigation.navigate('CityArea', { area, spot: at });
    },
    [navigation],
  );

  /** Drop the pin at `c`, work out which area that is, and open it. */
  const pinAt = useCallback(
    (c: Coords, manual: boolean) => {
      const at: ParkedSpot = { latitude: c.latitude, longitude: c.longitude, manual };
      setSpot(at);
      // Persist immediately — the pin is worth keeping even if you never start a
      // timer, and even if you back out of the screen we're about to open.
      void pinParkedSpot(at);
      // Detect against every area, including the ones hidden from this user, so
      // standing in a paid lot gets an explanation rather than "nothing found".
      const found = areaAt([c.longitude, c.latitude], areas);
      if (found && hidden(found)) {
        setStatus(`Pinned. ${found.name} is a paid city lot — sign in to park there.`);
      } else if (found) {
        setStatus(`Parked at ${found.name}`);
        openArea(found, at);
      } else {
        // Pin still stands — you parked somewhere, it's just not on the city map.
        setStatus('Pinned. No mapped parking area within 40 m — tap a coloured segment to pick one.');
      }
    },
    [areas, hidden, openArea],
  );

  // "Park here": pin from GPS and auto-detect the area. When there's no fix (a
  // garage, indoors, GPS off) fall back to letting the user tap the spot — the
  // pin is the point, so it must not depend on the GPS working.
  const parkHere = async () => {
    if (pinning) {
      setPinning(false);
      setStatus('Pin cancelled.');
      return;
    }
    const c = nativeFix.current ?? coords ?? (await refresh());
    if (!c) {
      setPinning(true);
      setStatus('No GPS fix — tap the map where you parked.');
      return;
    }
    cameraRef.current?.setCamera?.({
      centerCoordinate: [c.longitude, c.latitude],
      zoomLevel: 17,
      animationDuration: 500,
    });
    pinAt(c, false);
  };

  /** A tap on the map: places the manual pin, but only while we asked for one. */
  const onMapPress = (e: any) => {
    if (!pinning) return;
    const c = e?.geometry?.coordinates;
    if (!Array.isArray(c) || c.length !== 2) return;
    setPinning(false);
    pinAt({ latitude: c[1], longitude: c[0] }, true);
  };

  /** A tap on your own pin: the only way to take it down without ending a session. */
  const onSpotPress = () => {
    if (!spot) return;
    Alert.alert('Your car', 'Remove the parked pin?', [
      { text: 'Keep', style: 'cancel' },
      {
        text: 'Remove',
        style: 'destructive',
        onPress: () => {
          setSpot(null);
          void setParkedPin(null);
          setStatus('Pin removed.');
        },
      },
    ]);
  };

  /** A tap on a coloured segment: the other start flow, no pin involved. */
  const onAreaPress = (e: any) => {
    const id = e?.features?.[0]?.properties?.id;
    const found = visibleAreas.find((a) => a.id === id);
    if (found) openArea(found, spot ?? undefined);
  };

  const areaFeatures = useMemo(
    () => ({
      type: 'FeatureCollection' as const,
      features: visibleAreas.map((a) => ({
        type: 'Feature' as const,
        id: a.id,
        geometry: a.geometry,
        properties: { id: a.id, color: a.color, kind: a.kind },
      })),
    }),
    [visibleAreas],
  );

  const spotFeature = useMemo(
    () => ({
      type: 'FeatureCollection' as const,
      features: spot
        ? [
            {
              type: 'Feature' as const,
              id: 'parked',
              geometry: { type: 'Point' as const, coordinates: [spot.longitude, spot.latitude] },
              properties: {},
            },
          ]
        : [],
    }),
    [spot],
  );

  // Meters as a GeoJSON layer (GPU-drawn, coordinate-anchored) — far more stable
  // than React MarkerViews, which floated and thrashed the camera.
  const meterFeatures = useMemo(
    () => ({
      type: 'FeatureCollection' as const,
      features: zones.map((z, i) => ({
        type: 'Feature' as const,
        id: String(z.ZoneId ?? z.ScannerCode ?? i),
        geometry: {
          type: 'Point' as const,
          coordinates: [z.Long as number, z.Lat as number],
        },
        properties: { index: i, color: normalizeColor(z.BackgroundColor) ?? '#1e6f5c' },
      })),
    }),
    [zones],
  );

  const onPinPress = (e: any) => {
    const idx = e?.features?.[0]?.properties?.index;
    if (typeof idx === 'number' && zones[idx]) {
      navigation.navigate('MeterDetail', { zone: zones[idx] });
    }
  };

  // MapLibre's Camera re-applies its last imperative setCamera() stop whenever it
  // re-renders — so a search re-render (setZones) would snap the map back to the
  // last programmatic target. Memoize these elements so search re-renders never
  // touch them; only the marker ShapeSource below updates.
  const cameraEl = useMemo(() => <Camera ref={cameraRef} />, []);
  const userLocationEl = useMemo(
    () => (
      <UserLocation
        visible
        renderMode="normal"
        onUpdate={(loc: any) => {
          if (loc?.coords) {
            nativeFix.current = {
              latitude: loc.coords.latitude,
              longitude: loc.coords.longitude,
            };
          }
        }}
      />
    ),
    [],
  );

  return (
    <View style={styles.container}>
      <MapView
        ref={mapRef}
        style={styles.map}
        mapStyle={mapStyle}
        rotateEnabled={false}
        onDidFinishLoadingMap={() => setMapReady(true)}
        onPress={onMapPress}
        onRegionDidChange={(f: any) => {
          const c = f?.geometry?.coordinates;
          const z = f?.properties?.zoomLevel;
          if (Array.isArray(c) && c.length === 2) {
            viewRef.current = { center: [c[0], c[1]], zoom: typeof z === 'number' ? z : 14 };
          }
        }}
      >
        {cameraEl}
        {userLocationEl}

        {/* The city parking map, under the meter pins so pins stay tappable. */}
        {showAreas ? (
          <ShapeSource id="city-areas" shape={areaFeatures} onPress={onAreaPress}>
            <FillLayer
              id="city-area-fills"
              filter={['==', ['geometry-type'], 'Polygon']}
              style={{ fillColor: ['get', 'color'], fillOpacity: 0.45 }}
            />
            <LineLayer
              id="city-area-outlines"
              filter={['==', ['geometry-type'], 'Polygon']}
              style={{ lineColor: ['get', 'color'], lineWidth: 1.5, lineOpacity: 0.9 }}
            />
            {/* Street segments, scaled with zoom so they read as painted kerb. */}
            <LineLayer
              id="city-area-lines"
              filter={['==', ['geometry-type'], 'LineString']}
              style={{
                lineColor: ['get', 'color'],
                lineOpacity: 0.95,
                lineCap: 'round',
                lineWidth: ['interpolate', ['linear'], ['zoom'], 12, 2, 15, 5, 18, 11],
              }}
            />
            {/* A fat, near-invisible line purely to make thin segments tappable —
                a 5 px kerb stripe is far too small a target for a fingertip. */}
            <LineLayer
              id="city-area-touch"
              filter={['==', ['geometry-type'], 'LineString']}
              style={{ lineColor: '#000000', lineOpacity: 0.01, lineWidth: 24 }}
            />
          </ShapeSource>
        ) : null}

        {/* Where the car is. Drawn above everything — it's the thing you came back for. */}
        <ShapeSource id="parked-spot" shape={spotFeature} onPress={onSpotPress}>
          <CircleLayer
            id="parked-halo"
            style={{ circleColor: '#1e6f5c', circleOpacity: 0.25, circleRadius: 18 }}
          />
          <CircleLayer
            id="parked-dot"
            style={{
              circleColor: '#1e6f5c',
              circleStrokeColor: '#ffffff',
              circleStrokeWidth: 3,
              circleRadius: 8,
            }}
          />
        </ShapeSource>

        <ShapeSource id="meters" shape={meterFeatures} onPress={onPinPress}>
          <CircleLayer
            id="meter-circles"
            style={{
              circleColor: ['get', 'color'],
              circleStrokeColor: '#ffffff',
              circleStrokeWidth: 2,
              circlePitchAlignment: 'map',
              circleRadius: [
                'interpolate',
                ['linear'],
                ['zoom'],
                10,
                5,
                16,
                9,
              ],
            }}
          />
        </ShapeSource>
      </MapView>

      <View style={[styles.statusBar, { top: insets.top + 12 }]}>
        {loading ? <ActivityIndicator color="#fff" style={{ marginRight: 8 }} /> : null}
        <Text style={styles.statusText} numberOfLines={1}>
          {status}
        </Text>
      </View>

      <View style={[styles.controls, { bottom: insets.bottom + 24 }]}>
        <View style={styles.row}>
          <TouchableOpacity
            style={pinning ? styles.pillActive : styles.pillPrimary}
            onPress={parkHere}
          >
            <Text style={styles.pillText}>{pinning ? 'Tap the map…' : 'Park here'}</Text>
          </TouchableOpacity>
          <TouchableOpacity
            style={showAreas ? styles.pillOn : styles.pill}
            onPress={() => setShowAreas((v) => !v)}
          >
            <Text style={styles.pillText}>City map</Text>
          </TouchableOpacity>
        </View>
        <View style={styles.row}>
          <TouchableOpacity style={styles.pill} onPress={searchThisArea}>
            <Text style={styles.pillText}>Search this area</Text>
          </TouchableOpacity>
          <TouchableOpacity style={styles.pill} onPress={goToMyLocation}>
            <Text style={styles.pillText}>My location</Text>
          </TouchableOpacity>
          <TouchableOpacity style={styles.pill} onPress={searchLastSessionLot}>
            <Text style={styles.pillText}>Last lot</Text>
          </TouchableOpacity>
        </View>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  map: { flex: 1 },
  statusBar: {
    position: 'absolute',
    top: 12,
    left: 12,
    right: 12,
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: 'rgba(0,0,0,0.65)',
    paddingHorizontal: 12,
    paddingVertical: 8,
    borderRadius: 12,
  },
  statusText: { color: '#fff', fontSize: 13, flexShrink: 1 },
  controls: {
    position: 'absolute',
    bottom: 24,
    alignSelf: 'center',
    alignItems: 'center',
    gap: 8,
  },
  row: { flexDirection: 'row', gap: 8 },
  pill: {
    backgroundColor: '#444',
    paddingHorizontal: 14,
    paddingVertical: 10,
    borderRadius: 22,
  },
  pillPrimary: {
    backgroundColor: '#1e6f5c',
    paddingHorizontal: 16,
    paddingVertical: 10,
    borderRadius: 22,
  },
  /** Waiting for the user to tap where they parked. */
  pillActive: {
    backgroundColor: '#c07a12',
    paddingHorizontal: 16,
    paddingVertical: 10,
    borderRadius: 22,
  },
  pillOn: {
    backgroundColor: '#2f6f60',
    paddingHorizontal: 14,
    paddingVertical: 10,
    borderRadius: 22,
  },
  pillText: { color: '#fff', fontWeight: '600' },
});
