part of 'gateway_runtime_core.dart';

extension GatewayRuntimeApiInternal on GatewayRuntime {
  Future<Map<String, dynamic>> _healthInternal() async {
    final payload = asMap(await request('health'));
    snapshotInternal = snapshotInternal.copyWith(healthPayload: payload);
    appendLogInternal(this, 'debug', 'health', 'health snapshot refreshed');
    _notifyRuntimeChangedInternal();
    return payload;
  }

  Future<Map<String, dynamic>> _statusInternal() async {
    final payload = asMap(await request('status'));
    snapshotInternal = snapshotInternal.copyWith(statusPayload: payload);
    appendLogInternal(this, 'debug', 'health', 'status snapshot refreshed');
    _notifyRuntimeChangedInternal();
    return payload;
  }

  Future<List<GatewayAgentSummary>> _listAgentsInternal() async {
    final payload = asMap(
      await request('agents.list', params: const <String, dynamic>{}),
    );
    final agents = asList(payload['agents'])
        .map((item) {
          final map = asMap(item);
          final identity = asMap(map['identity']);
          return GatewayAgentSummary(
            id: stringValue(map['id']) ?? 'unknown',
            name:
                stringValue(map['name']) ??
                stringValue(identity['name']) ??
                'Agent',
            emoji: stringValue(identity['emoji']) ?? '·',
            theme: stringValue(identity['theme']) ?? 'default',
          );
        })
        .toList(growable: false);
    if (snapshotInternal.mainSessionKey == null ||
        snapshotInternal.mainSessionKey!.trim().isEmpty) {
      snapshotInternal = snapshotInternal.copyWith(
        mainSessionKey: stringValue(payload['mainKey']) ?? 'main',
      );
      _notifyRuntimeChangedInternal();
    }
    return agents;
  }

  Future<List<GatewaySessionSummary>> _listSessionsInternal({
    String? agentId,
    int limit = 24,
  }) async {
    final payload = asMap(
      await request(
        'sessions.list',
        params: <String, dynamic>{
          'includeGlobal': true,
          'includeUnknown': false,
          'includeDerivedTitles': true,
          'includeLastMessage': true,
          'limit': limit,
          if (agentId != null && agentId.trim().isNotEmpty)
            'agentId': agentId.trim(),
        },
      ),
    );
    return asList(payload['sessions'])
        .map((item) {
          final map = asMap(item);
          return GatewaySessionSummary(
            key: stringValue(map['key']) ?? 'main',
            kind: stringValue(map['kind']),
            displayName:
                stringValue(map['displayName']) ?? stringValue(map['label']),
            surface: stringValue(map['surface']),
            subject: stringValue(map['subject']),
            room: stringValue(map['room']),
            space: stringValue(map['space']),
            updatedAtMs: doubleValue(map['updatedAt']),
            sessionId: stringValue(map['sessionId']),
            systemSent: boolValue(map['systemSent']),
            abortedLastRun: boolValue(map['abortedLastRun']),
            thinkingLevel: stringValue(map['thinkingLevel']),
            verboseLevel: stringValue(map['verboseLevel']),
            inputTokens: intValue(map['inputTokens']),
            outputTokens: intValue(map['outputTokens']),
            totalTokens: intValue(map['totalTokens']),
            model: stringValue(map['model']),
            contextTokens: intValue(map['contextTokens']),
            derivedTitle: stringValue(map['derivedTitle']),
            lastMessagePreview: stringValue(map['lastMessagePreview']),
          );
        })
        .toList(growable: false);
  }

