import 'dart:convert';

import 'watch_session.dart';

/// Types of sync messages sent over the WebRTC data channel
enum SyncMessageType {
  /// Start playback
  play,

  /// Pause playback
  pause,

  /// Seek to position
  seek,

  /// Buffering state changed
  buffering,

  /// Periodic position update (for drift correction)
  positionSync,

  /// Playback rate changed
  rate,

  /// Participant joined the session
  join,

  /// Participant left the session
  leave,

  /// Session configuration (sent by host on join)
  sessionConfig,

  /// Ping for latency measurement
  ping,

  /// Pong response
  pong,

  /// Media switch (host changed content)
  mediaSwitch,

  /// Host exited the video player
  hostExitedPlayer,

  /// Player is ready (attached and loaded)
  playerReady,

  /// Request session config from host (guest recovery)
  requestSessionConfig,
}

/// A message sent over the WebRTC data channel for synchronization
class SyncMessage {
  /// Type of this message
  final SyncMessageType type;

  /// Timestamp when this message was created (Unix ms)
  final int timestamp;

  /// Position in milliseconds (for seek, positionSync)
  final int? positionMs;

  /// Buffering state (for buffering message)
  final bool? bufferingState;

  /// Playback rate (for rate message)
  final double? rate;

  /// Peer ID of the sender
  final String? peerId;

  /// Display name of the sender (for join message)
  final String? displayName;

  /// Whether the sender is the host (for join message)
  final bool? isHost;

  /// Control mode (for sessionConfig message)
  final ControlMode? controlMode;

  /// Ping ID for matching pong responses
  final int? pingId;

  /// Rating key of the media (for mediaSwitch message)
  final String? ratingKey;

  /// Server ID of the media (for mediaSwitch message)
  final String? serverId;

  /// Title of the media (for mediaSwitch message)
  final String? mediaTitle;

  /// Whether playback is currently playing (for positionSync heartbeat)
  final bool? isPlaying;

  const SyncMessage({
    required this.type,
    required this.timestamp,
    this.positionMs,
    this.bufferingState,
    this.rate,
    this.peerId,
    this.displayName,
    this.isHost,
    this.controlMode,
    this.pingId,
    this.ratingKey,
    this.serverId,
    this.mediaTitle,
    this.isPlaying,
  });

  /// Create a PLAY message
  factory SyncMessage.play({String? peerId, Duration? position}) {
    return SyncMessage(
      type: SyncMessageType.play,
      timestamp: DateTime.now().millisecondsSinceEpoch,
      peerId: peerId,
      positionMs: position?.inMilliseconds,
    );
  }

  /// Create a PAUSE message
  factory SyncMessage.pause({String? peerId}) {
    return SyncMessage(type: SyncMessageType.pause, timestamp: DateTime.now().millisecondsSinceEpoch, peerId: peerId);
  }

  /// Create a SEEK message
  factory SyncMessage.seek(Duration position, {String? peerId}) {
    return SyncMessage(
      type: SyncMessageType.seek,
      timestamp: DateTime.now().millisecondsSinceEpoch,
      positionMs: position.inMilliseconds,
      peerId: peerId,
    );
  }

  /// Create a BUFFERING message
  factory SyncMessage.buffering(bool isBuffering, {String? peerId}) {
    return SyncMessage(
      type: SyncMessageType.buffering,
      timestamp: DateTime.now().millisecondsSinceEpoch,
      bufferingState: isBuffering,
      peerId: peerId,
    );
  }

  /// Create a POSITION_SYNC message (heartbeat with optional play/pause state)
  factory SyncMessage.positionSync(Duration position, {String? peerId, bool? isPlaying}) {
    return SyncMessage(
      type: SyncMessageType.positionSync,
      timestamp: DateTime.now().millisecondsSinceEpoch,
      positionMs: position.inMilliseconds,
      peerId: peerId,
      isPlaying: isPlaying,
    );
  }

  /// Create a RATE message
  factory SyncMessage.rate(double playbackRate, {String? peerId}) {
    return SyncMessage(
      type: SyncMessageType.rate,
      timestamp: DateTime.now().millisecondsSinceEpoch,
      rate: playbackRate,
      peerId: peerId,
    );
  }

  /// Create a JOIN message
  factory SyncMessage.join({required String peerId, required String displayName, required bool isHost}) {
    return SyncMessage(
      type: SyncMessageType.join,
      timestamp: DateTime.now().millisecondsSinceEpoch,
      peerId: peerId,
      displayName: displayName,
      isHost: isHost,
    );
  }

  /// Create a LEAVE message
  factory SyncMessage.leave({required String peerId}) {
    return SyncMessage(type: SyncMessageType.leave, timestamp: DateTime.now().millisecondsSinceEpoch, peerId: peerId);
  }

  /// Create a SESSION_CONFIG message (sent by host to new guests)
  ///
  /// Optionally includes current media info so guests can catch up
  /// if they missed a mediaSwitch broadcast.
  factory SyncMessage.sessionConfig({
    required ControlMode controlMode,
    required Duration currentPosition,
    required bool isPlaying,
    required double playbackRate,
    String? peerId,
    String? ratingKey,
    String? serverId,
    String? mediaTitle,
  }) {
    return SyncMessage(
      type: SyncMessageType.sessionConfig,
      timestamp: DateTime.now().millisecondsSinceEpoch,
      controlMode: controlMode,
      positionMs: currentPosition.inMilliseconds,
      isPlaying: isPlaying,
      bufferingState: !isPlaying, // Legacy compat: false = playing, true = paused
      rate: playbackRate,
      peerId: peerId,
      ratingKey: ratingKey,
      serverId: serverId,
      mediaTitle: mediaTitle,
    );
  }

