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

import 'acp_endpoint_paths.dart';
import 'gateway_runtime_errors.dart';
import 'gateway_runtime_session_client.dart';
import 'runtime_models.dart';

const int gatewayAcpHttpHandshakeInterruptedRetryCount = 5;
const int gatewayAcpHttpConnectFailureRetryCount = 2;
const Duration gatewayAcpHttpConnectTimeout = Duration(seconds: 12);
const String gatewayAcpHttpHandshakeInterruptedCode =
    'ACP_HTTP_HANDSHAKE_INTERRUPTED';
const String gatewayAcpHttpConnectTimeoutCode = 'ACP_HTTP_CONNECT_TIMEOUT';
const String gatewayAcpHttpConnectFailedCode = 'ACP_HTTP_CONNECT_FAILED';

class GatewayAcpException implements Exception {
  const GatewayAcpException(
    this.message, {
    this.code,
    this.detailCode,
    this.details,
  });

  final String message;
  final String? code;
  final String? detailCode;
  final Object? details;

  @override
  String toString() => code == null ? message : '$code: $message';
}

class GatewayAcpCapabilities {
  const GatewayAcpCapabilities({
    required this.singleAgent,
    required this.multiAgent,
    required this.availableExecutionTargets,
    required this.providerCatalog,
    required this.gatewayProviderCatalog,
    required this.raw,
    this.diagnostics = const <String, dynamic>{},
  });

  const GatewayAcpCapabilities.empty()
    : singleAgent = false,
      multiAgent = false,
      availableExecutionTargets = const <AssistantExecutionTarget>[],
      providerCatalog = const <SingleAgentProvider>[],
      gatewayProviderCatalog = const <SingleAgentProvider>[],
      raw = const <String, dynamic>{},
      diagnostics = const <String, dynamic>{};

  final bool singleAgent;
  final bool multiAgent;
  final List<AssistantExecutionTarget> availableExecutionTargets;
  final List<SingleAgentProvider> providerCatalog;
  final List<SingleAgentProvider> gatewayProviderCatalog;
  final Map<String, dynamic> raw;
  final Map<String, dynamic> diagnostics;
}


enum _GatewayAcpHttpRequestPhase {
  connect,
  write,
  waitingForResponse,
  bodyRead,
}


class GatewayAcpClient {
  GatewayAcpClient({
    required this.endpointResolver,
    this.authorizationResolver,
  });

  final Uri? Function() endpointResolver;
  final Future<String?> Function(Uri endpoint)? authorizationResolver;

  int _requestCounter = 0;
  GatewayAcpCapabilities _cachedCapabilities =
      const GatewayAcpCapabilities.empty();
  DateTime? _capabilitiesRefreshedAt;

  Future<Map<String, dynamic>> fetchSystemStatus() async {
    final response = await _requestForResolvedEndpoint(
      _GatewayAcpRpcRequest(
        id: _nextRequestId('status'),
        method: 'system.logs',
        params: const <String, dynamic>{},
      ),
      onNotification: (_) {},
    );
    return asMap(response['result']);
  }

  Future<GatewayAcpCapabilities> loadCapabilities({
    bool forceRefresh = false,
    Uri? endpointOverride,
    String authorizationOverride = '',
  }) async {
    if (!forceRefresh &&
        _capabilitiesRefreshedAt != null &&
        DateTime.now().difference(_capabilitiesRefreshedAt!) <
            const Duration(seconds: 15)) {
      return _cachedCapabilities;
    }

    final response = await _requestForResolvedEndpoint(
      _GatewayAcpRpcRequest(
        id: _nextRequestId('capabilities'),
        method: 'acp.capabilities',
        params: const <String, dynamic>{},
      ),
      onNotification: (_) {},
      endpointOverride: endpointOverride,
      authorizationOverride: authorizationOverride,
    );
    final result = asMap(response['result']);
    final caps = asMap(result['capabilities']);
    final providerCatalog = _parseProviderCatalog(
      result['providerCatalog'] ?? caps['providerCatalog'],
      defaultTarget: AssistantExecutionTarget.agent,
    );
    final gatewayProviderCatalog = _parseProviderCatalog(
      result['gatewayProviders'] ?? caps['gatewayProviders'],
      defaultTarget: AssistantExecutionTarget.gateway,
    );
    final singleAgent =
        boolValue(result['singleAgent']) ??
        boolValue(caps['single_agent']) ??
        providerCatalog.isNotEmpty;
    final multiAgent =
        boolValue(result['multiAgent']) ??
        boolValue(caps['multi_agent']) ??
        true;
    _cachedCapabilities = GatewayAcpCapabilities(
      singleAgent: singleAgent,
      multiAgent: multiAgent,
      availableExecutionTargets: _parseAvailableExecutionTargets(
        result['availableExecutionTargets'] ??
            caps['availableExecutionTargets'],
        singleAgent: singleAgent,
        gatewayProviderCatalog: gatewayProviderCatalog,
      ),
      providerCatalog: providerCatalog,
      gatewayProviderCatalog: gatewayProviderCatalog,
      raw: result,
      diagnostics: asMap(response['_xworkmateDiagnostics']),
    );
    _capabilitiesRefreshedAt = DateTime.now();
    return _cachedCapabilities;
  }

