import 'dart:async';
import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:logger/logger.dart';
import 'package:rxdart/rxdart.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

import '../../domain/entities/camera_lock.dart';

/// Client for the COPILOT Coordinator service (:8090).
/// Handles camera locks, sequences, and keyboard coordination.
class CoordinationService {
  final Logger _logger = Logger();
  late final Dio _http;
  WebSocketChannel? _ws;
  StreamSubscription? _wsSubscription;
  Timer? _reconnectTimer;
  Timer? _lockResetTimer;
  bool _disposed = false;

  String _coordinatorUrl = '';
  String _keyboardId = '';

  // Reconnection config
  static const _initialReconnectDelay = Duration(seconds: 1);
  static const _maxReconnectDelay = Duration(seconds: 30);
  int _reconnectAttempts = 0;

  // Connection status
  final _connectedController = BehaviorSubject<bool>.seeded(false);

  // Lock state
  final _locksController = BehaviorSubject<Map<int, CameraLock>>.seeded({});
  final _notificationController =
      BehaviorSubject<CameraLockNotification?>.seeded(null);

  /// Whether connected to the coordinator
  Stream<bool> get connected => _connectedController.stream;
  bool get isConnected => _connectedController.value;

  /// Current camera locks (all keyboards)
  Stream<Map<int, CameraLock>> get locks => _locksController.stream;
  Map<int, CameraLock> get currentLocks => _locksController.value;

  /// Lock notifications targeted at this keyboard
  Stream<CameraLockNotification?> get notifications =>
      _notificationController.stream;

  /// Initialize with coordinator URL and keyboard identity
  Future<void> initialize(String coordinatorUrl, String keyboardId) async {
    _coordinatorUrl = coordinatorUrl;
    _keyboardId = keyboardId;

    _http = Dio(BaseOptions(
      baseUrl: coordinatorUrl,
      connectTimeout: const Duration(seconds: 5),
      receiveTimeout: const Duration(seconds: 10),
    ));

    _logger.i(
        'CoordinationService initialized: $coordinatorUrl (keyboard: $keyboardId)');
  }

  /// Connect to the coordinator (HTTP health check + WebSocket)
  Future<bool> connect() async {
    try {
      final response = await _http.get('/health');
      if (response.statusCode == 200) {
        _connectedController.add(true);
        _reconnectAttempts = 0;
        await _connectWebSocket();

        // Sync current lock state
        await syncLocks();
        _logger.i('Connected to coordinator');
        return true;
      }
    } catch (e) {
      _logger.e('Failed to connect to coordinator: $e');
    }

    _connectedController.add(false);
    _scheduleReconnect();
    return false;
  }

  /// Disconnect from the coordinator
  Future<void> disconnect() async {
    _reconnectTimer?.cancel();
    _lockResetTimer?.cancel();
    await _disconnectWebSocket();
    _connectedController.add(false);
  }

  // ============================================================
  // Lock Operations
  // ============================================================

  /// Try to acquire a lock on a camera
  Future<({bool acquired, CameraLock? lock})> tryLock(
    int cameraId, {
    CameraLockPriority priority = CameraLockPriority.low,
  }) async {
    try {
      final response = await _http.post('/locks/try', data: {
        'CameraId': cameraId,
        'KeyboardId': _keyboardId,
        'Priority': priority.name,
      });

      if (response.statusCode == 200) {
        final data = response.data as Map<String, dynamic>;
        final acquired = data['acquired'] as bool? ?? false;
        final lockJson = data['lock'] as Map<String, dynamic>?;
        final lock = lockJson != null ? CameraLock.fromJson(lockJson) : null;

        if (acquired && lock != null) {
          _updateLockState(lock);
          _startLockResetTimer(cameraId);
        }

        return (acquired: acquired, lock: lock);
      }
    } catch (e) {
      _logger.e('tryLock failed for camera $cameraId: $e');
    }
    return (acquired: false, lock: null);
  }

  /// Release a camera lock
  Future<bool> releaseLock(int cameraId) async {
    try {
      final response = await _http.post('/locks/release', data: {
        'CameraId': cameraId,
        'KeyboardId': _keyboardId,
      });

      if (response.statusCode == 200) {
        _removeLockState(cameraId);
        return true;
      }
    } catch (e) {
      _logger.e('releaseLock failed for camera $cameraId: $e');
    }
    return false;
  }

