import 'dart:async';

import 'package:watch_bridge_contract/watch_bridge_contract.dart';

import '../../application/use_cases.dart';
import '../../application/watch_companion_use_cases.dart';
import 'native_watch_bridge_channel.dart';

final class WatchWearDataLayerAdapter
    implements WatchProjectionPublisher, WatchAlertPublisher {
  WatchWearDataLayerAdapter({
    required WatchBridgeNativeChannel nativeChannel,
    required WatchCommandIngress commandIngress,
    required WatchProjectionSource projectionSource,
    WorkoutHistoryUseCases? workoutHistoryUseCases,
    ActiveWorkoutSensorUseCases? activeWorkoutSensorUseCases,
    WorkoutTelemetryUseCases? workoutTelemetryUseCases,
    Duration projectionRefreshInterval = const Duration(seconds: 5),
  }) : _nativeChannel = nativeChannel,
       _commandIngress = commandIngress,
       _projectionSource = projectionSource,
       _workoutHistoryUseCases = workoutHistoryUseCases,
       _activeWorkoutSensorUseCases = activeWorkoutSensorUseCases,
       _workoutTelemetryUseCases = workoutTelemetryUseCases,
       _projectionRefreshInterval = projectionRefreshInterval;

  final WatchBridgeNativeChannel _nativeChannel;
  final WatchCommandIngress _commandIngress;
  final WatchProjectionSource _projectionSource;
  final WorkoutHistoryUseCases? _workoutHistoryUseCases;
  final ActiveWorkoutSensorUseCases? _activeWorkoutSensorUseCases;
  final WorkoutTelemetryUseCases? _workoutTelemetryUseCases;
  final Duration _projectionRefreshInterval;
  final _commandAcks = <_WatchAdapterCommandKey, WatchCommandAck>{};
  final _subscriptions = <StreamSubscription<dynamic>>[];
  Future<void> _commandTail = Future<void>.value();
  Timer? _projectionRefreshTimer;
  WatchSessionProjection? _latestProjection;
  WatchSessionProjection? _lastPublishedProjection;
  bool _skipNextProjectionEmissionForForcedResync = false;
  int? _lastPublishedProjectionRevision;
  bool _started = false;
  bool _foregroundActive = false;

  Future<void> start() async {
    if (_started) {
      return;
    }
    _started = true;
    _ensureProjectionRefreshLoop();
    _subscriptions.add(
      _projectionSource.projections.listen((projection) {
        if (_skipNextProjectionEmissionForForcedResync) {
          _skipNextProjectionEmissionForForcedResync = false;
          return;
        }
        unawaited(publish(projection));
      }),
    );
    _subscriptions.add(
      _nativeChannel.commands.listen((command) {
        unawaited(_enqueueCommand(command));
      }),
    );
    final workoutHistoryUseCases = _workoutHistoryUseCases;
    if (workoutHistoryUseCases != null) {
      _subscriptions.add(
        _nativeChannel.sensorSummaries.listen((summary) {
          unawaited(workoutHistoryUseCases.updateHeartRateSummary(summary));
        }),
      );
    }
    final activeWorkoutSensorUseCases = _activeWorkoutSensorUseCases;
    final workoutTelemetryUseCases = _workoutTelemetryUseCases;
    if (activeWorkoutSensorUseCases != null ||
        workoutTelemetryUseCases != null) {
      _subscriptions.add(
        _nativeChannel.sensorSamples.listen((sample) {
          activeWorkoutSensorUseCases?.recordTelemetrySample(sample);
          unawaited(workoutTelemetryUseCases?.recordTelemetrySample(sample));
        }),
      );
    }
    _subscriptions.add(
      _nativeChannel.connectionEvents.listen((event) {
        if (event.isReachable || event.requestsResync) {
          unawaited(_forceProjectionResync());
        }
      }),
    );
    await _projectionSource.emitCurrentProjection();
    await _nativeChannel.requestCapabilityRefresh();
  }

  Future<void> stop() async {
    _projectionRefreshTimer?.cancel();
    _projectionRefreshTimer = null;
    for (final subscription in _subscriptions) {
      await subscription.cancel();
    }
    _subscriptions.clear();
    _started = false;
  }

  @override
  Future<void> publish(
    WatchSessionProjection projection, {
    bool urgent = true,
  }) async {
    await _publishProjection(projection, urgent: urgent, force: false);
  }

  Future<void> _publishProjection(
    WatchSessionProjection projection, {
    required bool urgent,
    required bool force,
  }) async {
    final previousProjection = _latestProjection;
    _latestProjection = projection;
    if (projection.phase == WatchSessionPhase.noActiveSession) {
      final previousSessionId = previousProjection?.deviceSessionId;
      if (previousSessionId != null && previousSessionId.isNotEmpty) {
        _activeWorkoutSensorUseCases?.clear(previousSessionId);
      }
    }
    if (!force &&
        _lastPublishedProjection != null &&
        _hasSameSignificantProjectionState(
          _lastPublishedProjection!,
          projection,
        )) {
      await _syncForegroundService(projection);
      return;
    }
    final revisionChanged =
        _lastPublishedProjectionRevision != projection.revision;
    await _nativeChannel.publishProjection(
      projection,
      urgent: force ? urgent : urgent && revisionChanged,
    );
    _lastPublishedProjection = projection;
    _lastPublishedProjectionRevision = projection.revision;
    await _syncForegroundService(projection);
  }

  @override
  Future<void> publishAlert(WatchAlertEnvelope alert) {
    return _nativeChannel.publishAlert(alert);
  }

  Future<void> _enqueueCommand(WatchCommandEnvelope command) {
    final run = _commandTail.then(
      (_) => _handleCommand(command),
      onError: (_) => _handleCommand(command),
    );
    _commandTail = run.then((_) {}, onError: (_) {});
    return run;
  }

  Future<void> _handleCommand(WatchCommandEnvelope command) async {
    final key = _WatchAdapterCommandKey(command);
    final cachedAck = _commandAcks[key];
    if (cachedAck != null) {
      await _sendAck(command, WatchCommandAck.acceptedNoOp);
      return;
    }
    final ack = await _commandIngress.dispatch(command);
    if (ack == WatchCommandAck.accepted ||
        ack == WatchCommandAck.acceptedNoOp) {
      _rememberAck(key, ack);
    }
    await _sendAck(command, ack);
  }

  Future<void> _sendAck(
    WatchCommandEnvelope command,
    WatchCommandAck ack,
  ) async {
    int? revisionAtAck;
    try {
      revisionAtAck = (await _projectionSource.currentProjection()).revision;
    } on Exception {
      revisionAtAck = _latestProjection?.revision;
    }
    await _nativeChannel.sendCommandAck(
      command,
      ack,
      revisionAtAck: revisionAtAck,
    );
  }

  void _rememberAck(_WatchAdapterCommandKey key, WatchCommandAck ack) {
    _commandAcks[key] = ack;
    if (_commandAcks.length <= 128) {
      return;
    }
    _commandAcks.remove(_commandAcks.keys.first);
  }

  Future<void> _syncForegroundService(WatchSessionProjection projection) async {
    final shouldRun =
        projection.phase != WatchSessionPhase.noActiveSession &&
        projection.deviceSessionId.isNotEmpty;
    if (shouldRun == _foregroundActive) {
      return;
    }
    _foregroundActive = shouldRun;
    if (shouldRun) {
      await _nativeChannel.startForegroundService();
    } else {
      await _nativeChannel.stopForegroundService();
    }
  }

  void _ensureProjectionRefreshLoop() {
    _projectionRefreshTimer ??= Timer.periodic(_projectionRefreshInterval, (_) {
      unawaited(_publishHeartbeat());
    });
  }

  Future<void> _publishHeartbeat() async {
    final projection = await _emitCurrentProjectionSkippingSourceEcho();
    await _publishProjection(projection, urgent: false, force: true);
  }

  Future<void> _forceProjectionResync() async {
    final projection = await _emitCurrentProjectionSkippingSourceEcho();
    await _publishProjection(projection, urgent: true, force: true);
  }

  Future<WatchSessionProjection>
  _emitCurrentProjectionSkippingSourceEcho() async {
    _skipNextProjectionEmissionForForcedResync = true;
    try {
      return await _projectionSource.emitCurrentProjection();
    } finally {
      unawaited(
        Future<void>.delayed(Duration.zero, () {
          _skipNextProjectionEmissionForForcedResync = false;
        }),
      );
    }
  }
}

