/**
 * Target-type auto-detect + field auto-mapping for imported FeatureCollections.
 *
 * Pure helpers — no DOM, no map, no DB. The mapping modal calls these once
 * after the file has been parsed to seed its initial state, and re-uses them
 * whenever the user changes target type so the field map updates accordingly.
 *
 * Contract per LUPMIS2_Import_Upload_Design.docx §3.
 *
 *   detectTargetType(fc)               → 'parcels'|'collector_zones'|'osm_roads'
 *                                        |'building_footprints'|'other'
 *   autoMapFields(fc, targetType)      → { [lupmisColumn]: sourceField | null }
 *   TARGET_TYPES                       → metadata used by the modal dropdown
 *   TARGET_FIELDS[targetType]          → ordered list of LUPMIS2 columns the
 *                                        upload payload must populate
 */

// ---------------------------------------------------------------------------
// Target-type registry
// ---------------------------------------------------------------------------

export const TARGET_TYPES = [
  { key: 'parcels',              label: 'Parcels',              geometryFamily: 'polygon' },
  { key: 'collector_zones',      label: 'Collector Zones',      geometryFamily: 'polygon' },
  { key: 'building_footprints',  label: 'Building Footprints',  geometryFamily: 'polygon' },
  { key: 'osm_roads',            label: 'OSM Roads',            geometryFamily: 'line'    },
  { key: 'other',                label: 'Other (view only)',    geometryFamily: 'any'     },
];

// LUPMIS2 column lists per target type, in the order the mapping modal
// should display them. Subset of the typed-table schemas the user actually
// edits — system columns (id, created_at, updated_at, fetched_at, status)
// are NOT user-mapped.
export const TARGET_FIELDS = {
  parcels: [
    'upn', 'landuse', 'zone_code', 'zone_name', 'sector', 'block',
    'parcel_no', 'prop_no', 'st_name', 'prop_add', 'fac_name',
    'min_height', 'max_height', 'eff_date', 'lp_name', 'locality',
    'mmda', 'last_update', 'remarks',
  ],
  collector_zones: [
    'zone_name',
  ],
  building_footprints: [
    // The remote table is a thin schema today; we leave room for the
    // database team to add columns later (the modal will accept more).
  ],
  osm_roads: [
    'osm_id', 'name', 'highway',
  ],
  other: [],
};

// ---------------------------------------------------------------------------
// Type-detect signals
// ---------------------------------------------------------------------------

/**
 * Inspect every feature's geometry type. We return the dominant family
 * because shapefiles can sometimes mix (rare but possible).
 *
 * @returns {'polygon'|'line'|'point'|'mixed'|'none'}
 */
function dominantGeometryFamily(fc) {
  if (!fc?.features?.length) return 'none';
  let polys = 0, lines = 0, points = 0;
  for (const f of fc.features) {
    const t = f?.geometry?.type;
    if (!t) continue;
    if (t === 'Polygon' || t === 'MultiPolygon') polys++;
    else if (t === 'LineString' || t === 'MultiLineString') lines++;
    else if (t === 'Point' || t === 'MultiPoint') points++;
  }
  const total = polys + lines + points;
  if (total === 0) return 'none';
  const max = Math.max(polys, lines, points);
  if (max < total * 0.85) return 'mixed';
  if (polys === max) return 'polygon';
  if (lines === max) return 'line';
  return 'point';
}

/**
 * Build a normalised attribute-name set across the first N features (so
 * inconsistent rows don't skew detection).
 */
function attributeFingerprint(fc, sampleSize = 50) {
  const set = new Set();
  const n = Math.min(sampleSize, fc.features.length);
  for (let i = 0; i < n; i++) {
    const props = fc.features[i]?.properties;
    if (!props || typeof props !== 'object') continue;
    for (const key of Object.keys(props)) {
      set.add(String(key).toLowerCase());
    }
  }
  return set;
}

/** Does the fingerprint contain at least one of the given candidates? */
function has(fp, ...candidates) {
  for (const c of candidates) if (fp.has(c)) return true;
  return false;
}

/**
 * Suggest a target type based on geometry family + attribute fingerprint.
 * Conservative: returns 'other' whenever the signal isn't strong enough,
 * leaving the user to choose explicitly.
 */
export function detectTargetType(fc) {
  const family = dominantGeometryFamily(fc);
  if (family === 'none' || family === 'mixed') return 'other';
  const fp = attributeFingerprint(fc);

  if (family === 'line') {
    if (has(fp, 'osm_id', 'highway')) return 'osm_roads';
    return 'other';
  }

  if (family === 'polygon') {
    // Parcels are the most attribute-rich polygon: UPN is the strongest tell.
    if (has(fp, 'upn', 'parcel_no', 'landuse', 'lu_code', 'zone_code') &&
        // a single "zone_name" without UPN is more likely a zone overlay
        has(fp, 'upn', 'parcel_no', 'landuse', 'lu_code')) {
      return 'parcels';
    }
    if (has(fp, 'zone_name', 'colzonename', 'colzonenr')) return 'collector_zones';
    if (has(fp, 'building', 'building:levels', 'building_levels', 'height',
                'min_height', 'max_height') &&
        !has(fp, 'upn', 'parcel_no')) {
      return 'building_footprints';
    }
    return 'other';
  }

  // Points have no upload target in the current design.
  return 'other';
}

// ---------------------------------------------------------------------------
// Field auto-mapping
// ---------------------------------------------------------------------------

