// ============================================================
// nymea_client.dart
// Client JSON-RPC WebSocket pour nymea — etm_powersync_app
//
// Protocole : JSON-RPC 2.0 sur WebSocket (port 2222)
// Namespace plugin : "NymeaEnergy" (v0–8)
// ============================================================

import 'dart:async';
import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/io.dart';

// ─────────────────────────────────────────────
// Modèles de données
// ─────────────────────────────────────────────

enum ChargingMode { normal, eco, ecoWithTargetTime }

enum ChargingState { idle, surplusCharging, spotMarketCharging, timeRequirement }

class ChargingInfo {
  final String evChargerId;
  final ChargingMode chargingMode;
  final ChargingState chargingState;
  final bool spotMarketChargingEnabled;
  final int dailySpotMarketPercentage;
  final int targetPercentage;
  final String? endDateTime;

  ChargingInfo({
    required this.evChargerId,
    required this.chargingMode,
    required this.chargingState,
    required this.spotMarketChargingEnabled,
    required this.dailySpotMarketPercentage,
    required this.targetPercentage,
    this.endDateTime,
  });

  factory ChargingInfo.fromJson(Map<String, dynamic> json) {
    final modeMap = {
      'ChargingModeNormal': ChargingMode.normal,
      'ChargingModeEco': ChargingMode.eco,
      'ChargingModeEcoWithTargetTime': ChargingMode.ecoWithTargetTime,
    };
    final stateMap = {
      'ChargingStateIdle': ChargingState.idle,
      'ChargingStateSurplusCharging': ChargingState.surplusCharging,
      'ChargingStateSpotMarketCharging': ChargingState.spotMarketCharging,
      'ChargingStateTimeRequirement': ChargingState.timeRequirement,
    };
    return ChargingInfo(
      evChargerId: json['evChargerId'] ?? '',
      chargingMode: modeMap[json['chargingMode']] ?? ChargingMode.normal,
      chargingState: stateMap[json['chargingState']] ?? ChargingState.idle,
      spotMarketChargingEnabled: json['spotMarketChargingEnabled'] ?? false,
      dailySpotMarketPercentage: json['dailySpotMarketPercentage'] ?? 80,
      targetPercentage: json['targetPercentage'] ?? 80,
      endDateTime: json['endDateTime'],
    );
  }
}

class ScoreEntry {
  final DateTime startDateTime;
  final DateTime endDateTime;
  final double value;
  final double weighting; // 0.0 (pire) → 1.0 (meilleur)

  ScoreEntry({
    required this.startDateTime,
    required this.endDateTime,
    required this.value,
    required this.weighting,
  });

  factory ScoreEntry.fromJson(Map<String, dynamic> json) {
    return ScoreEntry(
      startDateTime: DateTime.parse(json['startDateTime']),
      endDateTime: DateTime.parse(json['endDateTime']),
      value: (json['value'] as num).toDouble(),
      weighting: (json['weighting'] as num).toDouble(),
    );
  }
}

// ─────────────────────────────────────────────
// Client nymea principal
// ─────────────────────────────────────────────

class NymeaClient {
  final String host;
  final int port;

  WebSocketChannel? _channel;
  int _requestId = 1;
  String? _token;
  bool _authenticated = false;

  // Map id → Completer en attente de réponse
  final Map<int, Completer<Map<String, dynamic>>> _pendingRequests = {};

  // StreamController pour les notifications push
  final StreamController<Map<String, dynamic>> _notificationController =
      StreamController.broadcast();

  Stream<Map<String, dynamic>> get notifications =>
      _notificationController.stream;

  NymeaClient({required this.host, this.port = 2222});

  // ─── Connexion ───────────────────────────────

  Future<void> connect() async {
    final uri = Uri.parse('ws://$host:$port');
    _channel = IOWebSocketChannel.connect(uri);

    _channel!.stream.listen(
      (data) => _onMessage(data as String),
      onError: (e) => print('[nymea] Erreur WebSocket: $e'),
      onDone: () => print('[nymea] Connexion fermée'),
    );

    // Attendre le Hello initial de nymea
    await _waitForHello();
    print('[nymea] Connecté à $host:$port');
  }

  Future<void> _waitForHello() async {
    // nymea envoie spontanément un Hello dès la connexion
    // On attend simplement 500ms que la connexion s'établisse
    await Future.delayed(const Duration(milliseconds: 500));
  }

  void _onMessage(String raw) {
    final msg = jsonDecode(raw) as Map<String, dynamic>;

    if (msg.containsKey('id') && _pendingRequests.containsKey(msg['id'])) {
      // Réponse à une requête
      _pendingRequests[msg['id']]!.complete(msg);
      _pendingRequests.remove(msg['id']);
    } else if (msg.containsKey('notification')) {
      // Notification push
      _notificationController.add(msg);
    }
  }

