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

import 'package:flutter/foundation.dart';

import '../models/app_event.dart';
import '../services/web_socket_service.dart';

enum DebugLogType { ble, ws, http }

enum DebugLogFilter { all, ble, ws, http }

class DebugLogEntry {
  const DebugLogEntry({
    required this.timestamp,
    required this.type,
    required this.summary,
    this.details,
  });

  final DateTime timestamp;
  final DebugLogType type;
  final String summary;
  final String? details;

  String get label {
    switch (type) {
      case DebugLogType.ble:
        return 'BLE';
      case DebugLogType.ws:
        return 'WS';
      case DebugLogType.http:
        return 'HTTP';
    }
  }
}

class DebugProvider extends ChangeNotifier {
  DebugProvider({required WebSocketService webSocketService}) {
    _eventSubscription = webSocketService.events.listen(_handleEvent);
    _connectionSubscription = webSocketService.connectionStateStream.listen(
      _handleConnectionState,
    );
  }

  static const int maxEntries = 500;

  final List<DebugLogEntry> _entries = <DebugLogEntry>[];
  late final StreamSubscription<AppEvent> _eventSubscription;
  late final StreamSubscription<WsConnectionState> _connectionSubscription;

  DebugLogFilter _filter = DebugLogFilter.all;

  List<DebugLogEntry> get entries => List<DebugLogEntry>.unmodifiable(_entries);
  DebugLogFilter get filter => _filter;

  List<DebugLogEntry> get filteredEntries {
    switch (_filter) {
      case DebugLogFilter.all:
        return entries.reversed.toList(growable: false);
      case DebugLogFilter.ble:
        return _entries
            .where((entry) => entry.type == DebugLogType.ble)
            .toList(growable: false)
            .reversed
            .toList(growable: false);
      case DebugLogFilter.ws:
        return _entries
            .where((entry) => entry.type == DebugLogType.ws)
            .toList(growable: false)
            .reversed
            .toList(growable: false);
      case DebugLogFilter.http:
        return _entries
            .where((entry) => entry.type == DebugLogType.http)
            .toList(growable: false)
            .reversed
            .toList(growable: false);
    }
  }

  void setFilter(DebugLogFilter value) {
    if (_filter == value) {
      return;
    }
    _filter = value;
    notifyListeners();
  }

  void clearLogs() {
    if (_entries.isEmpty) {
      return;
    }
    _entries.clear();
    notifyListeners();
  }

  void addHttpLog(String summary, {Object? details}) {
    _addEntry(DebugLogType.http, summary, details: details);
  }

  void addBleLog(String summary, {Object? details}) {
    _addEntry(DebugLogType.ble, summary, details: details);
  }

  void addWsLog(String summary, {Object? details}) {
    _addEntry(DebugLogType.ws, summary, details: details);
  }

  void _handleEvent(AppEvent event) {
    final logType = _inferType(event);
    _addEntry(
      logType,
      _buildEventSummary(event, logType),
      details: <String, dynamic>{
        'type': event.type,
        'payload': event.payload,
      },
      notify: true,
    );
  }

  void _handleConnectionState(WsConnectionState state) {
    final String summary;
    switch (state) {
      case WsConnectionState.connected:
        summary = 'WebSocket connected';
        break;
      case WsConnectionState.connecting:
        summary = 'WebSocket reconnecting';
        break;
      case WsConnectionState.disconnected:
        summary = 'WebSocket disconnected';
        break;
    }
    _addEntry(DebugLogType.ws, summary, notify: true);
  }

  DebugLogType _inferType(AppEvent event) {
    if (event.type.contains('ble')) {
      return DebugLogType.ble;
    }
    return DebugLogType.ws;
  }

  String _buildEventSummary(AppEvent event, DebugLogType type) {
    final payload = event.payload;
    switch (event.type) {
      case 'ble_update':
        final ready = payload['ready'] ?? payload['running'];
        final name = payload['device_name'] ?? payload['name'];
        return 'BLE update${name != null ? ' - $name' : ''}${ready != null ? ' (ready: $ready)' : ''}';
      case 'wifi_update':
        final ssid = payload['ssid']?.toString();
        final connected = payload['connected'];
        return 'WS wifi update${ssid != null && ssid.isNotEmpty ? ' - $ssid' : ''}${connected != null ? ' (connected: $connected)' : ''}';
      case 'status_update':
        final current = payload['current_video']?.toString();
        final running = payload['running'];
        return 'WS playback status${current != null && current.isNotEmpty ? ' - $current' : ''}${running != null ? ' (running: $running)' : ''}';
      case 'state_update':
        final previous = payload['old_state']?.toString();
        final next = payload['new_state']?.toString();
        return 'WS state change${previous != null ? ' $previous ->' : ''} ${next ?? 'unknown'}'.trim();
      case 'config_update':
        return 'WS config update';
      default:
        final prefix = type == DebugLogType.ble ? 'BLE' : 'WS';
        final compactPayload = _stringify(details: payload);
        return '$prefix ${event.type}: $compactPayload';
    }
  }

  void _addEntry(
    DebugLogType type,
    String summary, {
    Object? details,
    bool notify = true,
  }) {
    _entries.add(
      DebugLogEntry(
        timestamp: DateTime.now(),
        type: type,
        summary: summary,
        details: details == null ? null : _stringify(details: details),
      ),
    );

    if (_entries.length > maxEntries) {
      _entries.removeRange(0, _entries.length - maxEntries);
    }

    if (notify) {
      notifyListeners();
    }
  }

  String _stringify({required Object details}) {
    try {
      final encoded = details is String ? details : jsonEncode(details);
      return encoded.length > 220 ? '${encoded.substring(0, 217)}...' : encoded;
    } catch (_) {
      final value = details.toString();
      return value.length > 220 ? '${value.substring(0, 217)}...' : value;
    }
  }

  @override
  void dispose() {
    unawaited(_eventSubscription.cancel());
    unawaited(_connectionSubscription.cancel());
    super.dispose();
  }
}