  Future<List<GatewayChatMessage>> _loadHistoryInternal(
    String sessionKey, {
    int limit = 120,
  }) async {
    final payload = asMap(
      await request(
        'chat.history',
        params: <String, dynamic>{'sessionKey': sessionKey, 'limit': limit},
      ),
    );
    return asList(payload['messages'])
        .map((item) {
          final map = asMap(item);
          return GatewayChatMessage(
            id: randomIdInternal(),
            role: stringValue(map['role']) ?? 'assistant',
            text: extractMessageText(map),
            timestampMs: doubleValue(map['timestamp']),
            toolCallId:
                stringValue(map['toolCallId']) ??
                stringValue(map['tool_call_id']),
            toolName:
                stringValue(map['toolName']) ?? stringValue(map['tool_name']),
            stopReason: stringValue(map['stopReason']),
            pending: false,
            error: false,
          );
        })
        .toList(growable: false);
  }

  Future<String> _sendChatInternal({
    required String sessionKey,
    required String message,
    required String thinking,
    List<GatewayChatAttachmentPayload> attachments =
        const <GatewayChatAttachmentPayload>[],
    String? agentId,
    Map<String, dynamic>? metadata,
  }) async {
    final runId = randomIdInternal();
    final payload = asMap(
      await request(
        'chat.send',
        params: <String, dynamic>{
          'sessionKey': sessionKey,
          'message': message,
          'thinking': thinking,
          'timeoutMs': 30000,
          'idempotencyKey': runId,
          if (agentId != null && agentId.trim().isNotEmpty)
            'agentId': agentId.trim(),
          if (attachments.isNotEmpty)
            'attachments': attachments
                .map((attachment) => attachment.toJson())
                .toList(growable: false),
        },
        timeout: const Duration(seconds: 35),
      ),
    );
    return stringValue(payload['runId']) ?? runId;
  }

  Future<void> _abortChatInternal({
    required String sessionKey,
    required String runId,
  }) async {
    await request(
      'chat.abort',
      params: <String, dynamic>{'sessionKey': sessionKey, 'runId': runId},
      timeout: const Duration(seconds: 10),
    );
  }

  Future<List<GatewayInstanceSummary>> _listInstancesInternal() async {
    final payload = await request(
      'system-presence',
      params: const <String, dynamic>{},
    );
    return asList(payload)
        .map((item) {
          final map = asMap(item);
          return GatewayInstanceSummary(
            id: stringValue(map['id']) ?? randomIdInternal(),
            host: stringValue(map['host']),
            ip: stringValue(map['ip']),
            version: stringValue(map['version']),
            platform: stringValue(map['platform']),
            deviceFamily: stringValue(map['deviceFamily']),
            modelIdentifier: stringValue(map['modelIdentifier']),
            lastInputSeconds: intValue(map['lastInputSeconds']),
            mode: stringValue(map['mode']),
            reason: stringValue(map['reason']),
            text: stringValue(map['text']) ?? '',
            timestampMs:
                doubleValue(map['ts']) ??
                DateTime.now().millisecondsSinceEpoch.toDouble(),
          );
        })
        .toList(growable: false);
  }

