import 'dart:async';
import 'dart:convert';

import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../models/ble_device.dart';
import '../models/bp_reading.dart';
import '../services/health_ble_service.dart';
import 'auth_provider.dart';
import 'data_providers.dart';

const _boundDevicesKey = 'bound_ble_devices';
const _readingFingerprintsKey = 'ble_reading_fingerprints';
const _legacyBpMacKey = 'bp_device_mac';
const _legacyBpNameKey = 'bp_device_name';
const _legacyBpLastSyncKey = 'bp_last_sync';

final healthBleServiceProvider = Provider<HealthBleService>((ref) {
  final service = HealthBleService();
  ref.onDispose(service.dispose);
  return service;
});

class DeviceBindState {
  final List<BoundBleDevice> devices;
  final bool isConnected;

  const DeviceBindState({this.devices = const [], this.isConnected = false});

  bool get isBound => devices.isNotEmpty;

  DeviceBindState copyWith({List<BoundBleDevice>? devices, bool? isConnected}) {
    return DeviceBindState(
      devices: devices ?? this.devices,
      isConnected: isConnected ?? this.isConnected,
    );
  }

  List<BoundBleDevice> devicesOfType(BleDeviceType type) {
    return devices.where((device) => device.type == type).toList();
  }

  BoundBleDevice? findById(String id) {
    for (final device in devices) {
      if (device.id == id) return device;
    }
    return null;
  }
}

class DeviceBindNotifier extends Notifier<DeviceBindState> {
  StreamSubscription<bool>? _connSub;

  @override
  DeviceBindState build() {
    ref.onDispose(() => _connSub?.cancel());
    _loadBinding();
    _listenConnection();
    return const DeviceBindState();
  }

  void _listenConnection() {
    _connSub?.cancel();
    _connSub = ref.read(healthBleServiceProvider).connectionState.listen((
      connected,
    ) {
      if (state.isConnected != connected) {
        state = state.copyWith(isConnected: connected);
      }
    });
  }

  Future<void> _loadBinding() async {
    final db = ref.read(localDbProvider);
    await _migrateLegacyBloodPressureDevice();
    final raw = await db.read(_boundDevicesKey);
    final devices = _decodeDevices(raw);
    state = state.copyWith(devices: devices);
  }

  Future<void> _migrateLegacyBloodPressureDevice() async {
    final db = ref.read(localDbProvider);
    final existing = await db.read(_boundDevicesKey);
    final legacyMac = await db.read(_legacyBpMacKey);
    if (existing != null || legacyMac == null || legacyMac.isEmpty) return;

    final legacyName = await db.read(_legacyBpNameKey);
    final migrated = BoundBleDevice(
      id: legacyMac,
      name: legacyName?.isNotEmpty == true ? legacyName! : '血压计',
      type: BleDeviceType.bloodPressure,
      serviceUuid: BleDeviceType.bloodPressure.serviceUuid,
    );
    await db.write(_boundDevicesKey, jsonEncode([migrated.toJson()]));
    await db.delete(_legacyBpMacKey);
    await db.delete(_legacyBpNameKey);
    await db.delete(_legacyBpLastSyncKey);
  }

  List<BoundBleDevice> _decodeDevices(String? raw) {
    if (raw == null || raw.isEmpty) return [];
    try {
      final decoded = jsonDecode(raw);
      if (decoded is! List) return [];
      return decoded
          .whereType<Map>()
          .map(
            (item) => BoundBleDevice.fromJson(Map<String, dynamic>.from(item)),
          )
          .whereType<BoundBleDevice>()
          .toList();
    } catch (_) {
      return [];
    }
  }

  Future<void> _saveDevices(List<BoundBleDevice> devices) async {
    final db = ref.read(localDbProvider);
    await db.write(
      _boundDevicesKey,
      jsonEncode(devices.map((device) => device.toJson()).toList()),
    );
    state = state.copyWith(devices: devices);
  }

  Future<void> bind(BoundBleDevice device) async {
    final devices = [...state.devices];
    final index = devices.indexWhere((item) => item.id == device.id);
    if (index >= 0) {
      devices[index] = device.copyWith(lastSyncAt: devices[index].lastSyncAt);
    } else {
      devices.add(device);
    }
    await _saveDevices(devices);
  }

  Future<void> unbind(String id) async {
    await _saveDevices(
      state.devices.where((device) => device.id != id).toList(),
    );
  }

  Future<void> recordSuccessfulSync({
    required BoundBleDevice device,
    required BpReading reading,
  }) async {
    await _rememberReadingFingerprint(device, reading);
    final syncedAt = DateTime.now();
    final devices = state.devices.map((item) {
      if (item.id != device.id) return item;
      return item.copyWith(lastSyncAt: syncedAt);
    }).toList();
    await _saveDevices(devices);
    ref.invalidate(latestHealthProvider);
  }

  Future<bool> isDuplicateReading(
    BoundBleDevice device,
    BpReading reading,
  ) async {
    final fingerprints = await _loadFingerprints();
    final now = DateTime.now();
    final pruned = _pruneFingerprints(fingerprints, now);
    if (pruned.length != fingerprints.length) {
      await _saveFingerprints(pruned);
    }

    final key = _readingFingerprint(device, reading);
    final lastSeenRaw = pruned[key];
    if (lastSeenRaw == null) return false;
    final lastSeen = DateTime.tryParse(lastSeenRaw)?.toLocal();
    if (lastSeen == null) return false;
    if (reading.hasDeviceTimestamp) return true;
    return now.difference(lastSeen).inSeconds < 10;
  }

  Future<void> _rememberReadingFingerprint(
    BoundBleDevice device,
    BpReading reading,
  ) async {
    final now = DateTime.now();
    final fingerprints = _pruneFingerprints(await _loadFingerprints(), now);
    fingerprints[_readingFingerprint(device, reading)] = now
        .toUtc()
        .toIso8601String();
    await _saveFingerprints(fingerprints);
  }

  Future<Map<String, String>> _loadFingerprints() async {
    final db = ref.read(localDbProvider);
    final raw = await db.read(_readingFingerprintsKey);
    if (raw == null || raw.isEmpty) return {};
    try {
      final decoded = jsonDecode(raw);
      if (decoded is! Map) return {};
      return decoded.map(
        (key, value) => MapEntry(key.toString(), value.toString()),
      );
    } catch (_) {
      return {};
    }
  }

  Future<void> _saveFingerprints(Map<String, String> fingerprints) async {
    final db = ref.read(localDbProvider);
    await db.write(_readingFingerprintsKey, jsonEncode(fingerprints));
  }

  Map<String, String> _pruneFingerprints(
    Map<String, String> fingerprints,
    DateTime now,
  ) {
    final pruned = <String, String>{};
    for (final entry in fingerprints.entries) {
      final seenAt = DateTime.tryParse(entry.value)?.toLocal();
      if (seenAt == null) continue;
      if (now.difference(seenAt).inHours <= 24) {
        pruned[entry.key] = entry.value;
      }
    }
    return pruned;
  }

  String _readingFingerprint(BoundBleDevice device, BpReading reading) {
    final timePart = reading.hasDeviceTimestamp
        ? reading.timestamp.toUtc().toIso8601String()
        : 'no-device-time';
    return [
      device.id,
      device.type.code,
      timePart,
      reading.systolic,
      reading.diastolic,
      reading.pulse ?? 0,
    ].join('|');
  }
}

final omronDeviceProvider =
    NotifierProvider<DeviceBindNotifier, DeviceBindState>(
      DeviceBindNotifier.new,
    );
