import 'dart:convert';

import 'package:crypto/crypto.dart' as crypto;

import 'runtime_models.dart';

enum GoTaskServiceRoute { externalAcpSingle, externalAcpMulti }

enum GoTaskServiceCollaborationMode { standard, multiAgent }

class ExternalCodeAgentAcpCapabilities {
  const ExternalCodeAgentAcpCapabilities({
    required this.singleAgent,
    required this.multiAgent,
    required this.availableExecutionTargets,
    required this.providerCatalog,
    required this.gatewayProviders,
    required this.raw,
  });

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

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

class ExternalCodeAgentAcpRoutingResolution {
  const ExternalCodeAgentAcpRoutingResolution({required this.raw});

  final Map<String, dynamic> raw;

  String get resolvedExecutionTarget =>
      raw['resolvedExecutionTarget']?.toString().trim() ?? '';

  String get resolvedEndpointTarget =>
      raw['resolvedEndpointTarget']?.toString().trim() ?? '';

  String get resolvedProviderId =>
      raw['resolvedProviderId']?.toString().trim() ?? '';

  String get resolvedGatewayProviderId =>
      raw['resolvedGatewayProviderId']?.toString().trim() ?? '';

  String get resolvedModel => raw['resolvedModel']?.toString().trim() ?? '';

  List<String> get resolvedSkills {
    final rawList = raw['resolvedSkills'];
    if (rawList is! List) {
      return const <String>[];
    }
    return rawList
        .map((item) => item?.toString().trim() ?? '')
        .where((item) => item.isNotEmpty)
        .toList(growable: false);
  }

  String get skillResolutionSource =>
      raw['skillResolutionSource']?.toString().trim() ?? '';

  bool get needsSkillInstall => _boolValue(raw['needsSkillInstall']) ?? false;

  String get skillInstallRequestId =>
      raw['skillInstallRequestId']?.toString().trim() ?? '';

  List<Map<String, dynamic>> get skillCandidates =>
      _castMapList(raw['skillCandidates']);

  bool get unavailable => _boolValue(raw['unavailable']) ?? false;

  String get unavailableCode => raw['unavailableCode']?.toString().trim() ?? '';

  String get unavailableMessage =>
      raw['unavailableMessage']?.toString().trim() ?? '';
}

enum ExternalCodeAgentAcpRoutingMode { auto, explicit }

class ExternalCodeAgentAcpAvailableSkill {
  const ExternalCodeAgentAcpAvailableSkill({
    required this.id,
    required this.label,
    required this.description,
    this.installed = true,
  });

  final String id;
  final String label;
  final String description;
  final bool installed;

  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'id': id.trim(),
      'label': label.trim(),
      'description': description.trim(),
      'installed': installed,
    };
  }
}

class ExternalCodeAgentAcpRoutingConfig {
  const ExternalCodeAgentAcpRoutingConfig({
    required this.mode,
    required this.preferredGatewayTarget,
    required this.explicitExecutionTarget,
    required this.explicitProviderId,
    required this.explicitModel,
    required this.explicitSkills,
    required this.allowSkillInstall,
    required this.availableSkills,
    this.installApproval,
  });

  const ExternalCodeAgentAcpRoutingConfig.auto({
    this.preferredGatewayTarget = '',
    this.availableSkills = const <ExternalCodeAgentAcpAvailableSkill>[],
  }) : mode = ExternalCodeAgentAcpRoutingMode.auto,
       explicitExecutionTarget = '',
       explicitProviderId = '',
       explicitModel = '',
       explicitSkills = const <String>[],
       allowSkillInstall = false,
       installApproval = null;

  final ExternalCodeAgentAcpRoutingMode mode;
  final String preferredGatewayTarget;
  final String explicitExecutionTarget;
  final String explicitProviderId;
  final String explicitModel;
  final List<String> explicitSkills;
  final bool allowSkillInstall;
  final List<ExternalCodeAgentAcpAvailableSkill> availableSkills;
  final ExternalCodeAgentAcpSkillInstallApproval? installApproval;

  bool get isAuto => mode == ExternalCodeAgentAcpRoutingMode.auto;

