import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Constants from 'expo-constants';
import {
  MapView,
  Camera,
  ShapeSource,
  CircleLayer,
  UserLocation,
} from '@maplibre/maplibre-react-native';
import { 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, getLastKnownSavedLocation, type Coords } from '@/features/location/useLocation';
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';

// Fallback view when we have no GPS and no cached location (continental US).
const DEFAULT_CENTER: Coords = { latitude: 39.5, longitude: -98.35 };
const DEFAULT_ZOOM = 4;

type Nav = NativeStackNavigationProp<RootStackParamList>;

/** 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 [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);

  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);

  // Seed the initial camera target from a cached location so we don't strand at 0,0.
  useEffect(() => {
    (async () => {
      const last = await getLastKnownSavedLocation();
      setInitialCenter(last ?? coords ?? 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 {
      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}`);
    } catch (e: any) {
      setZones([]);
      setStatus(
        e?.status === 401
          ? 'Session expired — sign in again.'
          : `Search failed: ${e?.serverMessage ?? e?.message ?? 'error'}`,
      );
    } finally {
      setLoading(false);
    }
  }, []);

  // Search whatever the map is currently centered on (works with no GPS).
  const searchThisArea = async () => {
    try {
      const center = await mapRef.current?.getCenter?.(); // [lng, lat]
      if (center && center.length === 2) {
        // 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');
        return;
      }
    } catch {
      /* fall through */
    }
    if (coords) await searchAt(coords, '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');
  };

  const searchLastKnown = async () => {
    const last = (await getLastKnownSavedLocation()) ?? coords;
    if (!last) {
      setStatus('No saved location yet — enable location once to cache it.');
      return;
    }
    cameraRef.current?.setCamera?.({
      centerCoordinate: [last.longitude, last.latitude],
      zoomLevel: 15,
      animationDuration: 500,
    });
    await searchAt(last, 'last location');
  };

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

        <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 }]}>
        <TouchableOpacity style={styles.pillPrimary} 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={searchLastKnown}>
          <Text style={styles.pillText}>Last</Text>
        </TouchableOpacity>
      </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',
    flexDirection: 'row',
    gap: 8,
  },
  pill: {
    backgroundColor: '#444',
    paddingHorizontal: 14,
    paddingVertical: 10,
    borderRadius: 22,
  },
  pillPrimary: {
    backgroundColor: '#1e6f5c',
    paddingHorizontal: 16,
    paddingVertical: 10,
    borderRadius: 22,
  },
  pillText: { color: '#fff', fontWeight: '600' },
});
