import 'dart:async';
import 'dart:math' as math;

import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:terepi_seged/controls/geoid_grid.dart';
import 'package:terepi_seged/services/app_database.dart';
import 'package:terepi_seged/services/coord_converter_service.dart';
import 'package:terepi_seged/services/gnss/gnss_service.dart';
import 'package:terepi_seged/services/project_service.dart';
import 'package:terepi_seged/services/tilt_service.dart';
import 'package:wakelock_plus/wakelock_plus.dart';

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

/// Kitűzési sorrend.
enum StakeoutOrder { idAsc, idDesc, nearest }

/// Eltérés-kijelzési mód a közeli fázisban.
enum DeviationMode {
  line('Inline / crossline'),
  travel('Haladási irány'),
  northEast('Észak / kelet');

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

/// A kitűzés "agya": célpont-kezelés, sorrend, vonal-geometria,
/// eltérés-számítás, tárolás, haptikus visszajelzés.
///
/// Az egyetlen térképnézetbe illeszkedik: a MapSurveyController a
/// setMode()-ban hívja a [setActive]-ot (mode == MapSurveyMode.stakeout),
/// a service pedig csak aktív állapotban dolgozik (haptika, fázis).
/// Minden adatbázisművelet az AppDatabase-en keresztül megy.
///
/// Geometria: minden számítás EOV-SÍKBAN, méterben. A crossline irány a
/// szomszédos állomások szakaszaiból SZÁMÍTÓDIK — a vonal első pontjánál
/// az első szakasz hátrafelé, az utolsónál az utolsó szakasz előre
/// extrapolálásával, belső töréspontnál a két irány átlagával; virtuális
/// segédpontot nem tárolunk.
class StakeoutService extends GetxService {
  static StakeoutService get to => Get.find();

  AppDatabase get _db => AppDatabase.instance;

  // ── Mód-aktiválás ────────────────────────────────────────────────
  final active = false.obs;

  /// A MapSurveyController hívja módváltáskor.
  Future<void> setActive(bool value) async {
    if (active.value == value) return;
    active.value = value;
    if (value) {
      WakelockPlus.enable();
      if (Get.isRegistered<TiltService>()) TiltService.to.setActive(true);
      await load();
      _applyPhase();
      _scheduleHaptic();
    } else {
      _hapticTimer?.cancel();
      WakelockPlus.disable();
      if (Get.isRegistered<TiltService>()) TiltService.to.setActive(false);
    }
  }

  // ── Állapot ──────────────────────────────────────────────────────
  final points = <StakeoutPoint>[].obs;
  final target = Rxn<StakeoutPoint>();
  final orderMode = StakeoutOrder.idAsc.obs;
  final deviationMode = DeviationMode.line.obs;

  /// Tűrés (m) — tárolásnál és a céltábla belső körénél.
  final toleranceXY = 0.03.obs;

  /// Közeli fázis (céltábla-nézet); automatikus 5 m alatt, kézzel
  /// felülbírálható.
  final nearPhase = false.obs;
  static const nearPhaseDistance = 5.0;
  bool? _manualPhase;

  final hapticsEnabled = true.obs;

  final soundEnabled = true.obs;

  /// Átlagolás tároláskor
  final averagingSeconds = 0.obs;
  final isAveraging = false.obs;
  final averagingElapsed = 0.obs;

  /// Max. megengedett crossline eltolás (m; 0=nincs korlát)
  final maxOffset = 0.0.obs;

  late GeoidGrid geoidGrid;

  // ── Aktuális pozíció (EOV) és navigációs értékek ────────────────
  final hasPosition = false.obs;
  final curEovY = 0.0.obs;
  final curEovX = 0.0.obs;
  final curAlt = 0.0.obs;

  final distance = 0.0.obs;
  final bearingToTarget = 0.0.obs; // fok, EOV-észak = 0, óramutató
  final travelHeading = Rxn<double>(); // null = állunk

  /// Eltérés az aktuális [deviationMode] szerint: előre(+)/hátra(−),
  /// jobbra(+)/balra(−); dz: fel(+)/le(−).
  final devForward = 0.0.obs;
  final devRight = 0.0.obs;
  final devDz = Rxn<double>();
  final withinTolerance = false.obs;

  double? _histY, _histX;
  Timer? _hapticTimer;
  bool _toleranceAnnounced = false;