  ExternalCodeAgentAcpRoutingConfig withoutExplicitModel() {
    if (explicitModel.trim().isEmpty) {
      return this;
    }
    return ExternalCodeAgentAcpRoutingConfig(
      mode: mode,
      preferredGatewayTarget: preferredGatewayTarget,
      explicitExecutionTarget: explicitExecutionTarget,
      explicitProviderId: explicitProviderId,
      explicitModel: '',
      explicitSkills: explicitSkills,
      allowSkillInstall: allowSkillInstall,
      availableSkills: availableSkills,
      installApproval: installApproval,
    );
  }

  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'routingMode': mode.name,
      if (preferredGatewayTarget.trim().isNotEmpty)
        'preferredGatewayProviderId': preferredGatewayTarget.trim(),
      if (explicitExecutionTarget.trim().isNotEmpty)
        'explicitExecutionTarget': explicitExecutionTarget.trim(),
      if (explicitProviderId.trim().isNotEmpty)
        'explicitProviderId': explicitProviderId.trim(),
      if (explicitModel.trim().isNotEmpty)
        'explicitModel': explicitModel.trim(),
      'explicitSkills': explicitSkills
          .map((item) => item.trim())
          .where((item) => item.isNotEmpty)
          .toList(growable: false),
      'allowSkillInstall': allowSkillInstall,
      'availableSkills': availableSkills
          .map((item) => item.toJson())
          .toList(growable: false),
      if (installApproval != null) 'installApproval': installApproval!.toJson(),
    };
  }
}

class ExternalCodeAgentAcpSkillInstallApproval {
  const ExternalCodeAgentAcpSkillInstallApproval({
    required this.requestId,
    required this.approvedSkillKeys,
  });

  final String requestId;
  final List<String> approvedSkillKeys;

  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'requestId': requestId.trim(),
      'approvedSkillKeys': approvedSkillKeys
          .map((item) => item.trim())
          .where((item) => item.isNotEmpty)
          .toList(growable: false),
    };
  }
}

class GoTaskServiceRequest {
  const GoTaskServiceRequest({
    required this.sessionId,
    required this.threadId,
    required this.target,
    required this.prompt,
    required this.workingDirectory,
    required this.model,
    required this.thinking,
    required this.selectedSkills,
    required this.inlineAttachments,
    required this.localAttachments,
    required this.agentId,
    required this.metadata,
    this.routing,
    this.routingHint = '',
    this.provider = SingleAgentProvider.unspecified,
    this.remoteWorkingDirectoryHint = '',
    this.resumeSession = false,
    this.collaborationMode = GoTaskServiceCollaborationMode.standard,
    this.multiAgent = false,
  });

  final String sessionId;
  final String threadId;
  final AssistantExecutionTarget target;
  final String prompt;
  final String workingDirectory;
  final String model;
  final String thinking;
  final List<String> selectedSkills;
  final List<GatewayChatAttachmentPayload> inlineAttachments;
  final List<CollaborationAttachment> localAttachments;
  final String agentId;
  final Map<String, dynamic> metadata;
  final ExternalCodeAgentAcpRoutingConfig? routing;
  final String routingHint;
  final SingleAgentProvider provider;
  final String remoteWorkingDirectoryHint;
  final bool resumeSession;
  final GoTaskServiceCollaborationMode collaborationMode;
  final bool multiAgent;

  bool get isMultiAgentRequest =>
      multiAgent ||
      collaborationMode == GoTaskServiceCollaborationMode.multiAgent;

  AssistantExecutionTarget get normalizedTarget => target;

  GoTaskServiceRoute get route {
    if (isMultiAgentRequest) {
      return GoTaskServiceRoute.externalAcpMulti;
    }
    return GoTaskServiceRoute.externalAcpSingle;
  }

  String get acpMode {
    return _gatewaySessionMode;
  }

  String get routingExecutionTarget {
    return normalizedTarget.promptValue;
  }

  bool get hasInlineAttachments => inlineAttachments.isNotEmpty;

  ExternalCodeAgentAcpRoutingConfig get effectiveRouting =>
      routing ?? _synthesizedRouting();

