// Shared Leaflet helpers for map rendering and geocoding.
// Tiles: Mapbox satellite-streets (sharper imagery + built-in labels).
// Geocoding: routed through the `geocode` edge function (Mapbox + fallbacks).
// A pre-fetched Mapbox public token is used; Esri is the fallback if the token
// is unavailable.

import L from "leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet.markercluster";
import "leaflet.markercluster/dist/MarkerCluster.css";
import "leaflet.markercluster/dist/MarkerCluster.Default.css";
import { supabase } from "@/integrations/supabase/client";

// Fix default marker icon paths (Vite/webpack break Leaflet's default icon resolution)
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
  iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png",
  iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png",
  shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
});

// ── Mapbox token resolution ──────────────────────────────────────────────
let mapboxTokenPromise: Promise<string | null> | null = null;
let mapboxTokenCached: string | null = null;

export async function getMapboxToken(): Promise<string | null> {
  if (mapboxTokenCached) return mapboxTokenCached;
  if (!mapboxTokenPromise) {
    mapboxTokenPromise = (async () => {
      try {
        const { data, error } = await supabase.functions.invoke("get-mapbox-token");
        if (error || !data?.token) return null;
        mapboxTokenCached = data.token as string;
        return mapboxTokenCached;
      } catch {
        return null;
      }
    })();
  }
  return mapboxTokenPromise;
}

// Fire and cache eagerly so map components find a ready token.
void getMapboxToken();

// Tiles. Use Mapbox if we have a token; otherwise Esri (no key needed).
function mapboxRasterUrl(styleId: string, token: string) {
  // Mapbox raster tiles for any vector style id. @2x for retina sharpness.
  return `https://api.mapbox.com/styles/v1/mapbox/${styleId}/tiles/256/{z}/{x}/{y}@2x?access_token=${token}`;
}

export const TILES = {
  street: {
    url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
    attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
    maxZoom: 19,
  },
  satellite: {
    url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
    attribution: "Tiles © Esri — Source: Esri, Maxar, Earthstar Geographics",
    maxZoom: 19,
  },
  labels: {
    url: "https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}",
    attribution: "",
    maxZoom: 19,
  },
};

// Add a Mapbox satellite-streets raster layer (sharp imagery + street labels).
// Falls back to Esri imagery + labels when no Mapbox token is configured.
export async function addMapboxSatelliteLayer(map: L.Map): Promise<L.Layer[]> {
  const token = await getMapboxToken();
  const layers: L.Layer[] = [];
  if (token) {
    const sat = L.tileLayer(mapboxRasterUrl("satellite-streets-v12", token), {
      attribution: '© <a href="https://www.mapbox.com/about/maps/">Mapbox</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
      maxZoom: 22,
      tileSize: 256,
    }).addTo(map);
    layers.push(sat);
  } else {
    const sat = L.tileLayer(TILES.satellite.url, {
      attribution: TILES.satellite.attribution,
      maxZoom: TILES.satellite.maxZoom,
    }).addTo(map);
    const lbl = L.tileLayer(TILES.labels.url, {
      attribution: TILES.labels.attribution,
      maxZoom: TILES.labels.maxZoom,
      opacity: 0.95,
    }).addTo(map);
    layers.push(sat, lbl);
  }
  return layers;
}

// Add a hybrid satellite layer to the given map. Uses the custom Mapbox
// Standard vector style (`avriacam/cmbi3rqgg002u01s90lm27v12`) when a token is
// available — rendered via mapbox-gl through the mapbox-gl-leaflet bridge.
// Falls back to Esri imagery + Esri labels when no token is configured.
export async function addHybridSatelliteLayer(map: L.Map): Promise<L.Layer[]> {
  const token = await getMapboxToken();
  const layers: L.Layer[] = [];
  if (token) {
    // Dynamic imports keep mapbox-gl out of the main bundle until needed.
    const [{ default: mapboxgl }, glLeaflet] = await Promise.all([
      import("mapbox-gl"),
      import("mapbox-gl-leaflet"),
      import("mapbox-gl/dist/mapbox-gl.css"),
    ]);
    (mapboxgl as any).accessToken = token;
    const L_any = L as any;
    const glLayer = L_any.mapboxGL({
      accessToken: token,
      style: "mapbox://styles/avriacam/cmbi3rqgg002u01s90lm27v12",
      attribution: '© <a href="https://www.mapbox.com/about/maps/">Mapbox</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
    }).addTo(map);
    layers.push(glLayer);
    void glLeaflet;
  } else {
    const sat = L.tileLayer(TILES.satellite.url, {
      attribution: TILES.satellite.attribution,
      maxZoom: TILES.satellite.maxZoom,
    }).addTo(map);
    const lbl = L.tileLayer(TILES.labels.url, {
      attribution: TILES.labels.attribution,
      maxZoom: TILES.labels.maxZoom,
      opacity: 0.95,
    }).addTo(map);
    layers.push(sat, lbl);
  }
  return layers;
}


export type LatLng = { lat: number; lng: number };

export interface GeocodeResult {
  lat: number;
  lng: number;
  display_name: string;
}

export interface GeocodeOptions {
  refresh?: boolean;
  context?: string;
}

// Cache geocoding results in memory + localStorage. Bumped to v4 so older
// bare-address matches are re-resolved with association locality hints.
const GEOCODE_KEY = "leaflet_geocode_cache_v4";
const REVERSE_KEY = "leaflet_reverse_cache_v2";

