/**
 * Pure transform from raw dictionary responses to consumer-facing shapes.
 *
 * Ports the rules from Angular's DictionariesService.handleLoading:
 * - drop cities whose title.ru is purely ASCII (garbage-data guard)
 * - drop airports without afl flights or with ASCII title.ru
 * - attach airports to their city (sorted by localized title)
 * - drop cities that end up with no afl airports
 * - enrich city.name, city.countryName from localized titles
 * - build lookup maps keyed by uppercase code
 * - partition cities into RU vs non-RU sets
 * - flatten regions (Russia first, Australia filtered out)
 */

import type {
  IAirport,
  ICity,
  ICountry,
  IDictionaries,
  IRegion,
  IRawAirport,
  IRawDictionaries,
  IRawRegion,
} from "./types.js";

const ASCII_ONLY = /^[a-zA-Z.,:; ]+$/;
const AUSTRALIA_REGION_ID = 500373;
const RUSSIA_REGION_ID = 500374;

export function transformDictionaries(
  raw: IRawDictionaries,
  lang: string,
): IDictionaries {
  const citiesAfterAsciiFilter = raw.cities.filter(
    (c) => !ASCII_ONLY.test(c.title["ru"] ?? ""),
  );

  const airportsFiltered = raw.airports.filter(
    (a) =>
      a.has_afl_flights === true &&
      !ASCII_ONLY.test(a.title["ru"] ?? ""),
  );

  const countriesByCode = new Map(
    raw.countries.map((c) => [c.code, c]),
  );

  const citiesEnriched: ICity[] = citiesAfterAsciiFilter.map((c) => {
    const airports = airportsFiltered
      .filter((a) => a.city_code === c.code)
      .map((a) => shapeAirport(a, lang))
      .sort((a, b) => a.name.localeCompare(b.name));

    const country = countriesByCode.get(c.country_code);

    return {
      code: c.code,
      name: c.title[lang] ?? c.code,
      location: c.location ?? { lat: 0, lon: 0 },
      country_code: c.country_code,
      countryName: country?.title[lang] ?? "",
      has_afl_flights: airports.length > 0,
      airports,
    };
  });

  const cities = citiesEnriched.filter((c) => c.has_afl_flights);

  const airports = cities.flatMap((c) => c.airports);

  const cityByCode = new Map<string, ICity>(
    cities.map((c) => [c.code.toUpperCase(), c]),
  );
  const airportByCode = new Map<string, IAirport>(
    airports.map((a) => [a.code.toUpperCase(), a]),
  );

  const ruCityCodes = new Set<string>();
  const otherCityCodes = new Set<string>();
  for (const c of cities) {
    if (c.country_code === "RU") ruCityCodes.add(c.code);
    else otherCityCodes.add(c.code);
  }

  const countries: ICountry[] = raw.countries.map((c) => ({
    code: c.code,
    name: c.title[lang] ?? c.code,
    world_region_id: c.world_region_id,
  }));

  const regions = buildRegions(raw.regions, countries, lang);

  return {
    regions,
    countries,
    cities,
    airports,
    cityByCode,
    airportByCode,
    ruCityCodes,
    otherCityCodes,
  };
}

function shapeAirport(a: IRawAirport, lang: string): IAirport {
  return {
    code: a.code,
    name: a.title[lang] ?? a.code,
    city_code: a.city_code,
    location: a.location ?? { lat: 0, lon: 0 },
    has_afl_flights: a.has_afl_flights,
  };
}

function buildRegions(
  raw: IRawRegion[],
  countries: ICountry[],
  lang: string,
): IRegion[] {
  const filtered = raw.filter((r) => r.world_region_id !== AUSTRALIA_REGION_ID);

  const sorted = [...filtered].sort((a, b) =>
    (a.title[lang] ?? "").localeCompare(b.title[lang] ?? ""),
  );
  const ruIdx = sorted.findIndex((r) => r.world_region_id === RUSSIA_REGION_ID);
  if (ruIdx > 0) {
    const [ru] = sorted.splice(ruIdx, 1);
    if (ru) sorted.unshift(ru);
  }

  return sorted.map((r) => ({
    id: r.world_region_id,
    name: r.title[lang] ?? "",
    countries: countries.filter((c) => c.world_region_id === r.world_region_id),
  }));
}

// ---------------------------------------------------------------------------
// Helpers (consumer-facing; pure, take dictionaries as argument)
// ---------------------------------------------------------------------------

export function getCityByCode(
  d: IDictionaries,
  code: string,
): ICity | undefined {
  return d.cityByCode.get(code.toUpperCase());
}

export function getAirportByCode(
  d: IDictionaries,
  code: string,
): IAirport | undefined {
  return d.airportByCode.get(code.toUpperCase());
}

export function getCityCodeByAirportCode(
  d: IDictionaries,
  airportCode: string,
): string | undefined {
  return d.airportByCode.get(airportCode.toUpperCase())?.city_code;
}

export function getCityOrAirport(
  d: IDictionaries,
  code: string,
): ICity | IAirport | undefined {
  return getCityByCode(d, code) ?? getAirportByCode(d, code);
}

/**
 * Returns the city whose location is the nearest to the given lat/lon
 * by great-circle (haversine) distance. Returns undefined when the
 * dictionaries have no cities.
 */
export function findCityByCoord(
  d: IDictionaries,
  lat: number,
  lon: number,
): ICity | undefined {
  if (d.cities.length === 0) return undefined;

  let best: ICity | undefined;
  let bestDist = Infinity;
  for (const city of d.cities) {
    const dist = haversineKm(lat, lon, city.location.lat, city.location.lon);
    if (dist < bestDist) {
      bestDist = dist;
      best = city;
    }
  }
  return best;
}

function haversineKm(
  lat1: number,
  lon1: number,
  lat2: number,
  lon2: number,
): number {
  const R = 6371;
  const dLat = toRad(lat2 - lat1);
  const dLon = toRad(lon2 - lon1);
  const a =
    Math.sin(dLat / 2) ** 2 +
    Math.sin(dLon / 2) ** 2 * Math.cos(toRad(lat1)) * Math.cos(toRad(lat2));
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return R * c;
}

function toRad(deg: number): number {
  return (deg * Math.PI) / 180;
}