  Future<Map<String, dynamic>> _send(
      String method, Map<String, dynamic> params) async {
    final id = _requestId++;
    final completer = Completer<Map<String, dynamic>>();
    _pendingRequests[id] = completer;

    final request = {'id': id, 'method': method, 'params': params};
    if (_token != null) {
      request['token'] = _token!;
    }

    _channel!.sink.add(jsonEncode(request));
    return completer.future.timeout(
      const Duration(seconds: 10),
      onTimeout: () => throw TimeoutException('Timeout pour $method'),
    );
  }

  // ─── Authentification ────────────────────────

  /// Crée un compte utilisateur (première utilisation uniquement)
  Future<bool> createUser(String username, String password) async {
    final response = await _send('JSONRPC.CreateUser', {
      'username': username,
      'password': password,
    });
    return response['params']?['error'] == null;
  }

  /// Authentifie la connexion et récupère un token
  Future<String?> authenticate(
      String username, String password, String deviceName) async {
    final response = await _send('JSONRPC.Authenticate', {
      'username': username,
      'password': password,
      'deviceName': deviceName,
    });

    final params = response['params'] as Map<String, dynamic>?;
    if (params?['success'] == true) {
      _token = params!['token'] as String;
      _authenticated = true;
      print('[nymea] Authentifié — token: ${_token!.substring(0, 8)}...');
      return _token;
    }
    print('[nymea] Échec authentification: ${params?['error']}');
    return null;
  }

  /// Connexion avec token existant (sessions suivantes)
  Future<bool> authenticateWithToken(String token) async {
    _token = token;
    final response = await _send('JSONRPC.IsTokenValid', {'token': token});
    final valid = response['params']?['valid'] == true;
    if (valid) _authenticated = true;
    return valid;
  }

  // ─── Abonnement aux notifications ───────────

  Future<void> subscribeToNotifications(List<String> namespaces) async {
    await _send('JSONRPC.SetNotificationStatus', {
      'namespaces': namespaces,
    });
    print('[nymea] Abonné aux namespaces: $namespaces');
  }

  // ─── Things ──────────────────────────────────

  /// Récupère tous les Things du système
  Future<List<Map<String, dynamic>>> getThings() async {
    final response = await _send('Integrations.GetThings', {});
    final things = response['params']?['things'] as List<dynamic>? ?? [];
    return things.cast<Map<String, dynamic>>();
  }

  /// Filtre les Things qui implémentent une interface donnée
  Future<List<Map<String, dynamic>>> getThingsByInterface(
      String interface) async {
    final allThings = await getThings();
    // nymea retourne les interfaces dans thingClass.interfaces
    // Pour filtrer, on utilise Integrations.GetThingClasses
    final classResponse = await _send('Integrations.GetThingClasses', {});
    final classes = classResponse['params']?['thingClasses']
            as List<dynamic>? ??
        [];

    // Construire un Set des classIds qui supportent l'interface
    final matchingClassIds = <String>{};
    for (final tc in classes.cast<Map<String, dynamic>>()) {
      final interfaces = (tc['interfaces'] as List<dynamic>?)?.cast<String>() ?? [];
      if (interfaces.contains(interface)) {
        matchingClassIds.add(tc['id'] as String);
      }
    }

    return allThings
        .where((t) => matchingClassIds.contains(t['thingClassId']))
        .toList();
  }

  /// Lit la valeur d'un State
  Future<dynamic> getStateValue(String thingId, String stateTypeId) async {
    final response = await _send('Integrations.GetStateValue', {
      'thingId': thingId,
      'stateTypeId': stateTypeId,
    });
    return response['params']?['value'];
  }

  /// Exécute une Action sur un Thing
  Future<String> executeAction({
    required String thingId,
    required String actionTypeId,
    Map<String, dynamic> params = const {},
  }) async {
    final actionParams = params.entries
        .map((e) => {'paramTypeId': e.key, 'value': e.value})
        .toList();

    final response = await _send('Integrations.ExecuteAction', {
      'thingId': thingId,
      'actionTypeId': actionTypeId,
      'params': actionParams,
    });
    return response['params']?['thingError'] ?? 'ThingErrorNoError';
  }

  // ─── API NymeaEnergy ─────────────────────────

  /// Récupère la limite de courant par phase
  Future<int> getPhasePowerLimit() async {
    final response = await _send('NymeaEnergy.GetPhasePowerLimit', {});
    return response['params']?['phasePowerLimit'] as int? ?? 0;
  }

  /// Définit la limite de courant par phase (0 = désactivé)
  Future<String> setPhasePowerLimit(int ampere) async {
    final response = await _send(
        'NymeaEnergy.SetPhasePowerLimit', {'phasePowerLimit': ampere});
    return response['params']?['energyError'] ?? 'EnergyErrorNoError';
  }