  Map<String, dynamic> toExternalAcpParams() {
    final resolvedRouting = normalizedTarget.isGateway
        ? effectiveRouting.withoutExplicitModel()
        : effectiveRouting;
    final requestModel = normalizedTarget.isGateway ? '' : model.trim();
    final providerId = provider.isUnspecified ? '' : provider.providerId;
    final agentProviderId = normalizedTarget.isGateway ? '' : providerId;
    final params = <String, dynamic>{
      'sessionId': sessionId,
      'threadId': threadId,
      'mode': acpMode,
      'taskPrompt': prompt,
      'workingDirectory': workingDirectory.trim(),
      'selectedSkills': selectedSkills,
      'attachments': <Map<String, dynamic>>[
        ...localAttachments.map(
          (item) => <String, dynamic>{
            'name': item.name,
            'description': item.description,
            'path': item.path,
          },
        ),
        ...inlineAttachments.map(
          (item) => <String, dynamic>{
            'name': item.fileName,
            'description': item.mimeType,
            'path': item.sourcePath.trim(),
          },
        ),
      ],
      if (inlineAttachments.isNotEmpty)
        'inlineAttachments': inlineAttachments
            .map(
              (item) => <String, dynamic>{
                'name': item.fileName,
                'mimeType': item.mimeType,
                'content': item.content,
                'sizeBytes': goTaskServiceBase64Size(item.content),
                if (goTaskServiceAttachmentSha256(item).isNotEmpty)
                  'sha256': goTaskServiceAttachmentSha256(item),
                if (item.sourcePath.trim().isNotEmpty)
                  'sourcePath': item.sourcePath.trim(),
              },
            )
            .toList(growable: false),
      if (agentProviderId.isNotEmpty) 'provider': agentProviderId,
      if (remoteWorkingDirectoryHint.trim().isNotEmpty)
        'remoteWorkingDirectoryHint': remoteWorkingDirectoryHint.trim(),
      if (requestModel.isNotEmpty) 'model': requestModel,
      if (thinking.trim().isNotEmpty) 'thinking': thinking.trim(),
      'routing': resolvedRouting.toJson(),
      if (routingHint.trim().isNotEmpty) 'routingHint': routingHint.trim(),
      'requestedExecutionTarget': normalizedTarget.promptValue,
      if (_usesGatewaySessionMode(acpMode)) ...<String, dynamic>{
        'executionTarget': normalizedTarget.promptValue,
        'appThreadKey': threadId,
        if (agentId.trim().isNotEmpty) 'agentId': agentId.trim(),
        if (metadata.isNotEmpty) 'metadata': metadata,
      },
    };
    return params;
  }

  ExternalCodeAgentAcpRoutingConfig _synthesizedRouting() {
    final gatewayTarget = normalizedTarget;
    final preferredGatewayTarget = switch (gatewayTarget) {
      AssistantExecutionTarget.agent => kCanonicalGatewayProviderId,
      AssistantExecutionTarget.gateway => kCanonicalGatewayProviderId,
    };
    final explicitExecutionTarget = switch (gatewayTarget) {
      AssistantExecutionTarget.agent => 'agent',
      AssistantExecutionTarget.gateway => 'gateway',
    };
    final explicitProviderId = provider.isUnspecified || gatewayTarget.isGateway
        ? ''
        : provider.providerId;
    final explicitModelValue = gatewayTarget.isGateway ? '' : model.trim();
    final explicitSkillsValue = selectedSkills
        .map((item) => item.trim())
        .where((item) => item.isNotEmpty)
        .toList(growable: false);
    final hasExplicitSelection =
        explicitExecutionTarget.isNotEmpty ||
        explicitProviderId.isNotEmpty ||
        explicitModelValue.isNotEmpty ||
        explicitSkillsValue.isNotEmpty;
    if (!hasExplicitSelection) {
      return ExternalCodeAgentAcpRoutingConfig.auto(
        preferredGatewayTarget: preferredGatewayTarget,
      );
    }
    return ExternalCodeAgentAcpRoutingConfig(
      mode: ExternalCodeAgentAcpRoutingMode.explicit,
      preferredGatewayTarget: preferredGatewayTarget,
      explicitExecutionTarget: explicitExecutionTarget,
      explicitProviderId: explicitProviderId,
      explicitModel: explicitModelValue,
      explicitSkills: explicitSkillsValue,
      allowSkillInstall: false,
      availableSkills: const <ExternalCodeAgentAcpAvailableSkill>[],
    );
  }
}