  List<SingleAgentProvider> _parseProviderCatalog(
    Object? raw, {
    required AssistantExecutionTarget defaultTarget,
  }) {
    final providers = <SingleAgentProvider>[];
    for (final item in asList(raw)) {
      final entry = asMap(item);
      final providerId = entry['providerId']?.toString().trim() ?? '';
      if (providerId.isEmpty) {
        continue;
      }
      final label = entry['label']?.toString().trim();
      final providerDisplay = asMap(entry['providerDisplay']);
      final targets = _parseProviderTargets(
        entry['targets'] ?? entry['executionTarget'],
        defaultTarget: defaultTarget,
      );
      final provider = SingleAgentProviderCopy.fromJsonValue(
        providerId,
        label: label?.isNotEmpty == true ? label : null,
        badge: entry['badge']?.toString().trim().isNotEmpty == true
            ? entry['badge']?.toString().trim()
            : providerDisplay['badge']?.toString().trim(),
        logoEmoji: entry['logoEmoji']?.toString().trim().isNotEmpty == true
            ? entry['logoEmoji']?.toString().trim()
            : providerDisplay['logoEmoji']?.toString().trim(),
        supportedTargets: targets,
        enabled: boolValue(entry['enabled']) ?? true,
        unavailableReason:
            entry['unavailableReason']?.toString().trim().isNotEmpty == true
            ? entry['unavailableReason']?.toString().trim()
            : '',
      );
      if (!provider.isUnspecified) {
        providers.add(provider);
      }
    }
    return normalizeSingleAgentProviderList(providers);
  }

  List<AssistantExecutionTarget> _parseAvailableExecutionTargets(
    Object? raw, {
    required bool singleAgent,
    required List<SingleAgentProvider> gatewayProviderCatalog,
  }) {
    final parsed = <AssistantExecutionTarget>[];
    for (final item in asList(raw)) {
      final normalized = item?.toString().trim().toLowerCase() ?? '';
      if (normalized == 'agent' || normalized == 'single-agent') {
        if (!parsed.contains(AssistantExecutionTarget.agent)) {
          parsed.add(AssistantExecutionTarget.agent);
        }
      } else if (normalized == 'gateway') {
        if (!parsed.contains(AssistantExecutionTarget.gateway)) {
          parsed.add(AssistantExecutionTarget.gateway);
        }
      }
    }
    if (parsed.isNotEmpty) {
      return parsed;
    }
    if (singleAgent) {
      parsed.add(AssistantExecutionTarget.agent);
    }
    if (gatewayProviderCatalog.isNotEmpty) {
      parsed.add(AssistantExecutionTarget.gateway);
    }
    return parsed;
  }

  List<AssistantExecutionTarget> _parseProviderTargets(
    Object? raw, {
    required AssistantExecutionTarget defaultTarget,
  }) {
    final parsed = <AssistantExecutionTarget>[];
    final items = raw is List ? raw : <Object?>[raw];
    for (final item in items) {
      final normalized = item?.toString().trim().toLowerCase() ?? '';
      if (normalized == 'agent' || normalized == 'single-agent') {
        if (!parsed.contains(AssistantExecutionTarget.agent)) {
          parsed.add(AssistantExecutionTarget.agent);
        }
      } else if (normalized == 'gateway') {
        if (!parsed.contains(AssistantExecutionTarget.gateway)) {
          parsed.add(AssistantExecutionTarget.gateway);
        }
      }
    }
    if (parsed.isNotEmpty) {
      return parsed;
    }
    return <AssistantExecutionTarget>[defaultTarget];
  }


  Future<void> cancelSession({
    required String sessionId,
    required String threadId,
    Uri? endpointOverride,
    String authorizationOverride = '',
  }) async {
    await _requestForResolvedEndpoint(
      _GatewayAcpRpcRequest(
        id: _nextRequestId('cancel'),
        method: 'session.cancel',
        params: <String, dynamic>{'sessionId': sessionId, 'threadId': threadId},
      ),
      onNotification: (_) {},
      endpointOverride: endpointOverride,
      authorizationOverride: authorizationOverride,
    );
  }

  Future<void> closeSession({
    required String sessionId,
    required String threadId,
    Uri? endpointOverride,
    String authorizationOverride = '',
  }) async {
    await _requestForResolvedEndpoint(
      _GatewayAcpRpcRequest(
        id: _nextRequestId('close'),
        method: 'session.close',
        params: <String, dynamic>{'sessionId': sessionId, 'threadId': threadId},
      ),
      onNotification: (_) {},
      endpointOverride: endpointOverride,
      authorizationOverride: authorizationOverride,
    );
  }

  Future<Map<String, dynamic>> request({
    required String method,
    required Map<String, dynamic> params,
    void Function(Map<String, dynamic>)? onNotification,
    Uri? endpointOverride,
    String authorizationOverride = '',
  }) async {
    return _requestForResolvedEndpoint(
      _GatewayAcpRpcRequest(
        id: _nextRequestId(method),
        method: method,
        params: params,
      ),
      onNotification: onNotification ?? (_) {},
      endpointOverride: endpointOverride,
      authorizationOverride: authorizationOverride,
    );
  }

