/**
 * S3. Explore (home) — MAPMAP-NAV-UI-SPEC.md.
 *
 * Full-screen offline map. Integration pass (see FINDINGS.md): the
 * territory's MapLibre style is now fetched once per active territory by
 * App.tsx's MainScreen and threaded down as `styleJson`, so this screen no
 * longer calls `getTerritoryStyle` itself — it's a pure function of props
 * again, matching S6/S7's pattern.
 */
import React, { useCallback, useMemo, useRef, useState } from "react";
import {
  Linking,
  NativeSyntheticEvent,
  Pressable,
  StyleSheet,
  Text,
  View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Svg, { Circle, Line, Path, SvgXml } from "react-native-svg";
import { MAPMAP_LOCKUP_ASPECT, MAPMAP_LOCKUP_SVG } from "../../brand/logo";
import {
  Camera,
  GeoJSONSource,
  Layer,
  Map as MapLibreMap,
  Marker,
  UserLocation,
} from "@maplibre/maplibre-react-native";
import type {
  CameraRef,
  MapRef,
  PressEvent,
  PressEventWithFeatures,
} from "@maplibre/maplibre-react-native";
import { Chip, FABStack, SearchPill } from "../../components";
import type { FABAction } from "../../components";
import { radius, spacing, theme, typeScale } from "../../theme";
import type { MapTheme, SelectedPlace } from "../../state/types";
import type { LatLng } from "../../../modules/mapmap-react-native/src/MapmapNav.types";
import { DestinationPin } from "../mapChrome";
import { LayersSheet } from "./LayersSheet";
import {
  POI_TAP_LAYERS,
  droppedPin,
  featureName,
  pickPlaceFromFeatures,
  tapToleranceBox,
} from "../../map/tapSelection";

export type ExploreScreenProps = {
  /** e.g. "UK · offline" for the territory badge chip. */
  offlineTerritoryLabel: string;
  isFollowingUser: boolean;
  hasLocationPermission: boolean;
  /** Resolved territory style JSON (App.tsx fetches once per active
   * territory); null before it resolves or if the fetch failed — the map
   * falls back to a background-only placeholder either way. */
  styleJson?: string | null;
  /** A place selected via search/tap — shown as a pin on the map. */
  selectedPlace?: SelectedPlace | null;
  /** Camera pitches for 3D buildings when on. */
  show3dBuildings: boolean;
  showPois: boolean;
  mapTheme: MapTheme;
  mapSource: "online" | "offline";
  onSearchPress: () => void;
  onSettingsPress: () => void;
  onLocateMePress: () => void;
  onTerritoryBadgePress: () => void;
  /** Tapping anywhere on the map drops a pin and routes-there flow. */
  onPlaceSelectedOnMap?: (place: SelectedPlace) => void;
  onLayerChange: (
    patch: Partial<{
      show3dBuildings: boolean;
      showPois: boolean;
      mapTheme: MapTheme;
    }>
  ) => void;
  onMapSourceChange: (source: "online" | "offline") => void;
  /** Reachable-area isochrone from a centre — "where can I get to in N
   * minutes?". Returns GeoJSON contour polygons (null on failure). */
  onReachableArea?: (center: LatLng) => Promise<GeoJSON.FeatureCollection | null>;
  /** Attribution tap → About/Licences (S11); falls back to the OSM
   * copyright URL when absent. */
  onShowAbout?: () => void;
};

/** London — a sane default centre before any territory style/location resolves. */
const DEFAULT_CENTER: [number, number] = [-0.1278, 51.5074];
// A district-level start keeps the first tile burst small enough for a
// metered key's rate limit (online maps); the user zooms in from there.
const DEFAULT_ZOOM = 11;
const TOP_BAR_HEIGHT = 56;
const OSM_COPYRIGHT_URL = "https://www.openstreetmap.org/copyright";

/** Background-only placeholder shown before a territory style has loaded. */
const FALLBACK_MAP_STYLE = JSON.stringify({
  version: 8,
  sources: {},
  layers: [
    {
      id: "background",
      type: "background",
      paint: { "background-color": theme.surfaceAlt },
    },
  ],
});

function GearIcon(): React.JSX.Element {
  return (
    <Svg width={18} height={18} viewBox="0 0 24 24">
      <Circle cx={12} cy={12} r={3} fill={theme.textSecondary} />
      <Path
        d="M12 2.5v3M12 18.5v3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M2.5 12h3M18.5 12h3M4.9 19.1 7 17M17 7l2.1-2.1"
        stroke={theme.textSecondary}
        strokeWidth={2}
        strokeLinecap="round"
      />
    </Svg>
  );
}

function LocateIcon({ active }: { active: boolean }): React.JSX.Element {
  const color = active ? theme.textOnSignal : theme.textPrimary;
  return (
    <Svg width={20} height={20} viewBox="0 0 24 24">
      <Circle cx={12} cy={12} r={3} fill={color} />
      <Circle cx={12} cy={12} r={7.5} stroke={color} strokeWidth={2} fill="none" />
      <Path
        d="M12 1.5v3M12 19.5v3M1.5 12h3M19.5 12h3"
        stroke={color}
        strokeWidth={2}
        strokeLinecap="round"
      />
    </Svg>
  );
}

function LayersIcon(): React.JSX.Element {
  return (
    <Svg width={20} height={20} viewBox="0 0 24 24">
      <Path
        d="M12 3 2.5 8 12 13l9.5-5L12 3Z"
        stroke={theme.textPrimary}
        strokeWidth={1.8}
        strokeLinejoin="round"
        fill="none"
      />
      <Path
        d="M2.5 13 12 18l9.5-5"
        stroke={theme.textPrimary}
        strokeWidth={1.8}
        strokeLinejoin="round"
        fill="none"
      />
    </Svg>
  );
}

function ZoomIcon({ sign }: { sign: "+" | "-" }): React.JSX.Element {
  return (
    <Svg width={22} height={22} viewBox="0 0 24 24">
      <Line x1={6} y1={12} x2={18} y2={12} stroke={theme.textPrimary} strokeWidth={2.4} strokeLinecap="round" />
      {sign === "+" && (
        <Line x1={12} y1={6} x2={12} y2={18} stroke={theme.textPrimary} strokeWidth={2.4} strokeLinecap="round" />
      )}
    </Svg>
  );
}

function ClockIcon({ active }: { active: boolean }): React.JSX.Element {
  const color = active ? theme.textOnSignal : theme.textPrimary;
  return (
    <Svg width={20} height={20} viewBox="0 0 24 24">
      <Circle cx={12} cy={12} r={8.5} stroke={color} strokeWidth={2} fill="none" />
      <Path d="M12 7.5V12l3 2" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" fill="none" />
    </Svg>
  );
}

export function ExploreScreen({
  offlineTerritoryLabel,
  isFollowingUser,
  hasLocationPermission,
  styleJson,
  selectedPlace,
  show3dBuildings,
  showPois,
  mapTheme,
  mapSource,
  onSearchPress,
  onSettingsPress,
  onLocateMePress,
  onTerritoryBadgePress,
  onPlaceSelectedOnMap,
  onLayerChange,
  onMapSourceChange,
  onReachableArea,
  onShowAbout,
}: ExploreScreenProps): React.JSX.Element {
  const insets = useSafeAreaInsets();
  const mapRef = useRef<MapRef>(null);
  const cameraRef = useRef<CameraRef>(null);
  const zoomRef = useRef(DEFAULT_ZOOM);
  const [layersOpen, setLayersOpen] = useState(false);
  const [isochrone, setIsochrone] = useState<GeoJSON.FeatureCollection | null>(null);

  // The outer (largest) contour's minute value drives the legend label, e.g.
  // "Reachable in 20 min" — `fetchIsochrone` tags every feature with the
  // contour minutes it was generated from (`mmMinutes`), so we don't need to
  // hardcode the caller's contour choice here.
  const reachableMinutes = useMemo(() => {
    if (!isochrone) return null;
    let max: number | null = null;
    for (const feature of isochrone.features) {
      const minutes = feature.properties?.mmMinutes;
      if (typeof minutes === "number" && (max === null || minutes > max)) {
        max = minutes;
      }
    }
    return max;
  }, [isochrone]);

  const toggleReachable = useCallback(async () => {
    if (isochrone) {
      setIsochrone(null);
      return;
    }
    if (!onReachableArea || !mapRef.current) return;
    try {
      const c = await mapRef.current.getCenter();
      const fc = await onReachableArea({ latitude: c[1], longitude: c[0] });
      setIsochrone(fc);
    } catch {
      setIsochrone(null);
    }
  }, [isochrone, onReachableArea]);

  // Tap anywhere → drop a pin and start the route-there flow. Resolving the
  // tap to a POI is finger-tolerant: we query a small box (not a single
  // pixel — a fingertip never lands on a 3.5px dot) and prefer the POI
  // dot/label layers so a real POI wins over an incidental road or polygon,
  // then fall back to whatever else is under the finger, then a plain
  // "Dropped pin" (reverse geocoding isn't available — FINDINGS: /geocode
  // is 501).
  const handleMapPress = useCallback(
    (
      event:
        | NativeSyntheticEvent<PressEvent>
        | NativeSyntheticEvent<PressEventWithFeatures>
    ) => {
      if (!onPlaceSelectedOnMap) return;
      const { lngLat, point } = event.nativeEvent;
      const position = { latitude: lngLat[1], longitude: lngLat[0] };
      const inline = (event.nativeEvent as PressEventWithFeatures).features;
      const map = mapRef.current;
      const box = tapToleranceBox(point);

      const resolveFeatures = async (): Promise<GeoJSON.Feature[]> => {
        if (inline && inline.length > 0) return inline;
        if (!map) return [];
        // POI layers first — the dots/labels the user is aiming at.
        const poi = await map
          .queryRenderedFeatures(box, { layers: [...POI_TAP_LAYERS] })
          .catch(() => [] as GeoJSON.Feature[]);
        if (poi.some((feature) => featureName(feature) !== null)) return poi;
        // Otherwise, anything under the finger (a named building, place…).
        return map
          .queryRenderedFeatures(box)
          .catch(() => [] as GeoJSON.Feature[]);
      };

      resolveFeatures()
        .then((features) =>
          onPlaceSelectedOnMap(pickPlaceFromFeatures(features, position))
        )
        .catch(() => onPlaceSelectedOnMap(droppedPin(position)));
    },
    [onPlaceSelectedOnMap]
  );

  const handleAttributionPress = useCallback(() => {
    if (onShowAbout) {
      onShowAbout();
    } else {
      void Linking.openURL(OSM_COPYRIGHT_URL);
    }
  }, [onShowAbout]);

  // Zoom relative to the map's *actual* current zoom, not a stale local
  // counter — otherwise a pinch-zoom leaves the +/- buttons jumping back to
  // wherever they last set it.
  const zoomBy = useCallback(async (delta: number) => {
    const current = (await mapRef.current?.getZoom().catch(() => undefined)) ?? zoomRef.current;
    const next = Math.max(2, Math.min(20, current + delta));
    zoomRef.current = next;
    cameraRef.current?.zoomTo(next, { duration: 250 });
  }, []);

  const fabActions: FABAction[] = useMemo(
    () => [
      {
        key: "reachable",
        icon: <ClockIcon active={!!isochrone} />,
        onPress: () => void toggleReachable(),
        active: !!isochrone,
        accessibilityLabel: "Reachable area (drive-time)",
        testID: "fab-reachable",
      },
      {
        key: "layers",
        icon: <LayersIcon />,
        onPress: () => setLayersOpen(true),
        accessibilityLabel: "Map layers",
        testID: "fab-layers",
      },
      {
        key: "zoom-in",
        icon: <ZoomIcon sign="+" />,
        onPress: () => zoomBy(1),
        accessibilityLabel: "Zoom in",
        testID: "fab-zoom-in",
      },
      {
        key: "zoom-out",
        icon: <ZoomIcon sign="-" />,
        onPress: () => zoomBy(-1),
        accessibilityLabel: "Zoom out",
        testID: "fab-zoom-out",
      },
      {
        key: "locate-me",
        icon: <LocateIcon active={isFollowingUser} />,
        onPress: onLocateMePress,
        active: isFollowingUser,
        accessibilityLabel: isFollowingUser
          ? "Stop following your location"
          : "Follow your location",
        testID: "fab-locate-me",
      },
    ],
    [isFollowingUser, onLocateMePress, zoomBy, isochrone, toggleReachable]
  );

  const trackUserLocation =
    hasLocationPermission && isFollowingUser ? "heading" : undefined;

  return (
    <View style={styles.container} testID="explore-screen">
      <MapLibreMap
        ref={mapRef}
        style={StyleSheet.absoluteFill}
        mapStyle={styleJson ?? FALLBACK_MAP_STYLE}
        compass
        compassHiddenFacingNorth
        compassPosition={{ top: insets.top + TOP_BAR_HEIGHT + spacing[3], right: spacing[4] }}
        touchPitch
        logo={false}
        attribution={false}
        onPress={handleMapPress}
      >
        <Camera
          ref={cameraRef}
          initialViewState={{ center: DEFAULT_CENTER, zoom: DEFAULT_ZOOM }}
          // Controlled (not just initial) pitch, driven by the 3D toggle.
          // touchPitch lets users pinch-tilt freely, but tilting back down
          // with two/three fingers is finicky on some devices/MapLibre
          // versions — this gives a dependable way back to flat: turning
          // "3D buildings" off in the Layers sheet snaps pitch to 0, and
          // turning it on restores the ~50° tilt.
          pitch={show3dBuildings ? 50 : 0}
          duration={400}
          trackUserLocation={trackUserLocation}
        />
        {isochrone && (
          <GeoJSONSource id="reachable" data={isochrone}>
            <Layer
              id="reachable-fill"
              type="fill"
              paint={{ "fill-color": ["get", "mmColor"], "fill-opacity": 0.18 }}
            />
            <Layer
              id="reachable-line"
              type="line"
              paint={{ "line-color": ["get", "mmColor"], "line-width": 2, "line-opacity": 0.7 }}
            />
          </GeoJSONSource>
        )}
        {hasLocationPermission && <UserLocation heading accuracy animated />}
        {selectedPlace && (
          <Marker
            lngLat={[selectedPlace.position.longitude, selectedPlace.position.latitude]}
            anchor="bottom"
          >
            <DestinationPin />
          </Marker>
        )}
      </MapLibreMap>

      <View style={[styles.topBar, { top: insets.top + spacing[3] }]} pointerEvents="box-none">
        <SearchPill
          onPress={onSearchPress}
          onSettingsPress={onSettingsPress}
          settingsIcon={<GearIcon />}
          testID="explore-search-pill"
        />
        <Chip
          label={offlineTerritoryLabel}
          onPress={onTerritoryBadgePress}
          testID="explore-territory-badge"
        />
      </View>

      {/* Column that grows upward from the FAB stack's anchor point, so the
          legend sits directly above the clock FAB without needing to know
          the stack's rendered height (and without risking overlap with the
          top bar/compass). */}
      <View
        style={[
          styles.fabContainer,
          { right: spacing[4], bottom: insets.bottom + spacing[10] },
        ]}
      >
        {isochrone && (
          <View pointerEvents="none" style={styles.reachableLegend}>
            <Chip
              label={
                reachableMinutes !== null
                  ? `Reachable in ${reachableMinutes} min`
                  : "Reachable area (drive-time)"
              }
              tone="signal"
              testID="reachable-legend"
            />
          </View>
        )}
        <FABStack actions={fabActions} testID="explore-fab-stack" />
      </View>

      <LayersSheet
        visible={layersOpen}
        onClose={() => setLayersOpen(false)}
        show3dBuildings={show3dBuildings}
        showPois={showPois}
        mapTheme={mapTheme}
        mapSource={mapSource}
        onChange={onLayerChange}
        onMapSourceChange={onMapSourceChange}
      />

      <View
        style={[styles.attributionRow, { bottom: insets.bottom + spacing[2] }]}
        pointerEvents="box-none"
      >
        <View style={styles.watermarkPill}>
          <SvgXml
            xml={MAPMAP_LOCKUP_SVG}
            width={78}
            height={78 / MAPMAP_LOCKUP_ASPECT}
          />
        </View>
        <Pressable
          onPress={handleAttributionPress}
          hitSlop={8}
          style={styles.attributionPill}
          accessibilityRole="link"
          accessibilityLabel="OpenStreetMap contributors — licence information"
        >
          <Text style={styles.attributionText}>© OpenStreetMap contributors</Text>
        </Pressable>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: theme.surfaceAlt,
  },
  topBar: {
    position: "absolute",
    left: spacing[4],
    right: spacing[4],
    gap: spacing[2],
  },
  fabContainer: {
    position: "absolute",
    alignItems: "flex-end",
  },
  reachableLegend: {
    marginBottom: spacing[3],
  },
  attributionRow: {
    position: "absolute",
    left: spacing[4],
    flexDirection: "row",
    alignItems: "center",
    gap: spacing[2],
  },
  watermarkPill: {
    backgroundColor: theme.surface,
    borderRadius: radius.sm,
    paddingHorizontal: spacing[2],
    paddingVertical: 2,
    opacity: 0.92,
  },
  watermarkText: {
    fontSize: typeScale.caption.fontSize,
    fontWeight: "700",
    color: theme.textPrimary,
  },
  attributionPill: {
    backgroundColor: theme.surface,
    borderRadius: radius.sm,
    paddingHorizontal: spacing[2],
    paddingVertical: 2,
    opacity: 0.92,
  },
  attributionText: {
    fontSize: 10,
    color: theme.textSecondary,
  },
});