const String _gatewaySessionMode = 'gateway-chat';

bool _usesGatewaySessionMode(String mode) {
  final normalized = mode.trim();
  return normalized == 'gateway' || normalized == _gatewaySessionMode;
}

class GoTaskServiceUpdate {
  const GoTaskServiceUpdate({
    required this.sessionId,
    required this.threadId,
    required this.turnId,
    required this.type,
    required this.text,
    required this.message,
    required this.pending,
    required this.error,
    required this.route,
    required this.payload,
  });

  final String sessionId;
  final String threadId;
  final String turnId;
  final String type;
  final String text;
  final String message;
  final bool pending;
  final bool error;
  final GoTaskServiceRoute route;
  final Map<String, dynamic> payload;

  bool get isDelta => type == 'delta' && text.isNotEmpty;
  bool get isDone => type == 'done' || payload['event'] == 'completed';
}

class GoTaskServiceArtifact {
  const GoTaskServiceArtifact({
    required this.relativePath,
    required this.label,
    required this.contentType,
    required this.encoding,
    required this.content,
    required this.downloadUrl,
    required this.sizeBytes,
    required this.sha256,
  });

  final String relativePath;
  final String label;
  final String contentType;
  final String encoding;
  final String content;
  final String downloadUrl;
  final int? sizeBytes;
  final String sha256;

  bool get hasInlineContent => content.trim().isNotEmpty;

  factory GoTaskServiceArtifact.fromJson(Map<String, dynamic> json) {
    int? parseSize(Object? value) {
      if (value is int) {
        return value;
      }
      if (value is num) {
        return value.toInt();
      }
      return int.tryParse(value?.toString() ?? '');
    }

    return GoTaskServiceArtifact(
      relativePath:
          json['relativePath']?.toString().trim() ??
          json['path']?.toString().trim() ??
          json['name']?.toString().trim() ??
          '',
      label: json['label']?.toString().trim() ?? '',
      contentType: json['contentType']?.toString().trim() ?? '',
      encoding: json['encoding']?.toString().trim() ?? '',
      content: json['content']?.toString() ?? '',
      downloadUrl:
          json['downloadUrl']?.toString().trim() ??
          json['downloadURL']?.toString().trim() ??
          json['download_url']?.toString().trim() ??
          '',
      sizeBytes: parseSize(json['sizeBytes'] ?? json['size']),
      sha256: json['sha256']?.toString().trim() ?? '',
    );
  }
}

class GoTaskServiceResult {
  const GoTaskServiceResult({
    required this.success,
    required this.message,
    required this.turnId,
    required this.raw,
    required this.errorMessage,
    required this.resolvedModel,
    required this.route,
  });

  final bool success;
  final String message;
  final String turnId;
  final Map<String, dynamic> raw;
  final String errorMessage;
  final String resolvedModel;
  final GoTaskServiceRoute route;

  String get resolvedWorkingDirectory =>
      raw['resolvedWorkingDirectory']?.toString().trim() ??
      raw['effectiveWorkingDirectory']?.toString().trim() ??
      raw['workingDirectory']?.toString().trim() ??
      '';

  String get resultSummary =>
      raw['resultSummary']?.toString().trim().isNotEmpty == true
      ? raw['resultSummary'].toString().trim()
      : raw['summary']?.toString().trim() ?? '';

  String get status => _firstNestedGoTaskString(raw, const <List<String>>[
    <String>['status'],
    <String>['error', 'status'],
    <String>['details', 'status'],
    <String>['payload', 'status'],
    <String>['result', 'status'],
  ]);

  String get code => _firstNestedGoTaskString(raw, const <List<String>>[
    <String>['code'],
    <String>['error', 'code'],
    <String>['error', 'details', 'code'],
    <String>['details', 'code'],
    <String>['payload', 'code'],
    <String>['result', 'code'],
  ]);