  Future<void> dispose() async {}

  Future<Map<String, dynamic>> _requestForResolvedEndpoint(
    _GatewayAcpRpcRequest request, {
    required void Function(Map<String, dynamic>) onNotification,
    Uri? endpointOverride,
    String authorizationOverride = '',
  }) async {
    final resolvedEndpoint = endpointOverride ?? endpointResolver();
    final scheme = resolvedEndpoint?.scheme.trim().toLowerCase() ?? '';

    if (scheme == 'http' || scheme == 'https') {
      return _requestViaHttp(
        request,
        onNotification: onNotification,
        endpointOverride: resolvedEndpoint,
        authorizationOverride: authorizationOverride,
      );
    }

    return _requestViaWebSocket(
      request,
      onNotification: onNotification,
      endpointOverride: resolvedEndpoint,
      authorizationOverride: authorizationOverride,
    );
  }

  Future<Map<String, dynamic>> _requestViaWebSocket(
    _GatewayAcpRpcRequest request, {
    required void Function(Map<String, dynamic>) onNotification,
    Uri? endpointOverride,
    String authorizationOverride = '',
  }) async {
    final endpoint = resolveAcpWebSocketEndpoint(
      endpointOverride ?? endpointResolver(),
    );
    if (endpoint == null) {
      throw const GatewayAcpException(
        'Missing ACP endpoint',
        code: 'ACP_ENDPOINT_MISSING',
      );
    }
    return _requestViaWebSocketEndpoint(
      request,
      endpoint: endpoint,
      onNotification: onNotification,
      authorizationOverride: authorizationOverride,
    );
  }

  Future<Map<String, dynamic>> _requestViaWebSocketEndpoint(
    _GatewayAcpRpcRequest request, {
    required Uri endpoint,
    required void Function(Map<String, dynamic>) onNotification,
    String authorizationOverride = '',
  }) async {
    final authorization = await _resolveAuthorizationHeader(
      endpoint,
      authorizationOverride: authorizationOverride,
    );
    final socket =
        await WebSocket.connect(
          endpoint.toString(),
          headers: authorization.isEmpty
              ? null
              : <String, dynamic>{
                  HttpHeaders.authorizationHeader: authorization,
                },
        ).timeout(
          const Duration(seconds: 6),
          onTimeout: () => throw const GatewayAcpException(
            'ACP websocket connect timeout',
            code: 'ACP_WS_CONNECT_TIMEOUT',
          ),
        );
    final completer = Completer<Map<String, dynamic>>();
    late final StreamSubscription<dynamic> subscription;
    subscription = socket.listen(
      (raw) {
        final json = _decodeMap(raw);
        final id = stringValue(json['id']);
        final method = stringValue(json['method']) ?? '';
        if (id == request.id &&
            (json.containsKey('result') || json.containsKey('error'))) {
          if (!completer.isCompleted) {
            completer.complete(json);
          }
          return;
        }
        if (method.isNotEmpty) {
          onNotification(json);
        }
      },
      onError: (Object error, StackTrace stackTrace) {
        if (!completer.isCompleted) {
          completer.completeError(
            GatewayAcpException(error.toString(), code: 'ACP_WS_RUNTIME_ERROR'),
          );
        }
      },
      onDone: () {
        if (!completer.isCompleted) {
          completer.completeError(
            const GatewayAcpException(
              'ACP websocket closed before response',
              code: 'ACP_WS_EARLY_CLOSE',
            ),
          );
        }
      },
      cancelOnError: true,
    );

    socket.add(
      jsonEncode(<String, dynamic>{
        'jsonrpc': '2.0',
        'id': request.id,
        'method': request.method,
        'params': request.params,
      }),
    );
    try {
      final response = await completer.future.timeout(
        const Duration(seconds: 120),
      );
      _throwIfJsonRpcError(response);
      return <String, dynamic>{
        ...response,
        '_xworkmateDiagnostics': <String, dynamic>{
          'transport': 'websocket',
          'requestUrl': endpoint.toString(),
          'statusCode': null,
          'contentType': '',
          'bodyRead': true,
        },
      };
    } finally {
      await subscription.cancel();
      await socket.close();
    }
  }

  Future<Map<String, dynamic>> _requestViaHttp(
    _GatewayAcpRpcRequest request, {
    required void Function(Map<String, dynamic>) onNotification,
    Uri? endpointOverride,
    String authorizationOverride = '',
  }) async {
    final endpoint = _resolveHttpRpcEndpoint(endpointOverride, request.method);
    if (endpoint == null) {
      throw const GatewayAcpException(
        'Missing ACP HTTP endpoint',
        code: 'ACP_HTTP_ENDPOINT_MISSING',
      );
    }

    GatewayAcpException? lastRetryableError;
    for (
      var attempt = 0;
      attempt <= gatewayAcpHttpHandshakeInterruptedRetryCount;
      attempt += 1
    ) {
      try {
        return await _requestViaHttpAttempt(
          request,
          endpoint: endpoint,
          onNotification: onNotification,
          authorizationOverride: authorizationOverride,
          retryAttempt: attempt,
        );
      } on GatewayAcpException catch (error) {
        final retryLimit = _httpRetryCountForError(error);
        if (retryLimit == null || attempt >= retryLimit) {
          rethrow;
        }
        lastRetryableError = error;
        await Future<void>.delayed(_httpRetryDelayFor(error, attempt));
      }
    }
    throw lastRetryableError ??
        const GatewayAcpException(
          'ACP HTTP handshake was interrupted before the response started',
          code: gatewayAcpHttpHandshakeInterruptedCode,
        );
  }

