/**
 * UK postcode search — a client-side stopgap.
 *
 * MapMap's `search()` matches POIs and place names but does NOT resolve UK
 * postcodes (the hosted `/geocode` endpoint is still a 501). Postcode search is
 * table stakes for a UK nav app, so until the platform ships it natively we
 * detect postcode-shaped queries and resolve them against **postcodes.io** — a
 * free, no-key open-data API over ONS/Royal Mail data (ODbL). It handles both
 * full postcodes ("BN11 1AA") and partial outcodes ("BN11", "SW1A").
 *
 * This is complementary to MapMap search, not a replacement: the result is
 * merged in alongside POI hits. See PLATFORM-IMPROVEMENTS.md — when MapMap
 * search learns postcodes this whole module can be deleted.
 */
import type { LatLng, SearchResult } from "../../../modules/mapmap-react-native/src/MapmapNav.types";
import { distanceMeters } from "../../../modules/mapmap-react-native/src/geo";

const POSTCODES_IO = "https://api.postcodes.io";

// UK postcode grammar, split into outward + inward code. Matched against the
// query with all whitespace removed and upper-cased. Outward = area letters +
// district (e.g. "BN11", "SW1A", "M2"); inward = digit + two letters ("1AA").
const FULL_POSTCODE = /^([A-Z]{1,2}\d[A-Z\d]?)(\d[A-Z]{2})$/;
const OUTCODE = /^[A-Z]{1,2}\d[A-Z\d]?$/;

export type PostcodeKind = "full" | "outcode";

export type PostcodeMatch = {
  kind: PostcodeKind;
  /** API lookup token (no spaces): "BN111AA" or "BN11". */
  token: string;
  /** Human-facing, correctly spaced: "BN11 1AA" or "BN11". */
  formatted: string;
};

/** Normalise + classify a query as a full postcode, an outcode, or neither.
 * Pure — no network. Invalid-but-plausible codes still return a match here and
 * are rejected later by the API (a 404), which keeps this cheap and total. */
export function matchUkPostcode(query: string): PostcodeMatch | null {
  const compact = query.toUpperCase().replace(/\s+/g, "");
  const full = FULL_POSTCODE.exec(compact);
  if (full) {
    return { kind: "full", token: compact, formatted: `${full[1]} ${full[2]}` };
  }
  if (OUTCODE.test(compact)) {
    return { kind: "outcode", token: compact, formatted: compact };
  }
  return null;
}

/** Shape of the postcodes.io fields we read (both endpoints overlap). */
type PostcodesIoResult = {
  postcode?: string;
  outcode?: string;
  latitude?: number;
  longitude?: number;
  admin_ward?: string | string[];
  admin_district?: string | string[];
  region?: string;
  country?: string;
};

function firstOf(value: string | string[] | undefined): string | undefined {
  if (Array.isArray(value)) return value.find((v) => v && v.length > 0);
  return value && value.length > 0 ? value : undefined;
}

function toSearchResult(
  match: PostcodeMatch,
  result: PostcodesIoResult,
  near: LatLng | undefined
): SearchResult | null {
  if (typeof result.latitude !== "number" || typeof result.longitude !== "number") {
    return null;
  }
  const position: LatLng = { latitude: result.latitude, longitude: result.longitude };
  // Secondary line: locality context, coarser for an outcode (district only).
  const parts =
    match.kind === "full"
      ? [firstOf(result.admin_ward), firstOf(result.admin_district), result.region]
      : ["Postcode district", firstOf(result.admin_district), result.region];
  const address = parts.filter((p): p is string => Boolean(p)).join(", ");
  return {
    name: match.formatted,
    address,
    position,
    ...(near ? { distanceMeters: distanceMeters(near, position) } : {}),
  };
}

/**
 * Resolve a postcode-shaped query to a single SearchResult, or null if the
 * query isn't a postcode / the lookup fails / there's no network. Never throws.
 * `fetchImpl` is injectable for tests; defaults to the global fetch.
 */
export async function resolveUkPostcode(
  query: string,
  near?: LatLng,
  fetchImpl: typeof fetch = fetch
): Promise<SearchResult | null> {
  const match = matchUkPostcode(query);
  if (!match) return null;
  const path = match.kind === "full" ? "postcodes" : "outcodes";
  try {
    const res = await fetchImpl(`${POSTCODES_IO}/${path}/${encodeURIComponent(match.token)}`);
    if (!res.ok) return null; // 404 for a well-formed but nonexistent code
    const body = (await res.json()) as { result?: PostcodesIoResult | null };
    if (!body.result) return null;
    return toSearchResult(match, body.result, near);
  } catch {
    return null;
  }
}
