// Geocode an address with precision-aware fallback chain:
//   1. Mapbox Geocoding v6 (rooftop, very precise — primary)
//   2. US Census Geocoder (rooftop interpolation, US-only fallback)
//   3. Nominatim (OSM) — only accept if result includes a house_number
//   4. Photon (Komoot OSM) — only accept if housenumber present
// Caches results in public.address_geocodes so subsequent lookups are instant.

import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0";

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers":
    "authorization, x-client-info, apikey, content-type, x-supabase-client-platform, x-supabase-client-platform-version, x-supabase-client-runtime, x-supabase-client-runtime-version",
  "Access-Control-Allow-Methods": "POST, OPTIONS",
};

function normalize(addr: string): string {
  return addr
    .toLowerCase()
    .replace(/\s+/g, " ")
    .replace(/[.,#]/g, "")
    .trim();
}

interface GeoResult {
  lat: number;
  lng: number;
  display_name: string;
  source: string;
  precision: "rooftop" | "interpolated" | "street" | "approximate";
}

// Mapbox Geocoding API v6 — highest precision; address-level results include
// match_code data and a precise rooftop point.
async function viaMapbox(
  address: string,
  options?: { context?: string | null },
): Promise<GeoResult | null> {
  const token =
    Deno.env.get("MAPBOX_SECRET_TOKEN") || Deno.env.get("MAPBOX_PUBLIC_TOKEN");
  if (!token) return null;
  const query = options?.context?.trim()
    ? `${address}, ${options.context.trim()}`
    : address;
  const params = new URLSearchParams({
    q: query,
    limit: "1",
    autocomplete: "false",
    types: "address,street",
    access_token: token,
  });
  const url = `https://api.mapbox.com/search/geocode/v6/forward?${params.toString()}`;
  try {
    const r = await fetch(url, { headers: { "User-Agent": "lovable-geocode/1.0" } });
    if (!r.ok) return null;
    const data = await r.json();
    const feat = data?.features?.[0];
    if (!feat) return null;
    const [lng, lat] = feat.geometry?.coordinates || [];
    if (typeof lat !== "number" || typeof lng !== "number") return null;
    const props = feat.properties || {};
    const featureType: string = props.feature_type || "";
    // address = rooftop, street = street centerline, place/locality = city, etc.
    let precision: GeoResult["precision"] = "approximate";
    if (featureType === "address") precision = "rooftop";
    else if (featureType === "street") precision = "street";
    else if (featureType === "block" || featureType === "secondary_address")
      precision = "interpolated";
    return {
      lat,
      lng,
      display_name: props.full_address || props.name || address,
      source: "mapbox",
      precision,
    };
  } catch (_e) {
    return null;
  }
}

// Census returns interpolated rooftop coordinates from TIGER address ranges.
async function viaCensus(address: string): Promise<GeoResult | null> {
  const url = `https://geocoding.geo.census.gov/geocoder/locations/onelineaddress?address=${encodeURIComponent(
    address,
  )}&benchmark=Public_AR_Current&format=json`;
  try {
    const r = await fetch(url, { headers: { "User-Agent": "lovable-geocode/1.0" } });
    if (!r.ok) return null;
    const data = await r.json();
    const match = data?.result?.addressMatches?.[0];
    if (!match?.coordinates) return null;
    return {
      lat: match.coordinates.y,
      lng: match.coordinates.x,
      display_name: match.matchedAddress || address,
      source: "census",
      precision: "interpolated",
    };
  } catch (_e) {
    return null;
  }
}

async function viaNominatim(address: string): Promise<GeoResult | null> {
  const url = `https://nominatim.openstreetmap.org/search?format=json&addressdetails=1&limit=5&q=${encodeURIComponent(
    address,
  )}`;
  try {
    const r = await fetch(url, {
      headers: {
        "User-Agent": "lovable-geocode/1.0 (contact: support@avria.cloud)",
        "Accept-Language": "en",
      },
    });
    if (!r.ok) return null;
    const data = await r.json();
    if (!Array.isArray(data) || !data.length) return null;
    const withHouse = data.find(
      (d: any) => d?.address?.house_number || d?.class === "building",
    );
    const pick = withHouse || data[0];
    const precision: GeoResult["precision"] = withHouse
      ? "rooftop"
      : pick?.type === "residential" || pick?.class === "highway"
        ? "street"
        : "approximate";
    return {
      lat: parseFloat(pick.lat),
      lng: parseFloat(pick.lon),
      display_name: pick.display_name,
      source: "nominatim",
      precision,
    };
  } catch (_e) {
    return null;
  }
}

async function viaPhoton(address: string): Promise<GeoResult | null> {
  const url = `https://photon.komoot.io/api/?limit=5&q=${encodeURIComponent(address)}`;
  try {
    const r = await fetch(url, { headers: { "User-Agent": "lovable-geocode/1.0" } });
    if (!r.ok) return null;
    const data = await r.json();
    const features: any[] = data?.features || [];
    if (!features.length) return null;
    const withHouse = features.find((f) => f?.properties?.housenumber);
    const pick = withHouse || features[0];
    const [lng, lat] = pick.geometry?.coordinates || [];
    if (typeof lat !== "number" || typeof lng !== "number") return null;
    const p = pick.properties || {};
    const display = [
      p.housenumber && p.street ? `${p.housenumber} ${p.street}` : p.street || p.name,
      p.city || p.town || p.village,
      p.state,
      p.postcode,
    ]
      .filter(Boolean)
      .join(", ");
    const precision: GeoResult["precision"] = withHouse
      ? "rooftop"
      : p.osm_key === "highway"
        ? "street"
        : "approximate";
    return {
      lat,
      lng,
      display_name: display || address,
      source: "photon",
      precision,
    };
  } catch (_e) {
    return null;
  }
}

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });

  try {
    const body = await req.json().catch(() => ({}));
    const address: string | undefined = body?.address;
    const context = typeof body?.context === "string" ? body.context.trim() : "";
    const forceRefresh: boolean = body?.refresh === true;
    if (!address || typeof address !== "string" || address.trim().length < 3) {
      return new Response(JSON.stringify({ error: "Missing address" }), {
        status: 400,
        headers: { ...corsHeaders, "Content-Type": "application/json" },
      });
    }
    const cleaned = address.trim();
    const effectiveQuery = context ? `${cleaned}, ${context}` : cleaned;
    const key = normalize(effectiveQuery);

    const supabase = createClient(
      Deno.env.get("SUPABASE_URL")!,
      Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
    );

    if (!forceRefresh) {
      const { data: cached } = await supabase
        .from("address_geocodes")
        .select("lat,lng,display_name,source,not_found")
        .eq("address_key", key)
        .maybeSingle();

      if (cached) {
        // Skip cache entries from older non-Mapbox sources so they re-resolve
        // through the more precise Mapbox provider.
        const sourceStr = String(cached.source || "");
        const isMapboxCache = sourceStr.startsWith("mapbox");
        if (isMapboxCache || cached.not_found) {
          if (cached.not_found || cached.lat == null) {
            return new Response(JSON.stringify({ result: null, cached: true }), {
              headers: { ...corsHeaders, "Content-Type": "application/json" },
            });
          }
          return new Response(
            JSON.stringify({
              result: {
                lat: cached.lat,
                lng: cached.lng,
                display_name: cached.display_name,
                source: cached.source,
              },
              cached: true,
            }),
            { headers: { ...corsHeaders, "Content-Type": "application/json" } },
          );
        }
      }
    }

    // Provider chain — Mapbox first (most precise), then free fallbacks.
    const mapbox = await viaMapbox(cleaned, { context });
    let result: GeoResult | null = mapbox;

    if (!result || result.precision === "approximate") {
      const census = await viaCensus(effectiveQuery);
      if (census && (!result || census.precision === "interpolated")) result = census;
    }
    if (!result || result.precision === "approximate") {
      const nom = await viaNominatim(effectiveQuery);
      if (nom && (nom.precision === "rooftop" || !result)) result = nom;
    }
    if (!result || (result.precision !== "rooftop" && result.precision !== "interpolated")) {
      const photon = await viaPhoton(effectiveQuery);
      if (photon && (photon.precision === "rooftop" || !result)) result = photon;
    }

    await supabase
      .from("address_geocodes")
      .upsert(
        {
          address_key: key,
          address: cleaned,
          lat: result?.lat ?? null,
          lng: result?.lng ?? null,
          display_name: result?.display_name ?? null,
          source: result?.source
            ? `${result.source}:${result.precision}`
            : null,
          not_found: !result,
        },
        { onConflict: "address_key" },
      );

    return new Response(JSON.stringify({ result, cached: false }), {
      headers: { ...corsHeaders, "Content-Type": "application/json" },
    });
  } catch (error: unknown) {
    const msg = error instanceof Error ? error.message : "Unknown error";
    return new Response(JSON.stringify({ error: msg }), {
      status: 500,
      headers: { ...corsHeaders, "Content-Type": "application/json" },
    });
  }
});
