import 'dart:convert';

import 'package:guru_utils/core/ext.dart';
import 'package:guru_utils/http/http_model.dart';
import 'package:guru_utils/log/log.dart';
import 'package:http/http.dart' as _http;

/// Created by Haoyi on 2022/8/9

class Cdn {
  final String cdnPrefix;
  final String srcPrefix;
  final Uri uri;

  const Cdn(this.uri, {required this.srcPrefix, this.cdnPrefix = ""});

  @override
  String toString() {
    return 'Cdn{cdnPrefix: $cdnPrefix, srcPrefix: $srcPrefix, uri: $uri}';
  }
}

class CdnDetails {
  final int count;
  final int failed;
  final int fatal;

  CdnDetails(this.count, this.failed, this.fatal);

  CdnDetails copyWith({int? count, int? failed, int? fatal}) {
    return CdnDetails(count ?? this.count, failed ?? this.failed, fatal ?? this.fatal);
  }

  @override
  String toString() {
    return 'CdnDetails{count: $count, failed: $failed, fatal: $fatal}';
  }
}

class HttpEx {
  static CdnConfig _cdnConfig = CdnConfig.fromJson({});
  static String _firebaseStoragePrefix = "";
  static final Map<String, CdnDetails> _cdnDetails = {};

  static const _fatalResponseCode = {
    401, // Unauthorized
    403, // Forbidden
    405, // Method Not Allowed
    406, // Not Acceptable
    407, // Proxy Authentication Required
    415, // Unsupported Media Type
    500, // Internal Server Error
    501, // Not Implemented
    503, // Service Unavailable
    505, // HTTP Version not Supported
  };

  static void init(CdnConfig cdnConfig, String firebaseStoragePrefix) {
    _cdnConfig = cdnConfig;
    _firebaseStoragePrefix = firebaseStoragePrefix;
    Log.d("HttpEx.init: $cdnConfig $firebaseStoragePrefix");
  }

  static bool isFirebaseStoragePrefix(String url) {
    return url.startsWith(_firebaseStoragePrefix);
  }

  static Cdn selectCdn(Uri srcUrl) {
    final prefixList = _cdnConfig.prefix;
    String cdnPrefix = _cdnConfig.cdnPrefix;
    // check fatal count! use reserve cdn
    if (DartExt.isNotBlank(cdnPrefix)) {
      final url = srcUrl.toString();
      final fatalCount = _cdnDetails[cdnPrefix]?.fatal ?? 0;
      Log.d("selectCdn fatalCount $fatalCount replacement:$cdnPrefix ");
      if (fatalCount >= _cdnConfig.fatalThreshold) {
        final _fallbackPrefix = _cdnConfig.fallbackPrefix;
        if (DartExt.isNotBlank(_fallbackPrefix)) {
          cdnPrefix = _fallbackPrefix;
        } else {
          return Cdn(srcUrl, srcPrefix: "", cdnPrefix: "");
        }
      }
      String newUrl = cdnPrefix;
      for (var prefix in prefixList) {
        if (url.startsWith(prefix)) {
          var urlPath = url.replaceFirst(prefix, "");
          final path = Uri.parse(urlPath).path;
          final decodedPathUri = Uri.decodeFull(path);
          if (isFirebaseStoragePrefix(cdnPrefix)) {
            if (decodedPathUri[0] == '/') {
              newUrl += '/${Uri.encodeComponent(decodedPathUri.substring(1))}';
            } else {
              newUrl += Uri.encodeComponent(decodedPathUri);
            }
            newUrl += "?alt=media";
            return Cdn(Uri.parse(newUrl), srcPrefix: prefix, cdnPrefix: cdnPrefix);
          } else {
            newUrl += Uri.encodeFull(decodedPathUri);
            return Cdn(Uri.parse(newUrl), srcPrefix: prefix, cdnPrefix: cdnPrefix);
          }
        }
      }
    }
    return Cdn(srcUrl, srcPrefix: "", cdnPrefix: "");
  }

  /// Sends an HTTP HEAD request with the given headers to the given URL.
  ///
  /// This automatically initializes a new [Client] and closes that client once
  /// the request is complete. If you're planning on making multiple requests to
  /// the same server, you should use a single [Client] for all of those requests.
  ///
  /// For more fine-grained control over the request, use [Request] instead.
  static Future<_http.Response> head(Uri url, {Map<String, String>? headers}) =>
      _http.head(url, headers: headers);

  static Future<_http.Response> _cdnGet(Cdn cdn, CdnDetails cdnDetails,
      {Map<String, String>? headers}) async {
    late _http.Response response;
    try {
      Log.d("_cdnGet: ${cdn.uri}");
      response = await _http.get(cdn.uri, headers: headers);
      Log.d("_cdnGet response: ${cdn.uri} ${response.statusCode}");
      if (_fatalResponseCode.contains(response.statusCode)) {
        _cdnDetails[cdn.cdnPrefix] = cdnDetails.copyWith(
            count: cdnDetails.count + 1,
            failed: cdnDetails.failed + 1,
            fatal: cdnDetails.fatal + 1);
      } else if (response.statusCode != 200) {
        _cdnDetails[cdn.cdnPrefix] =
            cdnDetails.copyWith(count: cdnDetails.count + 1, failed: cdnDetails.failed + 1);
      } else {
        _cdnDetails[cdn.cdnPrefix] = cdnDetails.copyWith(count: cdnDetails.count + 1);
      }
      return response;
    } catch (error) {
      Log.w("_cdnGet error: $error");
      _cdnDetails[cdn.cdnPrefix] =
          cdnDetails.copyWith(count: cdnDetails.count + 1, failed: cdnDetails.failed + 1);
      rethrow;
    }
  }