  Future<List<GatewaySkillSummary>> _listSkillsInternal({
    String? agentId,
  }) async {
    final params = <String, dynamic>{
      if (agentId != null && agentId.trim().isNotEmpty)
        'agentId': agentId.trim(),
    };
    if (sessionClientInternal == null) {
      throw GatewayRuntimeException(
        'skills.status requires bridge session (ACP transport)',
        code: 'BRIDGE_NOT_CONFIGURED',
      );
    }
    // Use allowErrorPayload so the bridge can return cached skills even when
    // the upstream OpenClaw gateway is temporarily offline (ok:false + payload).
    final payload = asMap(
      await sessionClientInternal!.request(
        runtimeId: runtimeIdInternal,
        method: 'skills.status',
        params: params,
        allowErrorPayload: true,
      ),
    );
    final skillsList = asList(payload['skills']);
    // When the skills key is entirely absent (not just an empty list), the
    // gateway may have returned a stub or error payload. Distinguish between
    // "genuinely no skills" and "gateway responded without skills data".
    if (!payload.containsKey('skills')) {
      final hasWorkspaceMeta = payload.containsKey('workspaceDir') ||
          payload.containsKey('managedSkillsDir');
      if (!hasWorkspaceMeta) {
        appendLogInternal(
          this,
          'warn',
          'skills',
          'skills.status returned payload without skills key and without'
          ' workspace metadata — likely a gateway error or unimplemented method',
        );
        throw GatewayRuntimeException(
          'OpenClaw gateway did not return skills data.'
          ' The gateway may not have skills.status implemented.',
          code: 'SKILLS_STATUS_MISSING',
        );
      }
      // Gateway responded with workspace metadata but no skills key —
      // genuinely no skills installed. Return empty list.
      appendLogInternal(
        this,
        'debug',
        'skills',
        'skills.status returned workspace metadata with zero skills',
      );
      return const <GatewaySkillSummary>[];
    }
    if (skillsList.isEmpty) {
      appendLogInternal(
        this,
        'debug',
        'skills',
        'skills.status returned empty skills list (${payload.length} payload keys)',
      );
    }
    return skillsList
        .map((item) {
          final map = asMap(item);
          return GatewaySkillSummary(
            name:
                stringValue(map['name']) ??
                stringValue(map['title']) ??
                stringValue(map['id']) ??
                'Skill',
            description: stringValue(map['description']) ?? '',
            source: stringValue(map['source']) ?? 'workspace',
            skillKey:
                stringValue(map['skillKey']) ??
                stringValue(map['skill_key']) ??
                stringValue(map['key']) ??
                stringValue(map['id']) ??
                stringValue(map['name']) ??
                'skill',
            primaryEnv: stringValue(map['primaryEnv']),
            eligible: boolValue(map['eligible']) ?? false,
            disabled: boolValue(map['disabled']) ?? false,
            missingBins: skillMissingListInternal(map, 'bins', 'missingBins'),
            missingEnv: skillMissingListInternal(map, 'env', 'missingEnv'),
            missingConfig: skillMissingListInternal(
              map,
              'config',
              'missingConfig',
            ),
          );
        })
        .toList(growable: false);
  }

  Future<List<GatewayConnectorSummary>> _listConnectorsInternal() async {
    final payload = asMap(
      await request(
        'channels.status',
        params: const <String, dynamic>{'probe': true, 'timeoutMs': 8000},
        timeout: const Duration(seconds: 16),
      ),
    );
    final channelMeta = <String, Map<String, dynamic>>{
      for (final entry in asList(payload['channelMeta']))
        if (stringValue(asMap(entry)['id']) != null)
          stringValue(asMap(entry)['id'])!: asMap(entry),
    };
    final labels = asMap(payload['channelLabels']);
    final detailLabels = asMap(payload['channelDetailLabels']);
    final accounts = asMap(payload['channelAccounts']);
    final order = stringList(payload['channelOrder']);

    final summaries = <GatewayConnectorSummary>[];
    for (final channelId in order) {
      final channelAccounts = asList(accounts[channelId]);
      if (channelAccounts.isEmpty) {
        final meta = channelMeta[channelId] ?? const <String, dynamic>{};
        summaries.add(
          GatewayConnectorSummary(
            id: channelId,
            label:
                stringValue(meta['label']) ??
                stringValue(labels[channelId]) ??
                channelId,
            detailLabel:
                stringValue(meta['detailLabel']) ??
                stringValue(detailLabels[channelId]) ??
                channelId,
            accountName: null,
            configured: false,
            enabled: false,
            running: false,
            connected: false,
            status: 'idle',
            lastError: null,
            meta: const <String>[],
          ),
        );
        continue;
      }
      for (final account in channelAccounts) {
        final map = asMap(account);
        final configured = boolValue(map['configured']) ?? false;
        final enabled = boolValue(map['enabled']) ?? configured;
        final running = boolValue(map['running']) ?? false;
        final connected =
            boolValue(map['connected']) ?? boolValue(map['linked']) ?? false;
        final lastError = stringValue(map['lastError']);
        final status = lastError != null && lastError.trim().isNotEmpty
            ? 'error'
            : connected
            ? 'connected'
            : running
            ? 'running'
            : configured
            ? 'configured'
            : 'idle';
        final mode = stringValue(map['mode']);
        final tokenSource = stringValue(map['tokenSource']);
        final baseUrl = stringValue(map['baseUrl']);
        summaries.add(
          GatewayConnectorSummary(
            id: channelId,
            label:
                stringValue(channelMeta[channelId]?['label']) ??
                stringValue(labels[channelId]) ??
                channelId,
            detailLabel:
                stringValue(channelMeta[channelId]?['detailLabel']) ??
                stringValue(detailLabels[channelId]) ??
                channelId,
            accountName:
                stringValue(map['name']) ?? stringValue(map['accountId']),
            configured: configured,
            enabled: enabled,
            running: running,
            connected: connected,
            status: status,
            lastError: lastError,
            meta: [
              ...?(mode == null ? null : <String>[mode]),
              ...?(tokenSource == null ? null : <String>[tokenSource]),
              ...?(baseUrl == null ? null : <String>[baseUrl]),
            ],
          ),
        );
      }
    }
    return summaries;
  }