  bool get isOpenClawRunningTaskHandle {
    final normalizedStatus = status.trim().toLowerCase();
    final runId = raw['runId']?.toString().trim() ?? '';
    final artifactScope = raw['artifactScope']?.toString().trim() ?? '';
    final provider =
        raw['resolvedGatewayProviderId']?.toString().trim().toLowerCase() ??
        raw['gatewayProviderId']?.toString().trim().toLowerCase() ??
        '';
    return normalizedStatus == 'running' &&
        runId.isNotEmpty &&
        artifactScope.isNotEmpty &&
        provider.contains('openclaw');
  }

  OpenClawTaskAssociation? get openClawTaskAssociation {
    final association = OpenClawTaskAssociation.fromJsonOrNull(raw);
    if (association == null) {
      return null;
    }
    final provider = association.gatewayProviderId.trim().toLowerCase();
    return provider.contains('openclaw') ? association : null;
  }

  String get resolvedExecutionTarget =>
      raw['resolvedExecutionTarget']?.toString().trim() ?? '';

  String get resolvedEndpointTarget =>
      raw['resolvedEndpointTarget']?.toString().trim() ?? '';

  String get resolvedProviderId =>
      raw['resolvedProviderId']?.toString().trim() ?? '';

  List<String> get resolvedSkills {
    final rawList = raw['resolvedSkills'];
    if (rawList is! List) {
      return const <String>[];
    }
    return rawList
        .map((item) => item?.toString().trim() ?? '')
        .where((item) => item.isNotEmpty)
        .toList(growable: false);
  }

  String get skillResolutionSource =>
      raw['skillResolutionSource']?.toString().trim() ?? '';

  bool get needsSkillInstall => _boolValue(raw['needsSkillInstall']) ?? false;

  String get skillInstallRequestId =>
      raw['skillInstallRequestId']?.toString().trim() ?? '';

  List<Map<String, dynamic>> get skillCandidates =>
      _castMapList(raw['skillCandidates']);

  List<Map<String, dynamic>> get memorySources =>
      _castMapList(raw['memorySources']);

  List<GoTaskServiceArtifact> get artifacts {
    final rawArtifacts = _firstGoTaskArtifactList(raw);
    if (rawArtifacts is! List) {
      return const <GoTaskServiceArtifact>[];
    }
    return rawArtifacts
        .whereType<Map>()
        .map(
          (item) =>
              GoTaskServiceArtifact.fromJson(item.cast<String, dynamic>()),
        )
        .where((item) => item.relativePath.isNotEmpty)
        .toList(growable: false);
  }

  WorkspaceRefKind? get resolvedWorkspaceRefKind {
    final rawValue = raw['resolvedWorkspaceRefKind']?.toString().trim() ?? '';
    if (rawValue.isEmpty) {
      return null;
    }
    return WorkspaceRefKindCopy.fromJsonValue(rawValue);
  }

  String get remoteWorkingDirectory {
    final remoteExecution = _castMap(raw['remoteExecution']);
    return remoteExecution['remoteWorkingDirectory']?.toString().trim() ??
        raw['remoteWorkingDirectory']?.toString().trim() ??
        resolvedWorkingDirectory;
  }

  WorkspaceRefKind? get remoteWorkspaceRefKind {
    final remoteExecution = _castMap(raw['remoteExecution']);
    final rawValue =
        remoteExecution['remoteWorkspaceRefKind']?.toString().trim() ??
        raw['remoteWorkspaceRefKind']?.toString().trim() ??
        '';
    if (rawValue.isEmpty) {
      return resolvedWorkspaceRefKind;
    }
    return WorkspaceRefKindCopy.fromJsonValue(rawValue);
  }
}