  /// Create a REQUEST_SESSION_CONFIG message (sent by guest to request current config from host)
  factory SyncMessage.requestSessionConfig({String? peerId}) {
    return SyncMessage(
      type: SyncMessageType.requestSessionConfig,
      timestamp: DateTime.now().millisecondsSinceEpoch,
      peerId: peerId,
    );
  }

  /// Create a PING message
  factory SyncMessage.ping(int pingId, {String? peerId}) {
    return SyncMessage(
      type: SyncMessageType.ping,
      timestamp: DateTime.now().millisecondsSinceEpoch,
      pingId: pingId,
      peerId: peerId,
    );
  }

  /// Create a PONG message
  factory SyncMessage.pong(int pingId, {String? peerId}) {
    return SyncMessage(
      type: SyncMessageType.pong,
      timestamp: DateTime.now().millisecondsSinceEpoch,
      pingId: pingId,
      peerId: peerId,
    );
  }

  /// Create a MEDIA_SWITCH message (sent by host when changing content)
  factory SyncMessage.mediaSwitch({
    required String ratingKey,
    required String serverId,
    required String mediaTitle,
    String? peerId,
  }) {
    return SyncMessage(
      type: SyncMessageType.mediaSwitch,
      timestamp: DateTime.now().millisecondsSinceEpoch,
      ratingKey: ratingKey,
      serverId: serverId,
      mediaTitle: mediaTitle,
      peerId: peerId,
    );
  }

  /// Create a HOST_EXITED_PLAYER message (sent by host when exiting video player)
  factory SyncMessage.hostExitedPlayer({String? peerId}) {
    return SyncMessage(
      type: SyncMessageType.hostExitedPlayer,
      timestamp: DateTime.now().millisecondsSinceEpoch,
      peerId: peerId,
    );
  }

  /// Create a PLAYER_READY message (sent when player is attached and ready)
  factory SyncMessage.playerReady({required String peerId, required bool ready}) {
    return SyncMessage(
      type: SyncMessageType.playerReady,
      timestamp: DateTime.now().millisecondsSinceEpoch,
      peerId: peerId,
      bufferingState: ready, // Reuse bufferingState field for ready status
    );
  }

  /// Position as Duration (convenience getter)
  Duration? get position => positionMs != null ? Duration(milliseconds: positionMs!) : null;

  SyncMessage copyWith({String? peerId}) {
    return SyncMessage(
      type: type,
      timestamp: timestamp,
      positionMs: positionMs,
      bufferingState: bufferingState,
      rate: rate,
      peerId: peerId ?? this.peerId,
      displayName: displayName,
      isHost: isHost,
      controlMode: controlMode,
      pingId: pingId,
      ratingKey: ratingKey,
      serverId: serverId,
      mediaTitle: mediaTitle,
      isPlaying: isPlaying,
    );
  }

  /// Serialize to JSON string for sending over data channel
  String toJson() {
    final map = <String, dynamic>{'t': type.name, 'ts': timestamp};

    if (positionMs != null) map['pos'] = positionMs;
    if (bufferingState != null) map['buf'] = bufferingState;
    if (rate != null) map['r'] = rate;
    if (peerId != null) map['pid'] = peerId;
    if (displayName != null) map['name'] = displayName;
    if (isHost != null) map['host'] = isHost;
    if (controlMode != null) map['ctrl'] = controlMode!.index;
    if (pingId != null) map['ping'] = pingId;
    if (ratingKey != null) map['rk'] = ratingKey;
    if (serverId != null) map['sid'] = serverId;
    if (mediaTitle != null) map['title'] = mediaTitle;
    if (isPlaying != null) map['pl'] = isPlaying;

    return jsonEncode(map);
  }

  /// Parse from JSON string received from data channel
  factory SyncMessage.fromJson(String jsonString) {
    final map = jsonDecode(jsonString) as Map<String, dynamic>;

    final typeString = map['t'] as String;
    final type =
        SyncMessageType.values.asNameMap()[typeString] ?? (throw FormatException('Unknown message type: $typeString'));

    return SyncMessage(
      type: type,
      timestamp: map['ts'] as int,
      positionMs: map['pos'] as int?,
      bufferingState: map['buf'] as bool?,
      rate: (map['r'] as num?)?.toDouble(),
      peerId: map['pid'] as String?,
      displayName: map['name'] as String?,
      isHost: map['host'] as bool?,
      controlMode: map['ctrl'] != null ? ControlMode.values[map['ctrl'] as int] : null,
      pingId: map['ping'] as int?,
      ratingKey: map['rk'] as String?,
      serverId: map['sid'] as String?,
      mediaTitle: map['title'] as String?,
      isPlaying: map['pl'] as bool?,
    );
  }

  @override
  String toString() {
    return 'SyncMessage(type: $type, timestamp: $timestamp, positionMs: $positionMs, '
        'bufferingState: $bufferingState, rate: $rate, peerId: $peerId)';
  }
}
