import 'dart:convert';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:x_action_cable_v2/x_action_cable.dart';

import '../core/config.dart';
import '../providers/auth_provider.dart';
import '../providers/score_provider.dart';
import '../providers/session_provider.dart';
import 'models/device_state.dart';
import 'models/score_state.dart';

final websocketServiceProvider = Provider<WebsocketService>((ref) {
  final service = WebsocketService(ref);
  ref.onDispose(service.dispose);
  return service;
});

typedef CableMessageHandler = void Function(Map<String, dynamic> data);

class WebsocketService {
  WebsocketService(this._ref);

  final Ref _ref;
  ActionCable? _cable;
  ActionChannel? _channel;
  String? _sessionId;

  bool get isConnected => _cable != null && _channel != null;

  Future<void> connect({
    required String sessionId,
    required String deviceRole,
  }) async {
    if (_sessionId == sessionId && isConnected) return;

    await disconnect();

    final token = _ref.read(authProvider).accessToken;
    if (token == null || token.isEmpty) {
      throw StateError('Token di accesso mancante');
    }

    _sessionId = sessionId;

    _cable = ActionCable.connect(
      AppConfig.cableUrl,
      headers: {'Authorization': 'Bearer $token'},
      onConnected: () {
        _ref.read(sessionProvider.notifier).setConnected(true);
      },
      onConnectionLost: () {
        _ref.read(sessionProvider.notifier).setConnected(false);
      },
      onCannotConnect: (_) {
        _ref.read(sessionProvider.notifier).setConnected(false);
      },
    );

    _channel = _cable!.subscribe(
      'SessionChannel',
      channelParams: {
        'session_id': sessionId,
        'device_role': deviceRole,
      },
      onSubscribed: () {
        _ref.read(sessionProvider.notifier).setConnected(true);
      },
      onDisconnected: () {
        _ref.read(sessionProvider.notifier).setConnected(false);
      },
      callbacks: [
        ActionCallback(
          name: 'receive_message',
          callback: _handleMessage,
        ),
      ],
    );
  }

  void _handleMessage(ActionResponse response) {
    if (response.hasError || response.data == null) return;
    _dispatch(Map<String, dynamic>.from(response.data!));
  }

  void _dispatch(Map<String, dynamic> data) {
    final type = data['type'] as String?;

    switch (type) {
      case 'score_update':
        _ref.read(scoreProvider.notifier).applyRemote(ScoreState.fromJson(data));
        break;
      case 'device_state':
        _ref
            .read(sessionProvider.notifier)
            .updateDeviceState(DeviceStateModel.fromJson(data));
        break;
      case 'timeout':
        final team = data['team'] as String?;
        if (team == 'home') {
          _ref.read(scoreProvider.notifier).setTimeoutHome(true);
        } else if (team == 'away') {
          _ref.read(scoreProvider.notifier).setTimeoutAway(true);
        }
        break;
      case 'command':
        final action = data['action'] as String?;
        if (action != null) {
          _ref.read(sessionProvider.notifier).applyCommand(action);
        }
        break;
      default:
        break;
    }
  }

  void sendScoreUpdate(ScoreState score) {
    _perform({'type': 'score_update', ...score.toCablePayload()});
  }

  void sendTimeout(String team) {
    _perform({'type': 'timeout', 'team': team});
  }

  void sendCommand(String action) {
    _perform({'type': 'command', 'action': action});
  }

  void sendDeviceState(DeviceStateModel state) {
    _perform({
      'type': 'device_state',
      'device_role': state.deviceRole,
      'battery': state.batteryLevel,
      'network': state.networkType,
      'bitrate': state.currentBitrate,
      'fps': state.fps,
    });
  }

  void _perform(Map<String, dynamic> payload) {
    _channel?.performAction('receive', params: payload);
  }

  Future<void> disconnect() async {
    _channel?.unsubscribe();
    _channel = null;
    _cable?.disconnect();
    _cable = null;
    _sessionId = null;
    _ref.read(sessionProvider.notifier).setConnected(false);
  }

  void dispose() {
    disconnect();
  }

  String encodeQrPayload(Map<String, dynamic> payload) => jsonEncode(payload);
}
