import 'dart:async';

import 'package:protobuf/protobuf.dart';
import 'package:protobuf/well_known_types/google/protobuf/struct.pb.dart';
import 'package:proto_socket/proto_socket.dart';

import 'proto_socket_endpoint_config.dart';
import 'proto_socket_envelope.dart';
import 'proto_socket_lifecycle.dart';

typedef ProtoSocketFactory =
    Future<dynamic> Function(ProtoSocketEndpointConfig config);

typedef ProtoSocketTransportFactory =
    ProtoSocketTransport Function(
      dynamic socket,
      ProtoSocketEndpointConfig config,
    );

abstract interface class NomadProtoSocketPeer {
  Future<ProtoSocketEnvelope> sendEnvelopeRequest(
    ProtoSocketEnvelope envelope, {
    Duration timeout = const Duration(seconds: 30),
  });
  Stream<ProtoSocketEnvelope> get envelopeEvents;
  Future<void> close();
}

Map<String, GeneratedMessage Function(List<int>)> protoSocketStructParserMap() {
  final qualifiedName = Struct.getDefault().info_.qualifiedMessageName;
  return <String, GeneratedMessage Function(List<int>)>{
    (Struct).toString(): Struct.fromBuffer,
    qualifiedName: Struct.fromBuffer,
  };
}

class NomadProtoSocketClient extends WsProtobufClient
    implements NomadProtoSocketPeer {
  final StreamController<ProtoSocketEnvelope> _events =
      StreamController<ProtoSocketEnvelope>.broadcast();

  NomadProtoSocketClient(
    dynamic socket, {
    required int heartbeatIntervalSeconds,
    required int heartbeatWaitSeconds,
  }) : super(
         socket,
         heartbeatIntervalSeconds,
         heartbeatWaitSeconds,
         protoSocketStructParserMap(),
       ) {
    addListener<Struct>(_handleStructEvent);
  }

  @override
  Stream<ProtoSocketEnvelope> get envelopeEvents => _events.stream;

  @override
  Future<ProtoSocketEnvelope> sendEnvelopeRequest(
    ProtoSocketEnvelope envelope, {
    Duration timeout = const Duration(seconds: 30),
  }) async {
    final response = await sendRequest<Struct, Struct>(
      envelope.toStruct(),
      timeout: timeout,
    );
    return ProtoSocketEnvelope.fromStruct(response);
  }

  @override
  void onError(dynamic e) {
    if (!_events.isClosed) {
      _events.addError(e);
    }
  }

  @override
  Future<void> close() async {
    try {
      await super.close();
    } finally {
      if (!_events.isClosed) {
        await _events.close();
      }
    }
  }

  void _handleStructEvent(Struct value) {
    if (!_events.isClosed) {
      _events.add(ProtoSocketEnvelope.fromStruct(value));
    }
  }
}

class ProtoSocketClientTransport implements ProtoSocketTransport {
  final NomadProtoSocketPeer _client;

  ProtoSocketClientTransport(this._client);

  @override
  Stream<ProtoSocketEnvelope> get events => _client.envelopeEvents;

  @override
  Future<ProtoSocketEnvelope> sendRequest(
    ProtoSocketEnvelope envelope, {
    Duration timeout = const Duration(seconds: 30),
  }) {
    return _client.sendEnvelopeRequest(envelope, timeout: timeout);
  }

  @override
  Future<void> close() => _client.close();
}

class RealProtoSocketConnector implements ProtoSocketConnector {
  final ProtoSocketFactory _socketFactory;
  final ProtoSocketTransportFactory _transportFactory;
  ProtoSocketTransport? _transport;

  RealProtoSocketConnector({
    ProtoSocketFactory? socketFactory,
    ProtoSocketTransportFactory? transportFactory,
  }) : _socketFactory = socketFactory ?? _connectSocket,
       _transportFactory = transportFactory ?? _createTransport;

  @override
  Future<ProtoSocketTransport> connect(ProtoSocketEndpointConfig config) async {
    final socket = await _socketFactory(config);
    final transport = _transportFactory(socket, config);
    _transport = transport;
    return transport;
  }

  @override
  Future<void> disconnect() async {
    final transport = _transport;
    _transport = null;
    await transport?.close();
  }

  static Future<dynamic> _connectSocket(ProtoSocketEndpointConfig config) {
    if (config.secure) {
      return WsProtobufClient.connectSecure(
        config.host,
        config.port,
        path: config.path,
      );
    }
    return WsProtobufClient.connect(
      config.host,
      config.port,
      path: config.path,
    );
  }

  static ProtoSocketTransport _createTransport(
    dynamic socket,
    ProtoSocketEndpointConfig config,
  ) {
    return ProtoSocketClientTransport(
      NomadProtoSocketClient(
        socket,
        heartbeatIntervalSeconds: config.heartbeatIntervalSeconds,
        heartbeatWaitSeconds: config.heartbeatWaitSeconds,
      ),
    );
  }
}