function readCache(key: string): Record<string, any> {
  try { return JSON.parse(localStorage.getItem(key) || "{}"); } catch { return {}; }
}
function writeCache(key: string, data: Record<string, any>) {
  try { localStorage.setItem(key, JSON.stringify(data)); } catch { /* ignore */ }
}

export async function geocodeAddress(
  address: string,
  opts?: GeocodeOptions,
): Promise<GeocodeResult | null> {
  if (!address?.trim()) return null;
  const cacheKey = [
    address.trim().toLowerCase(),
    opts?.context?.trim().toLowerCase() ? `ctx:${opts.context.trim().toLowerCase()}` : null,
  ]
    .filter(Boolean)
    .join("|");
  const cache = readCache(GEOCODE_KEY);
  if (!opts?.refresh && cache[cacheKey] !== undefined) return cache[cacheKey];

  try {
    const { data, error } = await supabase.functions.invoke("geocode", {
      body: {
        address,
        refresh: opts?.refresh === true,
        context: opts?.context?.trim() || null,
      },
    });
    if (error) {
      console.warn("[geocode] edge function error", error);
      return null;
    }
    const r = data?.result;
    if (!r || typeof r.lat !== "number" || typeof r.lng !== "number") {
      cache[cacheKey] = null as any;
      writeCache(GEOCODE_KEY, cache);
      return null;
    }
    const result: GeocodeResult = {
      lat: r.lat,
      lng: r.lng,
      display_name: r.display_name || address,
    };
    cache[cacheKey] = result;
    writeCache(GEOCODE_KEY, cache);
    return result;
  } catch (e) {
    console.warn("[geocode] failed", e);
    return null;
  }
}

export async function reverseGeocode(lat: number, lng: number): Promise<string | null> {
  const key = `${lat.toFixed(5)},${lng.toFixed(5)}`;
  const cache = readCache(REVERSE_KEY);
  if (cache[key]) return cache[key];

  // Prefer Mapbox reverse geocoding when a token is available — much more
  // precise than OSM's Nominatim.
  const token = await getMapboxToken();
  try {
    if (token) {
      const url = `https://api.mapbox.com/search/geocode/v6/reverse?longitude=${lng}&latitude=${lat}&limit=1&access_token=${token}`;
      const res = await fetch(url);
      if (res.ok) {
        const data = await res.json();
        const feat = data?.features?.[0];
        const display = feat?.properties?.full_address || feat?.properties?.name || null;
        if (display) {
          cache[key] = display;
          writeCache(REVERSE_KEY, cache);
          return display;
        }
      }
    }
    // Nominatim fallback
    const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`;
    const res = await fetch(url, { headers: { "Accept-Language": "en" } });
    if (!res.ok) return null;
    const data = await res.json();
    const display = data?.display_name || null;
    if (display) {
      cache[key] = display;
      writeCache(REVERSE_KEY, cache);
    }
    return display;
  } catch {
    return null;
  }
}

// Build a circular DivIcon (used in place of Google's SymbolPath.CIRCLE markers)
export function circleDivIcon(opts: {
  color: string;
  size?: number;
  label?: string;
  textColor?: string;
  fontSize?: string;
  selected?: boolean;
}): L.DivIcon {
  const size = opts.size ?? 24;
  const stroke = opts.selected ? 3 : 2;
  const html = `
    <div style="
      width:${size}px;height:${size}px;border-radius:50%;
      background:${opts.color};border:${stroke}px solid #fff;
      box-shadow:0 1px 4px rgba(0,0,0,.3);
      display:flex;align-items:center;justify-content:center;
      color:${opts.textColor || "#fff"};font-weight:700;
      font-size:${opts.fontSize || "10px"};font-family:Inter,system-ui,sans-serif;
      line-height:1;">
      ${opts.label ?? ""}
    </div>`;
  return L.divIcon({
    className: "lf-circle-icon",
    html,
    iconSize: [size, size],
    iconAnchor: [size / 2, size / 2],
  });
}

// Returns a static satellite image URL. Prefers Mapbox Static Images API when
// a token is cached (ultra-sharp); falls back to Esri's export endpoint.
export function staticSatelliteUrl(lat: number, lng: number, width = 320, height = 240, zoom = 18): string {
  if (mapboxTokenCached) {
    // Mapbox Static Images API. Caps: width/height ≤ 1280, zoom 0–22.
    const w = Math.min(Math.round(width), 1280);
    const h = Math.min(Math.round(height), 1280);
    const z = Math.max(0, Math.min(22, Math.round(zoom)));
    return `https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v12/static/${lng},${lat},${z},0/${w}x${h}@2x?access_token=${mapboxTokenCached}`;
  }
  // Esri fallback — bbox export.
  const scale = 156543.03392 * Math.cos((lat * Math.PI) / 180) / Math.pow(2, zoom);
  const halfW = (width / 2) * scale;
  const halfH = (height / 2) * scale;
  const metersToLat = halfH / 111320;
  const metersToLng = halfW / (111320 * Math.cos((lat * Math.PI) / 180));
  const minLng = lng - metersToLng;
  const maxLng = lng + metersToLng;
  const minLat = lat - metersToLat;
  const maxLat = lat + metersToLat;
  return `https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/export?bbox=${minLng},${minLat},${maxLng},${maxLat}&bboxSR=4326&imageSR=3857&size=${width},${height}&format=jpg&f=image`;
}

export { L };
