/**
 * Online basemap — a MapLibre style over MapMap's hosted vector tiles.
 *
 * Shows a real map with no territory download. Uses the gateway's
 * authenticated XYZ vector-tile endpoint (`/tiles/{id}/{z}/{x}/{y}.pbf`,
 * `application/vnd.mapbox-vector-tile`) — universally supported by
 * MapLibre Native, unlike the `pmtiles://` scheme the web SDK uses. Live
 * even while the signed offline-territory channel is down (FINDINGS F2).
 *
 * Prefers MapMap's own hosted "MapMap Light" style (22 branded layers),
 * swapping its pmtiles source for the authenticated XYZ source; falls back
 * to a compact hand-authored OpenMapTiles style if that fetch fails.
 */

const API_HOST = "https://api.mapmap.ai";
const GLYPHS = "https://fonts.mapmap.ai/{fontstack}/{range}.pbf";
const HOSTED_LIGHT = `${API_HOST}/styles/mapmap-light-76ef8d@1.json`;
const HOSTED_DARK = `${API_HOST}/styles/mapmap-dark-762117@1.json`;
const FONT = ["Noto Sans Regular"];

export type StyleOptions = {
  buildings3d?: boolean;
  pois?: boolean;
  theme?: "light" | "dark";
};

/** The authenticated XYZ vector source for a territory (key in the URL — the
 * only auth MapLibre Native reliably threads through tile requests in RN). */
function xyzSource(apiKey: string, territoryId: string) {
  return {
    type: "vector" as const,
    tiles: [
      `${API_HOST}/tiles/${territoryId}/{z}/{x}/{y}.pbf?api_key=${apiKey}`,
    ],
    minzoom: 0,
    maxzoom: 14,
    attribution: "© OpenStreetMap contributors © OpenMapTiles",
  };
}

/** Build the online-basemap style JSON string. Async because it prefers the
 * hosted branded style; always resolves (falls back on any error). */
export async function buildOnlineStyle(
  apiKey: string,
  territoryId = "uk",
  opts: StyleOptions = {}
): Promise<string> {
  const { buildings3d = true, pois = true, theme = "light" } = opts;
  try {
    const res = await fetch(theme === "dark" ? HOSTED_DARK : HOSTED_LIGHT);
    if (res.ok) {
      const style = (await res.json()) as {
        sources: Record<string, unknown>;
        [k: string]: unknown;
      };
      // The hosted style's layers all read from the "territory" source;
      // swap that source's pmtiles URL for the authenticated XYZ source and
      // every branded layer renders unchanged.
      const sourceId = "territory" in style.sources ? "territory" : Object.keys(style.sources)[0];
      style.sources[sourceId] = xyzSource(apiKey, territoryId);
      const styleLayers = style as unknown as { layers: unknown[] };
      // The hosted "MapMap Light" style ships its own POI and (potentially)
      // 3D-extrusion layers, so honouring the toggles means *removing* those
      // too — skipping our appended layers alone would leave the branded
      // POIs on screen and make the toggle look dead.
      filterToggledLayers(styleLayers, { buildings3d, pois });
      alignRoadWidths(styleLayers, theme);
      appendShowcaseLayers(styleLayers, sourceId, { buildings3d, pois });
      return JSON.stringify(style);
    }
  } catch {
    // fall through to the built-in style
  }
  const fallback = fallbackStyle(apiKey, territoryId);
  // Honour the toggles on the fallback too: its `building` layer is a flat
  // fill, so 3D + POIs come from the shared showcase layers (source "mm").
  alignRoadWidths(fallback as unknown as { layers: unknown[] }, theme);
  appendShowcaseLayers(
    fallback as unknown as { layers: unknown[] },
    "mm",
    { buildings3d, pois }
  );
  return JSON.stringify(fallback);
}

/**
 * Road weights, based on the hosted MapMap style (crates/sn-style,
 * `compile.rs` on main): exponential width ramps to z20, with a WIDER casing
 * drawn under a NARROWER fill so roads read as bodies with an edge rather than
 * as hairlines.
 *
 * ONE deliberate divergence from the platform: the low-zoom stops (z6–z12,
 * major roads especially) are bumped bolder so the zoomed-out overview reads
 * strongly on a small nav screen — then they converge back onto the platform
 * curve at z14 and share its exact z14→z20 ramp. (Platform major casing is
 * z6→2; here z6→4.) High/street zoom stays platform-identical.
 */
type RoadClass = "major" | "minor" | "path";

const ROAD_FILL_WIDTH: Record<RoadClass, unknown> = {
  major: ["interpolate", ["exponential", 1.2], ["zoom"], 6, 2.4, 10, 3.6, 14, 5.5, 20, 26],
  minor: ["interpolate", ["exponential", 1.2], ["zoom"], 8, 0.6, 13, 2, 14, 3, 20, 18],
  path: ["interpolate", ["exponential", 1.2], ["zoom"], 13, 0.8, 20, 5],
};
const ROAD_CASING_WIDTH: Record<"major" | "minor", unknown> = {
  major: ["interpolate", ["exponential", 1.2], ["zoom"], 6, 4, 10, 5.6, 14, 8, 20, 32],
  minor: ["interpolate", ["exponential", 1.2], ["zoom"], 11, 1, 13, 3.8, 14, 5, 20, 23],
};