  @override
  void onInit() async {
    super.onInit();
    await _loadSettings();
    geoidGrid = await GeoidGrid.load('assets/Grids/geoid_eht2014.gtx');

    if (Get.isRegistered<GnssService>()) {
      ever(GnssService.to.lastGgaLine, (_) => _onPosition());
    }
  }

  @override
  void onClose() {
    _hapticTimer?.cancel();
    super.onClose();
  }

  Future<void> _loadSettings() async {
    final prefs = await SharedPreferences.getInstance();
    toleranceXY.value = prefs.getDouble('stakeout_tolerance') ?? 0.03;
    averagingSeconds.value = prefs.getInt('stakeout_avg_seconds') ?? 0;
    hapticsEnabled.value = prefs.getBool('stakeout_haptics') ?? true;
    soundEnabled.value = prefs.getBool('stakeout_sound') ?? true;
    maxOffset.value = prefs.getDouble('stakeout_max_offset') ?? 0;
  }

  Future<void> saveSettings() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setDouble('stakeout_tolerance', toleranceXY.value);
    await prefs.setInt('stakeout_avg_seconds', averagingSeconds.value);
    await prefs.setBool('stakeout_haptics', hapticsEnabled.value);
    await prefs.setBool('stakeout_sound', soundEnabled.value);
    await prefs.setDouble('stakeout_max_offset', maxOffset.value);
  }

  // ═════════════════════════════════════════════════════════════════
  // Betöltés / cél-kezelés
  // ═════════════════════════════════════════════════════════════════

  Future<void> load() async {
    final projectId = ProjectService.to.activeProjectId;
    if (projectId == null) {
      points.clear();
      target.value = null;
      return;
    }
    points.value = await _db.listStakeoutPoints(projectId);
    if (target.value == null ||
        !points.any((p) => p.uuid == target.value!.uuid)) {
      target.value = _firstPending();
    }
    _recompute();
  }

  List<String> get lines =>
      points.map((p) => p.lineId).toSet().toList()..sort();

  Map<String, ({int total, int staked})> get lineProgress {
    final m = <String, ({int total, int staked})>{};
    for (final p in points) {
      final cur = m[p.lineId] ?? (total: 0, staked: 0);
      m[p.lineId] = (
        total: cur.total + 1,
        staked: cur.staked + (p.status == StakeoutStatus.staked ? 1 : 0),
      );
    }
    return m;
  }

  void setTarget(StakeoutPoint p) {
    target.value = p;
    _manualPhase = null; // új célnál vissza automatikus fázisra
    _toleranceAnnounced = false;
    _recompute();
  }

  void togglePhase() {
    _manualPhase = !nearPhase.value;
    _applyPhase();
  }

  StakeoutPoint? _firstPending() {
    final pending =
        points.where((p) => p.status == StakeoutStatus.pending).toList();
    if (pending.isEmpty) return null;
    pending.sort((a, b) => a.lineId != b.lineId
        ? a.lineId.compareTo(b.lineId)
        : a.station.compareTo(b.station));
    return orderMode.value == StakeoutOrder.idDesc
        ? pending.last
        : pending.first;
  }

  /// Következő cél a sorrend-mód szerint — a CÉL VONALÁN belül lép,
  /// elfogyva a következő vonalra.
  StakeoutPoint? nextTarget({bool backwards = false}) {
    final cur = target.value;
    if (cur == null) return _firstPending();
    final pending = points
        .where((p) => p.status == StakeoutStatus.pending && p.uuid != cur.uuid)
        .toList();
    if (pending.isEmpty) return null;

    final sameLine = pending.where((p) => p.lineId == cur.lineId).toList()
      ..sort((a, b) => a.station.compareTo(b.station));

    switch (orderMode.value) {
      case StakeoutOrder.nearest:
        final pool = sameLine.isNotEmpty ? sameLine : pending;
        pool.sort((a, b) => _distTo(a).compareTo(_distTo(b)));
        return pool.first;

      case StakeoutOrder.idAsc:
      case StakeoutOrder.idDesc:
        final asc = (orderMode.value == StakeoutOrder.idAsc) != backwards;
        final candidates = sameLine.where(
            (p) => asc ? p.station > cur.station : p.station < cur.station);
        if (candidates.isNotEmpty) {
          return asc
              ? candidates.reduce((a, b) => a.station < b.station ? a : b)
              : candidates.reduce((a, b) => a.station > b.station ? a : b);
        }
        final others = pending.where((p) => p.lineId != cur.lineId).toList();
        if (others.isEmpty) return null;
        others.sort((a, b) => a.lineId != b.lineId
            ? a.lineId.compareTo(b.lineId)
            : a.station.compareTo(b.station));
        return asc ? others.first : others.last;
    }
  }