  Future<List<GatewayModelSummary>> _listModelsInternal() async {
    final payload = asMap(
      await request(
        'models.list',
        params: const <String, dynamic>{},
        timeout: const Duration(seconds: 16),
      ),
    );
    return asList(payload['models'])
        .map((item) {
          final map = asMap(item);
          return GatewayModelSummary(
            id: stringValue(map['id']) ?? 'unknown',
            name:
                stringValue(map['name']) ?? stringValue(map['id']) ?? 'unknown',
            provider: stringValue(map['provider']) ?? 'unknown',
            contextWindow: intValue(map['contextWindow']),
            maxOutputTokens: intValue(map['maxOutputTokens']),
          );
        })
        .toList(growable: false);
  }

  Future<List<GatewayCronJobSummary>> _listCronJobsInternal() async {
    final payload = asMap(
      await request(
        'cron.list',
        params: const <String, dynamic>{'includeDisabled': true},
        timeout: const Duration(seconds: 16),
      ),
    );
    return asList(payload['jobs'])
        .map((item) {
          final map = asMap(item);
          final state = asMap(map['state']);
          return GatewayCronJobSummary(
            id: stringValue(map['id']) ?? randomIdInternal(),
            name: stringValue(map['name']) ?? 'Untitled job',
            description: stringValue(map['description']),
            enabled: boolValue(map['enabled']) ?? true,
            agentId: stringValue(map['agentId']),
            scheduleLabel: cronScheduleLabelInternal(asMap(map['schedule'])),
            nextRunAtMs: intValue(state['nextRunAtMs']),
            lastRunAtMs: intValue(state['lastRunAtMs']),
            lastStatus: stringValue(state['lastStatus']),
            lastError: stringValue(state['lastError']),
          );
        })
        .toList(growable: false);
  }

  Future<GatewayDevicePairingList> _listDevicePairingInternal() async {
    final payload = asMap(
      await request(
        'device.pair.list',
        params: const <String, dynamic>{},
        timeout: const Duration(seconds: 12),
      ),
    );
    final identity = await storeInternal.loadDeviceIdentity();
    return GatewayDevicePairingList(
      pending: asList(payload['pending'])
          .map((item) => parsePendingDeviceInternal(asMap(item)))
          .toList(growable: false),
      paired: asList(payload['paired'])
          .map(
            (item) => parsePairedDeviceInternal(
              asMap(item),
              currentDeviceId: identity?.deviceId,
            ),
          )
          .toList(growable: false),
    );
  }

