// ignore_for_file: avoid_init_to_null, prefer_final_fields, deprecated_member_use

import 'dart:async';
import 'dart:collection';
import 'dart:html' as html;
import 'dart:typed_data';

import 'package:protobuf/protobuf.dart';
import 'base_client.dart';
import 'inbound_gateway_web.dart';
import 'packets/message_common.pb.dart';
import 'transport.dart';

abstract class WsProtobufClient extends BaseClient<WsProtobufClient> {
  final html.WebSocket _ws;
  late final _WebSocketTransport _transport;

  /// Plain WebSocket connection.
  static Future<html.WebSocket> connect(String host, int port,
          {String path = '/'}) =>
      _open('ws://$host:$port$path');

  /// Secure WebSocket (WSS) connection.
  ///
  /// Browser handles TLS via the user agent's trust store, so no
  /// `SecurityContext` is accepted on web.
  static Future<html.WebSocket> connectSecure(String host, int port,
          {String path = '/'}) =>
      _open('wss://$host:$port$path');

  static Future<html.WebSocket> _open(String url) {
    final ws = html.WebSocket(url);
    ws.binaryType = 'arraybuffer';
    if (ws.readyState == html.WebSocket.OPEN) {
      return Future.value(ws);
    }
    final completer = Completer<html.WebSocket>();
    late StreamSubscription<html.Event> openSub;
    late StreamSubscription<html.Event> errorSub;
    void cleanup() {
      openSub.cancel();
      errorSub.cancel();
    }

    openSub = ws.onOpen.listen((_) {
      if (!completer.isCompleted) {
        cleanup();
        completer.complete(ws);
      }
    });
    errorSub = ws.onError.listen((event) {
      if (!completer.isCompleted) {
        cleanup();
        completer.completeError(
            StateError('WebSocket failed to open: $url'), StackTrace.current);
      }
    });
    return completer.future;
  }

  StreamSubscription<html.MessageEvent>? _subscription;
  final Queue<dynamic> _pending = Queue<dynamic>();
  bool _draining = false;

  WsProtobufClient(this._ws, int heartbeatIntervalTime, int heartbeatWaitTime,
      Map<String, GeneratedMessage Function(List<int>)> parserMap) {
    initSelf(this);
    initHeartbeat(heartbeatIntervalTime, heartbeatWaitTime);
    _transport = _WebSocketTransport(_ws);
    isAlive = true;
    parserMap.addAll({HeartBeat.getDefault().info_.qualifiedMessageName: HeartBeat.fromBuffer});
    super.initialize(parserMap, transport: _transport);
    // web에는 worker isolate가 없으므로 IsolateInboundGateway는 SyncInboundGateway
    // fallback으로 in-process decode하되 동일한 seq-ordered receive coordinator
    // contract를 유지한다.
    final gateway = IsolateInboundGateway();
    attachInboundGateway(gateway);
    unawaited(gateway.start());
    _subscription = _ws.onMessage.listen(_onMessage, onError: onError);
    _ws.onClose.listen((_) => close());
    addListener(onHeartBeat);
    sendHeartBeat();
  }

  void onError(dynamic e) {}

  // burst 상황에서 pause()는 동기적으로 적용되지 않아 이미 전달된 frame이 남는다.
  // 도착한 frame을 drop하지 않고 queue에 넣은 뒤, 단일 drain loop가 순서대로
  // 변환·dispatch한다. pause/resume은 source backpressure 용도로 drain 단위에서만 건다.
  void _onMessage(html.MessageEvent event) {
    _pending.add(event.data);
    _drain();
  }

  Future<void> _drain() async {
    if (_draining) return;
    _draining = true;
    _subscription?.pause();
    try {
      while (_pending.isNotEmpty) {
        final data = _pending.removeFirst();
        if (data is ByteBuffer) {
          await _dispatch(data.asUint8List());
        } else if (data is Uint8List) {
          await _dispatch(data);
        } else if (data is List<int>) {
          await _dispatch(data);
        } else if (data is html.Blob) {
          final reader = html.FileReader();
          reader.readAsArrayBuffer(data);
          await reader.onLoad.first;
          final result = reader.result;
          if (result is ByteBuffer) {
            await _dispatch(result.asUint8List());
          }
        }
      }
    } finally {
      _draining = false;
      _subscription?.resume();
    }
  }

  Future<void> _dispatch(List<int> bytes) async {
    await onReceivedFrame(bytes);
    sendHeartBeat();
  }

  @override
  Future<void> closeTransport() async {
    await _transport.close();
  }

  @override
  bool get isSourcePaused => _subscription?.isPaused ?? false;
}

class _WebSocketTransport implements Transport {
  final html.WebSocket _ws;

  _WebSocketTransport(this._ws);

  @override
  Future<void> writePacket(PacketBase base) async {
    _ws.send(base.writeToBuffer());
  }

  @override
  Future<void> close() async {
    try {
      _ws.close();
    } catch (_) {
      // already closed by peer
    }
  }
}