  void advance({bool backwards = false}) {
    final n = nextTarget(backwards: backwards);
    if (n != null) setTarget(n);
  }

  double _distTo(StakeoutPoint p) {
    final dy = p.planEovY - curEovY.value;
    final dx = p.planEovX - curEovX.value;
    return math.sqrt(dy * dy + dx * dx);
  }

  // ═════════════════════════════════════════════════════════════════
  // Pozíció + eltérés
  // ═════════════════════════════════════════════════════════════════

  void _onPosition() {
    final gnss = GnssService.to;
    if (gnss.gpsQuality.value <= 0 ||
        gnss.latitude.value == 0 ||
        !Get.isRegistered<CoordConverterService>()) {
      return;
    }
    final lat = gnss.latitude.value;
    final lon = gnss.longitude.value;
    final alt = gnss.altitude.value;
    final sep = gnss.geoidSeparation.value;

    final p = CoordConverterService.to.wgsToEovPoint(lon, lat);
    curEovY.value = p.x;
    curEovX.value = p.y;

    curAlt.value = geoidGrid.toEovHeight(lat, lon, alt, sep)!;

    //curAlt.value = gnss.altitude.value;
    hasPosition.value = true;

    // Haladási irány: legalább 0,5 m elmozdulásból (állva zajos lenne).
    if (_histY != null) {
      final dy = curEovY.value - _histY!;
      final dx = curEovX.value - _histX!;
      if (math.sqrt(dy * dy + dx * dx) >= 0.5) {
        travelHeading.value = _bearingDeg(dy, dx);
        _histY = curEovY.value;
        _histX = curEovX.value;
      }
    } else {
      _histY = curEovY.value;
      _histX = curEovX.value;
    }

    _recompute();
  }

  void _recompute() {
    final t = target.value;
    if (t == null || !hasPosition.value) {
      withinTolerance.value = false;
      return;
    }

    final dy = t.planEovY - curEovY.value;
    final dx = t.planEovX - curEovX.value;
    distance.value = math.sqrt(dy * dy + dx * dx);
    bearingToTarget.value = _bearingDeg(dy, dx);

    final double fwdBearing;
    switch (deviationMode.value) {
      case DeviationMode.line:
        fwdBearing = lineBearingAt(t) ?? travelHeading.value ?? 0;
      case DeviationMode.travel:
        fwdBearing = travelHeading.value ?? 0;
      case DeviationMode.northEast:
        fwdBearing = 0;
    }

    final rad = fwdBearing * math.pi / 180;
    devForward.value = dy * math.sin(rad) + dx * math.cos(rad);
    devRight.value = dy * math.cos(rad) - dx * math.sin(rad);
    devDz.value = t.planEovZ != null ? t.planEovZ! - curAlt.value : null;

    final wasWithin = withinTolerance.value;
    withinTolerance.value = distance.value <= toleranceXY.value;

    if (active.value) {
      _applyPhase();
      if (withinTolerance.value && !wasWithin && !_toleranceAnnounced) {
        _toleranceAnnounced = true;
        if (hapticsEnabled.value) HapticFeedback.heavyImpact();
        if (soundEnabled.value) SystemSound.play(SystemSoundType.alert);
      } else if (!withinTolerance.value) {
        _toleranceAnnounced = false;
      }
      _scheduleHaptic();
    }
  }

  void _applyPhase() {
    nearPhase.value = _manualPhase ??
        (hasPosition.value &&
            target.value != null &&
            distance.value <= nearPhaseDistance);
  }