Object? _firstGoTaskArtifactList(Map<String, dynamic> result) {
  final artifacts = <Object?>[];
  for (final candidate in <Object?>[
    result['artifacts'],
    result['files'],
    result['attachments'],
    _castMap(result['payload'])['artifacts'],
    _castMap(result['payload'])['files'],
    _castMap(result['payload'])['attachments'],
    _castMap(result['result'])['artifacts'],
    _castMap(result['result'])['files'],
    _castMap(result['result'])['attachments'],
    _castMap(result['data'])['artifacts'],
    _castMap(result['data'])['files'],
    _castMap(result['data'])['attachments'],
  ]) {
    if (candidate is List) {
      artifacts.addAll(candidate);
    } else if (candidate is Map) {
      final items = _castList(candidate.cast<String, dynamic>()['items']);
      if (items.isNotEmpty) {
        artifacts.addAll(items);
      }
    }
  }
  return artifacts.isEmpty ? null : artifacts;
}

String? goTaskServiceGatewayEntryState({
  required AssistantExecutionTarget requestedTarget,
  required GoTaskServiceResult result,
}) {
  final resolvedExecutionTarget = result.resolvedExecutionTarget
      .trim()
      .toLowerCase();
  switch (resolvedExecutionTarget) {
    case 'gateway':
      final resolvedEndpointTarget = result.resolvedEndpointTarget
          .trim()
          .toLowerCase();
      if (resolvedEndpointTarget.isEmpty ||
          resolvedEndpointTarget == 'gateway' ||
          resolvedEndpointTarget == 'local' ||
          resolvedEndpointTarget == 'remote') {
        return AssistantExecutionTarget.gateway.promptValue;
      }
      throw StateError(
        'Bridge protocol mismatch: unsupported resolvedEndpointTarget "$resolvedEndpointTarget".',
      );
    case 'multi-agent':
      return AssistantExecutionTarget.gateway.promptValue;
    case 'local':
    case 'remote':
      throw StateError(
        'Bridge protocol mismatch: unsupported resolvedExecutionTarget "$resolvedExecutionTarget".',
      );
    default:
      return requestedTarget.isGateway
          ? AssistantExecutionTarget.gateway.promptValue
          : requestedTarget.promptValue;
  }
}

abstract class GoTaskServiceClient {
  Future<GoTaskServiceResult> executeTask(
    GoTaskServiceRequest request, {
    required void Function(GoTaskServiceUpdate update) onUpdate,
  });

  Future<GoTaskServiceResult> getTask({
    required AssistantExecutionTarget target,
    required OpenClawTaskAssociation association,
    required GoTaskServiceRoute route,
  });

  Future<void> cancelTask({
    required GoTaskServiceRoute route,
    required AssistantExecutionTarget target,
    required String sessionId,
    required String threadId,
    OpenClawTaskAssociation? association,
  });

  Future<void> dispose();
}

GoTaskServiceUpdate? goTaskServiceUpdateFromAcpNotification(
  Map<String, dynamic> notification,
) {
  final method = notification['method']?.toString().trim().toLowerCase() ?? '';
  if (method != 'session.update' && method != 'acp.session.update') {
    return null;
  }
  final params = _castMap(notification['params']);
  final payload = params.isNotEmpty
      ? params
      : _castMap(notification['payload']);
  final type =
      payload['type']?.toString().trim().toLowerCase() ??
      payload['state']?.toString().trim().toLowerCase() ??
      payload['event']?.toString().trim().toLowerCase() ??
      'status';
  return GoTaskServiceUpdate(
    sessionId: payload['sessionId']?.toString().trim().isNotEmpty == true
        ? payload['sessionId'].toString().trim()
        : payload['threadId']?.toString().trim() ?? '',
    threadId: payload['threadId']?.toString().trim() ?? '',
    turnId: payload['turnId']?.toString().trim() ?? '',
    type: type,
    text:
        payload['delta']?.toString() ??
        payload['text']?.toString() ??
        _castMap(payload['message'])['content']?.toString() ??
        '',
    message: _extractGoTaskDisplayText(payload['message']),
    pending: _boolValue(payload['pending']) ?? false,
    error: _boolValue(payload['error']) ?? false,
    route:
        (payload['mode']?.toString().trim() == 'multi-agent' ||
            payload['resolvedExecutionTarget']?.toString().trim() ==
                'multi-agent')
        ? GoTaskServiceRoute.externalAcpMulti
        : GoTaskServiceRoute.externalAcpSingle,
    payload: payload,
  );
}

