// Generic GeoJSON-ish layer factory (Wave 3). One function that expresses what
// quakes.js and fires.js used to hand-roll, driven entirely by a `spec` object
// from js/registry.js — so adding a feed is data entry, not new code.
//
// A spec (all but id/name/url optional; sensible defaults shown in registry.js):
//   { id, name, category, defaultOn, url, refreshMs,
//     adapt(json)     -> raw feature array           (default: json.features)
//     usable(f)       -> bool                         (default: finite lon/lat)
//     locate(f)       -> {lon,lat} | null             (default: GeoJSON geometry)
//     featureId(f)    -> string | null                (null => rebuild wholesale)
//     sig(f)          -> string                        (revision key; default props+coords)
//     timeAt(f)       -> ms|Date|null                  (non-null => time-anchored availability)
//     sortKey(f)      -> number                        (cap keeps highest; default 0)
//     cap             -> number
//     style(f, ctx)   -> { point?, billboard?, label? } Cesium graphics options
//     description(f, ctx) -> html
//     status({shown, features}) -> string }

export function createGeoJsonLayer(ctx, spec) {
  const { viewer, Cesium, lib, ui, start, stop } = ctx;
  const ds = new Cesium.CustomDataSource(spec.id);
  viewer.dataSources.add(ds);

  const refreshMs = spec.refreshMs || 300000;
  const adapt = spec.adapt || ((json) => (json && json.features) || []);
  const locate = spec.locate || defaultLocate;
  const usable = spec.usable || ((f) => { const p = locate(f); return !!p && isFinite(p.lon) && isFinite(p.lat); });
  const sig = spec.sig || ((f) => JSON.stringify([f.properties, f.geometry && f.geometry.coordinates]));
  const status = spec.status || (({ shown }) => `${shown} features`);

  // id → { entity, sig } for dedupe + USGS-style revision detection.
  const byId = spec.featureId ? new Map() : null;

  let enabled = spec.defaultOn !== false;
  let timer = null;
  let inFlight = false;

  ds.show = enabled; // datasources default to visible — honor a default-off layer
  ui.addLayer(spec.id, spec.name, enabled, (on) => { enabled = on; ds.show = on; }, spec.category);
  ui.setStatus(spec.id, 'loading…', 'warn');

  // Build one Cesium.Entity from a feature via the spec's style/description.
  function buildEntity(f, id) {
    const p = locate(f);
    const s = (spec.style && spec.style(f, ctx)) || {};
    const opts = {
      position: Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.height || 0),
    };
    if (id != null) opts.id = `${spec.id}:${id}`;
    if (spec.timeAt) {
      const when = spec.timeAt(f);
      if (when != null) opts.availability = availabilityFor(when);
    }
    if (s.point) opts.point = s.point;
    if (s.billboard) opts.billboard = s.billboard;
    if (s.label) {
      opts.label = {
        font: '11px "Segoe UI", system-ui, sans-serif',
        fillColor: Cesium.Color.WHITE,
        outlineColor: Cesium.Color.fromCssColorString('#0a0f14'),
        outlineWidth: 3,
        style: Cesium.LabelStyle.FILL_AND_OUTLINE,
        verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
        pixelOffset: new Cesium.Cartesian2(0, -10),
        disableDepthTestDistance: 50000,
        ...s.label,
      };
      if (s.label.fade) {
        const [near, far] = s.label.fade;
        opts.label.translucencyByDistance = new Cesium.NearFarScalar(near, 1.0, far, 0.0);
        delete opts.label.fade;
      }
    }
    if (spec.description) opts.description = spec.description(f, ctx);
    return new Cesium.Entity(opts);
  }

  // Availability start clamped into the fixed clock window [start, stop], so a
  // real event appears exactly as the slider crosses its moment.
  function availabilityFor(when) {
    const jd = Cesium.JulianDate.fromDate(when instanceof Date ? when : new Date(when));
    let sJd = jd;
    if (Cesium.JulianDate.lessThan(sJd, start)) sJd = start.clone();
    if (Cesium.JulianDate.greaterThan(sJd, stop)) sJd = stop.clone();
    return new Cesium.TimeIntervalCollection([new Cesium.TimeInterval({ start: sJd, stop })]);
  }

  function apply(rawFeatures) {
    let features = (rawFeatures || []).filter(usable);
    if (spec.sortKey) features.sort((a, b) => spec.sortKey(b) - spec.sortKey(a));
    if (spec.cap) features = features.slice(0, spec.cap);

    if (!byId) {
      // Wholesale rebuild (feeds with no stable id, e.g. fires).
      ds.entities.removeAll();
      for (const f of features) ds.entities.add(buildEntity(f, null));
      ui.setStatus(spec.id, status({ shown: features.length, features }), 'ok');
      return;
    }

    // Dedupe by id + rebuild only revised entities (e.g. USGS magnitude updates).
    const keep = new Map(features.map((f) => [`${spec.id}:${spec.featureId(f)}`, f]));
    for (const [key, rec] of byId) {
      if (!keep.has(key)) { ds.entities.remove(rec.entity); byId.delete(key); }
    }
    for (const [key, f] of keep) {
      const s = sig(f);
      const existing = byId.get(key);
      if (existing) {
        if (existing.sig === s) continue;
        ds.entities.remove(existing.entity);
      }
      const ent = buildEntity(f, spec.featureId(f));
      ds.entities.add(ent);
      byId.set(key, { entity: ent, sig: s });
    }
    ui.setStatus(spec.id, status({ shown: byId.size, features }), 'ok');
  }

  async function poll() {
    let res;
    try {
      res = await fetch(spec.url);
    } catch {
      ui.setStatus(spec.id, 'network error — retrying', 'warn');
      return;
    }
    if (!res.ok) {
      ui.setStatus(spec.id, `HTTP ${res.status} — retrying`, 'warn');
      return;
    }
    let json;
    try {
      json = await res.json(); // never gate on Content-Type (EONET lies rss+xml)
    } catch {
      ui.setStatus(spec.id, 'bad response — retrying', 'err');
      return;
    }
    try {
      apply(adapt(json));
    } catch (err) {
      console.error(`[godsigh] ${spec.id} apply failed:`, err);
      ui.setStatus(spec.id, 'render error', 'err');
    }
  }

  // Self-scheduling loop; keeps polling regardless of scrub (historical feeds).
  async function tick() {
    timer = null;
    if (inFlight) return;
    inFlight = true;
    try { await poll(); } finally { inFlight = false; }
    timer = setTimeout(tick, refreshMs);
  }
  tick();

  return { id: spec.id };
}