  /// Haptikus "geiger": közeledve sűrűsödő pulzus, tűrésen belül gyors.
  void _scheduleHaptic() {
    _hapticTimer?.cancel();
    if (!active.value ||
        !hapticsEnabled.value ||
        !hasPosition.value ||
        target.value == null) {
      return;
    }
    final d = distance.value;
    if (d > 30) return;

    final ms = withinTolerance.value
        ? 150
        : d <= 1
            ? 250
            : d <= 3
                ? 450
                : d <= 10
                    ? 800
                    : 1500;

    _hapticTimer = Timer(Duration(milliseconds: ms), () {
      if (withinTolerance.value) {
        HapticFeedback.mediumImpact();
      } else {
        HapticFeedback.lightImpact();
      }
      if (soundEnabled.value) SystemSound.play(SystemSoundType.click);
      _scheduleHaptic();
    });
  }

  static double _bearingDeg(double dy, double dx) {
    final b = math.atan2(dy, dx) * 180 / math.pi;
    return (b + 360) % 360;
  }

  // ── Vonal-geometria ──────────────────────────────────────────────

  List<StakeoutPoint> _linePoints(String lineId) {
    final lp = points.where((p) => p.lineId == lineId && !p.isOffset).toList()
      ..sort((a, b) => a.station.compareTo(b.station));
    return lp;
  }

  /// A vonal iránya (fok) az adott pontnál, a station-növekedés felé.
  double? lineBearingAt(StakeoutPoint p) {
    final lp = _linePoints(p.lineId);
    if (lp.length < 2) return null;
    var idx = lp.indexWhere((e) => e.uuid == p.uuid);
    if (idx < 0) {
      // Eltolt pont: a legközelebbi vonalpont szerint.
      var bestD = double.infinity;
      for (var k = 0; k < lp.length; k++) {
        final d = math.pow(lp[k].planEovY - p.planEovY, 2) +
            math.pow(lp[k].planEovX - p.planEovX, 2);
        if (d < bestD) {
          bestD = d.toDouble();
          idx = k;
        }
      }
    }

    double segBearing(StakeoutPoint a, StakeoutPoint b) =>
        _bearingDeg(b.planEovY - a.planEovY, b.planEovX - a.planEovX);

    if (idx == 0) return segBearing(lp[0], lp[1]);
    if (idx == lp.length - 1) return segBearing(lp[idx - 1], lp[idx]);
    final b1 = segBearing(lp[idx - 1], lp[idx]) * math.pi / 180;
    final b2 = segBearing(lp[idx], lp[idx + 1]) * math.pi / 180;
    return _bearingDeg(
        math.sin(b1) + math.sin(b2), math.cos(b1) + math.cos(b2));
  }

  // ═════════════════════════════════════════════════════════════════
  // Műveletek (adatbázis: AppDatabase)
  // ═════════════════════════════════════════════════════════════════

  /// Tárolás a jelenlegi (vagy átlagolt) mért pozícióval. Ha az
  /// [averagingSeconds] > 0, a megadott ideig gyűjti és átlagolja a
  /// pozíciókat — csökkenti a pillanatnyi zaj hatását. A rekordba az
  /// eltérés mindig vonal-relatívan (inline/crossline) kerül; vonal
  /// híján É/K bontásban.
  Future<StakeoutPoint?> storeCurrent() async {
    final t = target.value;
    if (t == null || !hasPosition.value || isAveraging.value) return null;
    final gnss = GnssService.to;

    double useY = curEovY.value;
    double useX = curEovX.value;
    double useAlt = curAlt.value;

    if (averagingSeconds.value > 0) {
      final avg = await _collectAverage(averagingSeconds.value);
      useY = avg.y;
      useX = avg.x;
      useAlt = avg.alt;
    }

    final dy = t.planEovY - useY;
    final dx = t.planEovX - useX;
    final lineBearing = lineBearingAt(t);
    final rad = (lineBearing ?? 0) * math.pi / 180;
    final inline =
        lineBearing != null ? dy * math.sin(rad) + dx * math.cos(rad) : dx;
    final crossline =
        lineBearing != null ? dy * math.cos(rad) - dx * math.sin(rad) : dy;

    final w = CoordConverterService.to.eovToWgsPoint(useY, useX);

    final updated = t.copyWith(
      status: StakeoutStatus.staked,
      measuredEovY: useY,
      measuredEovX: useX,
      measuredEovZ: useAlt,
      measuredLat: w.y,
      measuredLon: w.x,
      devInline: inline,
      devCrossline: crossline,
      devDz: t.planEovZ != null ? t.planEovZ! - useAlt : null,
      tiltDeg: Get.isRegistered<TiltService>() &&
              TiltService.to.isCalibrated.value &&
              !TiltService.to.isMoving.value
          ? TiltService.to.tiltDeg.value
          : null,
      fixQuality: gnss.gpsQuality.value,
      accuracy: gnss.horizontalAccuracy,
      stakedAt: DateTime.now(),
    );
    await _db.updateStakeoutPoint(updated);
    final i = points.indexWhere((p) => p.uuid == t.uuid);
    if (i >= 0) points[i] = updated;
    points.refresh();
    return updated;
  }

