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

import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:http/io_client.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/exceptions/api_exception.dart';
import 'package:mc_common_app/main.dart';
import 'package:mc_common_app/models/user_models/refresh_token.dart';
import 'package:mc_common_app/models/user_models/user.dart';
import 'package:mc_common_app/utils/shared_prefrence.dart';

typedef FactoryConstructor<U> = U Function(dynamic);

class APIError {
  int errorCode;
  String errorMessage;

  APIError(this.errorCode, this.errorMessage);

  Map<String, dynamic> toJson() => {'errorCode': errorCode, 'errorMessage': errorMessage};

  @override
  String toString() {
    return jsonEncode(this);
  }

  static APIException throwAPIException(Response response) {
    switch (response.statusCode) {
      case 400:
        APIError? apiError;
        if (response.body != null && response.body.isNotEmpty) {
          var jsonError = jsonDecode(response.body);
          apiError = APIError(jsonError['errorCode'], jsonError['errorMessage']);
        }
        return APIException(APIException.BAD_REQUEST, error: apiError);
      case 401:
        return const APIException(APIException.UNAUTHORIZED);
      case 403:
        return const APIException(APIException.FORBIDDEN);
      case 404:
        return const APIException(APIException.NOT_FOUND);
      case 500:
        return const APIException(APIException.INTERNAL_SERVER_ERROR);
      case 444:
        var downloadUrl = response.headers["location"];
        return APIException(APIException.UPGRADE_REQUIRED, arguments: downloadUrl);
      default:
        return const APIException(APIException.OTHER);
    }
  }
}

abstract class ApiClient {
  Future<U> postJsonForObject<T, U>(FactoryConstructor<U> factoryConstructor, String url, T jsonObject, {String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0});

  Future<Response> postJsonForResponse<T>(String url, T jsonObject, {String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0});

  Future<U> getJsonForObject<T, U>(FactoryConstructor<U> factoryConstructor, String url, {String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0});

  Future<Response> getJsonForResponse<T>(String url, {String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0});
}

class ApiClientImp implements ApiClient {
  @override
  Future<U> postJsonForObject<T, U>(FactoryConstructor<U> factoryConstructor, String url, T jsonObject, {String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0}) async {
    var headers0 = {'Accept': 'application/json'};
    if (headers != null && headers.isNotEmpty) {
      headers0.addAll(headers);
    }

    var response = await postJsonForResponse(url, jsonObject, token: token, queryParameters: queryParameters, headers: headers0, retryTimes: retryTimes);
    try {
      if (!kReleaseMode) {
        log("statusCode:${response.statusCode}");
      }
      var jsonData = jsonDecode(response.body);
      return factoryConstructor(jsonData);
    } catch (ex) {
      log(ex.toString());
      log("exception:$ex");
      throw APIException(APIException.BAD_RESPONSE_FORMAT, arguments: ex);
    }
  }

  @override
  Future<Response> postJsonForResponse<T>(String url, T jsonObject, {String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0}) async {
    String? requestBody;
    if (jsonObject != null) {
      requestBody = jsonEncode(jsonObject);
      if (headers == null) {
        headers = {'Content-Type': 'application/json'};
      } else {
        headers['Content-Type'] = 'application/json';
      }
    }

    return await _postForResponse(url, requestBody, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes);
  }

  Future<Response> _postForResponse(
    String url,
    requestBody, {
    String? token,
    Map<String, dynamic>? queryParameters,
    Map<String, String>? headers,
    int retryTimes = 0,
  }) async {
    try {
      var headers0 = <String, String>{};
      if (token != null) {
        headers0['Authorization'] = 'Bearer $token';
      }

      if (headers != null && headers.isNotEmpty) {
        headers0.addAll(headers);
      }

      if (queryParameters != null) {
        var queryString = Uri(queryParameters: queryParameters).query;
        url = '$url?$queryString';
      }
      Response response;

      response = await _post(Uri.parse(url), body: requestBody, headers: headers0).timeout(const Duration(seconds: 100));

      if (!kReleaseMode) {
        logger.d("------URL------");
        logger.i(url);
        logger.d("------Payload------");
        logger.i(jsonDecode(requestBody));
        logger.d("------Response------");
        logger.i(jsonDecode(response.body));
      }
      if (response.statusCode >= 200 && response.statusCode < 500) {
        var jsonData = jsonDecode(response.body);
        if (jsonData["StatusMessage"] != null && jsonData["StatusMessage"] == "Unauthorized user attempt to access API") {
          String mToken = await updateUserToken();
          return await _postForResponse(url, requestBody, token: mToken, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes);
        }
        return response;
      } else {
        throw APIError.throwAPIException(response);
      }
    } on SocketException catch (e) {
      if (retryTimes > 0) {
        log('will retry after 3 seconds...');
        await Future.delayed(const Duration(seconds: 3));
        return await _postForResponse(url, requestBody, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes - 1);
      } else {
        throw APIException(APIException.OTHER, arguments: e);
      }
    } on HttpException catch (e) {
      if (retryTimes > 0) {
        log('will retry after 3 seconds...');
        await Future.delayed(const Duration(seconds: 3));
        return await _postForResponse(url, requestBody, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes - 1);
      } else {
        throw APIException(APIException.OTHER, arguments: e);
      }
    } on TimeoutException catch (e) {
      throw APIException(APIException.TIMEOUT, arguments: e);
    } on ClientException catch (e) {
      if (retryTimes > 0) {
        log('will retry after 3 seconds...');
        await Future.delayed(const Duration(seconds: 3));
        return await _postForResponse(url, requestBody, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes - 1);
      } else {
        throw APIException(APIException.OTHER, arguments: e);
      }
    } catch (ex) {
      log("exception1:$ex");
      throw APIException(APIException.BAD_RESPONSE_FORMAT, arguments: ex);
    }
  }

