import 'dart:convert';
import 'dart:io';

import 'package:terepi_seged/services/coord_converter_service.dart';

import '../../models/stakeout_point.dart';

/// Oszlop-szerepek a CSV-megfeleltetéshez.
enum ColumnRole {
  ignore('—'),
  station('Állomás / pontszám'),
  line('Vonal'),
  name('Megnevezés'),
  eovY('EOV Y (kelet)'),
  eovX('EOV X (észak)'),
  lat('Szélesség (lat)'),
  lon('Hosszúság (lon)'),
  elevation('Magasság');

  final String label;
  const ColumnRole(this.label);
}

/// Az elemzés eredménye — ebből épül az előnézeti képernyő.
class CsvPreview {
  final String fileName;
  final String delimiter;
  final bool hasHeader;
  final bool decimalComma;
  final List<String> headers; // fejléc vagy "1. oszlop" ...
  final List<List<String>> sampleRows; // első max. 6 adatsor
  final List<List<String>> allRows; // minden adatsor (nyers)
  final List<ColumnRole> guessedRoles;
  final String detectedCrs; // 'EOV' | 'WGS84' | '?'
  final int badRowCount;

  CsvPreview({
    required this.fileName,
    required this.delimiter,
    required this.hasHeader,
    required this.decimalComma,
    required this.headers,
    required this.sampleRows,
    required this.allRows,
    required this.guessedRoles,
    required this.detectedCrs,
    required this.badRowCount,
  });

  int get rowCount => allRows.length;
}

/// CSV / GeoJSON kitűzési pont import — magyar terepi sajátosságokkal:
///  * elválasztó-felismerés (';' a magyar Excel alapértelmezése, ',', tab)
///  * tizedesVESSZŐ kezelése
///  * kódolás: UTF-8, hibánál Latin-1 visszaesés (a koordináták így is
///    hibátlanok, legfeljebb az ő/ű torzulhat a nevekben)
///  * oszlopszerep-felismerés ÉRTÉKTARTOMÁNY alapján — Magyarországon az
///    EOV Y (421–960 ezer), EOV X (48–384 ezer), lat (45,5–48,8) és
///    lon (16–23) tartományok páronként diszjunktak, így a felismerés
///    nagyon megbízható; a fejlécnevek csak megerősítésként számítanak.
class StakeoutImportService {
  StakeoutImportService._();

  // ── Magyar értéktartományok ──────────────────────────────────────
  static bool _isEovY(double v) => v >= 421000 && v <= 960000;
  static bool _isEovX(double v) => v >= 48000 && v <= 384000;
  static bool _isLat(double v) => v >= 45.5 && v <= 48.8;
  static bool _isLon(double v) => v >= 16.0 && v <= 23.0;
  static bool _isElev(double v) => v >= -50 && v <= 3000;

  // ═════════════════════════════════════════════════════════════════
  // CSV elemzés
  // ═════════════════════════════════════════════════════════════════

  static Future<CsvPreview> analyzeCsv(File file) async {
    final bytes = await file.readAsBytes();
    String text;
    try {
      text = utf8.decode(bytes);
    } catch (_) {
      text = latin1.decode(bytes); // Windows-1250 közelítése
    }
    // BOM eltávolítás
    if (text.isNotEmpty && text.codeUnitAt(0) == 0xFEFF) {
      text = text.substring(1);
    }

    final lines = text
        .split(RegExp(r'\r\n|\r|\n'))
        .where((l) => l.trim().isNotEmpty)
        .toList();
    if (lines.isEmpty) {
      throw const FormatException('A fájl üres.');
    }

    // 1. Elválasztó: amelyikből a legtöbb van KONZISZTENSEN a sorokban.
    final delimiter = _detectDelimiter(lines.take(20).toList());

    // 2. Sorok felbontása.
    var rows = lines.map((l) => _splitLine(l, delimiter)).toList();
    final colCount = rows.map((r) => r.length).reduce((a, b) => a > b ? a : b);
    // Rövid sorok kipótlása üres cellákkal, hogy a táblázat téglalap legyen.
    rows = rows
        .map((r) => [...r, ...List.filled(colCount - r.length, '')])
        .toList();

    // 3. Tizedesvessző? Ha ';' az elválasztó és sok "123,45" mintájú cella
    //    van, akkor a vessző tizedesjel.
    final decimalComma = _detectDecimalComma(rows.take(30).toList());

    // 4. Fejléc: az első sor akkor fejléc, ha a cellái NEM számok, de a
    //    második soréi többségében igen.
    final hasHeader = rows.length > 1 &&
        _numericRatio(rows.first, decimalComma) < 0.5 &&
        _numericRatio(rows[1], decimalComma) >= 0.5;

    final headers = hasHeader
        ? rows.first.map((h) => h.trim()).toList()
        : List.generate(colCount, (i) => '${i + 1}. oszlop');
    final dataRows = hasHeader ? rows.sublist(1) : rows;

    // 5. Oszlopszerepek felismerése.
    final roles = _guessRoles(headers, dataRows, decimalComma);

    final crs =
        roles.contains(ColumnRole.eovY) && roles.contains(ColumnRole.eovX)
            ? 'EOV'
            : roles.contains(ColumnRole.lat) && roles.contains(ColumnRole.lon)
                ? 'WGS84'
                : '?';

    return CsvPreview(
      fileName: file.uri.pathSegments.last,
      delimiter: delimiter,
      hasHeader: hasHeader,
      decimalComma: decimalComma,
      headers: headers,
      sampleRows: dataRows.take(6).toList(),
      allRows: dataRows,
      guessedRoles: roles,
      detectedCrs: crs,
      badRowCount: 0, // a tényleges építéskor derül ki
    );
  }