/** Classify a road line layer by its id (major = motorway/trunk/primary; path =
 * foot/track; everything else on the transportation layer = minor). Null for
 * non-road lines and our own casings. */
function roadClassOf(id: string | undefined): RoadClass | null {
  const s = (id ?? "").toLowerCase();
  if (!s || /casing/.test(s)) return null;
  if (/(path|track|pedestrian|footway|steps)/.test(s)) return "path";
  if (/(major|motorway|trunk)/.test(s)) return "major";
  if (/(road|street|minor|secondary|tertiary|service|primary|highway)/.test(s)) return "minor";
  return null;
}

function mixHex(a: string, b: string, t: number): string {
  const ch = (h: string, i: number) => parseInt(h.slice(1 + i * 2, 3 + i * 2), 16);
  const out = [0, 1, 2].map((i) => Math.round(ch(a, i) * (1 - t) + ch(b, i) * t));
  return "#" + out.map((v) => v.toString(16).padStart(2, "0")).join("");
}

/** Casing colour: the fill darkened toward a neutral outline (matches the
 * platform's `mix(road, textSecondary, .45)`), with a per-theme fallback when
 * the fill colour isn't a plain hex. */
function casingColour(fill: unknown, theme: "light" | "dark"): string {
  const outline = theme === "dark" ? "#0b0e13" : "#3f4652";
  if (typeof fill === "string" && /^#[0-9a-fA-F]{6}$/.test(fill)) return mixHex(fill, outline, 0.45);
  return theme === "dark" ? "#2a2f38" : "#b7bdc7";
}

type RoadLayer = {
  id?: string;
  type?: string;
  source?: string;
  "source-layer"?: string;
  filter?: unknown;
  minzoom?: number;
  paint?: Record<string, unknown>;
};

/**
 * Set every road fill layer to the platform width curve and inject a matching
 * casing beneath it. ALL casings are inserted before the first non-path road
 * fill so a minor road's casing never paints over a major road's fill at a
 * junction (the platform's ordering). Idempotent by casing id.
 */
function alignRoadWidths(style: { layers: unknown[] }, theme: "light" | "dark"): void {
  const layers = style.layers as RoadLayer[];
  const casings: RoadLayer[] = [];
  let casingAnchor = -1;
  layers.forEach((layer, i) => {
    if (layer.type !== "line" || !layer.paint) return;
    const cls = roadClassOf(layer.id);
    if (!cls) return;
    layer.paint["line-width"] = ROAD_FILL_WIDTH[cls];
    if (cls === "path") return; // paths have no casing
    if (casingAnchor < 0) casingAnchor = i;
    const casingId = `${layer.id}-casing`;
    if (layers.some((l) => l.id === casingId)) return;
    casings.push({
      id: casingId,
      type: "line",
      source: layer.source,
      "source-layer": layer["source-layer"],
      ...(layer.filter !== undefined ? { filter: layer.filter } : {}),
      ...(typeof layer.minzoom === "number" ? { minzoom: layer.minzoom } : {}),
      layout: { "line-cap": "round", "line-join": "round" },
      paint: {
        "line-color": casingColour(layer.paint["line-color"], theme),
        "line-width": ROAD_CASING_WIDTH[cls],
      },
    } as RoadLayer & { layout: Record<string, unknown> });
  });
  if (casings.length > 0 && casingAnchor >= 0) {
    (style.layers as unknown[]).splice(casingAnchor, 0, ...casings);
  }
}

/** True for any layer that renders POIs (by source-layer or id convention). */
function isPoiLayer(layer: { id?: string; "source-layer"?: string }): boolean {
  if (layer["source-layer"] === "poi") return true;
  return typeof layer.id === "string" && /poi/i.test(layer.id);
}

/**
 * Strip the hosted style's own toggle-controlled layers when the matching
 * toggle is off, so the toggles visibly change the map rather than fighting
 * the branded layers underneath. (Our own showcase layers are added
 * afterwards, gated by the same flags.)
 */
function filterToggledLayers(
  style: { layers: unknown[] },
  opts: { buildings3d: boolean; pois: boolean }
): void {
  const layers = style.layers as Array<{
    id?: string;
    type?: string;
    "source-layer"?: string;
  }>;
  style.layers = layers.filter((layer) => {
    if (!opts.pois && isPoiLayer(layer)) return false;
    if (!opts.buildings3d && layer.type === "fill-extrusion") return false;
    return true;
  });
}

/**
 * Demo-showcase additions on top of the hosted style: 3D building
 * extrusions (the tiles carry render_height) and POI dots + labels from
 * the poi source-layer. Idempotent by layer id.
 */
function appendShowcaseLayers(
  style: { layers: unknown[] },
  sourceId: string,
  opts: { buildings3d: boolean; pois: boolean }
): void {
  const layers = style.layers as Array<{ id?: string }>;
  if (opts.buildings3d && !layers.some((l) => l.id === "mmnav-building-3d")) {
    layers.push({
      id: "mmnav-building-3d",
      type: "fill-extrusion",
      source: sourceId,
      "source-layer": "building",
      minzoom: 14.5,
      paint: {
        "fill-extrusion-color": "#ded9d2",
        "fill-extrusion-opacity": 0.75,
        "fill-extrusion-height": ["coalesce", ["get", "render_height"], 8],
        "fill-extrusion-base": ["coalesce", ["get", "render_min_height"], 0],
      },
    } as never);
  }
  if (opts.pois && !layers.some((l) => l.id === "mmnav-poi")) {
    layers.push(
      {
        id: "mmnav-poi",
        type: "circle",
        source: sourceId,
        "source-layer": "poi",
        minzoom: 13,
        paint: {
          "circle-radius": 3.5,
          "circle-color": "#8a7fb5",
          "circle-stroke-width": 1.5,
          "circle-stroke-color": "#ffffff",
        },
      } as never,
      {
        id: "mmnav-poi-label",
        type: "symbol",
        source: sourceId,
        "source-layer": "poi",
        minzoom: 14.5,
        layout: {
          "text-field": ["coalesce", ["get", "name:en"], ["get", "name"]],
          "text-font": FONT,
          "text-size": 11,
          "text-offset": [0, 0.8],
          "text-anchor": "top",
          "text-optional": true,
        },
        paint: {
          "text-color": "#5b5470",
          "text-halo-color": "#ffffff",
          "text-halo-width": 1.2,
        },
      } as never
    );
  }
}

function fallbackStyle(apiKey: string, territoryId: string) {
  return {
    version: 8,
    name: "MapMap Online",
    glyphs: GLYPHS,
    sources: { mm: xyzSource(apiKey, territoryId) },
    layers: [
      { id: "background", type: "background", paint: { "background-color": "#eef1f5" } },
      { id: "landcover", type: "fill", source: "mm", "source-layer": "landcover", paint: { "fill-color": "#e3ebe0", "fill-opacity": 0.6 } },
      { id: "park", type: "fill", source: "mm", "source-layer": "park", paint: { "fill-color": "#d7e8cf", "fill-opacity": 0.7 } },
      { id: "water", type: "fill", source: "mm", "source-layer": "water", paint: { "fill-color": "#a9cce6" } },
      { id: "building", type: "fill", source: "mm", "source-layer": "building", minzoom: 13, paint: { "fill-color": "#e2e0da", "fill-outline-color": "#d3d0c8" } },
      // Road classes match the platform (major = motorway/trunk/primary; path =
      // foot/track; minor = the rest). Widths + casings are applied by
      // alignRoadWidths, so no line-width here.
      { id: "road-path", type: "line", source: "mm", "source-layer": "transportation", minzoom: 13, filter: ["in", ["get", "class"], ["literal", ["path", "track", "pedestrian"]]], paint: { "line-color": "#c9b79a", "line-dasharray": [3, 1.5] } },
      { id: "road-minor", type: "line", source: "mm", "source-layer": "transportation", filter: ["!", ["in", ["get", "class"], ["literal", ["motorway", "trunk", "primary", "rail", "transit", "path", "track", "pedestrian", "ferry"]]]], paint: { "line-color": "#ffffff" } },
      { id: "road-major", type: "line", source: "mm", "source-layer": "transportation", filter: ["in", ["get", "class"], ["literal", ["motorway", "trunk", "primary"]]], paint: { "line-color": "#ffe0a3" } },
      { id: "boundary", type: "line", source: "mm", "source-layer": "boundary", filter: ["<=", ["get", "admin_level"], 2], paint: { "line-color": "#9aa6b2", "line-dasharray": [3, 2], "line-width": 1 } },
      { id: "place-label", type: "symbol", source: "mm", "source-layer": "place", layout: { "text-field": ["coalesce", ["get", "name:en"], ["get", "name"]], "text-font": FONT, "text-size": ["interpolate", ["linear"], ["zoom"], 4, 11, 12, 18] }, paint: { "text-color": "#3a4250", "text-halo-color": "#ffffff", "text-halo-width": 1.4 } },
      { id: "road-label", type: "symbol", source: "mm", "source-layer": "transportation_name", minzoom: 13, layout: { "text-field": ["coalesce", ["get", "name:en"], ["get", "name"]], "text-font": FONT, "text-size": 11, "symbol-placement": "line" }, paint: { "text-color": "#5a6270", "text-halo-color": "#ffffff", "text-halo-width": 1.2 } },
    ],
  };
}