  /// Request takeover of a camera locked by another keyboard
  Future<bool> requestTakeover(
    int cameraId, {
    CameraLockPriority priority = CameraLockPriority.low,
  }) async {
    try {
      final response = await _http.post('/locks/takeover', data: {
        'CameraId': cameraId,
        'KeyboardId': _keyboardId,
        'Priority': priority.name,
      });
      return response.statusCode == 200;
    } catch (e) {
      _logger.e('requestTakeover failed for camera $cameraId: $e');
      return false;
    }
  }

  /// Confirm or reject a takeover request
  Future<bool> confirmTakeover(int cameraId, bool confirm) async {
    try {
      final response = await _http.post('/locks/confirm', data: {
        'CameraId': cameraId,
        'KeyboardId': _keyboardId,
        'Confirm': confirm,
      });
      return response.statusCode == 200;
    } catch (e) {
      _logger.e('confirmTakeover failed for camera $cameraId: $e');
      return false;
    }
  }

  /// Reset lock expiration timer (called periodically while PTZ is active)
  Future<bool> resetExpiration(int cameraId) async {
    try {
      final response = await _http.post('/locks/reset', data: {
        'CameraId': cameraId,
        'KeyboardId': _keyboardId,
      });
      return response.statusCode == 200;
    } catch (e) {
      _logger.e('resetExpiration failed for camera $cameraId: $e');
      return false;
    }
  }

  /// Get all current locks from the coordinator
  Future<void> syncLocks() async {
    try {
      final response = await _http.get('/locks');
      if (response.statusCode == 200) {
        final lockList = response.data as List<dynamic>;
        final locks = <int, CameraLock>{};
        for (final lockJson in lockList) {
          final lock = CameraLock.fromJson(lockJson as Map<String, dynamic>);
          locks[lock.cameraId] = lock;
        }
        _locksController.add(locks);
      }
    } catch (e) {
      _logger.e('syncLocks failed: $e');
    }
  }

  /// Get camera IDs locked by this keyboard
  Future<List<int>> getMyLockedCameras() async {
    try {
      final response = await _http.get('/locks/$_keyboardId');
      if (response.statusCode == 200) {
        final ids = response.data as List<dynamic>;
        return ids.cast<int>();
      }
    } catch (e) {
      _logger.e('getMyLockedCameras failed: $e');
    }
    return [];
  }

  /// Check if a camera is locked by this keyboard
  bool isCameraLockedByMe(int cameraId) {
    final lock = currentLocks[cameraId];
    return lock != null &&
        lock.ownerName.toLowerCase() == _keyboardId.toLowerCase();
  }

  /// Check if a camera is locked by another keyboard
  bool isCameraLockedByOther(int cameraId) {
    final lock = currentLocks[cameraId];
    return lock != null &&
        lock.ownerName.toLowerCase() != _keyboardId.toLowerCase();
  }

  // ============================================================
  // Sequence Operations
  // ============================================================

  /// Start a sequence on a viewer
  Future<Map<String, dynamic>?> startSequence(
      int viewerId, int sequenceId) async {
    try {
      final response = await _http.post('/sequences/start', data: {
        'ViewerId': viewerId,
        'SequenceId': sequenceId,
      });
      if (response.statusCode == 200) {
        return response.data as Map<String, dynamic>;
      }
    } catch (e) {
      _logger.e('startSequence failed: $e');
    }
    return null;
  }

  /// Stop a sequence on a viewer
  Future<bool> stopSequence(int viewerId) async {
    try {
      final response = await _http.post('/sequences/stop', data: {
        'ViewerId': viewerId,
      });
      return response.statusCode == 200;
    } catch (e) {
      _logger.e('stopSequence failed: $e');
      return false;
    }
  }

  /// Get running sequences
  Future<List<Map<String, dynamic>>> getRunningSequences() async {
    try {
      final response = await _http.get('/sequences/running');
      if (response.statusCode == 200) {
        final list = response.data as List<dynamic>;
        return list.cast<Map<String, dynamic>>();
      }
    } catch (e) {
      _logger.e('getRunningSequences failed: $e');
    }
    return [];
  }

  /// Get available sequences, optionally filtered by category
  Future<List<Map<String, dynamic>>> getSequences({int? categoryId}) async {
    try {
      final queryParams = categoryId != null ? {'categoryId': categoryId} : null;
      final response =
          await _http.get('/sequences', queryParameters: queryParams);
      if (response.statusCode == 200) {
        final list = response.data as List<dynamic>;
        return list.cast<Map<String, dynamic>>();
      }
    } catch (e) {
      _logger.e('getSequences failed: $e');
    }
    return [];
  }