bool _hasSameSignificantProjectionState(
  WatchSessionProjection left,
  WatchSessionProjection right,
) {
  return left.schemaVersion == right.schemaVersion &&
      left.deviceSessionId == right.deviceSessionId &&
      left.revision == right.revision &&
      left.phase == right.phase &&
      left.phoneReachable == right.phoneReachable &&
      left.seriesIndex == right.seriesIndex &&
      left.seriesTotal == right.seriesTotal &&
      left.exerciseName == right.exerciseName &&
      left.programIndex == right.programIndex &&
      left.exerciseIndex == right.exerciseIndex &&
      left.setIndex == right.setIndex &&
      left.passageIndex == right.passageIndex &&
      left.passageTotal == right.passageTotal &&
      left.stepIndex == right.stepIndex &&
      left.stepTotal == right.stepTotal &&
      left.stepName == right.stepName &&
      left.stepType == right.stepType &&
      left.stepTargetValue == right.stepTargetValue &&
      _hasSameSignificantTimerState(left.dominantTimer, right.dominantTimer) &&
      _hasSameSignificantTimerListState(
        left.secondaryTimers,
        right.secondaryTimers,
      ) &&
      left.primaryAction == right.primaryAction &&
      _listEquals(left.secondaryActions, right.secondaryActions) &&
      left.nextExerciseName == right.nextExerciseName &&
      left.statusLabel == right.statusLabel &&
      left.hasManualScore == right.hasManualScore &&
      left.currentManualScoreValue == right.currentManualScoreValue &&
      left.canDecrementScore == right.canDecrementScore &&
      left.manualScoreTargetValue == right.manualScoreTargetValue &&
      left.manualScoreTargetLabel == right.manualScoreTargetLabel &&
      left.manualScoreRepsTargetValue == right.manualScoreRepsTargetValue &&
      left.manualScoreScope == right.manualScoreScope;
}