  /// Pozíció-átlagolás: [seconds] másodpercig minden GGA-pozíciót
  /// gyűjt, majd átlagol. Ha nem jött minta, a pillanatnyi értéket adja.
  Future<({double y, double x, double alt})> _collectAverage(
      int seconds) async {
    isAveraging.value = true;
    averagingElapsed.value = 0;
    final ys = <double>[], xs = <double>[], alts = <double>[];

    final worker = ever(curEovY, (_) {
      ys.add(curEovY.value);
      xs.add(curEovX.value);
      alts.add(curAlt.value);
    });
    final ticker = Timer.periodic(
        const Duration(seconds: 1), (_) => averagingElapsed.value++);
    try {
      await Future.delayed(Duration(seconds: seconds));
    } finally {
      worker.dispose();
      ticker.cancel();
      isAveraging.value = false;
    }

    if (ys.isEmpty) {
      return (y: curEovY.value, x: curEovX.value, alt: curAlt.value);
    }
    double mean(List<double> l) => l.reduce((a, b) => a + b) / l.length;
    return (y: mean(ys), x: mean(xs), alt: mean(alts));
  }

  Future<void> skipCurrent() async {
    final t = target.value;
    if (t == null) return;
    final updated = t.copyWith(status: StakeoutStatus.skipped);
    await _db.updateStakeoutPoint(updated);
    final i = points.indexWhere((p) => p.uuid == t.uuid);
    if (i >= 0) points[i] = updated;
    points.refresh();
    advance();
  }

  /// Státusz-állítás a listából (kihagyás / visszaállítás).
  Future<void> setPointStatus(StakeoutPoint p, StakeoutStatus status) async {
    final updated = p.copyWith(status: status);
    await _db.updateStakeoutPoint(updated);
    final i = points.indexWhere((e) => e.uuid == p.uuid);
    if (i >= 0) points[i] = updated;
    points.refresh();
    if (target.value?.uuid == p.uuid && status != StakeoutStatus.pending) {
      advance();
    }
  }

  /// Pont törlése (soft delete — a szinkron viszi a többi eszközre is).
  Future<void> deletePoint(StakeoutPoint p) async {
    await _db.softDeleteStakeoutPoint(p.id!);
    points.removeWhere((e) => e.uuid == p.uuid);
    if (target.value?.uuid == p.uuid) {
      target.value = _firstPending();
      _recompute();
    }
  }

  /// Transzverzális eltolt pont: a vonalra merőlegesen [dist] méterre
  /// ([toRight] = jobbra a station-növekedés irányából nézve). Az új
  /// pont lesz a cél; az eltolás-vektor a rekordba kerül.
  Future<StakeoutPoint?> createOffset(
      {required double dist, required bool toRight}) async {
    final t = target.value;
    if (t == null) return null;

    final base = lineBearingAt(t) ?? travelHeading.value ?? 0;
    final bearing = (base + (toRight ? 90 : -90) + 360) % 360;
    final rad = bearing * math.pi / 180;
    final eovY = t.planEovY + dist * math.sin(rad);
    final eovX = t.planEovX + dist * math.cos(rad);
    final w = CoordConverterService.to.eovToWgsPoint(eovY, eovX);

    final offset = StakeoutPoint(
      projectId: t.projectId,
      lineId: t.lineId,
      station: t.station,
      name: '${t.name}/E',
      pointType: t.pointType,
      source: 'offset',
      planEovY: eovY,
      planEovX: eovX,
      planEovZ: t.planEovZ,
      planLat: w.y,
      planLon: w.x,
      isOffset: true,
      parentUuid: t.uuid,
      offsetDist: dist,
      offsetBearing: bearing,
    );
    final id = await _db.insertStakeoutPoint(offset);
    final saved = offset.copyWith(id: id);
    points.add(saved);
    setTarget(saved);
    return saved;
  }
}
