/**
 * LUPMIS2 — iframe embed bridge
 *
 * Implements the postMessage contract defined in
 *   LUPMIS2_Permit_Map_Integration.docx §2
 * which itself follows the iframe channel from
 *   LUPMIS2_Reusable_Mapping_Concept.docx §3.2 / §4.
 *
 *   Outbound (embed → host):
 *     { type: 'ready' }
 *     { type: 'parcel:select', upn, parcel_id, lon, lat,
 *                              zone_code, zone_name, landuse,
 *                              min_height, max_height }
 *     { type: 'parcel:cleared' }
 *     { type: 'error', code, message }
 *
 *   Inbound  (host  → embed):
 *     { type: 'set:view',     lon, lat, zoom }
 *     { type: 'set:selected', upn }
 *     { type: 'clear:selected' }
 *     { type: 'set:basemap',  key }
 *
 * The bridge is framework-agnostic apart from the OpenLayers imports — it
 * lives one level above the proposed `map-core` library so once that library
 * is extracted (concept §3.1) this file can be lifted into it unchanged.
 *
 * Note on security: outbound messages are sent with target origin `*` because
 * the embed cannot know its parent's origin in advance and the payload is
 * non-sensitive (selection metadata only). The HOST is expected to verify
 * `event.origin === '<embed origin>'` before trusting any message, as
 * documented in §2.2 of the integration doc. Inbound commands are
 * type-checked against a strict whitelist before being acted on.
 */

import { fromLonLat, toLonLat } from 'ol/proj.js';
import { getCenter } from 'ol/extent.js';
import VectorLayer from 'ol/layer/Vector.js';
import VectorSource from 'ol/source/Vector.js';
import { Style, Stroke, Fill } from 'ol/style.js';

const KNOWN_COMMANDS = new Set([
  'set:view',
  'set:selected',
  'clear:selected',
  'set:basemap',
]);

/**
 * Create and install the embed bridge.
 *
 * @param {Object} opts
 * @param {Object} opts.mapView       — the MapView instance (uses onClick,
 *                                       getMap, setBaseMap).
 * @param {Object} opts.embedConfig   — window.LUPMIS_EMBED contents.
 * @returns {{ attachParcelsLayer: (layer) => void, emitError: (code, msg) => void }}
 */
