import 'dart:async';
import 'dart:io';

import 'package:proto_socket/proto_socket.dart';

const _host = '127.0.0.1';
const _wsPath = '/';
const _heartbeatIntervalSeconds = 30;
const _heartbeatWaitSeconds = 10;
const _connectWindow = Duration(seconds: 3);
const _requestWindow = Duration(seconds: 10);

class _TcpClient extends ProtobufClient {
  _TcpClient(Socket socket)
      : super(socket, _heartbeatIntervalSeconds, _heartbeatWaitSeconds, {
          TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer,
        });
}

class _WsClient extends WsProtobufClient {
  _WsClient(WebSocket ws)
      : super(ws, _heartbeatIntervalSeconds, _heartbeatWaitSeconds, {
          TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer,
        });
}

class _ClientHandle {
  final Communicator client;
  final Future<void> Function() close;

  _ClientHandle(this.client, this.close);
}

Future<void> main(List<String> args) async {
  final mode = _argValue(args, 'mode') ?? 'tcp';
  final phase = _argValue(args, 'phase') ?? 'send-push';
  final port = int.tryParse(_argValue(args, 'port') ?? '');
  final certPath = _argValue(args, 'cert');

  print(
      'INFO typeName dart=${TestData.getDefault().info_.qualifiedMessageName}');

  if (port == null) {
    _fail('setup', 'port is required');
    exitCode = 1;
    return;
  }

  late _ClientHandle handle;
  try {
    handle = await _connectWithRetry(mode, port, certPath: certPath);
  } catch (error) {
    _fail('setup', error.toString());
    exitCode = 1;
    return;
  }

  var ok = false;
  try {
    switch (phase) {
      case 'send-push':
        ok = await _runSendPush(handle.client);
        break;
      case 'requests':
        ok = await _runRequests(handle.client);
        break;
      case 'legacy-receive':
        ok = await _runLegacyReceive(handle.client);
        break;
      default:
        _fail('setup', 'unknown phase "$phase"');
    }
  } finally {
    await handle.close();
  }

  if (!ok) {
    exitCode = 1;
  }
  exit(exitCode);
}

Future<_ClientHandle> _connectWithRetry(String mode, int port,
    {String? certPath}) async {
  final deadline = DateTime.now().add(_connectWindow);
  Object? lastError;

  while (DateTime.now().isBefore(deadline)) {
    try {
      switch (mode) {
        case 'tcp':
          final socket = await ProtobufClient.connect(_host, port)
              .timeout(const Duration(milliseconds: 300));
          final client = _TcpClient(socket);
          return _ClientHandle(client, client.close);
        case 'ws':
          final ws = await WsProtobufClient.connect(_host, port, path: _wsPath)
              .timeout(const Duration(milliseconds: 300));
          final client = _WsClient(ws);
          return _ClientHandle(client, client.close);
        case 'tls':
          final ctx = _buildClientSecurityContext(certPath, mode);
          final socket = await ProtobufClient.connectSecure(
            _host,
            port,
            context: ctx,
            onBadCertificate: (certificate) =>
                _acceptTestCertificate(certPath, certificate),
          ).timeout(const Duration(milliseconds: 300));
          final client = _TcpClient(socket);
          return _ClientHandle(client, client.close);
        case 'wss':
          final ctx = _buildClientSecurityContext(certPath, mode);
          final ws = await WsProtobufClient.connectSecure(_host, port,
                  path: _wsPath,
                  context: ctx,
                  onBadCertificate: (certificate) =>
                      _acceptTestCertificate(certPath, certificate))
              .timeout(const Duration(milliseconds: 300));
          final client = _WsClient(ws);
          return _ClientHandle(client, client.close);
        default:
          throw ArgumentError('unknown mode "$mode"');
      }
    } catch (error) {
      lastError = error;
      await Future<void>.delayed(const Duration(milliseconds: 100));
    }
  }

  throw TimeoutException(
      'connect $mode:$port timed out: $lastError', _connectWindow);
}

SecurityContext _buildClientSecurityContext(String? certPath, String mode) {
  if (certPath == null || certPath.isEmpty) {
    throw ArgumentError('--cert is required for $mode mode');
  }
  return SecurityContext(withTrustedRoots: false)
    ..setTrustedCertificates(certPath);
}

bool _acceptTestCertificate(String? certPath, X509Certificate certificate) {
  if (certPath == null || certPath.isEmpty) {
    return false;
  }
  return certificate.pem == File(certPath).readAsStringSync();
}

Future<bool> _runSendPush(Communicator client) async {
  final pushCompleter = Completer<TestData>();
  client.addListener<TestData>((data) {
    if (!pushCompleter.isCompleted) {
      pushCompleter.complete(data);
    }
  });

  await client.send(TestData()
    ..index = 101
    ..message = 'fire from dart client');
  _pass('1', 'fire-and-forget sent');

  try {
    final push = await pushCompleter.future.timeout(_requestWindow);
    if (push.index != 200 || push.message != 'push from python server') {
      _fail(
          '2', 'unexpected push index=${push.index} message="${push.message}"');
      return false;
    }
    _pass('2', 'received push from python server');
    return true;
  } catch (error) {
    _fail('2', error.toString());
    return false;
  }
}

Future<bool> _runRequests(Communicator client) async {
  try {
    final single = await client
        .sendRequest<TestData, TestData>(TestData()
          ..index = 21
          ..message = 'single request from dart')
        .timeout(_requestWindow);
    if (single.index != 42 ||
        single.message != 'echo: single request from dart') {
      _fail('3',
          'unexpected response index=${single.index} message="${single.message}"');
      return false;
    }
    _pass('3', 'single request response matched');

    final futures = <Future<void>>[];
    for (var i = 0; i < _concurrencyCount(); i++) {
      futures.add(() async {
        final index = 30 + i;
        final message = 'multi request $i from dart';
        final res = await client
            .sendRequest<TestData, TestData>(TestData()
              ..index = index
              ..message = message)
            .timeout(_requestWindow);
        if (res.index != index * 2 || res.message != 'echo: $message') {
          throw StateError(
              'request $i got index=${res.index} message="${res.message}"');
        }
      }());
    }
    await Future.wait(futures);
    _pass('4', 'concurrent request responses matched');
    return true;
  } catch (error) {
    _fail('4', error.toString());
    return false;
  }
}

Future<bool> _runLegacyReceive(Communicator client) async {
  final data = (TestData()
        ..index = 101
        ..message = 'legacy fire from dart')
      .writeToBuffer();
  await client.queuePacket(PacketBase()
        ..typeName = 'TestData'
        ..nonce = 1
        ..data = data);
  await Future<void>.delayed(const Duration(milliseconds: 150));
  return true;
}

int _concurrencyCount() {
  final raw = Platform.environment["PROTO_SOCKET_CROSSTEST_CONCURRENCY"] ?? "5";
  final value = int.tryParse(raw);
  if (value == null || value <= 0) {
    return 5;
  }
  return value;
}

String? _argValue(List<String> args, String name) {
  final prefix = '--$name=';
  for (final arg in args) {
    if (arg.startsWith(prefix)) {
      return arg.substring(prefix.length);
    }
  }
  return null;
}

void _pass(String scenario, String detail) {
  print('PASS scenario=$scenario detail=$detail');
}

void _fail(String scenario, String error) {
  print('FAIL scenario=$scenario error=$error');
}