  static String _detectDelimiter(List<String> lines) {
    var best = ';';
    var bestScore = -1;
    for (final d in [';', ',', '\t']) {
      final counts = lines.map((l) => d.allMatches(l).length).toList();
      final min = counts.reduce((a, b) => a < b ? a : b);
      // Pontszám: minden sorban legyen legalább 1, és konzisztens legyen.
      final score = min > 0 ? min * 10 - (counts.toSet().length - 1) : -1;
      if (score > bestScore) {
        bestScore = score;
        best = d;
      }
    }
    return best;
  }

  /// Egyszerű felbontás idézőjel-kezeléssel ("a;b" egyben marad).
  static List<String> _splitLine(String line, String delimiter) {
    final cells = <String>[];
    final sb = StringBuffer();
    var inQuotes = false;
    for (var i = 0; i < line.length; i++) {
      final c = line[i];
      if (c == '"') {
        inQuotes = !inQuotes;
      } else if (c == delimiter && !inQuotes) {
        cells.add(sb.toString().trim());
        sb.clear();
      } else {
        sb.write(c);
      }
    }
    cells.add(sb.toString().trim());
    return cells;
  }

  static final _decimalCommaRe = RegExp(r'^-?\d+,\d+$');
  static final _decimalDotRe = RegExp(r'^-?\d+\.\d+$');

  static bool _detectDecimalComma(List<List<String>> rows) {
    var comma = 0, dot = 0;
    for (final row in rows) {
      for (final cell in row) {
        if (_decimalCommaRe.hasMatch(cell)) comma++;
        if (_decimalDotRe.hasMatch(cell)) dot++;
      }
    }
    return comma > dot;
  }

  /// Cella → szám, a tizedesjel-beállítás figyelembevételével.
  static double? parseNum(String cell, bool decimalComma) {
    var s = cell.trim();
    if (s.isEmpty) return null;
    if (decimalComma) {
      s = s.replaceAll(' ', '').replaceAll(',', '.');
    }
    return double.tryParse(s);
  }

  static double _numericRatio(List<String> row, bool decimalComma) {
    if (row.isEmpty) return 0;
    final n = row.where((c) => parseNum(c, decimalComma) != null).length;
    return n / row.length;
  }

  // ── Szerepfelismerés ─────────────────────────────────────────────