  int? _httpRetryCountForError(GatewayAcpException error) {
    return switch (error.code) {
      gatewayAcpHttpHandshakeInterruptedCode =>
        gatewayAcpHttpHandshakeInterruptedRetryCount,
      gatewayAcpHttpConnectTimeoutCode ||
      gatewayAcpHttpConnectFailedCode => gatewayAcpHttpConnectFailureRetryCount,
      _ => null,
    };
  }

  Duration _httpRetryDelayFor(GatewayAcpException error, int attempt) {
    if (error.code == gatewayAcpHttpConnectTimeoutCode ||
        error.code == gatewayAcpHttpConnectFailedCode) {
      return Duration(milliseconds: 200 * (1 << attempt));
    }
    return Duration(milliseconds: 50 * (attempt + 1));
  }

  Future<Map<String, dynamic>> _requestViaHttpAttempt(
    _GatewayAcpRpcRequest request, {
    required Uri endpoint,
    required void Function(Map<String, dynamic>) onNotification,
    required String authorizationOverride,
    required int retryAttempt,
  }) async {
    final client = HttpClient()
      ..connectionTimeout = gatewayAcpHttpConnectTimeout;
    var statusCode = 0;
    var contentType = '';
    var bodyRead = false;
    var phase = _GatewayAcpHttpRequestPhase.connect;
    try {
      final authorization = await _resolveAuthorizationHeader(
        endpoint,
        authorizationOverride: authorizationOverride,
      );
      phase = _GatewayAcpHttpRequestPhase.connect;
      final httpRequest = await client
          .postUrl(endpoint)
          .timeout(gatewayAcpHttpConnectTimeout);
      phase = _GatewayAcpHttpRequestPhase.write;
      httpRequest.headers.set(
        HttpHeaders.contentTypeHeader,
        'application/json; charset=utf-8',
      );
      httpRequest.headers.set(
        HttpHeaders.acceptHeader,
        'text/event-stream, application/json',
      );
      if (authorization.isNotEmpty) {
        httpRequest.headers.set(HttpHeaders.authorizationHeader, authorization);
      }
      httpRequest.add(
        utf8.encode(
          jsonEncode(<String, dynamic>{
            'jsonrpc': '2.0',
            'id': request.id,
            'method': request.method,
            'params': request.params,
          }),
        ),
      );
      phase = _GatewayAcpHttpRequestPhase.waitingForResponse;
      final response = await httpRequest.close().timeout(
        gatewayAcpHttpResponseTimeoutFor(
          endpoint,
          request.method,
          request.params,
        ),
      );
      statusCode = response.statusCode;
      contentType =
          response.headers.contentType?.mimeType.toLowerCase() ??
          response.headers
              .value(HttpHeaders.contentTypeHeader)
              ?.toLowerCase() ??
          '';
      phase = _GatewayAcpHttpRequestPhase.bodyRead;
      if (response.statusCode < 200 || response.statusCode >= 300) {
        final body = await response.transform(utf8.decoder).join();
        bodyRead = body.isNotEmpty;
        throw GatewayAcpException(
          _describeHttpError(
            statusCode: response.statusCode,
            contentType: contentType,
            body: body,
          ),
          code: 'ACP_HTTP_${response.statusCode}',
          details: <String, dynamic>{
            'requestUrl': endpoint.toString(),
            'statusCode': response.statusCode,
            'contentType': contentType,
            'bodyRead': bodyRead,
          },
        );
      }
      if (contentType.contains('text/event-stream')) {
        final decoded = await _consumeSseRpcResponse(
          response: response,
          requestId: request.id,
          onNotification: onNotification,
        );
        return <String, dynamic>{
          ...decoded.response,
          '_xworkmateDiagnostics': <String, dynamic>{
            'transport': 'http-sse',
            'requestUrl': endpoint.toString(),
            'statusCode': response.statusCode,
            'contentType': contentType,
            'bodyRead': true,
            'sseKeepaliveReceived': decoded.keepaliveReceived,
            'sseLastEventAtMs': decoded.lastEventAtMs,
            'sseEventCount': decoded.eventCount,
          },
        };
      }
      final body = await response.transform(utf8.decoder).join();
      bodyRead = body.isNotEmpty;
      final decoded = _decodeMap(body);
      _throwIfJsonRpcError(decoded);
      return <String, dynamic>{
        ...decoded,
        '_xworkmateDiagnostics': <String, dynamic>{
          'transport': 'http',
          'requestUrl': endpoint.toString(),
          'statusCode': response.statusCode,
          'contentType': contentType,
          'bodyRead': bodyRead,
        },
      };
    } on GatewayAcpException {
      rethrow;
    } on TimeoutException catch (error) {
      if (phase == _GatewayAcpHttpRequestPhase.connect) {
        throw _connectException(
          endpoint: endpoint,
          statusCode: statusCode,
          contentType: contentType,
          bodyRead: bodyRead,
          retryAttempt: retryAttempt,
          phase: phase,
          originalError: error,
          timeout: true,
        );
      }
      rethrow;
    } on HandshakeException catch (error) {
      throw _handshakeInterruptedException(
        endpoint: endpoint,
        statusCode: statusCode,
        contentType: contentType,
        bodyRead: bodyRead,
        retryAttempt: retryAttempt,
        originalError: error,
      );
    } on SocketException catch (error) {
      if (_looksLikeHandshakeInterruptedSocketError(
        error.toString(),
        endpoint: endpoint,
        statusCode: statusCode,
        bodyRead: bodyRead,
      )) {
        throw _handshakeInterruptedException(
          endpoint: endpoint,
          statusCode: statusCode,
          contentType: contentType,
          bodyRead: bodyRead,
          retryAttempt: retryAttempt,
          originalError: error,
        );
      }
      if (phase == _GatewayAcpHttpRequestPhase.connect &&
          statusCode == 0 &&
          !bodyRead) {
        throw _connectException(
          endpoint: endpoint,
          statusCode: statusCode,
          contentType: contentType,
          bodyRead: bodyRead,
          retryAttempt: retryAttempt,
          phase: phase,
          originalError: error,
          timeout: _looksLikeConnectTimeout(error.toString()),
        );
      }
      rethrow;
    } on HttpException catch (error) {
      if (_looksLikeConnectionClosedBeforeResponse(error.toString())) {
        throw GatewayAcpException(
          'ACP HTTP connection closed before the response finished arriving',
          code: 'ACP_HTTP_CONNECTION_CLOSED',
          details: <String, dynamic>{
            'requestUrl': endpoint.toString(),
            'statusCode': statusCode,
            'contentType': contentType,
            'bodyRead': bodyRead,
            'originalError': error.toString(),
          },
        );
      }
      rethrow;
    } finally {
      client.close(force: true);
    }
  }