bool _hasSameSignificantTimerListState(
  List<WatchTimerProjection> left,
  List<WatchTimerProjection> right,
) {
  if (left.length != right.length) {
    return false;
  }
  for (var index = 0; index < left.length; index += 1) {
    if (!_hasSameSignificantTimerState(left[index], right[index])) {
      return false;
    }
  }
  return true;
}

bool _hasSameSignificantTimerState(
  WatchTimerProjection? left,
  WatchTimerProjection? right,
) {
  if (left == null || right == null) {
    return left == right;
  }
  return left.kind == right.kind &&
      left.label == right.label &&
      left.displayMode == right.displayMode &&
      left.runState == right.runState &&
      left.targetMs == right.targetMs;
}

bool _listEquals<T>(List<T> left, List<T> right) {
  if (left.length != right.length) {
    return false;
  }
  for (var index = 0; index < left.length; index += 1) {
    if (left[index] != right[index]) {
      return false;
    }
  }
  return true;
}

final class _WatchAdapterCommandKey {
  _WatchAdapterCommandKey(WatchCommandEnvelope command)
    : sessionId = command.sessionId,
      expectedRevision = command.expectedRevision,
      commandId = command.commandId,
      type = command.type;

  final String sessionId;
  final int expectedRevision;
  final String commandId;
  final WatchCommandType type;

  @override
  bool operator ==(Object other) {
    return identical(this, other) ||
        other is _WatchAdapterCommandKey &&
            sessionId == other.sessionId &&
            expectedRevision == other.expectedRevision &&
            commandId == other.commandId &&
            type == other.type;
  }

  @override
  int get hashCode => Object.hash(sessionId, expectedRevision, commandId, type);
}
