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

import 'package:get/get.dart';
import 'package:sensors_plus/sensors_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';

/// "Telefon rúd-libella" — a kitűzőrúd függőlegességének jelzése a
/// telefon gyorsulásmérőjéből.
///
/// NEM tilt-kompenzáció (a pozíciót nem javítja), hanem tilt-JELZÉS:
/// a fizikai szelencés libellát váltja ki a képernyőn. Előny az
/// iránytűvel szemben: csak a gravitáció irányát méri, ezért a vevő és
/// a fém rúd MÁGNESES zavara nem érinti.
///
/// Kalibráció (mert a telefontartó sosem áll pontosan a rúd tengelyében):
///  1. lépés: rúd függőlegesen a szelencés libella szerint → 2 s átlagolt
///     gravitáció-vektor = referencia.
///  2. lépés (opcionális, pontosabb): ugyanez a telefon rúd-tengely
///     körüli 180°-os átforgatása után — a két vektor átlagában a tartó
///     ferdesége kiesik, a valódi rúd-tengely marad.
///
/// Használhatósági részletek:
///  * MOZGÁS-detektálás: séta közben a gyorsulásmérő a lépések
///    gyorsulását is méri, a "gravitáció" iránya hamis → a jelzés
///    ilyenkor inaktív (szürke), csak megálláskor élesedik. Ez egybeesik
///    a munkafolyamattal: a libella a tárolás előtti pillanatban számít.
///  * SIMÍTÁS: exponenciális aluláteresztő szűrő, ~10 Hz-es kijelzési
///    frissítés — enélkül a buborék idegesítően remegne.
///  * A dőlésből számolt VÍZSZINTES HIBA a beszédes érték:
///    hiba ≈ rúdmagasság × sin(dőlés) — 2 m-es rúdnál 0,5° már ~1,7 cm.
class TiltService extends GetxService {
  static TiltService get to => Get.find();

  // ── Reaktív állapot ──────────────────────────────────────────────
  final active = false.obs;
  final isCalibrated = false.obs;
  final isMoving = false.obs;
  final isCapturing = false.obs; // kalibrációs mintavétel fut

  /// Dőlés a kalibrált rúd-tengelyhez képest (fok).
  final tiltDeg = 0.0.obs;

  /// A buborék 2D iránya a telefon képernyő-síkjában (egység: a dőlés
  /// szinusza; a widget skálázza). x: jobbra +, y: felfelé +.
  final bubbleX = 0.0.obs;
  final bubbleY = 0.0.obs;

  /// Rúdmagasság (m) — a vízszintes hiba számításához; szerkeszthető.
  final poleHeight = 2.0.obs;

  /// Dőlésből számolt vízszintes eltolódás (cm).
  double get horizontalErrorCm =>
      poleHeight.value * math.sin(tiltDeg.value * math.pi / 180) * 100;

  // ── Belső állapot ────────────────────────────────────────────────
  StreamSubscription? _sub;

  // Referencia (rúd-tengely) egységvektor a telefon koordinátáiban.
  double _refX = 0, _refY = 0, _refZ = 1;

  // Szűrt gyorsulás-vektor.
  double _fx = 0, _fy = 0, _fz = 0;
  bool _filterSeeded = false;
  static const _alpha = 0.15; // aluláteresztő szűrő együttható

  // Mozgás-detektálás: |a| szórása csúszóablakban.
  final _magWindow = <double>[];
  static const _windowLen = 25; // ~0,5 s 50 Hz-en
  static const _movingStdThreshold = 0.35; // m/s²

  DateTime _lastUiPush = DateTime.fromMillisecondsSinceEpoch(0);

  @override
  Future<void> onInit() async {
    super.onInit();
    final prefs = await SharedPreferences.getInstance();
    poleHeight.value = prefs.getDouble('tilt_pole_height') ?? 2.0;
    final refJson = prefs.getString('tilt_reference');
    if (refJson != null) {
      final r = jsonDecode(refJson) as Map<String, dynamic>;
      _refX = (r['x'] as num).toDouble();
      _refY = (r['y'] as num).toDouble();
      _refZ = (r['z'] as num).toDouble();
      isCalibrated.value = true;
    }
  }

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

  /// A szenzor-stream csak aktív kitűzés/mérés módban fut (akku-kímélés).
  void setActive(bool value) {
    if (active.value == value) return;
    active.value = value;
    if (value) {
      _filterSeeded = false;
      _magWindow.clear();
      _sub = accelerometerEventStream(
              samplingPeriod: const Duration(milliseconds: 20))
          .listen(_onSample);
    } else {
      _sub?.cancel();
      _sub = null;
    }
  }