/**
 * Per-LUPMIS2-column candidate source field names. Lower-case, ordered by
 * preference. Matching is case-insensitive and ignores spaces / underscores
 * / hyphens.
 */
const FIELD_CANDIDATES = {
  // Parcels (lu_parcels)
  upn:        ['upn', 'unique_parcel_no', 'parcel_id', 'pid'],
  landuse:    ['landuse', 'land_use', 'lu', 'lu_code'],
  zone_code:  ['zone_code', 'zonecode', 'zone'],
  zone_name:  ['zone_name', 'zonename', 'colzonename'],
  sector:     ['sector', 'sec'],
  block:      ['block', 'blk'],
  parcel_no:  ['parcel_no', 'parcelno', 'plot_no', 'plotno'],
  prop_no:    ['prop_no', 'propertyno', 'property_no'],
  st_name:    ['st_name', 'street', 'street_name', 'road'],
  prop_add:   ['prop_add', 'address', 'addr'],
  fac_name:   ['fac_name', 'facility', 'facilityname'],
  min_height: ['min_height', 'minheight', 'h_min'],
  max_height: ['max_height', 'maxheight', 'h_max', 'height'],
  eff_date:   ['eff_date', 'effectivedate', 'effdate'],
  lp_name:    ['lp_name', 'lpname', 'localplan'],
  locality:   ['locality', 'town', 'settlement'],
  mmda:       ['mmda', 'district', 'assembly'],
  last_update:['last_update', 'lastupdate', 'updated'],
  remarks:    ['remarks', 'notes', 'comments'],

  // OSM roads
  osm_id:     ['osm_id', 'osmid', 'id'],
  name:       ['name', 'street_name', 'st_name'],
  highway:    ['highway', 'road_class', 'class'],
};

/** Normalise a field name for matching (lowercase, strip _ - and spaces). */
function normaliseKey(s) {
  return String(s).toLowerCase().replace(/[\s_\-]+/g, '');
}

/**
 * Suggest a source-field → LUPMIS2-column mapping for the given target type.
 * Unmapped columns get null so the modal can show "(none)" and let the user
 * pick from a dropdown of available source fields.
 *
 * @param {Object} fc          — parsed FeatureCollection
 * @param {string} targetType  — one of TARGET_TYPES keys
 * @returns {Object<string, string|null>}  { [lupmisColumn]: sourceField|null }
 */
export function autoMapFields(fc, targetType) {
  const columns = TARGET_FIELDS[targetType] || [];
  if (columns.length === 0) return {};

  const fp = attributeFingerprint(fc, 50);
  // Build normalised → original lookup for available source fields
  const sourceByNorm = new Map();
  for (const key of fp) {
    sourceByNorm.set(normaliseKey(key), key);
  }

  const mapping = {};
  for (const col of columns) {
    const candidates = FIELD_CANDIDATES[col] || [col];
    let matched = null;
    for (const cand of candidates) {
      const n = normaliseKey(cand);
      if (sourceByNorm.has(n)) {
        matched = sourceByNorm.get(n);
        break;
      }
    }
    mapping[col] = matched;
  }
  return mapping;
}

/**
 * Apply a mapping to one source-feature properties object and return the
 * LUPMIS2-shaped properties (column-name keys). Source fields not in the
 * mapping are dropped. Used during the staging-row insert.
 */
export function applyFieldMapping(sourceProps, mapping) {
  const out = {};
  for (const [targetCol, sourceKey] of Object.entries(mapping || {})) {
    if (sourceKey == null) continue;
    if (sourceProps && Object.prototype.hasOwnProperty.call(sourceProps, sourceKey)) {
      out[targetCol] = sourceProps[sourceKey];
    }
  }
  return out;
}

/**
 * Return the list of distinct source-property names (the union across all
 * features), preserving discovery order — used to populate the per-column
 * dropdown in the mapping modal.
 */
export function listSourceFields(fc) {
  const seen = new Map();
  for (const f of fc.features || []) {
    const props = f?.properties;
    if (!props || typeof props !== 'object') continue;
    for (const k of Object.keys(props)) {
      if (!seen.has(k)) seen.set(k, true);
    }
  }
  return Array.from(seen.keys());
}

/**
 * For each source-property name in the FeatureCollection, return one sample
 * value drawn from the first feature that has a non-empty value for it.
 * Used by the mapping modal to show users what each attribute actually
 * contains — e.g. dropdown options become `upn  [12345-6789]` rather than
 * just `upn`. Empty / null / undefined values are skipped while scanning;
 * arrays and objects are JSON-stringified so something readable surfaces.
 *
 * @param {Object} fc       — parsed FeatureCollection
 * @param {number} [maxLen] — max sample length (truncated with `…`)
 * @returns {Object<string,string>}  { [sourceField]: sample }
 */
export function sampleSourceValues(fc, maxLen = 35) {
  const samples = {};
  for (const feature of fc.features || []) {
    const props = feature?.properties;
    if (!props || typeof props !== 'object') continue;
    for (const [key, value] of Object.entries(props)) {
      if (samples[key] !== undefined) continue;       // already have one
      if (value === null || value === undefined || value === '') continue;

      let str;
      if (typeof value === 'object') {
        try { str = JSON.stringify(value); } catch { str = String(value); }
      } else {
        str = String(value);
      }
      // Collapse whitespace so a multi-line cell value still renders inline.
      str = str.replace(/\s+/g, ' ').trim();
      if (!str) continue;
      if (str.length > maxLen) str = str.slice(0, maxLen - 1) + '…';
      samples[key] = str;
    }
  }
  return samples;
}