  Future<GatewayPairedDevice?> _approveDevicePairingInternal(
    String requestId,
  ) async {
    appendLogInternal(this, 'info', 'pairing', 'approve request $requestId');
    final payload = asMap(
      await request(
        'device.pair.approve',
        params: <String, dynamic>{'requestId': requestId},
        timeout: const Duration(seconds: 12),
      ),
    );
    final identity = await storeInternal.loadDeviceIdentity();
    final device = asMap(payload['device']);
    if (device.isEmpty) {
      return null;
    }
    return parsePairedDeviceInternal(
      device,
      currentDeviceId: identity?.deviceId,
    );
  }

  Future<void> _rejectDevicePairingInternal(String requestId) async {
    appendLogInternal(this, 'info', 'pairing', 'reject request $requestId');
    await request(
      'device.pair.reject',
      params: <String, dynamic>{'requestId': requestId},
      timeout: const Duration(seconds: 12),
    );
  }

  Future<void> _removePairedDeviceInternal(String deviceId) async {
    appendLogInternal(this, 'info', 'pairing', 'remove device $deviceId');
    await request(
      'device.pair.remove',
      params: <String, dynamic>{'deviceId': deviceId},
      timeout: const Duration(seconds: 12),
    );
  }

  Future<String> _rotateDeviceTokenInternal({
    required String deviceId,
    required String role,
    List<String> scopes = const <String>[],
  }) async {
    appendLogInternal(
      this,
      'info',
      'token',
      'rotate role token | device: $deviceId | role: $role',
    );
    final payload = asMap(
      await request(
        'device.token.rotate',
        params: <String, dynamic>{
          'deviceId': deviceId,
          'role': role,
          if (scopes.isNotEmpty) 'scopes': scopes,
        },
        timeout: const Duration(seconds: 12),
      ),
    );
    final token = stringValue(payload['token']) ?? '';
    final identity = await storeInternal.loadDeviceIdentity();
    final resolvedRole = stringValue(payload['role']) ?? role;
    if (token.isNotEmpty &&
        identity != null &&
        (stringValue(payload['deviceId']) ?? deviceId) == identity.deviceId) {
      await storeInternal.saveDeviceToken(
        deviceId: identity.deviceId,
        role: resolvedRole,
        token: token,
      );
    }
    return token;
  }

  Future<void> _revokeDeviceTokenInternal({
    required String deviceId,
    required String role,
  }) async {
    appendLogInternal(
      this,
      'info',
      'token',
      'revoke role token | device: $deviceId | role: $role',
    );
    await request(
      'device.token.revoke',
      params: <String, dynamic>{'deviceId': deviceId, 'role': role},
      timeout: const Duration(seconds: 12),
    );
    final identity = await storeInternal.loadDeviceIdentity();
    if (identity != null && deviceId == identity.deviceId) {
      await storeInternal.clearDeviceToken(
        deviceId: identity.deviceId,
        role: role,
      );
    }
  }

  Future<dynamic> _requestInternal(
    String method, {
    Map<String, dynamic>? params,
    Duration timeout = const Duration(seconds: 15),
  }) async {
    if (sessionClientInternal != null) {
      if (!isConnected) {
        appendLogInternal(
          this,
          'warn',
          'rpc',
          'blocked request $method | offline',
        );
        throw GatewayRuntimeException('gateway not connected', code: 'OFFLINE');
      }
      return sessionClientInternal!.request(
        runtimeId: runtimeIdInternal,
        method: method,
        params: params,
        timeout: timeout,
      );
    }
    if (channelInternal == null || !isConnected) {
      appendLogInternal(
        this,
        'warn',
        'rpc',
        'blocked request $method | offline',
      );
      throw GatewayRuntimeException('gateway not connected', code: 'OFFLINE');
    }
    final result = await requestRawInternal(
      this,
      method,
      params: params,
      timeout: timeout,
    );
    return result.payload;
  }
}

List<String> skillMissingListInternal(
  Map<String, dynamic> skill,
  String nestedKey,
  String flatKey,
) {
  final flat = stringList(skill[flatKey]);
  if (flat.isNotEmpty) {
    return flat;
  }
  return stringList(asMap(skill['missing'])[nestedKey]);
}