  void _onSample(AccelerometerEvent e) {
    // Szűrés.
    if (!_filterSeeded) {
      _fx = e.x;
      _fy = e.y;
      _fz = e.z;
      _filterSeeded = true;
    } else {
      _fx += _alpha * (e.x - _fx);
      _fy += _alpha * (e.y - _fy);
      _fz += _alpha * (e.z - _fz);
    }

    // Mozgás-detektálás a NYERS magnitúdó szórásából.
    final mag = math.sqrt(e.x * e.x + e.y * e.y + e.z * e.z);
    _magWindow.add(mag);
    if (_magWindow.length > _windowLen) _magWindow.removeAt(0);
    if (_magWindow.length >= _windowLen ~/ 2) {
      final mean = _magWindow.reduce((a, b) => a + b) / _magWindow.length;
      final varSum =
          _magWindow.fold<double>(0, (s, v) => s + (v - mean) * (v - mean));
      final std = math.sqrt(varSum / _magWindow.length);
      isMoving.value = std > _movingStdThreshold;
    }

    // UI-frissítés max. ~12 Hz-en.
    final now = DateTime.now();
    if (now.difference(_lastUiPush).inMilliseconds < 80) return;
    _lastUiPush = now;

    _updateTilt();
  }

  void _updateTilt() {
    final fmag = math.sqrt(_fx * _fx + _fy * _fy + _fz * _fz);
    if (fmag < 1e-6) return;

    // Dőlés = a szűrt gravitáció és a referencia-tengely szöge.
    final dot = (_fx * _refX + _fy * _refY + _fz * _refZ) / fmag;
    tiltDeg.value = math.acos(dot.clamp(-1.0, 1.0)) * 180 / math.pi;

    // Buborék-irány: a gravitáció referencia-síkra vetített komponense,
    // a telefon képernyő-tengelyeiben. (x: jobbra, y: a képernyőn felfelé
    // — a szenzor y-tengelye a képernyőn felfelé mutat.)
    final gx = _fx / fmag, gy = _fy / fmag, gz = _fz / fmag;
    final proj = gx * _refX + gy * _refY + gz * _refZ;
    bubbleX.value = gx - proj * _refX;
    bubbleY.value = gy - proj * _refY;
    // A z-komponenst a 2D buborék nem jeleníti meg — függőleges tartónál
    // (telefon képernyője ~vízszintes normálissal) x/y hordozza az infót.
  }

  // ═════════════════════════════════════════════════════════════════
  // Kalibráció
  // ═════════════════════════════════════════════════════════════════

  /// [seconds] másodpercig átlagolja a NYERS gravitáció-vektort.
  /// Mozgás esetén a minta zajos lesz — a hívó UI figyelmeztet előtte.
  Future<({double x, double y, double z})> captureVector(
      {int seconds = 2}) async {
    isCapturing.value = true;
    double sx = 0, sy = 0, sz = 0;
    var n = 0;
    final sub = accelerometerEventStream(
            samplingPeriod: const Duration(milliseconds: 20))
        .listen((e) {
      sx += e.x;
      sy += e.y;
      sz += e.z;
      n++;
    });
    try {
      await Future.delayed(Duration(seconds: seconds));
    } finally {
      await sub.cancel();
      isCapturing.value = false;
    }
    if (n == 0) return (x: 0.0, y: 0.0, z: 1.0);
    final mag = math.sqrt(sx * sx + sy * sy + sz * sz);
    return (x: sx / mag, y: sy / mag, z: sz / mag);
  }

  /// Referencia mentése. Egy lépésnél [v2] = null; két lépésnél
  /// (180°-os átforgatás) a két egységvektor ÁTLAGA a rúd-tengely —
  /// a tartó ferdesége a két mérésben ellentétes előjelű, így kiesik.
  Future<void> saveReference(({double x, double y, double z}) v1,
      [({double x, double y, double z})? v2]) async {
    double x = v1.x, y = v1.y, z = v1.z;
    if (v2 != null) {
      x = (v1.x + v2.x) / 2;
      y = (v1.y + v2.y) / 2;
      z = (v1.z + v2.z) / 2;
    }
    final mag = math.sqrt(x * x + y * y + z * z);
    if (mag < 1e-6) return;
    _refX = x / mag;
    _refY = y / mag;
    _refZ = z / mag;
    isCalibrated.value = true;

    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(
        'tilt_reference', jsonEncode({'x': _refX, 'y': _refY, 'z': _refZ}));
  }

  Future<void> setPoleHeight(double meters) async {
    if (meters <= 0) return;
    poleHeight.value = meters;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setDouble('tilt_pole_height', meters);
  }

  Future<void> clearCalibration() async {
    isCalibrated.value = false;
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove('tilt_reference');
  }
}
