import 'dart:async';

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;
}

abstract interface class WatchBridgeNativeChannel {
  Stream<WatchCommandEnvelope> get commands;

  Stream<WatchSensorSummary> get sensorSummaries;

  Stream<WatchSensorSample> get sensorSamples;

  Stream<WatchBridgeConnectionEvent> get connectionEvents;

  Future<void> publishProjection(
    WatchSessionProjection projection, {
    bool urgent = true,
  });

  Future<void> publishAlert(WatchAlertEnvelope alert);

  Future<void> sendCommandAck(
    WatchCommandEnvelope command,
    WatchCommandAck ack, {
    int? revisionAtAck,
  });

  Future<void> requestCapabilityRefresh();

  Future<void> startForegroundService();

  Future<void> stopForegroundService();
}

final class MethodChannelWatchBridgeNativeChannel
    implements WatchBridgeNativeChannel {
  const MethodChannelWatchBridgeNativeChannel({
    MethodChannel methodChannel = const MethodChannel(_methodChannelName),
    EventChannel commandChannel = const EventChannel(_commandChannelName),
    EventChannel sensorSummaryChannel = const EventChannel(
      _sensorSummaryChannelName,
    ),
    EventChannel sensorSampleChannel = const EventChannel(
      _sensorSampleChannelName,
    ),
    EventChannel connectionChannel = const EventChannel(_connectionChannelName),
  }) : _methodChannel = methodChannel,
       _commandChannel = commandChannel,
       _sensorSummaryChannel = sensorSummaryChannel,
       _sensorSampleChannel = sensorSampleChannel,
       _connectionChannel = connectionChannel;

  static const _methodChannelName = 'gametime.watch_bridge/methods';
  static const _commandChannelName = 'gametime.watch_bridge/commands';
  static const _sensorSummaryChannelName =
      'gametime.watch_bridge/sensor_summaries';
  static const _sensorSampleChannelName =
      'gametime.watch_bridge/sensor_samples';
  static const _connectionChannelName = 'gametime.watch_bridge/connection';

  final MethodChannel _methodChannel;
  final EventChannel _commandChannel;
  final EventChannel _sensorSummaryChannel;
  final EventChannel _sensorSampleChannel;
  final EventChannel _connectionChannel;

  @override
  Stream<WatchCommandEnvelope> get commands {
    return _commandChannel
        .receiveBroadcastStream()
        .where((event) {
          return event is Map;
        })
        .map((event) {
          return WatchCommandEnvelope.fromJson(_stringObjectMap(event));
        });
  }

  @override
  Stream<WatchSensorSummary> get sensorSummaries {
    return _sensorSummaryChannel
        .receiveBroadcastStream()
        .where((event) {
          return event is Map;
        })
        .map((event) {
          return WatchSensorSummary.fromJson(_stringObjectMap(event));
        });
  }

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

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

  @override
  Future<void> publishProjection(
    WatchSessionProjection projection, {
    bool urgent = true,
  }) {
    return _invokeIgnoringMissingPlugin('publishProjection', {
      ...projection.toJson(),
      'urgent': urgent,
    });
  }

  @override
  Future<void> publishAlert(WatchAlertEnvelope alert) {
    return _invokeIgnoringMissingPlugin('publishAlert', alert.toJson());
  }

  @override
  Future<void> requestCapabilityRefresh() {
    return _invokeIgnoringMissingPlugin('requestCapabilityRefresh');
  }

  @override
  Future<void> sendCommandAck(
    WatchCommandEnvelope command,
    WatchCommandAck ack, {
    int? revisionAtAck,
  }) {
    return _invokeIgnoringMissingPlugin('sendCommandAck', {
      'schemaVersion': watchBridgeSchemaVersion,
      'commandId': command.commandId,
      'sessionId': command.sessionId,
      'expectedRevision': command.expectedRevision,
      'status': ack.name,
      'revisionAtAck': revisionAtAck,
      'ackedAtEpochMs': DateTime.now().toUtc().millisecondsSinceEpoch,
    });
  }

  @override
  Future<void> startForegroundService() {
    return _invokeIgnoringMissingPlugin('startForegroundService');
  }

  @override
  Future<void> stopForegroundService() {
    return _invokeIgnoringMissingPlugin('stopForegroundService');
  }

  Future<void> _invokeIgnoringMissingPlugin(
    String method, [
    Object? arguments,
  ]) {
    return _methodChannel
        .invokeMethod<void>(method, arguments)
        .onError<MissingPluginException>((_, _) {});
  }
}

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