  static List<ColumnRole> _guessRoles(
      List<String> headers, List<List<String>> rows, bool decimalComma) {
    final n = headers.length;
    final sample = rows.take(200).toList();
    final roles = List<ColumnRole>.filled(n, ColumnRole.ignore);

    // Oszloponkénti statisztika.
    final stats = List.generate(n, (c) {
      final values = <double>[];
      final raw = <String>[];
      for (final row in sample) {
        if (c >= row.length || row[c].isEmpty) continue;
        raw.add(row[c]);
        final v = parseNum(row[c], decimalComma);
        if (v != null) values.add(v);
      }
      final distinct = raw.toSet().length;
      final allInt =
          values.isNotEmpty && values.every((v) => v == v.roundToDouble());
      double share(bool Function(double) test) =>
          values.isEmpty ? 0 : values.where(test).length / values.length;
      return (
        numericRatio: raw.isEmpty ? 0.0 : values.length / raw.length,
        values: values,
        distinct: distinct,
        count: raw.length,
        allInt: allInt,
        eovY: share(_isEovY),
        eovX: share(_isEovX),
        lat: share(_isLat),
        lon: share(_isLon),
        elev: share(_isElev),
      );
    });

    String h(int c) => headers[c].toLowerCase();
    bool hHas(int c, List<String> keys) => keys.any((k) => h(c).contains(k));

    // 1. Koordináták — tartomány alapján (95% feletti találat kell),
    //    fejlécnév csak döntetlennél számít.
    int pick(double Function(int) score, List<String> headerKeys) {
      var best = -1;
      var bestScore = 0.94;
      for (var c = 0; c < n; c++) {
        if (roles[c] != ColumnRole.ignore) continue;
        var s = score(c);
        if (s > 0.94 && hHas(c, headerKeys)) s += 0.05;
        if (s > bestScore) {
          bestScore = s;
          best = c;
        }
      }
      return best;
    }

    final cEovY = pick((c) => stats[c].eovY, ['y', 'kelet', 'east']);
    if (cEovY >= 0) roles[cEovY] = ColumnRole.eovY;
    final cEovX = pick((c) => stats[c].eovX, ['x', 'észak', 'eszak', 'north']);
    if (cEovX >= 0) roles[cEovX] = ColumnRole.eovX;
    final cLat = pick((c) => stats[c].lat, ['lat', 'fi', 'szél', 'szel']);
    if (cLat >= 0) roles[cLat] = ColumnRole.lat;
    final cLon = pick((c) => stats[c].lon, ['lon', 'lambda', 'hossz']);
    if (cLon >= 0) roles[cLon] = ColumnRole.lon;

    // 2. Magasság: numerikus, elfogadható tartomány, NEM egész-azonosító
    //    jellegű; fejléc segít.
    for (var c = 0; c < n; c++) {
      if (roles[c] != ColumnRole.ignore) continue;
      final s = stats[c];
      if (s.numericRatio > 0.9 &&
          s.elev > 0.94 &&
          (hHas(c, ['z', 'mag', 'elev', 'h']) || !s.allInt)) {
        roles[c] = ColumnRole.elevation;
        break;
      }
    }

    // 3. Állomás/pontszám: egész, (közel) egyedi értékek.
    var bestStation = -1;
    var bestUnique = 0.9;
    for (var c = 0; c < n; c++) {
      if (roles[c] != ColumnRole.ignore) continue;
      final s = stats[c];
      if (s.numericRatio > 0.95 && s.allInt && s.count > 0) {
        var unique = s.distinct / s.count;
        if (hHas(c, ['psz', 'pont', 'station', 'áll', 'all', 'id'])) {
          unique += 0.05;
        }
        if (unique > bestUnique) {
          bestUnique = unique;
          bestStation = c;
        }
      }
    }
    if (bestStation >= 0) roles[bestStation] = ColumnRole.station;

    // 4. Vonal: kevés egyedi értékű oszlop (a pontszám sokszorosa tartozik
    //    egy vonalhoz); fejléc segít.
    for (var c = 0; c < n; c++) {
      if (roles[c] != ColumnRole.ignore) continue;
      final s = stats[c];
      final lowCardinality =
          s.count >= 10 && s.distinct <= (s.count / 4).ceil();
      if (hHas(c, ['line', 'vonal', 'ln']) || lowCardinality) {
        roles[c] = ColumnRole.line;
        break;
      }
    }

    // 5. Név: az első még szabad, többségében szöveges oszlop.
    for (var c = 0; c < n; c++) {
      if (roles[c] != ColumnRole.ignore) continue;
      if (stats[c].numericRatio < 0.5 && stats[c].count > 0) {
        roles[c] = ColumnRole.name;
        break;
      }
    }

    return roles;
  }

  // ═════════════════════════════════════════════════════════════════
  // Pontok építése a megfeleltetés alapján
  // ═════════════════════════════════════════════════════════════════