  GatewayAcpException _connectException({
    required Uri endpoint,
    required int statusCode,
    required String contentType,
    required bool bodyRead,
    required int retryAttempt,
    required _GatewayAcpHttpRequestPhase phase,
    required Object originalError,
    required bool timeout,
  }) {
    final code = timeout
        ? gatewayAcpHttpConnectTimeoutCode
        : gatewayAcpHttpConnectFailedCode;
    final message = timeout
        ? 'ACP HTTP connection timed out before the request was confirmed'
        : 'ACP HTTP connection failed before the request was confirmed';
    return GatewayAcpException(
      message,
      code: code,
      details: <String, dynamic>{
        'requestUrl': endpoint.toString(),
        'statusCode': statusCode,
        'contentType': contentType,
        'bodyRead': bodyRead,
        'phase': phase.name,
        'retryAttempt': retryAttempt,
        'maxRetryAttempts': gatewayAcpHttpConnectFailureRetryCount,
        'originalError': originalError.toString(),
      },
    );
  }

  GatewayAcpException _handshakeInterruptedException({
    required Uri endpoint,
    required int statusCode,
    required String contentType,
    required bool bodyRead,
    required int retryAttempt,
    required Object originalError,
  }) {
    return GatewayAcpException(
      'ACP HTTP handshake was interrupted before the response started',
      code: gatewayAcpHttpHandshakeInterruptedCode,
      details: <String, dynamic>{
        'requestUrl': endpoint.toString(),
        'statusCode': statusCode,
        'contentType': contentType,
        'bodyRead': bodyRead,
        'retryAttempt': retryAttempt,
        'maxRetryAttempts': gatewayAcpHttpHandshakeInterruptedRetryCount,
        'originalError': originalError.toString(),
      },
    );
  }

  bool _looksLikeHandshakeInterruptedSocketError(
    String raw, {
    required Uri endpoint,
    required int statusCode,
    required bool bodyRead,
  }) {
    if (endpoint.scheme != 'https' || statusCode != 0 || bodyRead) {
      return false;
    }
    final lowered = raw.toLowerCase();
    return lowered.contains('connection reset') ||
        lowered.contains('read failed') ||
        lowered.contains('connection terminated during handshake');
  }

  bool _looksLikeConnectionClosedBeforeResponse(String raw) {
    final lowered = raw.toLowerCase();
    return lowered.contains('connection closed before full header') ||
        lowered.contains('connection closed while receiving data') ||
        lowered.contains('connection terminated during body read') ||
        lowered.contains('stream closed');
  }

  bool _looksLikeConnectTimeout(String raw) {
    final lowered = raw.toLowerCase();
    return lowered.contains('connection timed out') ||
        lowered.contains('timed out') ||
        lowered.contains('timeout');
  }