export function createEmbedBridge({ mapView, embedConfig }) {
  const map = mapView.getMap();
  const parent = (window.parent && window.parent !== window) ? window.parent : null;

  // -------------------------------------------------------------------------
  // Highlight layer — visual cue for the currently-selected parcel.
  // -------------------------------------------------------------------------
  const highlightSource = new VectorSource();
  const highlightLayer = new VectorLayer({
    source: highlightSource,
    zIndex: 9999,
    style: new Style({
      stroke: new Stroke({ color: '#f97316', width: 3 }),
      fill:   new Fill({ color: 'rgba(249,115,22,0.18)' }),
    }),
    properties: {
      title: 'Permit selection',
      displayInLayerSwitcher: false,
    },
  });
  map.addLayer(highlightLayer);

  let parcelsLayer    = null;
  let pendingSelectUpn = embedConfig?.upn ? String(embedConfig.upn) : null;
  let readyEmitted     = false;

  // -------------------------------------------------------------------------
  // Outbound
  // -------------------------------------------------------------------------
  function send(message) {
    if (!parent) {
      console.warn('[embed-bridge] No parent window — would have sent:', message);
      return;
    }
    try {
      parent.postMessage(message, '*');
    } catch (e) {
      console.warn('[embed-bridge] postMessage failed:', e);
    }
  }

  function emitError(code, message) {
    send({ type: 'error', code, message });
  }

  function emitReady() {
    if (readyEmitted) return;
    readyEmitted = true;
    send({ type: 'ready' });
  }

  /** Build a `parcel:select` payload from a feature and (optionally) the click point. */
  function parcelPayload(feature, lon, lat) {
    const p = feature.getProperties();
    let outLon = lon, outLat = lat;
    if (outLon == null || outLat == null) {
      const ext = feature.getGeometry()?.getExtent();
      if (ext) {
        const [cx, cy] = toLonLat(getCenter(ext));
        outLon = cx; outLat = cy;
      }
    }
    return {
      type: 'parcel:select',
      upn:        p.upn        ?? null,
      parcel_id:  p.id         ?? null,
      lon:        outLon       ?? null,
      lat:        outLat       ?? null,
      zone_code:  p.zone_code  ?? null,
      zone_name:  p.zone_name  ?? null,
      landuse:    p.landuse    ?? null,
      min_height: p.min_height ?? null,
      max_height: p.max_height ?? null,
    };
  }

  function highlightFeature(feature) {
    highlightSource.clear();
    if (feature) {
      const clone = feature.clone();
      highlightSource.addFeature(clone);
    }
  }

  // -------------------------------------------------------------------------
  // Click → parcel:select / parcel:cleared
  // -------------------------------------------------------------------------
  mapView.onClick((lon, lat, _markerFeature, evt) => {
    let parcelFeature = null;
    map.forEachFeatureAtPixel(evt.pixel, (f) => {
      if (f.get('_layerType') === 'parcel') {
        parcelFeature = f;
        return true;
      }
    });
    if (parcelFeature) {
      highlightFeature(parcelFeature);
      send(parcelPayload(parcelFeature, lon, lat));
    } else {
      highlightFeature(null);
      send({ type: 'parcel:cleared' });
    }
  });

  // -------------------------------------------------------------------------
  // Inbound commands
  // -------------------------------------------------------------------------
  window.addEventListener('message', (event) => {
    const msg = event.data;
    if (!msg || typeof msg !== 'object' || !KNOWN_COMMANDS.has(msg.type)) return;
    try {
      switch (msg.type) {
        case 'set:view': {
          if (typeof msg.lon === 'number' && typeof msg.lat === 'number') {
            const view = map.getView();
            view.setCenter(fromLonLat([msg.lon, msg.lat]));
            if (typeof msg.zoom === 'number') view.setZoom(msg.zoom);
          }
          break;
        }
        case 'set:selected':
          if (msg.upn) selectByUpn(String(msg.upn));
          break;
        case 'clear:selected':
          highlightFeature(null);
          pendingSelectUpn = null;
          break;
        case 'set:basemap':
          if (msg.key && typeof mapView.setBaseMap === 'function') {
            mapView.setBaseMap(msg.key);
          }
          break;
      }
    } catch (e) {
      emitError('COMMAND_FAILED', `Failed to handle ${msg.type}: ${e.message}`);
    }
  });

  /**
   * Find the parcel with the given UPN, highlight it, fit the view to it,
   * and emit a synthesized parcel:select so the host receives the metadata.
   * If the parcels haven't finished loading yet, the UPN is queued and the
   * lookup is retried as features stream in.
   */
  function selectByUpn(upn) {
    if (!parcelsLayer) { pendingSelectUpn = upn; return; }
    const features = parcelsLayer.getSource().getFeatures();
    const feature = features.find((f) => String(f.get('upn') ?? '') === upn);
    if (!feature) { pendingSelectUpn = upn; return; }

    pendingSelectUpn = null;
    highlightFeature(feature);
    const ext = feature.getGeometry()?.getExtent();
    if (ext) {
      map.getView().fit(ext, { padding: [50, 50, 50, 50], duration: 400, maxZoom: 17 });
    }
    send(parcelPayload(feature, null, null));
  }

  // -------------------------------------------------------------------------
  // Parcels attached → emit `ready`, drain any pending set:selected
  // -------------------------------------------------------------------------
  function attachParcelsLayer(layer) {
    parcelsLayer = layer;
    const source = layer.getSource();

    const drain = () => {
      // Microtask hop so a batch addFeatures() finishes before we react.
      queueMicrotask(() => {
        if (pendingSelectUpn) selectByUpn(pendingSelectUpn);
        emitReady();
      });
    };

    if (source.getFeatures().length > 0) {
      drain();
    } else {
      // Stay subscribed: parcels may arrive in waves (cache then API refresh),
      // and a pending UPN may only resolve after the second wave.
      let scheduled = false;
      source.on('addfeature', () => {
        if (scheduled) return;
        scheduled = true;
        queueMicrotask(() => {
          scheduled = false;
          if (pendingSelectUpn) selectByUpn(pendingSelectUpn);
          emitReady();
        });
      });
    }
  }

  // -------------------------------------------------------------------------
  // Apply initial config: basemap + view (UPN is handled after parcels load)
  // -------------------------------------------------------------------------
  if (embedConfig?.basemap && typeof mapView.setBaseMap === 'function') {
    mapView.setBaseMap(embedConfig.basemap);
  }
  if (typeof embedConfig?.lon === 'number' && typeof embedConfig?.lat === 'number') {
    const view = map.getView();
    view.setCenter(fromLonLat([embedConfig.lon, embedConfig.lat]));
    view.setZoom(typeof embedConfig?.zoom === 'number' ? embedConfig.zoom : 15);
  }

  return { attachParcelsLayer, emitError };
}