GoTaskServiceResult goTaskServiceResultFromAcpResponse(
  Map<String, dynamic> response, {
  required GoTaskServiceRoute route,
  String streamedText = '',
  String? completedMessage,
}) {
  final result = _castMap(response['result']);
  final skillCandidates = _castMapList(result['skillCandidates'])
      .map((item) => item['id']?.toString().trim() ?? '')
      .where((item) => item.isNotEmpty)
      .toList(growable: false);
  final success = _boolValue(result['success']) ?? _inferGoTaskSuccess(result);
  final structuredFailureText = () {
    if (success) {
      return '';
    }
    final errorText = _firstGoTaskFailureText(result);
    final needsSkillInstall = _boolValue(result['needsSkillInstall']) ?? false;
    if (needsSkillInstall && skillCandidates.isNotEmpty) {
      final candidateText = skillCandidates.join(', ');
      if (errorText.isNotEmpty) {
        return '$errorText (skills: $candidateText)';
      }
      return 'Skill install required: $candidateText';
    }
    return _diagnosticGoTaskFailureText(result, errorText);
  }();
  final responseText = _extractGoTaskDisplayText(result);
  final primaryText =
      (responseText.isNotEmpty
              ? responseText
              : completedMessage?.trim().isNotEmpty == true
              ? completedMessage!.trim()
              : structuredFailureText.isNotEmpty
              ? structuredFailureText
              : streamedText.trim().isNotEmpty
              ? streamedText.trim()
              : '')
          .trim();
  final directErrorMessage = _extractGoTaskDisplayText(result['error']);
  final effectiveErrorMessage = success
      ? directErrorMessage
      : structuredFailureText.isNotEmpty
      ? structuredFailureText
      : primaryText.isNotEmpty
      ? primaryText
      : directErrorMessage;
  return GoTaskServiceResult(
    success: success,
    message: primaryText,
    turnId: result['turnId']?.toString().trim() ?? '',
    raw: result,
    errorMessage: effectiveErrorMessage,
    resolvedModel:
        result['model']?.toString().trim() ??
        result['resolvedModel']?.toString().trim() ??
        '',
    route: route,
  );
}

bool _inferGoTaskSuccess(Map<String, dynamic> result) {
  if (result.containsKey('error')) {
    return false;
  }
  final status = _firstNestedGoTaskString(result, const <List<String>>[
    <String>['status'],
    <String>['details', 'status'],
    <String>['payload', 'status'],
    <String>['result', 'status'],
  ]).toLowerCase();
  if (status == 'failed' ||
      status == 'error' ||
      status == 'cancelled' ||
      status == 'canceled') {
    return false;
  }
  return true;
}

String _firstGoTaskFailureText(Map<String, dynamic> result) {
  for (final key in const <String>[
    'error',
    'errorMessage',
    'message',
    'unavailableMessage',
    'unavailableCode',
  ]) {
    final text = _extractGoTaskDisplayText(result[key]);
    if (text.isNotEmpty) {
      return text;
    }
  }
  return '';
}

String _diagnosticGoTaskFailureText(
  Map<String, dynamic> result,
  String errorText,
) {
  final text = errorText.trim();
  if (text.isEmpty) {
    return '';
  }
  final diagnostics = <String>[];
  final code = result['unavailableCode']?.toString().trim() ?? '';
  if (code.isNotEmpty && !text.contains(code)) {
    diagnostics.add('code: $code');
  }
  final upstreamMethod = result['upstreamMethod']?.toString().trim() ?? '';
  if (upstreamMethod.isNotEmpty) {
    diagnostics.add('upstream: $upstreamMethod');
  }
  if (diagnostics.isEmpty) {
    return text;
  }
  return '$text (${diagnostics.join(', ')})';
}