  bool _certificateCheck(X509Certificate cert, String host, int port) => true;

  Future<T> _withClient<T>(Future<T> Function(Client) fn) async {
    var httpClient = HttpClient()..badCertificateCallback = _certificateCheck;
    var client = IOClient(httpClient);
    try {
      return await fn(client);
    } finally {
      client.close();
    }
  }

  Future<Response> _post(url, {Map<String, String>? headers, body, Encoding? encoding}) => _withClient((client) => client.post(url, headers: headers, body: body, encoding: encoding));

  @override
  Future<U> getJsonForObject<T, U>(FactoryConstructor<U> factoryConstructor, String url, {String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0}) async {
    var headers0 = {'Accept': 'application/json'};
    if (headers != null && headers.isNotEmpty) {
      headers0.addAll(headers);
    }
    var response = await getJsonForResponse(url, token: token, queryParameters: queryParameters, headers: headers0, retryTimes: retryTimes);
    try {
      var jsonData = jsonDecode(response.body);
      return factoryConstructor(jsonData);
    } catch (ex) {
      log("exception:${response.body}");
      throw APIException(APIException.BAD_RESPONSE_FORMAT, arguments: ex);
    }
  }

  @override
  Future<Response> getJsonForResponse<T>(String url, {String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0}) async {
    if (headers == null) {
      headers = {'Content-Type': 'application/json'};
    } else {
      headers['Content-Type'] = 'application/json';
    }
    return await _getForResponse(url, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes);
  }

  bool isFirstCall = true;

  Future<Response> _getForResponse(String url, {String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0}) async {
    try {
      var headers0 = <String, String>{};
      if (token != null) {
        headers0['Authorization'] = 'Bearer $token';
      }

      if (headers != null && headers.isNotEmpty) {
        headers0.addAll(headers);
      }
      if (queryParameters != null) {
        String queryString = Uri(queryParameters: queryParameters).query;
        if (isFirstCall) url = '$url?$queryString';
      }

      if (!kReleaseMode) {
        logger.i(url);
        logger.i("$queryParameters");
      }
      var response = await _get(Uri.parse(url), headers: headers0).timeout(const Duration(seconds: 60));

      if (!kReleaseMode) {
        logger.i(jsonDecode(response.body));
      }
      if (response.statusCode >= 200 && response.statusCode < 500) {
        var jsonData = jsonDecode(response.body);
        if (jsonData["StatusMessage"] != null && jsonData["StatusMessage"] == "Unauthorized user attempt to access API") {
          String mToken = await updateUserToken();
          isFirstCall = false;
          return await _getForResponse(url, token: mToken, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes);
        }
        return response;
      } else {
        throw APIError.throwAPIException(response);
      }
    } on SocketException catch (e) {
      if (retryTimes > 0) {
        log('will retry after 3 seconds...');
        await Future.delayed(const Duration(seconds: 3));
        return await _getForResponse(url, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes - 1);
      } else {
        throw APIException(APIException.OTHER, arguments: e);
      }
    } on HttpException catch (e) {
      if (retryTimes > 0) {
        log('will retry after 3 seconds...');
        await Future.delayed(const Duration(seconds: 3));
        return await _getForResponse(url, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes - 1);
      } else {
        throw APIException(APIException.OTHER, arguments: e);
      }
    } on TimeoutException catch (e) {
      throw APIException(APIException.TIMEOUT, arguments: e);
    } on ClientException catch (e) {
      if (retryTimes > 0) {
        await Future.delayed(const Duration(seconds: 3));
        return await _getForResponse(url, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes - 1);
      } else {
        throw APIException(APIException.OTHER, arguments: e);
      }
    }
  }

  Future<Response> _get(url, {Map<String, String>? headers}) => _withClient((client) => client.get(url, headers: headers));

// Future<RefreshToken> refreshTokenAPI(String token, String refreshToken) async {
//   var postParams = {"token": token, "refreshToken": refreshToken};
//   // String t = AppState().getUser.data!.accessToken ?? "";
//   return await postJsonForObject((json) => RefreshToken.fromJson(json), ApiConsts.RefreshToken, postParams);
// }

  // //TODO: needs to solve later this.
  // String updateUserToken() {
  //   return "";
  // }

  Future<RefreshToken> refreshTokenAPI(String token, String refreshToken) async {
    var postParams = {"token": token, "refreshToken": refreshToken};
    // String t = AppState().getUser.data!.accessToken ?? "";
    return await postJsonForObject((json) => RefreshToken.fromJson(json), ApiConsts.refreshToken, postParams);
  }

  Future<String> updateUserToken() async {
    String token = await SharedPrefManager.getUserToken();
    String refreshToken = await SharedPrefManager.getRefreshToken();
    RefreshToken refresh = await refreshTokenAPI(token, refreshToken);
    SharedPrefManager.setUserToken(refresh.data!.accessToken ?? "");
    SharedPrefManager.setRefreshToken(refresh.data!.refreshToken ?? "");
    String mdata = await SharedPrefManager.getData();
    UserInfo info = UserInfo.fromJson(jsonDecode(mdata));
    User user = User();
    user.data = UserData(accessToken: refresh.data!.accessToken ?? "", refreshToken: refresh.data!.refreshToken ?? "", userInfo: info);
    AppState().setUser = user;
    return refresh.data!.accessToken ?? "";
  }
}
