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

import 'runtime_models.dart';

class AccountRuntimeException implements Exception {
  const AccountRuntimeException({
    required this.statusCode,
    required this.errorCode,
    required this.message,
  });

  final int statusCode;
  final String errorCode;
  final String message;

  @override
  String toString() {
    return 'AccountRuntimeException($statusCode, $errorCode, $message)';
  }
}

class AccountRuntimeClient {
  AccountRuntimeClient({required String baseUrl})
    : baseUrl = _normalizeBaseUrl(baseUrl);

  final String baseUrl;

  static String _normalizeBaseUrl(String raw) {
    final trimmed = raw.trim();
    if (trimmed.isEmpty) {
      return '';
    }
    return trimmed.endsWith('/')
        ? trimmed.substring(0, trimmed.length - 1)
        : trimmed;
  }

  Future<Map<String, dynamic>> login({
    required String identifier,
    required String password,
  }) {
    return _requestJson(
      method: 'POST',
      path: '/api/auth/login',
      body: <String, Object?>{
        'identifier': identifier.trim(),
        'password': password,
      },
    );
  }

  Future<Map<String, dynamic>> verifyMfa({
    required String mfaToken,
    required String code,
  }) {
    return _requestJson(
      method: 'POST',
      path: '/api/auth/mfa/verify',
      body: <String, Object?>{
        'mfaToken': mfaToken.trim(),
        'method': 'totp',
        'totpCode': code.trim(),
        'code': code.trim(),
      },
    );
  }

  Future<AccountSessionSummary> loadSession({required String token}) async {
    final payload = await loadProfile(token: token);
    final user = _asMap(payload['user']);
    return _accountSessionSummaryFromUserJson(user);
  }

  Future<Map<String, dynamic>> loadProfile({required String token}) {
    return _requestJson(
      method: 'GET',
      path: '/api/auth/session',
      bearerToken: token,
    );
  }

  Future<Map<String, dynamic>> loadXWorkmateProfileSync({
    required String token,
  }) {
    return _requestJson(
      method: 'GET',
      path: '/api/auth/xworkmate/profile/sync',
      bearerToken: token,
    );
  }

  Future<String> readVaultSecretValue({
    required String vaultUrl,
    required String namespace,
    required String vaultToken,
    required String secretPath,
    required String secretKey,
  }) async {
    final uri = _vaultReadUri(vaultUrl, secretPath);
    final payload = await _requestJson(
      method: 'GET',
      uriOverride: uri,
      rawHeaders: <String, String>{
        if (namespace.trim().isNotEmpty) 'X-Vault-Namespace': namespace.trim(),
        if (vaultToken.trim().isNotEmpty) 'X-Vault-Token': vaultToken.trim(),
      },
    );
    final data = _asMap(payload['data']);
    final secretData = _asMap(data['data']);
    return _stringValue(secretData[secretKey]);
  }

  AccountSessionSummary _accountSessionSummaryFromUserJson(
    Map<String, dynamic> user,
  ) {
    final mfa = _asMap(user['mfa']);
    final totpEnabled = mfa['totpEnabled'] as bool? ?? false;
    final totpPending = mfa['totpPending'] as bool? ?? false;
    return AccountSessionSummary(
      userId: _stringValue(user['id']),
      email: _stringValue(user['email']),
      name: _stringValue(user['name']).isNotEmpty
          ? _stringValue(user['name'])
          : _stringValue(user['username']),
      role: _stringValue(user['role']),
      mfaEnabled: user['mfaEnabled'] as bool? ?? totpEnabled,
      totpEnabled: totpEnabled,
      totpPending: totpPending,
    );
  }

  Uri _vaultReadUri(String rawBaseUrl, String secretPath) {
    final base = Uri.parse(_normalizeBaseUrl(rawBaseUrl));
    final trimmedPath = secretPath.trim().replaceAll(RegExp(r'^/+|/+$'), '');
    final segments = trimmedPath
        .split('/')
        .map((item) => item.trim())
        .where((item) => item.isNotEmpty)
        .toList(growable: false);
    if (segments.length < 2) {
      throw const AccountRuntimeException(
        statusCode: 400,
        errorCode: 'invalid_vault_path',
        message: 'invalid vault path',
      );
    }
    final mount = segments.first;
    final path = segments.skip(1).toList(growable: false);
    return base.replace(pathSegments: <String>['v1', mount, 'data', ...path]);
  }

  Future<Map<String, dynamic>> _requestJson({
    required String method,
    String path = '',
    Uri? uriOverride,
    String bearerToken = '',
    Map<String, Object?>? body,
    Map<String, String> rawHeaders = const <String, String>{},
  }) async {
    final uri = uriOverride ?? Uri.parse('$baseUrl$path');
    final client = HttpClient()..connectionTimeout = const Duration(seconds: 6);
    try {
      final request = await switch (method.toUpperCase()) {
        'POST' => client.postUrl(uri),
        'GET' => client.getUrl(uri),
        _ => throw UnsupportedError('Unsupported method $method'),
      };
      request.headers.set(HttpHeaders.acceptHeader, 'application/json');
      if (bearerToken.trim().isNotEmpty) {
        request.headers.set(
          HttpHeaders.authorizationHeader,
          'Bearer ${bearerToken.trim()}',
        );
      }
      for (final entry in rawHeaders.entries) {
        request.headers.set(entry.key, entry.value);
      }
      if (body != null) {
        request.headers.contentType = ContentType.json;
        request.write(jsonEncode(body));
      }
      final response = await request.close().timeout(
        const Duration(seconds: 6),
      );
      final rawBody = await utf8.decoder.bind(response).join();
      final decoded = rawBody.trim().isEmpty
          ? const <String, dynamic>{}
          : _asMap(jsonDecode(rawBody));
      if (response.statusCode < 200 || response.statusCode >= 300) {
        throw AccountRuntimeException(
          statusCode: response.statusCode,
          errorCode: _stringValue(decoded['error']).isNotEmpty
              ? _stringValue(decoded['error'])
              : 'request_failed',
          message: _stringValue(decoded['message']).isNotEmpty
              ? _stringValue(decoded['message'])
              : rawBody.trim(),
        );
      }
      return decoded;
    } finally {
      client.close(force: true);
    }
  }

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

  static String _stringValue(Object? value) {
    return value?.toString().trim() ?? '';
  }
}