  /// A (kézzel jóváhagyott) szerep-kiosztás alapján felépíti a pontokat.
  /// Visszaadja a pontokat és a kihagyott (hibás) sorok számát.
  static ({List<StakeoutPoint> points, int skipped}) buildPoints({
    required CsvPreview preview,
    required List<ColumnRole> roles,
    required int projectId,
  }) {
    int col(ColumnRole r) => roles.indexOf(r);
    final cStation = col(ColumnRole.station);
    final cLine = col(ColumnRole.line);
    final cName = col(ColumnRole.name);
    final cEovY = col(ColumnRole.eovY);
    final cEovX = col(ColumnRole.eovX);
    final cLat = col(ColumnRole.lat);
    final cLon = col(ColumnRole.lon);
    final cElev = col(ColumnRole.elevation);

    final isEov = cEovY >= 0 && cEovX >= 0;
    if (!isEov && !(cLat >= 0 && cLon >= 0)) {
      throw const FormatException(
          'Hiányzó koordináta-oszlopok: EOV Y+X vagy lat+lon kell.');
    }

    final conv = CoordConverterService.to;
    final points = <StakeoutPoint>[];
    var skipped = 0;
    var autoStation = 1;

    for (final row in preview.allRows) {
      double? get(int c) => c >= 0 && c < row.length
          ? parseNum(row[c], preview.decimalComma)
          : null;

      double eovY, eovX, lat, lon;
      if (isEov) {
        final y = get(cEovY), x = get(cEovX);
        if (y == null || x == null || !_isEovY(y) || !_isEovX(x)) {
          skipped++;
          continue;
        }
        eovY = y;
        eovX = x;
        final p = conv.eovToWgsPoint(y, x);
        lon = p.x;
        lat = p.y;
      } else {
        final la = get(cLat), lo = get(cLon);
        if (la == null || lo == null || !_isLat(la) || !_isLon(lo)) {
          skipped++;
          continue;
        }
        lat = la;
        lon = lo;
        final p = conv.wgsToEovPoint(lo, la);
        eovY = p.x;
        eovX = p.y;
      }

      final stationVal = get(cStation);
      final station = stationVal?.round() ?? autoStation++;
      final line = cLine >= 0 && cLine < row.length ? row[cLine].trim() : '';
      final name = cName >= 0 && cName < row.length && row[cName].isNotEmpty
          ? row[cName].trim()
          : station.toString();

      points.add(StakeoutPoint(
        projectId: projectId,
        lineId: line,
        station: station,
        name: name,
        source: 'csv',
        planEovY: eovY,
        planEovX: eovX,
        planEovZ: get(cElev),
        planLat: lat,
        planLon: lon,
      ));
    }
    return (points: points, skipped: skipped);
  }

  // ═════════════════════════════════════════════════════════════════
  // GeoJSON
  // ═════════════════════════════════════════════════════════════════

  /// GeoJSON → CsvPreview-kompatibilis táblázat: a property-k az oszlopok,
  /// plusz a kinyert lon/lat. Így ugyanaz az előnézeti/megfeleltetési
  /// képernyő szolgálja ki, mint a CSV-t.
  static Future<CsvPreview> analyzeGeojson(File file) async {
    final json = jsonDecode(await file.readAsString());
    final features = (json['features'] as List?) ?? [];

    final propKeys = <String>{};
    final rows = <List<String>>[];
    for (final f in features) {
      final geom = f['geometry'];
      if (geom == null || geom['type'] != 'Point') continue;
      propKeys.addAll(
          ((f['properties'] as Map?) ?? {}).keys.map((k) => k.toString()));
    }
    final keys = propKeys.toList();

    for (final f in features) {
      final geom = f['geometry'];
      if (geom == null || geom['type'] != 'Point') continue;
      final coords = geom['coordinates'] as List;
      final props = (f['properties'] as Map?) ?? {};
      rows.add([
        for (final k in keys) '${props[k] ?? ''}',
        '${coords[0]}', // lon
        '${coords[1]}', // lat
        if (coords.length > 2) '${coords[2]}' else '',
      ]);
    }
    if (rows.isEmpty) {
      throw const FormatException('A GeoJSON nem tartalmaz Point elemet.');
    }

    final headers = [...keys, 'lon', 'lat', 'z'];
    final roles = _guessRoles(headers, rows, false);
    // A GeoJSON szabvány szerint mindig WGS84 — a koordináta-oszlopokat
    // ismerjük, kényszerítjük.
    roles[headers.length - 3] = ColumnRole.lon;
    roles[headers.length - 2] = ColumnRole.lat;
    if (rows.any((r) => r.last.isNotEmpty)) {
      roles[headers.length - 1] = ColumnRole.elevation;
    }

    return CsvPreview(
      fileName: file.uri.pathSegments.last,
      delimiter: ',',
      hasHeader: true,
      decimalComma: false,
      headers: headers,
      sampleRows: rows.take(6).toList(),
      allRows: rows,
      guessedRoles: roles,
      detectedCrs: 'WGS84',
      badRowCount: 0,
    );
  }
}