  /// Get sequence categories
  Future<List<Map<String, dynamic>>> getSequenceCategories() async {
    try {
      final response = await _http.get('/sequences/categories');
      if (response.statusCode == 200) {
        final list = response.data as List<dynamic>;
        return list.cast<Map<String, dynamic>>();
      }
    } catch (e) {
      _logger.e('getSequenceCategories failed: $e');
    }
    return [];
  }

  // ============================================================
  // Private Methods
  // ============================================================

  Future<void> _connectWebSocket() async {
    await _disconnectWebSocket();

    try {
      final wsUrl =
          '${_coordinatorUrl.replaceFirst('http', 'ws')}/ws?keyboard=$_keyboardId';
      _logger.d('Connecting coordinator WebSocket: $wsUrl');

      _ws = WebSocketChannel.connect(Uri.parse(wsUrl));

      _wsSubscription = _ws!.stream.listen(
        (message) => _handleWsMessage(message as String),
        onError: (error) {
          _logger.e('Coordinator WebSocket error: $error');
          _connectedController.add(false);
          _scheduleReconnect();
        },
        onDone: () {
          _logger.w('Coordinator WebSocket closed');
          _connectedController.add(false);
          _scheduleReconnect();
        },
      );
    } catch (e) {
      _logger.e('Failed to connect coordinator WebSocket: $e');
      _scheduleReconnect();
    }
  }

  Future<void> _disconnectWebSocket() async {
    await _wsSubscription?.cancel();
    _wsSubscription = null;
    await _ws?.sink.close();
    _ws = null;
  }

  void _handleWsMessage(String message) {
    try {
      final json = jsonDecode(message) as Map<String, dynamic>;
      final type = json['type'] as String?;
      final data = json['data'];

      switch (type) {
        case 'lock_acquired':
          if (data is Map<String, dynamic>) {
            final lock = CameraLock.fromJson(data);
            _updateLockState(lock);
          }
          break;

        case 'lock_released':
          if (data is Map<String, dynamic>) {
            final cameraId =
                data['cameraId'] as int? ?? data['CameraId'] as int? ?? 0;
            _removeLockState(cameraId);
          }
          break;

        case 'lock_notification':
          if (data is Map<String, dynamic>) {
            final notification = CameraLockNotification.fromJson(data);
            _notificationController.add(notification);
            _logger.i(
                'Lock notification: ${notification.type.name} camera ${notification.cameraId}');
          }
          break;

        case 'sequence_started':
        case 'sequence_stopped':
          _logger.d('Sequence event: $type');
          break;

        case 'keyboard_online':
        case 'keyboard_offline':
          _logger.d('Keyboard event: $type');
          break;

        default:
          _logger.d('Unknown coordinator event: $type');
      }
    } catch (e) {
      _logger.e('Failed to parse coordinator message: $e');
    }
  }

  void _updateLockState(CameraLock lock) {
    final locks = Map<int, CameraLock>.from(_locksController.value);
    locks[lock.cameraId] = lock;
    _locksController.add(locks);
  }

  void _removeLockState(int cameraId) {
    final locks = Map<int, CameraLock>.from(_locksController.value);
    locks.remove(cameraId);
    _locksController.add(locks);
  }

  /// Auto-reset lock expiration every 2 minutes while a lock is held
  void _startLockResetTimer(int cameraId) {
    _lockResetTimer?.cancel();
    _lockResetTimer = Timer.periodic(const Duration(minutes: 2), (_) {
      if (isCameraLockedByMe(cameraId)) {
        resetExpiration(cameraId);
      } else {
        _lockResetTimer?.cancel();
      }
    });
  }

  void _scheduleReconnect() {
    if (_disposed) return;
    _reconnectTimer?.cancel();

    final delay = Duration(
      milliseconds: (_initialReconnectDelay.inMilliseconds *
              (1 << _reconnectAttempts.clamp(0, 5)))
          .clamp(0, _maxReconnectDelay.inMilliseconds),
    );

    _logger.i(
        'Scheduling coordinator reconnect in ${delay.inSeconds}s (attempt ${_reconnectAttempts + 1})');

    _reconnectTimer = Timer(delay, () async {
      if (_disposed) return;
      _reconnectAttempts++;
      await connect();
    });
  }

  /// Dispose all resources
  void dispose() {
    _disposed = true;
    _reconnectTimer?.cancel();
    _lockResetTimer?.cancel();
    _wsSubscription?.cancel();
    _ws?.sink.close();
    _connectedController.close();
    _locksController.close();
    _notificationController.close();
  }
}