  String _describeHttpError({
    required int statusCode,
    required String contentType,
    required String body,
  }) {
    final base = 'ACP HTTP request failed ($statusCode)';
    final normalizedType = contentType.trim();
    final detail = _extractErrorDetail(body);
    if (normalizedType.isNotEmpty &&
        !_contentTypeLooksJsonOrSse(normalizedType)) {
      if (detail.isNotEmpty) {
        return '$base · $detail · unexpected content type: $normalizedType';
      }
      return '$base · unexpected content type: $normalizedType';
    }

    if (detail.isNotEmpty) {
      return '$base · $detail';
    }
    return base;
  }

  bool _contentTypeLooksJsonOrSse(String contentType) {
    return contentType.contains('application/json') ||
        contentType.contains('application/problem+json') ||
        contentType.contains('text/json') ||
        contentType.contains('text/event-stream');
  }

  String _extractErrorDetail(String body) {
    final trimmed = body.trim();
    if (trimmed.isEmpty) {
      return '';
    }
    try {
      final decoded = _decodeMap(trimmed);
      final detail = _extractStructuredErrorDetail(decoded);
      if (detail.isNotEmpty) {
        return detail;
      }
    } on FormatException {
      // Fall through to textual snippet extraction below.
    }

    final singleLine = trimmed.replaceAll(RegExp(r'\s+'), ' ');
    if (singleLine.isEmpty) {
      return '';
    }
    return singleLine.length <= 160
        ? singleLine
        : '${singleLine.substring(0, 157)}...';
  }

  String _extractStructuredErrorDetail(Map<String, dynamic> decoded) {
    final candidates = <String>[];
    void addCandidate(Object? value) {
      final text = _extractStructuredErrorText(value).trim();
      if (text.isNotEmpty && !candidates.contains(text)) {
        candidates.add(text);
      }
    }

    final error = decoded['error'];
    addCandidate(error);
    if (error is Map) {
      final errorMap = error.cast<String, dynamic>();
      addCandidate(errorMap['data']);
      addCandidate(errorMap['details']);
    }
    for (final key in const <String>[
      'message',
      'detail',
      'errorMessage',
      'unavailableMessage',
      'reason',
      'description',
      'body',
    ]) {
      addCandidate(decoded[key]);
    }
    final code = stringValue(decoded['code']) ?? '';
    if (candidates.isEmpty && code.isNotEmpty) {
      candidates.add(code);
    }
    return candidates.join(' · ');
  }

  String _extractStructuredErrorText(Object? value, [Set<Object>? visited]) {
    if (value == null) {
      return '';
    }
    if (value is String) {
      return value.trim();
    }
    if (value is num || value is bool) {
      return value.toString();
    }
    final seen = visited ?? <Object>{};
    if (!seen.add(value)) {
      return '';
    }
    if (value is Map) {
      final map = value.cast<String, dynamic>();
      final parts = <String>[];
      for (final key in const <String>[
        'message',
        'detail',
        'error',
        'errorMessage',
        'unavailableMessage',
        'upstreamError',
        'reason',
        'description',
      ]) {
        final text = _extractStructuredErrorText(map[key], seen);
        if (text.isNotEmpty && !parts.contains(text)) {
          parts.add(text);
        }
      }
      final code =
          stringValue(map['code']) ?? stringValue(map['unavailableCode']) ?? '';
      final upstream =
          stringValue(map['upstreamMethod']) ??
          stringValue(map['upstream']) ??
          '';
      if (code.isNotEmpty && parts.every((part) => !part.contains(code))) {
        parts.add('code: $code');
      }
      if (upstream.isNotEmpty) {
        parts.add('upstream: $upstream');
      }
      if (parts.length <= 1) {
        return parts.join();
      }
      return '${parts.first} (${parts.skip(1).join(', ')})';
    }
    if (value is Iterable) {
      return value
          .map((item) => _extractStructuredErrorText(item, seen))
          .where((item) => item.isNotEmpty)
          .join(' · ');
    }
    return value.toString().trim();
  }

  Future<String> _resolveAuthorizationHeader(
    Uri endpoint, {
    String authorizationOverride = '',
  }) async {
    final override = _normalizeAuthorizationHeader(authorizationOverride);
    if (override.isNotEmpty) {
      return override;
    }
    return _normalizeAuthorizationHeader(
      (await authorizationResolver?.call(endpoint))?.trim() ?? '',
    );
  }

  String _normalizeAuthorizationHeader(String raw) {
    final trimmed = raw.trim();
    if (trimmed.isEmpty) {
      return '';
    }
    if (_looksLikeAuthorizationHeader(trimmed)) {
      return trimmed;
    }
    return 'Bearer $trimmed';
  }

  bool _looksLikeAuthorizationHeader(String raw) {
    final separatorIndex = raw.indexOf(RegExp(r'\s'));
    if (separatorIndex <= 0 || separatorIndex >= raw.length - 1) {
      return false;
    }
    final scheme = raw.substring(0, separatorIndex);
    return RegExp(r"^[A-Za-z][A-Za-z0-9!#$%&'*+.^_`|~-]*$").hasMatch(scheme);
  }

