import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import Constants from 'expo-constants';
import {
  MapView,
  Camera,
  MarkerView,
  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 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) {
        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');
  };

  return (
    <View style={styles.container}>
      <MapView
        ref={mapRef}
        style={styles.map}
        mapStyle={mapStyle}
        rotateEnabled={false}
        onDidFinishLoadingMap={() => setMapReady(true)}
      >
        {/* Uncontrolled camera; we position it imperatively (once on load, then
            only on explicit user actions) so re-renders never move the map. */}
        <Camera ref={cameraRef} />

        {/* renderMode="normal" draws the dot in the JS layer and does NOT engage
            the native location component, which was tracking the camera back to
            the user on re-render. */}
        <UserLocation
          visible
          renderMode="normal"
          onUpdate={(loc: any) => {
            if (loc?.coords) {
              nativeFix.current = {
                latitude: loc.coords.latitude,
                longitude: loc.coords.longitude,
              };
            }
          }}
        />

        {zones.map((z) => (
          <MarkerView
            key={String(z.ZoneId ?? z.ScannerCode ?? z.TerminalSerNo)}
            coordinate={[z.Long as number, z.Lat as number]}
            anchor={{ x: 0.5, y: 1 }}
          >
            <TouchableOpacity
              onPress={() => navigation.navigate('MeterDetail', { zone: z })}
              activeOpacity={0.7}
              hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
            >
              <Ionicons
                name="location-sharp"
                size={34}
                color={normalizeColor(z.BackgroundColor) ?? '#1e6f5c'}
                style={styles.pinShadow}
              />
            </TouchableOpacity>
          </MarkerView>
        ))}
      </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 },
  // White halo so the colored pin stays visible on both light and dark tiles.
  pinShadow: {
    textShadowColor: 'rgba(255,255,255,0.9)',
    textShadowOffset: { width: 0, height: 0 },
    textShadowRadius: 3,
  },
  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' },
});