  /// Sends an HTTP GET request with the given headers to the given URL.
  ///
  /// This automatically initializes a new [Client] and closes that client once
  /// the request is complete. If you're planning on making multiple requests to
  /// the same server, you should use a single [Client] for all of those requests.
  ///
  /// For more fine-grained control over the request, use [Request] instead.
  static Future<_http.Response> get(Uri url, {Map<String, String>? headers}) async {
    final cdn = selectCdn(url);
    final cdnDetails = _cdnDetails[cdn.cdnPrefix] ?? CdnDetails(0, 0, 0);
    return await _cdnGet(cdn, cdnDetails, headers: headers);

    // final _monitor = _cdnConfig.monitor;
    // if (_monitor) {
    //   final metric = FirebasePerformance.instance.newHttpMetric(cdn.uri.toString(), HttpMethod.Get);
    //   metric.putAttribute("count", cdnDetails.count.toString());
    //   metric.putAttribute("failed", cdnDetails.failed.toString());
    //   metric.putAttribute("fatal", cdnDetails.fatal.toString());
    //   await metric.start();
    //   try {
    //     final response = await _cdnGet(cdn, cdnDetails, headers: headers);
    //     metric.responseContentType = response.headers['Content-Type'];
    //     metric.responsePayloadSize = response.contentLength;
    //     metric.httpResponseCode = response.statusCode;
    //     return response;
    //   } catch (error, stacktrace) {
    //     metric.responseContentType = "crash";
    //     metric.responsePayloadSize = 0;
    //     metric.httpResponseCode = 400; // bad request
    //     rethrow;
    //   } finally {
    //     await metric.stop();
    //   }
    // } else {
    //   return await _cdnGet(cdn, cdnDetails, headers: headers);
    // }
  }

  /// Sends an HTTP POST request with the given headers and body to the given URL.
  ///
  /// [body] sets the body of the request. It can be a [String], a [List<int>] or
  /// a [Map<String, String>]. If it's a String, it's encoded using [encoding] and
  /// used as the body of the request. The content-type of the request will
  /// default to "text/plain".
  ///
  /// If [body] is a List, it's used as a list of bytes for the body of the
  /// request.
  ///
  /// If [body] is a Map, it's encoded as form fields using [encoding]. The
  /// content-type of the request will be set to
  /// `"application/x-www-form-urlencoded"`; this cannot be overridden.
  ///
  /// [encoding] defaults to [utf8].
  ///
  /// For more fine-grained control over the request, use [Request] or
  /// [StreamedRequest] instead.
  static Future<_http.Response> post(Uri url,
          {Map<String, String>? headers, Object? body, Encoding? encoding}) =>
      _http.post(url, headers: headers, body: body, encoding: encoding);

  /// Sends an HTTP PUT request with the given headers and body to the given URL.
  ///
  /// [body] sets the body of the request. It can be a [String], a [List<int>] or
  /// a [Map<String, String>]. If it's a String, it's encoded using [encoding] and
  /// used as the body of the request. The content-type of the request will
  /// default to "text/plain".
  ///
  /// If [body] is a List, it's used as a list of bytes for the body of the
  /// request.
  ///
  /// If [body] is a Map, it's encoded as form fields using [encoding]. The
  /// content-type of the request will be set to
  /// `"application/x-www-form-urlencoded"`; this cannot be overridden.
  ///
  /// [encoding] defaults to [utf8].
  ///
  /// For more fine-grained control over the request, use [Request] or
  /// [StreamedRequest] instead.
  static Future<_http.Response> put(Uri url,
          {Map<String, String>? headers, Object? body, Encoding? encoding}) =>
      _http.put(url, headers: headers, body: body, encoding: encoding);

  /// Sends an HTTP PATCH request with the given headers and body to the given
  /// URL.
  ///
  /// [body] sets the body of the request. It can be a [String], a [List<int>] or
  /// a [Map<String, String>]. If it's a String, it's encoded using [encoding] and
  /// used as the body of the request. The content-type of the request will
  /// default to "text/plain".
  ///
  /// If [body] is a List, it's used as a list of bytes for the body of the
  /// request.
  ///
  /// If [body] is a Map, it's encoded as form fields using [encoding]. The
  /// content-type of the request will be set to
  /// `"application/x-www-form-urlencoded"`; this cannot be overridden.
  ///
  /// [encoding] defaults to [utf8].
  ///
  /// For more fine-grained control over the request, use [Request] or
  /// [StreamedRequest] instead.
  static Future<_http.Response> patch(Uri url,
          {Map<String, String>? headers, Object? body, Encoding? encoding}) =>
      _http.patch(url, headers: headers, body: body, encoding: encoding);

  /// Sends an HTTP DELETE request with the given headers to the given URL.
  ///
  /// This automatically initializes a new [Client] and closes that client once
  /// the request is complete. If you're planning on making multiple requests to
  /// the same server, you should use a single [Client] for all of those requests.
  ///
  /// For more fine-grained control over the request, use [Request] instead.
  static Future<_http.Response> delete(Uri url,
          {Map<String, String>? headers, Object? body, Encoding? encoding}) =>
      _http.delete(url, headers: headers, body: body, encoding: encoding);

  /// Sends an HTTP GET request with the given headers to the given URL and
  /// returns a Future that completes to the body of the response as a [String].
  ///
  /// The Future will emit a [ClientException] if the response doesn't have a
  /// success status code.
  ///
  /// This automatically initializes a new [Client] and closes that client once
  /// the request is complete. If you're planning on making multiple requests to
  /// the same server, you should use a single [Client] for all of those requests.
  ///
  /// For more fine-grained control over the request and response, use [Request]
  /// instead.
  static Future<String> read(Uri url, {Map<String, String>? headers}) =>
      _http.read(selectCdn(url).uri, headers: headers);
}
