import 'package:flutter/services.dart';
import 'package:watch_bridge_contract/watch_bridge_contract.dart';

final class WatchBridgeConnectionEvent {
  const WatchBridgeConnectionEvent({
    required this.isReachable,
    this.requestsResync = false,
  });

  final bool isReachable;
  final bool requestsResync;
}

final class WatchCommandAckEvent {
  const WatchCommandAckEvent({
    required this.commandId,
    required this.status,
    required this.sessionId,
    this.revisionAtAck,
    this.reasonCode,
  });

  final String commandId;
  final WatchCommandAck status;
  final String sessionId;
  final int? revisionAtAck;
  final String? reasonCode;
}

abstract interface class NativeWatchBridgeClient {
  Stream<WatchSessionProjection> get projections;

  Stream<WatchSensorSample> get sensorSamples;

  Stream<WatchCommandAckEvent> get acks;

  Stream<WatchAlertEnvelope> get alerts;

  Stream<WatchBridgeConnectionEvent> get connectionEvents;

  Future<void> sendCommand(WatchCommandEnvelope command);

  Future<void> requestResync();

  Future<void> requestCapabilityRefresh();

  Future<void> invalidateActiveProjection();
}

final class MethodChannelNativeWatchBridgeClient
    implements NativeWatchBridgeClient {
  const MethodChannelNativeWatchBridgeClient({
    MethodChannel methodChannel = const MethodChannel(_methodChannelName),
    EventChannel projectionChannel = const EventChannel(_projectionChannelName),
    EventChannel sensorSampleChannel = const EventChannel(
      _sensorSampleChannelName,
    ),
    EventChannel ackChannel = const EventChannel(_ackChannelName),
    EventChannel alertChannel = const EventChannel(_alertChannelName),
    EventChannel connectionChannel = const EventChannel(_connectionChannelName),
  }) : _methodChannel = methodChannel,
       _projectionChannel = projectionChannel,
       _sensorSampleChannel = sensorSampleChannel,
       _ackChannel = ackChannel,
       _alertChannel = alertChannel,
       _connectionChannel = connectionChannel;

  static const _methodChannelName = 'gametime.watch_bridge/methods';
  static const _projectionChannelName = 'gametime.watch_bridge/projections';
  static const _sensorSampleChannelName =
      'gametime.watch_bridge/sensor_samples';
  static const _ackChannelName = 'gametime.watch_bridge/acks';
  static const _alertChannelName = 'gametime.watch_bridge/alerts';
  static const _connectionChannelName = 'gametime.watch_bridge/connection';

  final MethodChannel _methodChannel;
  final EventChannel _projectionChannel;
  final EventChannel _sensorSampleChannel;
  final EventChannel _ackChannel;
  final EventChannel _alertChannel;
  final EventChannel _connectionChannel;

  @override
  Stream<WatchSessionProjection> get projections {
    return _projectionChannel
        .receiveBroadcastStream()
        .where((event) => event is Map)
        .map((event) {
          return WatchSessionProjection.fromJson(_stringObjectMap(event));
        });
  }

  @override
  Stream<WatchSensorSample> get sensorSamples {
    return _sensorSampleChannel
        .receiveBroadcastStream()
        .where((event) => event is Map)
        .map((event) {
          return WatchSensorSample.fromJson(_stringObjectMap(event));
        });
  }

  @override
  Stream<WatchCommandAckEvent> get acks {
    return _ackChannel
        .receiveBroadcastStream()
        .where((event) => event is Map)
        .map((event) {
          final json = _stringObjectMap(event);
          return WatchCommandAckEvent(
            commandId: _stringFromJson(json['commandId']),
            status: _enumFromJson(
              json['status'],
              WatchCommandAck.values,
              WatchCommandAck.rejectedPhoneBusy,
            ),
            sessionId: _stringFromJson(json['sessionId']),
            revisionAtAck: _nullableIntFromJson(json['revisionAtAck']),
            reasonCode: _nullableStringFromJson(json['reasonCode']),
          );
        });
  }

  @override
  Stream<WatchAlertEnvelope> get alerts {
    return _alertChannel
        .receiveBroadcastStream()
        .where((event) => event is Map)
        .map((event) {
          return WatchAlertEnvelope.fromJson(_stringObjectMap(event));
        });
  }

  @override
  Stream<WatchBridgeConnectionEvent> get connectionEvents {
    return _connectionChannel
        .receiveBroadcastStream()
        .where((event) => event is Map)
        .map((event) {
          final json = _stringObjectMap(event);
          return WatchBridgeConnectionEvent(
            isReachable: json['isReachable'] == true,
            requestsResync: json['requestsResync'] == true,
          );
        });
  }

  @override
  Future<void> sendCommand(WatchCommandEnvelope command) {
    return _methodChannel.invokeMethod<void>('sendCommand', command.toJson());
  }

  @override
  Future<void> requestCapabilityRefresh() {
    return _methodChannel.invokeMethod<void>('requestCapabilityRefresh');
  }

  @override
  Future<void> requestResync() {
    return _methodChannel.invokeMethod<void>('requestResync');
  }

  @override
  Future<void> invalidateActiveProjection() {
    return _methodChannel.invokeMethod<void>('invalidateActiveProjection');
  }
}

Map<String, Object?> _stringObjectMap(Object? value) {
  if (value is Map) {
    return value.map((key, value) => MapEntry(key.toString(), value));
  }
  return const {};
}

String _stringFromJson(Object? value) {
  return value is String ? value : '';
}

String? _nullableStringFromJson(Object? value) {
  return value is String ? value : null;
}

int? _nullableIntFromJson(Object? value) {
  return value is int
      ? value
      : value is num
      ? value.toInt()
      : null;
}

T _enumFromJson<T extends Enum>(Object? value, List<T> values, T fallback) {
  if (value is String) {
    for (final enumValue in values) {
      if (enumValue.name == value) {
        return enumValue;
      }
    }
  }
  return fallback;
}
