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

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

import '../../application/application.dart';

final class HttpApiClient {
  HttpApiClient({
    required this.baseUrl,
    http.Client? client,
    this.timeout = const Duration(seconds: 10),
  }) : client = client ?? http.Client();

  static const defaultBaseUrl = String.fromEnvironment(
    'GAMETIME_API_BASE_URL',
    defaultValue: 'http://localhost:8080',
  );

  final Uri baseUrl;
  final http.Client client;
  final Duration timeout;

  Future<Map<String, Object?>> postJson(
    String path, {
    Map<String, Object?>? body,
    String? bearerToken,
    Set<int> expectedStatuses = const {200, 201},
  }) async {
    final response = await _send(
      () => client.post(
        _resolve(path),
        headers: _headers(bearerToken),
        body: body == null ? null : jsonEncode(body),
      ),
    );
    if (!expectedStatuses.contains(response.statusCode)) {
      throw _exceptionForStatus(response.statusCode, response.body);
    }
    if (response.body.trim().isEmpty) {
      return const {};
    }
    final Object? decoded;
    try {
      decoded = jsonDecode(response.body);
    } on FormatException catch (error) {
      throw RemoteAuthException(RemoteAuthFailure.unknown, error.message);
    }
    if (decoded is Map) {
      return Map<String, Object?>.from(decoded);
    }
    throw const RemoteAuthException(
      RemoteAuthFailure.unknown,
      'Unexpected JSON response.',
    );
  }

  Future<void> postEmpty(
    String path, {
    String? bearerToken,
    Set<int> expectedStatuses = const {204},
  }) async {
    final response = await _send(
      () => client.post(_resolve(path), headers: _headers(bearerToken)),
    );
    if (!expectedStatuses.contains(response.statusCode)) {
      throw _exceptionForStatus(response.statusCode, response.body);
    }
  }

  Future<Map<String, Object?>> getJson(
    String path, {
    Map<String, String?> queryParameters = const {},
    String? bearerToken,
    Set<int> expectedStatuses = const {200},
  }) async {
    final response = await _send(
      () => client.get(
        _resolve(path, queryParameters: queryParameters),
        headers: _headers(bearerToken),
      ),
    );
    if (!expectedStatuses.contains(response.statusCode)) {
      throw _exceptionForStatus(response.statusCode, response.body);
    }
    if (response.body.trim().isEmpty) {
      return const {};
    }
    final Object? decoded;
    try {
      decoded = jsonDecode(response.body);
    } on FormatException catch (error) {
      throw RemoteAuthException(RemoteAuthFailure.unknown, error.message);
    }
    if (decoded is Map) {
      return Map<String, Object?>.from(decoded);
    }
    throw const RemoteAuthException(
      RemoteAuthFailure.unknown,
      'Unexpected JSON response.',
    );
  }

  Uri _resolve(String path, {Map<String, String?> queryParameters = const {}}) {
    final normalized = path.startsWith('/') ? path.substring(1) : path;
    final base = baseUrl.toString().endsWith('/')
        ? baseUrl
        : Uri.parse('${baseUrl.toString()}/');
    final resolved = base.resolve(normalized);
    final cleanQuery = {
      for (final entry in queryParameters.entries)
        if (entry.value != null) entry.key: entry.value!,
    };
    if (cleanQuery.isEmpty) {
      return resolved;
    }
    return resolved.replace(
      queryParameters: {...resolved.queryParameters, ...cleanQuery},
    );
  }

  Map<String, String> _headers(String? bearerToken) => {
    'accept': 'application/json',
    'content-type': 'application/json',
    if (bearerToken != null) 'authorization': 'Bearer $bearerToken',
  };

  Future<http.Response> _send(Future<http.Response> Function() send) async {
    try {
      return await send().timeout(timeout);
    } on TimeoutException {
      throw const RemoteAuthException(RemoteAuthFailure.network, 'Timeout.');
    } on http.ClientException catch (error) {
      throw RemoteAuthException(RemoteAuthFailure.network, error.message);
    } on FormatException catch (error) {
      throw RemoteAuthException(RemoteAuthFailure.unknown, error.message);
    }
  }

  RemoteAuthException _exceptionForStatus(int statusCode, String body) {
    final message = _messageFromBody(body);
    return switch (statusCode) {
      401 => RemoteAuthException(RemoteAuthFailure.invalidCredentials, message),
      409 => RemoteAuthException(RemoteAuthFailure.emailAlreadyUsed, message),
      >= 500 => RemoteAuthException(RemoteAuthFailure.network, message),
      _ => RemoteAuthException(RemoteAuthFailure.unknown, message),
    };
  }

  String? _messageFromBody(String body) {
    if (body.trim().isEmpty) {
      return null;
    }
    try {
      final decoded = jsonDecode(body);
      if (decoded is Map && decoded['error'] is String) {
        return decoded['error'] as String;
      }
      if (decoded is Map && decoded['message'] is String) {
        return decoded['message'] as String;
      }
    } on FormatException {
      return body;
    }
    return body;
  }
}
