/**
 * GIS export — write a list of OL features (with renamed properties) to
 * GeoJSON / KML / Shapefile, and trigger a browser download.
 *
 * Public API:
 *
 *   exportFeaturesToGis({
 *     features,                  // Array<ol/Feature> in EPSG:3857 (map projection)
 *     rename,                    // Record<sourceField, exportFieldOrEmpty>
 *     format,                    // 'geojson' | 'shp' | 'kml'
 *     filenameBase,              // 'area_analysis' (no extension)
 *   }) → Promise<void>
 *
 * The output uses EPSG:4326 (lon/lat) in every format. Source fields whose
 * rename value is empty are dropped. Geometry is preserved as-is.
 *
 * SHP attribute names are truncated/sanitised to 10 chars + alphanumeric/
 * underscore (DBF requirement). Collisions are resolved by appending a
 * numeric suffix.
 */

import GeoJSONFormat from 'ol/format/GeoJSON.js';
import KMLFormat     from 'ol/format/KML.js';
import * as shpwrite from 'shp-write';

const PROJ_MAP   = 'EPSG:3857';
const PROJ_WGS84 = 'EPSG:4326';

// ---------------------------------------------------------------------------
// Public entry
// ---------------------------------------------------------------------------

export async function exportFeaturesToGis({
  features,
  rename,
  format,
  filenameBase = 'export',
}) {
  if (!Array.isArray(features) || features.length === 0) {
    throw new Error('No features to export');
  }

  // 1. Apply the rename map → produce GeoJSON Features in EPSG:4326.
  const geojson = buildRenamedGeoJSON(features, rename);

  // 2. Format-specific writer + download.
  switch (format) {
    case 'geojson':
      return downloadBlob(
        new Blob([JSON.stringify(geojson, null, 2)], { type: 'application/geo+json' }),
        `${filenameBase}.geojson`
      );

    case 'kml':
      return downloadBlob(
        new Blob([writeKml(features, rename)], { type: 'application/vnd.google-earth.kml+xml' }),
        `${filenameBase}.kml`
      );

    case 'shp':
      return writeShp(geojson, filenameBase);

    default:
      throw new Error(`Unknown export format: ${format}`);
  }
}

// ---------------------------------------------------------------------------
// Shared: apply rename → GeoJSON in WGS84
// ---------------------------------------------------------------------------

/**
 * Produce a GeoJSON FeatureCollection in EPSG:4326 with properties remapped
 * per the user's rename table. Fields whose rename value is empty/null are
 * dropped.
 */
function buildRenamedGeoJSON(features, rename) {
  const fmt = new GeoJSONFormat();
  const out = { type: 'FeatureCollection', features: [] };

  for (const f of features) {
    if (!f.getGeometry()) continue;

    const geomGeoJson = fmt.writeGeometryObject(f.getGeometry(), {
      dataProjection: PROJ_WGS84,
      featureProjection: PROJ_MAP,
    });

    const sourceProps = stripGeometryFromProps(f.getProperties());
    const outProps = {};
    for (const [src, target] of Object.entries(rename || {})) {
      if (!target) continue;                       // user cleared → drop
      if (Object.prototype.hasOwnProperty.call(sourceProps, src)) {
        outProps[target] = sourceProps[src];
      }
    }

    out.features.push({ type: 'Feature', geometry: geomGeoJson, properties: outProps });
  }
  return out;
}

function stripGeometryFromProps(props) {
  const out = {};
  for (const [k, v] of Object.entries(props || {})) {
    if (k === 'geometry') continue;
    out[k] = v;
  }
  return out;
}

// ---------------------------------------------------------------------------
// KML — uses OL's writer for proper styling/structure
// ---------------------------------------------------------------------------

function writeKml(features, rename) {
  // KML carries arbitrary string properties via ExtendedData; OL's writer
  // serialises every property. Clone each feature so we can swap its
  // properties to the renamed set without mutating the source.
  const clones = features
    .filter((f) => f.getGeometry())
    .map((f) => {
      const clone = f.clone();
      const sourceProps = stripGeometryFromProps(f.getProperties());
      const outProps = {};
      for (const [src, target] of Object.entries(rename || {})) {
        if (!target) continue;
        if (Object.prototype.hasOwnProperty.call(sourceProps, src)) {
          outProps[target] = sourceProps[src];
        }
      }
      clone.setProperties(outProps, /* silent */ true);
      // Use the first renamed value that looks like a label for KML's <name>
      const labelKey = Object.values(rename || {}).find(
        (v) => v && outProps[v] != null && outProps[v] !== ''
      );
      if (labelKey) clone.set('name', String(outProps[labelKey]));
      return clone;
    });

  return new KMLFormat({ extractStyles: false }).writeFeatures(clones, {
    dataProjection: PROJ_WGS84,
    featureProjection: PROJ_MAP,
  });
}

// ---------------------------------------------------------------------------
// Shapefile — uses shp-write to build a .zip with .shp/.shx/.dbf/.prj
// ---------------------------------------------------------------------------

/**
 * shp-write's `download` accepts a GeoJSON FeatureCollection and a config.
 * It splits geometries by type and writes one shapefile per type inside a
 * single .zip download. We pre-sanitise field names to honour DBF's 10-char
 * limit (otherwise shp-write silently truncates, possibly causing collisions).
 */
async function writeShp(geojson, filenameBase) {
  const sanitised = sanitiseShpFieldNames(geojson);

  // shp-write 0.3 emits a .zip via DOM download. There's no Promise return,
  // but the call is synchronous in practice. Wrap to a Promise so callers
  // can await uniformly.
  return new Promise((resolve, reject) => {
    try {
      shpwrite.download(sanitised, {
        folder: filenameBase,
        outputType: 'blob',
        compression: 'DEFLATE',
        types: {
          point:    `${filenameBase}_point`,
          polygon:  `${filenameBase}_polygon`,
          polyline: `${filenameBase}_line`,
        },
      });
      resolve();
    } catch (err) {
      reject(err);
    }
  });
}

/**
 * Make every property key valid for DBF: ≤10 chars, alphanumeric/underscore,
 * unique across the set. Returns a new GeoJSON FeatureCollection.
 */
function sanitiseShpFieldNames(geojson) {
  // Collect the union of source keys (sources are already user-renamed).
  const allKeys = new Set();
  for (const f of geojson.features) {
    for (const k of Object.keys(f.properties || {})) allKeys.add(k);
  }

  // Build map sourceKey → safeKey.
  const used = new Set();
  const remap = {};
  for (const k of allKeys) {
    let base = String(k).replace(/[^A-Za-z0-9_]+/g, '_').slice(0, 10) || 'field';
    let candidate = base;
    let i = 1;
    while (used.has(candidate)) {
      const suffix = String(i++);
      candidate = base.slice(0, Math.max(1, 10 - suffix.length)) + suffix;
    }
    used.add(candidate);
    remap[k] = candidate;
  }

  return {
    type: 'FeatureCollection',
    features: geojson.features.map((f) => {
      const newProps = {};
      for (const [k, v] of Object.entries(f.properties || {})) {
        newProps[remap[k]] = v;
      }
      return { type: 'Feature', geometry: f.geometry, properties: newProps };
    }),
  };
}

// ---------------------------------------------------------------------------
// Generic download helper
// ---------------------------------------------------------------------------

function downloadBlob(blob, filename) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}