  Future<_GatewayAcpSseRpcResponse> _consumeSseRpcResponse({
    required HttpClientResponse response,
    required String requestId,
    required void Function(Map<String, dynamic>) onNotification,
  }) async {
    Map<String, dynamic>? resolvedResponse;
    final eventLines = <String>[];
    var eventCount = 0;
    var keepaliveReceived = false;
    var lastEventAtMs = 0;

    void consumeEventPayload(String payload) {
      final trimmed = payload.trim();
      if (trimmed.isEmpty || trimmed == '[DONE]') {
        return;
      }
      eventCount += 1;
      lastEventAtMs = DateTime.now().millisecondsSinceEpoch;
      final json = _decodeMap(trimmed);
      if (stringValue(json['id']) == requestId &&
          (json.containsKey('result') || json.containsKey('error'))) {
        resolvedResponse ??= json;
        return;
      }
      final method = stringValue(json['method']) ?? '';
      if (method == 'xworkmate.bridge.keepalive') {
        keepaliveReceived = true;
      }
      if (method.isNotEmpty) {
        onNotification(json);
      }
    }

    try {
      await for (final line
          in response.transform(utf8.decoder).transform(const LineSplitter())) {
        if (line.isEmpty) {
          if (eventLines.isNotEmpty) {
            consumeEventPayload(eventLines.join('\n'));
            eventLines.clear();
            if (resolvedResponse != null) {
              break;
            }
          }
          continue;
        }
        if (line.startsWith('data:')) {
          eventLines.add(line.substring(5).trimLeft());
        }
      }
    } on HttpException catch (error, stackTrace) {
      if (resolvedResponse == null && eventLines.isNotEmpty) {
        try {
          consumeEventPayload(eventLines.join('\n'));
          eventLines.clear();
        } on FormatException {
          Error.throwWithStackTrace(error, stackTrace);
        }
      }
      if (resolvedResponse == null) {
        rethrow;
      }
    }

    if (eventLines.isNotEmpty && resolvedResponse == null) {
      consumeEventPayload(eventLines.join('\n'));
    }
    final resolved = resolvedResponse;
    if (resolved == null) {
      throw GatewayAcpException(
        'ACP SSE ended without JSON-RPC response for request $requestId',
        code: 'ACP_SSE_NO_RESULT',
      );
    }
    _throwIfJsonRpcError(resolved);
    return _GatewayAcpSseRpcResponse(
      response: resolved,
      keepaliveReceived: keepaliveReceived,
      lastEventAtMs: lastEventAtMs,
      eventCount: eventCount,
    );
  }



  Map<String, dynamic> asMap(Object? raw) {
    if (raw is Map<String, dynamic>) {
      return raw;
    }
    if (raw is Map) {
      return raw.cast<String, dynamic>();
    }
    return const <String, dynamic>{};
  }

  List<Object?> asList(Object? raw) {
    if (raw is List<Object?>) {
      return raw;
    }
    if (raw is List) {
      return raw.cast<Object?>();
    }
    return const <Object?>[];
  }

  String? stringValue(Object? raw) {
    if (raw == null) {
      return null;
    }
    final value = raw.toString().trim();
    return value.isEmpty ? null : value;
  }

  bool? boolValue(Object? raw) {
    if (raw is bool) {
      return raw;
    }
    if (raw is num) {
      return raw != 0;
    }
    final text = raw?.toString().trim().toLowerCase();
    if (text == null || text.isEmpty) {
      return null;
    }
    if (text == 'true' || text == '1' || text == 'yes') {
      return true;
    }
    if (text == 'false' || text == '0' || text == 'no') {
      return false;
    }
    return null;
  }

  int? intValue(Object? raw) {
    if (raw is int) {
      return raw;
    }
    if (raw is num) {
      return raw.toInt();
    }
    return int.tryParse(raw?.toString().trim() ?? '');
  }

  void _throwIfJsonRpcError(Map<String, dynamic> envelope) {
    final error = asMap(envelope['error']);
    if (error.isEmpty) {
      return;
    }
    final details = error['data'] ?? error['details'];
    throw GatewayAcpException(
      stringValue(error['message']) ?? 'ACP JSON-RPC request failed',
      code: stringValue(error['code']),
      detailCode: _jsonRpcErrorDetailCode(details),
      details: details,
    );
  }

  String? _jsonRpcErrorDetailCode(Object? details) {
    final data = asMap(details);
    return stringValue(data['code']) ??
        stringValue(data['detailCode']) ??
        stringValue(data['errorCode']);
  }

  Map<String, dynamic> _decodeMap(dynamic raw) {
    if (raw is Map<String, dynamic>) {
      return raw;
    }
    if (raw is Map) {
      return raw.cast<String, dynamic>();
    }
    final text = raw is String ? raw : utf8.decode(raw as List<int>);
    final decoded = jsonDecode(_extractFirstJsonDocument(text));
    if (decoded is Map<String, dynamic>) {
      return decoded;
    }
    if (decoded is Map) {
      return decoded.cast<String, dynamic>();
    }
    return const <String, dynamic>{};
  }

  Uri? _resolveHttpRpcEndpoint([Uri? endpointOverride, String method = '']) {
    final endpoint = endpointOverride ?? endpointResolver();
    return resolveAcpHttpRpcEndpoint(endpoint);
  }