  /// Récupère les infos de charge de tous les chargeurs
  Future<List<ChargingInfo>> getChargingInfos({String? evChargerId}) async {
    final params = <String, dynamic>{};
    if (evChargerId != null) params['evChargerId'] = evChargerId;

    final response = await _send('NymeaEnergy.GetChargingInfos', params);
    final infos =
        response['params']?['chargingInfos'] as List<dynamic>? ?? [];
    return infos
        .cast<Map<String, dynamic>>()
        .map(ChargingInfo.fromJson)
        .toList();
  }

  /// Modifie la config de charge d'un chargeur
  Future<String> setChargingInfo({
    required String evChargerId,
    ChargingMode? mode,
    bool? spotMarketEnabled,
    int? dailySpotMarketPercentage,
    int? targetPercentage,
    String? endDateTime,
  }) async {
    final modeNames = {
      ChargingMode.normal: 'ChargingModeNormal',
      ChargingMode.eco: 'ChargingModeEco',
      ChargingMode.ecoWithTargetTime: 'ChargingModeEcoWithTargetTime',
    };

    final chargingInfoMap = <String, dynamic>{
      'evChargerId': evChargerId,
    };
    if (mode != null) chargingInfoMap['chargingMode'] = modeNames[mode];
    if (spotMarketEnabled != null) {
      chargingInfoMap['spotMarketChargingEnabled'] = spotMarketEnabled;
    }
    if (dailySpotMarketPercentage != null) {
      chargingInfoMap['dailySpotMarketPercentage'] = dailySpotMarketPercentage;
    }
    if (targetPercentage != null) {
      chargingInfoMap['targetPercentage'] = targetPercentage;
    }
    if (endDateTime != null) chargingInfoMap['endDateTime'] = endDateTime;

    final response = await _send(
        'NymeaEnergy.SetChargingInfo', {'chargingInfo': chargingInfoMap});
    return response['params']?['energyError'] ?? 'EnergyErrorNoError';
  }

  /// Récupère les fournisseurs spot market disponibles
  Future<List<Map<String, dynamic>>> getAvailableSpotMarketProviders() async {
    final response =
        await _send('NymeaEnergy.GetAvailableSpotMarketProviders', {});
    return (response['params']?['providers'] as List<dynamic>? ?? [])
        .cast<Map<String, dynamic>>();
  }

  /// Active le spot market avec un fournisseur
  Future<String> setSpotMarketConfiguration({
    required bool enabled,
    String? providerId,
  }) async {
    final params = <String, dynamic>{'enabled': enabled};
    if (providerId != null) params['providerId'] = providerId;

    final response =
        await _send('NymeaEnergy.SetSpotMarketConfiguration', params);
    return response['params']?['energyError'] ?? 'EnergyErrorNoError';
  }

  /// Récupère les scores de prix spot (0=pire, 1=meilleur)
  Future<List<ScoreEntry>> getSpotMarketScoreEntries() async {
    final response =
        await _send('NymeaEnergy.GetSpotMarketScoreEntries', {});
    final entries =
        response['params']?['spotMarketScoreEntries'] as List<dynamic>? ?? [];
    return entries.cast<Map<String, dynamic>>().map(ScoreEntry.fromJson).toList();
  }

  /// Récupère les plannings de charge calculés
  Future<List<Map<String, dynamic>>> getChargingSchedules() async {
    final response = await _send('NymeaEnergy.GetChargingSchedules', {});
    return (response['params']?['chargingSchedules'] as List<dynamic>? ?? [])
        .cast<Map<String, dynamic>>();
  }

  // ─── Écoute des Events en temps réel ─────────

  /// Stream filtré sur un type de notification NymeaEnergy
  Stream<Map<String, dynamic>> onNotification(String notificationName) {
    return notifications.where((msg) =>
        msg['notification'] == 'NymeaEnergy.$notificationName');
  }

  Stream<ChargingInfo> get onChargingInfoChanged {
    return onNotification('ChargingInfoChanged').map((msg) =>
        ChargingInfo.fromJson(
            msg['params']?['chargingInfo'] as Map<String, dynamic>));
  }

  Stream<List<ScoreEntry>> get onSpotMarketScoreEntriesChanged {
    return onNotification('SpotMarketScoreEntriesChanged').map((msg) {
      final entries = msg['params']?['spotMarketScoreEntries']
              as List<dynamic>? ??
          [];
      return entries
          .cast<Map<String, dynamic>>()
          .map(ScoreEntry.fromJson)
          .toList();
    });
  }

  Stream<List<Map<String, dynamic>>> get onChargingSchedulesChanged {
    return onNotification('ChargingSchedulesChanged').map((msg) =>
        (msg['params']?['chargingSchedules'] as List<dynamic>? ?? [])
            .cast<Map<String, dynamic>>());
  }

  // ─── Fermeture ───────────────────────────────

  Future<void> disconnect() async {
    await _channel?.sink.close();
    await _notificationController.close();
  }
}