// Default geometry → {lon, lat}: Point, Polygon (ring centroid), or a
// GeometryCollection (first usable Point, else first Polygon centroid).
export function defaultLocate(f) {
  const g = f && f.geometry;
  if (!g) return null;
  return fromGeometry(g);
}

function fromGeometry(g) {
  if (!g) return null;
  if (g.type === 'Point' && Array.isArray(g.coordinates)) {
    const [lon, lat] = g.coordinates;
    return isFinite(lon) && isFinite(lat) ? { lon, lat } : null;
  }
  if (g.type === 'Polygon' && Array.isArray(g.coordinates) && Array.isArray(g.coordinates[0])) {
    return centroid(g.coordinates[0]);
  }
  if (g.type === 'GeometryCollection' && Array.isArray(g.geometries)) {
    for (const sub of g.geometries) if (sub.type === 'Point') { const p = fromGeometry(sub); if (p) return p; }
    for (const sub of g.geometries) { const p = fromGeometry(sub); if (p) return p; }
  }
  return null;
}

function centroid(ring) {
  let sx = 0, sy = 0, n = 0;
  for (const pt of ring) {
    if (Array.isArray(pt) && isFinite(pt[0]) && isFinite(pt[1])) { sx += pt[0]; sy += pt[1]; n++; }
  }
  return n ? { lon: sx / n, lat: sy / n } : null;
}
