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

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:guru_app/account/account_data_store.dart';
import 'package:guru_app/account/model/user.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_utils/auth/auth_credential_manager.dart';
import 'package:guru_utils/device/device_info.dart';
import 'package:guru_utils/device/device_utils.dart';
import 'package:retrofit/retrofit.dart';
import 'custom_transformer.dart';
import 'data/orders/orders_model.dart';

/// Created by Haoyi on 6/3/21
part 'modules/guru_api_extension.dart';

part 'guru_api.g.dart';

abstract class DioBuilder {
  Dio build();
}

class GuruDioBuilder extends DioBuilder {
  final AccountDataStore accountDataStore;

  GuruDioBuilder() : accountDataStore = AccountDataStore.instance;

  Options toOptions(RequestOptions ro) {
    return Options(
        method: ro.method,
        sendTimeout: ro.sendTimeout,
        receiveTimeout: ro.receiveTimeout,
        extra: ro.extra,
        headers: ro.headers,
        responseType: ro.responseType,
        contentType: ro.contentType,
        validateStatus: ro.validateStatus,
        receiveDataWhenStatusError: ro.receiveDataWhenStatusError,
        followRedirects: ro.followRedirects,
        maxRedirects: ro.maxRedirects,
        requestEncoder: ro.requestEncoder,
        responseDecoder: ro.responseDecoder,
        listFormat: ro.listFormat);
  }

  @override
  Dio build() {
    Dio dio = Dio()
      ..transformer = CustomTransformer()
      ..options.connectTimeout = Duration(milliseconds: GuruApp.instance.appSpec.deployment.apiConnectTimeout)
      ..options.receiveTimeout = Duration(milliseconds: GuruApp.instance.appSpec.deployment.apiReceiveTimeout);

    dio.interceptors.add(InterceptorsWrapper(
        onRequest: (RequestOptions options, RequestInterceptorHandler handler) async {
      DeviceInfo? deviceInfo = accountDataStore.currentDevice;
      final deviceId = await AppProperty.getInstance().getDeviceId();
      deviceInfo ??=
          await DeviceUtils.buildDeviceInfo(deviceId: deviceId, firebasePushToken: "", uid: "");
      final token = accountDataStore.saasToken;
      options.headers
          .addAll({"X-APP-ID": GuruApp.instance.details.saasAppId, "X-ACCESS-TOKEN": token ?? ''});
      options.headers.addAll({"X-DEVICE-INFO": Uri.encodeFull(deviceInfo.toXDeviceInfo())});
      handler.next(options);
    }, onResponse: (Response response, ResponseInterceptorHandler handler) {
      // Log.v("### onResponse ${response.data}");
      response.data = response.data["data"] ?? response.data;
      handler.next(response);
    }, onError: (DioError err, ErrorInterceptorHandler handler) async {
      final token = accountDataStore.saasToken;
      final response = err.response;
      Log.v("### onError ${err.toString()}");
      if (response != null && token != null && response.statusCode == 401) {
        // dio.lock();
        try {
          Log.v("accountDataStore.refreshAuth()");
          await accountDataStore.refreshAuth(); //获取新token
        } catch (e) {
          // Log.v("[NETWORK]: RefreshToken Failed.");
          handler.reject(err);
        } finally {
          // dio.unlock();
        }
        final options = err.requestOptions.copyWith();
        options.headers["X-ACCESS-TOKEN"] = accountDataStore.saasToken;
        try {
          final response = await dio.request(options.path,
              data: options.data,
              queryParameters: options.queryParameters,
              cancelToken: options.cancelToken,
              onReceiveProgress: options.onReceiveProgress,
              options: toOptions(options));
          handler.resolve(response);
        } catch (error, stacktrace) {
          Log.v("re-request error:$error $stacktrace");
          handler.reject(err);
        }
      } else {
        handler.reject(err);
      }
    }));
    dio.interceptors
        .add(LogInterceptor(requestBody: true, responseBody: true, logPrint: platformLogPrint));
    return dio;
  }
}

@RestApi()
abstract class GuruApiMethods {
  factory GuruApiMethods(Dio dio, {String baseUrl}) = _GuruApiMethods;

  static GuruApiMethods create(GuruDioBuilder dioBuilder, String baseUrl) {
    return GuruApiMethods(dioBuilder.build(), baseUrl: baseUrl);
  }

  // 上报 Device, 初次或者 token 更新后
  @POST("/device/api/v1/devices")
  Future reportDevice(@Body() DeviceInfo body);

  // Auth
  @POST("/auth/api/v1/tokens/provider/secret")
  Future<GuruUser> signInWithAnonymous(@Body() AnonymousLoginReqBody body);

  @POST("/auth/api/v1/tokens/provider/facebook-gaming")
  Future<GuruUser> signInWithFacebook(@Body() FacebookLoginReqBody body);

  @POST("/auth/api/v1/tokens/provider/google")
  Future<GuruUser> signInWithGoogle(@Body() GoogleLoginReqBody body);

  @POST("/auth/api/v1/tokens/provider/apple")
  Future<GuruUser> signInWithApple(@Body() AppleLoginReqBody body);

  @POST("/auth/api/v1/bindings/provider/facebook-gaming")
  Future<GuruUser> associateWithFacebook(@Body() FacebookLoginReqBody body);

  @POST("/auth/api/v1/bindings/provider/google")
  Future<GuruUser> associateWithGoogle(@Body() GoogleLoginReqBody body);

  @POST("/auth/api/v1/bindings/provider/apple")
  Future<GuruUser> associateWithApple(@Body() AppleLoginReqBody body);

  @POST("/auth/api/v1/renewals/token")
  Future<GuruUser> refreshSaasToken();

  @POST("/auth/api/v1/renewals/firebase")
  Future<FirebaseTokenData> renewFirebaseToken();

  @POST("/order/api/v1/orders/ios")
  Future<OrdersResponse> iOSOrdersReport(@Body() OrdersReport body);

  @POST("/order/api/v1/orders/android")
  Future<OrdersResponse> androidOrdersReport(@Body() OrdersReport body);
}

class GuruApi {
  static const String _saasApiDevHost = "https://dev.saas.castbox.fm";
  static const String _saasApiReleaseHost = "https://saas.castbox.fm";

  static bool useReleaseApi = kReleaseMode;

  static final GuruApi _releaseApi =
      GuruApi._(GuruApiMethods.create(GuruDioBuilder(), _saasApiReleaseHost));
  static final GuruApi _debugApi =
      GuruApi._(GuruApiMethods.create(GuruDioBuilder(), _saasApiDevHost));

  final GuruApiMethods _methods;

  static GuruApi get instance => useReleaseApi ? _releaseApi : _debugApi;

  static String get saasApiHost => useReleaseApi ? _saasApiReleaseHost : _saasApiDevHost;

  GuruApiMethods get methods => _methods;

  GuruApi._(this._methods);
}