String _extractGoTaskDisplayText(Object? value, [Set<Object>? visited]) {
  final seen = visited ?? <Object>{};
  if (value == null) {
    return '';
  }
  if (value is String) {
    return value.trim();
  }
  if (value is Map) {
    if (!seen.add(value)) {
      return '';
    }
    final map = value.cast<String, dynamic>();
    for (final key in const <String>[
      'output',
      'summary',
      'resultSummary',
      'message',
      'content',
      'text',
      'output_text',
    ]) {
      final extracted = _extractGoTaskTextCandidate(map[key], seen);
      if (extracted.isNotEmpty) {
        return extracted;
      }
    }
    final choices = _castList(map['choices']);
    if (choices.isNotEmpty) {
      for (final choice in choices) {
        final extracted = _extractGoTaskDisplayText(
          _castMap(choice)['message'],
          seen,
        );
        if (extracted.isNotEmpty) {
          return extracted;
        }
      }
    }
    for (final key in const <String>[
      'result',
      'payload',
      'data',
      'response',
      'body',
    ]) {
      final extracted = _extractGoTaskDisplayText(map[key], seen);
      if (extracted.isNotEmpty) {
        return extracted;
      }
    }
    return '';
  }
  if (value is List) {
    if (!seen.add(value)) {
      return '';
    }
    final parts = <String>[];
    for (final item in value) {
      final extracted = _extractGoTaskDisplayText(item, seen);
      if (extracted.isNotEmpty) {
        parts.add(extracted);
      }
    }
    return parts.join('\n').trim();
  }
  return '';
}

String _extractGoTaskTextCandidate(Object? value, Set<Object> visited) {
  if (value == null) {
    return '';
  }
  if (value is String) {
    return value.trim();
  }
  if (value is List) {
    return _extractGoTaskDisplayText(value, visited);
  }
  if (value is Map) {
    final map = value.cast<String, dynamic>();
    final type = map['type']?.toString().trim();
    if (type == 'output_text') {
      final text =
          map['text']?.toString().trim() ?? map['value']?.toString().trim();
      if (text != null && text.isNotEmpty) {
        return text;
      }
    }
    return _extractGoTaskDisplayText(map, visited);
  }
  return '';
}

Map<String, dynamic> mergeGoTaskServiceResponseResult(
  Map<String, dynamic> response,
  Map<String, dynamic> overlay,
) {
  if (overlay.isEmpty) {
    return response;
  }
  final next = Map<String, dynamic>.from(response);
  final result = Map<String, dynamic>.from(_castMap(next['result']));
  overlay.forEach((key, value) {
    if (value == null) {
      return;
    }
    if (value is String && value.trim().isEmpty) {
      if (result.containsKey(key)) {
        return;
      }
    }
    result[key] = value;
  });
  next['result'] = result;
  return next;
}

int goTaskServiceBase64Size(String base64) {
  final normalized = base64.trim().split(',').last.trim();
  if (normalized.isEmpty) {
    return 0;
  }
  final padding = normalized.endsWith('==')
      ? 2
      : (normalized.endsWith('=') ? 1 : 0);
  return (normalized.length * 3 ~/ 4) - padding;
}

String goTaskServiceAttachmentSha256(GatewayChatAttachmentPayload attachment) {
  final declared = attachment.sha256.trim().toLowerCase();
  if (declared.isNotEmpty) {
    return declared;
  }
  try {
    return crypto.sha256
        .convert(base64Decode(attachment.content.trim().split(',').last.trim()))
        .toString();
  } on FormatException {
    return '';
  }
}

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

bool? _boolValue(Object? raw) {
  if (raw is bool) {
    return raw;
  }
  if (raw is num) {
    return raw != 0;
  }
  if (raw is String) {
    final normalized = raw.trim().toLowerCase();
    if (normalized == 'true') {
      return true;
    }
    if (normalized == 'false') {
      return false;
    }
  }
  return null;
}

List<Map<String, dynamic>> _castMapList(Object? raw) {
  if (raw is! List) {
    return const <Map<String, dynamic>>[];
  }
  return raw.map(_castMap).toList(growable: false);
}

String _firstNestedGoTaskString(
  Map<String, dynamic> source,
  List<List<String>> paths,
) {
  for (final path in paths) {
    Object? current = source;
    for (final key in path) {
      if (current is! Map) {
        current = null;
        break;
      }
      current = current[key];
    }
    final value = current?.toString().trim() ?? '';
    if (value.isNotEmpty) {
      return value;
    }
  }
  return '';
}

List<dynamic> _castList(Object? raw) {
  if (raw is List) {
    return raw;
  }
  return const <dynamic>[];
}