  String _nextRequestId(String method) {
    return '${DateTime.now().microsecondsSinceEpoch}-$method-${_requestCounter++}';
  }

  String _extractFirstJsonDocument(String text) {
    final trimmed = text.trim();
    if (trimmed.isEmpty) {
      throw const FormatException('Empty response body');
    }
    final objectStart = trimmed.indexOf('{');
    final arrayStart = trimmed.indexOf('[');
    var start = -1;
    if (objectStart >= 0 && arrayStart >= 0) {
      start = objectStart < arrayStart ? objectStart : arrayStart;
    } else if (objectStart >= 0) {
      start = objectStart;
    } else if (arrayStart >= 0) {
      start = arrayStart;
    }
    if (start < 0) {
      throw const FormatException('Missing JSON document');
    }

    var depth = 0;
    var inString = false;
    var escaped = false;
    for (var index = start; index < trimmed.length; index++) {
      final char = trimmed[index];
      if (inString) {
        if (escaped) {
          escaped = false;
        } else if (char == r'\') {
          escaped = true;
        } else if (char == '"') {
          inString = false;
        }
        continue;
      }
      if (char == '"') {
        inString = true;
        continue;
      }
      if (char == '{' || char == '[') {
        depth += 1;
      } else if (char == '}' || char == ']') {
        depth -= 1;
        if (depth == 0) {
          return trimmed.substring(start, index + 1);
        }
      }
    }
    throw const FormatException('Unterminated JSON document');
  }
}

class GatewayAcpRuntimeSessionClient implements GatewayRuntimeSessionClient {
  GatewayAcpRuntimeSessionClient({required this.client});

  final GatewayAcpClient client;
  final StreamController<GatewayRuntimeSessionUpdate> _updates =
      StreamController<GatewayRuntimeSessionUpdate>.broadcast();

  @override
  Stream<GatewayRuntimeSessionUpdate> get updates => _updates.stream;

  @override
  Future<GatewayRuntimeSessionConnectResult> connect(
    GatewayRuntimeSessionConnectRequest request,
  ) async {
    final envelope = await client.request(
      method: 'xworkmate.gateway.connect',
      params: request.toJson(),
      onNotification: _handleNotification,
    );
    final result = client.asMap(envelope['result']);
    _throwGatewayResultIfNeeded(result, 'gateway connect failed');
    return GatewayRuntimeSessionConnectResult.fromJson(result);
  }

  @override
  Future<dynamic> request({
    required String runtimeId,
    required String method,
    Map<String, dynamic>? params,
    Duration timeout = const Duration(seconds: 15),
    bool allowErrorPayload = false,
  }) async {
    final envelope = await client.request(
      method: 'xworkmate.gateway.request',
      params: <String, dynamic>{
        'runtimeId': runtimeId,
        'method': method,
        'params': params ?? const <String, dynamic>{},
        'timeoutMs': timeout.inMilliseconds,
      },
      onNotification: _handleNotification,
    );
    final result = client.asMap(envelope['result']);
    if (allowErrorPayload &&
        !(client.boolValue(result['ok']) ?? false) &&
        result.containsKey('payload')) {
      return result['payload'];
    }
    _throwGatewayResultIfNeeded(result, '$method request failed');
    return result['payload'];
  }

  @override
  Future<void> disconnect({required String runtimeId}) async {
    await client.request(
      method: 'xworkmate.gateway.disconnect',
      params: <String, dynamic>{'runtimeId': runtimeId},
      onNotification: _handleNotification,
    );
  }

  @override
  Future<void> dispose() async {
    await _updates.close();
  }

  void _handleNotification(Map<String, dynamic> notification) {
    final method = notification['method']?.toString().trim() ?? '';
    if (!method.startsWith('xworkmate.gateway.')) {
      return;
    }
    try {
      _updates.add(GatewayRuntimeSessionUpdate.fromNotification(notification));
    } on GatewayRuntimeException {
      // Other bridge notifications are intentionally ignored by this adapter.
    }
  }

  void _throwGatewayResultIfNeeded(
    Map<String, dynamic> result,
    String fallbackMessage,
  ) {
    if (client.boolValue(result['ok']) ?? false) {
      return;
    }
    final error = client.asMap(result['error']);
    throw GatewayRuntimeException(
      client.stringValue(error['message']) ?? fallbackMessage,
      code: client.stringValue(error['code']),
      details: error['details'] ?? error,
    );
  }
}

Duration gatewayAcpHttpResponseTimeoutFor(
  Uri endpoint,
  String method, [
  Map<String, dynamic> params = const <String, dynamic>{},
]) {
  return const Duration(seconds: 120);
}

class _GatewayAcpRpcRequest {
  const _GatewayAcpRpcRequest({
    required this.id,
    required this.method,
    required this.params,
  });

  final String id;
  final String method;
  final Map<String, dynamic> params;
}

class _GatewayAcpSseRpcResponse {
  const _GatewayAcpSseRpcResponse({
    required this.response,
    required this.keepaliveReceived,
    required this.lastEventAtMs,
    required this.eventCount,
  });

  final Map<String, dynamic> response;
  final bool keepaliveReceived;
  final int lastEventAtMs;
  final int eventCount;
}
