/**
 * Postal-code search — country-aware dispatcher over the UK shim.
 *
 * The gap (see ukPostcode.ts): MapMap `search()` can't resolve postal codes.
 * The UK is handled richly by postcodes.io; every other supported country goes
 * through **Zippopotam.us** (free, no key, ~60 countries). Which country to
 * query is driven by the active territory, because postal formats collide
 * across borders (US/DE/FR/ES/IT are all five digits) and can only be
 * disambiguated by context.
 *
 * Still a stopgap: MapMap search should resolve postal codes natively and
 * offline. It also means territories ought to expose an ISO country code —
 * today we infer it from the territory id/name (see PLATFORM-IMPROVEMENTS.md).
 */
import type { LatLng, SearchResult } from "../../../modules/mapmap-react-native/src/MapmapNav.types";
import { distanceMeters } from "../../../modules/mapmap-react-native/src/geo";
import { resolveUkPostcode } from "./ukPostcode";

export type CountryCode =
  | "GB" | "IE" | "US" | "CA" | "DE" | "FR" | "ES" | "IT" | "NL" | "BE" | "AU" | "NZ";

/** Territory id/name (lower-cased) -> ISO 3166-1 alpha-2. Covers the mock's
 * territories plus the obvious names/codes a real catalogue would use. */
const TERRITORY_COUNTRY: Record<string, CountryCode> = {
  uk: "GB", gb: "GB", britain: "GB", "great-britain": "GB", "united-kingdom": "GB", england: "GB",
  ireland: "IE", ie: "IE",
  us: "US", usa: "US", "united-states": "US",
  ca: "CA", canada: "CA",
  de: "DE", germany: "DE", deutschland: "DE",
  fr: "FR", france: "FR",
  es: "ES", spain: "ES",
  it: "IT", italy: "IT",
  nl: "NL", netherlands: "NL",
  be: "BE", belgium: "BE",
  au: "AU", australia: "AU",
  nz: "NZ", "new-zealand": "NZ",
};

const KNOWN: ReadonlySet<string> = new Set<CountryCode>([
  "GB", "IE", "US", "CA", "DE", "FR", "ES", "IT", "NL", "BE", "AU", "NZ",
]);

/** Best-effort country for the active territory. Falls back to GB (the app's
 * primary, UK-first market) when there's no offline territory or the id is
 * unrecognised — e.g. when running on online tiles. */
export function countryForTerritory(territoryId: string | null | undefined): CountryCode {
  if (!territoryId) return "GB";
  const key = territoryId.toLowerCase();
  if (TERRITORY_COUNTRY[key]) return TERRITORY_COUNTRY[key];
  const upper = territoryId.toUpperCase();
  return KNOWN.has(upper) ? (upper as CountryCode) : "GB";
}

/** Per-country Zippopotam config: how to recognise a postal code, the API token
 * to look it up by, and how to present it. `compact` is upper-cased, spaceless. */
type ZippoSpec = {
  detect: RegExp;
  token: (compact: string) => string;
  format: (compact: string) => string;
};

const FIVE_DIGIT: ZippoSpec = {
  detect: /^\d{5}$/,
  token: (c) => c,
  format: (c) => c,
};

// GB is handled by postcodes.io; IE (Eircode) has no Zippopotam coverage, so it
// is intentionally absent and resolves to null.
const ZIPPO: Partial<Record<CountryCode, ZippoSpec>> = {
  US: { detect: /^\d{5}(\d{4})?$/, token: (c) => c.slice(0, 5), format: (c) => c.slice(0, 5) },
  DE: FIVE_DIGIT,
  FR: FIVE_DIGIT,
  ES: FIVE_DIGIT,
  IT: FIVE_DIGIT,
  BE: { detect: /^\d{4}$/, token: (c) => c, format: (c) => c },
  AU: { detect: /^\d{4}$/, token: (c) => c, format: (c) => c },
  NZ: { detect: /^\d{4}$/, token: (c) => c, format: (c) => c },
  NL: {
    detect: /^\d{4}[A-Z]{2}$/,
    token: (c) => c.slice(0, 4), // Zippopotam NL is keyed by the 4-digit part
    format: (c) => `${c.slice(0, 4)} ${c.slice(4)}`,
  },
  CA: {
    // Full ("M5V 3L9") or just the forward-sortation area ("M5V"); Zippopotam
    // CA is keyed by the 3-char FSA.
    detect: /^[A-Z]\d[A-Z](\d[A-Z]\d)?$/,
    token: (c) => c.slice(0, 3),
    format: (c) => c.slice(0, 3),
  },
};

type ZippoResponse = {
  places?: Array<{
    "place name"?: string;
    state?: string;
    latitude?: string;
    longitude?: string;
  }>;
};

async function resolveViaZippopotam(
  country: CountryCode,
  spec: ZippoSpec,
  query: string,
  near: LatLng | undefined,
  fetchImpl: typeof fetch
): Promise<SearchResult | null> {
  const compact = query.toUpperCase().replace(/\s+/g, "");
  if (!spec.detect.test(compact)) return null;
  try {
    const url = `https://api.zippopotam.us/${country.toLowerCase()}/${encodeURIComponent(spec.token(compact))}`;
    const res = await fetchImpl(url);
    if (!res.ok) return null;
    const body = (await res.json()) as ZippoResponse;
    const place = body.places?.[0];
    if (!place) return null;
    const latitude = Number(place.latitude);
    const longitude = Number(place.longitude);
    if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null;
    const position: LatLng = { latitude, longitude };
    const address = [place["place name"], place.state].filter((p): p is string => Boolean(p)).join(", ");
    return {
      name: spec.format(compact),
      address,
      position,
      ...(near ? { distanceMeters: distanceMeters(near, position) } : {}),
    };
  } catch {
    return null;
  }
}

/**
 * Resolve a postal-code-shaped query for the given country to a single
 * SearchResult, or null if it isn't a postal code / isn't supported / the
 * lookup fails. Never throws. `fetchImpl` is injectable for tests.
 */
export async function resolvePostalCode(
  query: string,
  country: CountryCode,
  near?: LatLng,
  fetchImpl: typeof fetch = fetch
): Promise<SearchResult | null> {
  if (country === "GB") return resolveUkPostcode(query, near, fetchImpl);
  const spec = ZIPPO[country];
  if (!spec) return null; // e.g. IE — no free coverage yet
  return resolveViaZippopotam(country, spec, query, near, fetchImpl);
}
