import 'dart:async';
import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:guru_app/ads/ads_manager.dart';
import 'package:guru_app/ads/core/ads.dart';
import 'package:guru_app/ads/core/strategy/ad_unit.dart';
import 'package:guru_app/ads/core/strategy/interstitial/max_interstitial_ad_unit.dart';
import 'package:guru_app/ads/core/strategy/strategy_ads.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_utils/ads/ads_delegate.dart';
import 'package:guru_utils/ads/data/ads_model.dart';
import 'package:guru_utils/ads/handler/ads_handler.dart';
import 'package:guru_utils/extensions/extensions.dart';
import 'package:guru_utils/log/log.dart';
import 'package:connectivity_plus/connectivity_plus.dart';

/// Created by Haoyi on 2023/6/28

typedef AdsEventDispatcher = void Function(AdsEvent, {Map<dynamic, dynamic> arguments});

abstract class AdsStrategy extends AdsLifecycleObserver {
  final AdsEventDispatcher eventDispatcher;

  AdsStrategy(this.eventDispatcher);

  bool get loaded;

  Stream<bool> get observableLoaded;

  Future<AdCause> requestLoad();

  Future<AdCause> requestShow({required String scene});

  Future<bool> requestHide();

  Future<AdCause> requestReset();

  Future<AdState> getState();

  Future<bool> requestDispose();

  void dispatchEvent(AdsEvent adsEvent,
      {Map<dynamic, dynamic> arguments = const <dynamic, dynamic>{}}) {
    eventDispatcher.call(adsEvent, arguments: arguments);
  }
}

class AdUnitRetryAgent {
  MaxInterstitialAdUnit? adUnit;
  final RetryConfig retryConfig;
  int retryAttempt = 0;

  Timer? _retryTimer;

  AdUnitRetryAgent(this.adUnit, this.retryConfig);

  void dispose() {
    _retryTimer?.cancel();
    _retryTimer = null;
    adUnit = null;
  }

  Future<bool> retry() async {
    final ad = adUnit;
    if (_retryTimer?.isActive == true || ad == null) {
      return false;
    }
    final adState = await ad.getState();
    if (adState == AdState.loaded) {
      return false;
    }
    retryAttempt++;
    final delaySecond = pow(2, retryAttempt).toInt();
    final config = retryConfig;
    final duration = Duration(seconds: delaySecond.clamp(config.minInSecond, config.maxInSecond));
    _retryTimer = Timer(duration, () {
      adUnit?.requestLoad();
    });
    return true;
  }
}

class MaxInterstitialStrategy extends AdsStrategy {
  final List<MaxInterstitialAdUnit> adUnits = [];

  MaxInterstitialAdUnit? _showingAdUnit;

  final BehaviorSubject<StrategyAdsState> _strategyAdsStateSubject =
      BehaviorSubject.seeded(StrategyAdsState.init);

  StrategyAdsState get strategyAdsState => _strategyAdsStateSubject.value;

  set strategyAdsState(StrategyAdsState value) {
    _strategyAdsStateSubject.add(value);
  }

  Stream<StrategyAdsState> get observableStrategyAdsState => _strategyAdsStateSubject.stream;

  @override
  bool get loaded => strategyAdsState == StrategyAdsState.loaded;

  final Map<dynamic, dynamic> _loadedAdsArguments = {};

  bool upcomingAdLoadedEvent = false;

  @override
  Stream<bool> get observableLoaded =>
      observableStrategyAdsState.map((event) => event == StrategyAdsState.loaded);

  final List<Timer> loadTimers = [];

  AdUnitRetryAgent? retryAgent;

  MaxInterstitialStrategy(List<AdId> adIds, AdsEventDispatcher dispatcher) : super(dispatcher) {
    for (var adId in adIds) {
      adUnits.add(MaxInterstitialAdUnit.create(adId.adUnitId, adId.amazonAdSlotId)
        ..addObserver(this)
        ..init());
    }
  }

  @override
  void onAdLoaded(AdsBundle adsBundle) {
    if (strategyAdsState != StrategyAdsState.loaded) {
      disposeRetryAgent();
      strategyAdsState = StrategyAdsState.loaded;
      _loadedAdsArguments.clear();
      _loadedAdsArguments.addAll(adsBundle.arguments);
      if (_showingAdUnit == null) {
        dispatchEvent(AdsEvent.adLoaded, arguments: adsBundle.arguments);
      } else {
        upcomingAdLoadedEvent = true;
      }
    }
  }

  @override
  void onAdLoadFailed(AdsBundle adsBundle) async {
    // 如果 retryAgent 存在，证明无底价的广告正在加载，这个时候高价广告不需要加载
    // 这样的设计是该 strategy 的定义，如需修改，请重新定义一个新的 strategy
    final _retryAgent = retryAgent;
    if (_retryAgent != null) {
      // 如果最在 retryAgent，将忽略其它的广告加载失败
      if (_retryAgent.adUnit != adsBundle.ads) {
        return;
      }
      _retryAgent.retry();
      return;
    }
    final noReservePriceAdUnit = adUnits.safeLast;
    // 如果是无底价广告加载失败，那么将会触发重试机制
    if (adsBundle.ads == noReservePriceAdUnit) {
      retryAgent = AdUnitRetryAgent(noReservePriceAdUnit, RetryConfig(4, 30))..retry();
      return;
    }
    checkAndRetry(msg: "onAdLoadFailed");
  }

  @override
  void onAdDisplayed(AdsBundle adsBundle) async {
    _showingAdUnit = adsBundle.ads as MaxInterstitialAdUnit;
    dispatchEvent(AdsEvent.adDisplayed, arguments: adsBundle.arguments);
  }

  @override
  void onAdDisplayFailed(AdsBundle adsBundle) async {
    final failedAdUnit = adsBundle.ads as MaxInterstitialAdUnit;
    for (var adUnit in adUnits) {
      final state = await adUnit.getState();
      if (state != AdState.loaded || failedAdUnit == adUnit) {
        continue;
      }
      Log.d("[${failedAdUnit.adUnitId}] AdDisplayFailed! use ${adUnit.adUnitId} Ads Instead",
          tag: PropertyTags.strategyAds);
      try {
        final result = await adUnit.show(scene: failedAdUnit.scene);
        if (result == AdCause.success) {
          _showingAdUnit = adUnit;
          return;
        } else {
          continue;
        }
      } catch (error, stacktrace) {
        Log.w("requestShow error",
            tag: PropertyTags.strategyAds,
            error: error,
            stackTrace: stacktrace,
            syncFirebase: true);
        continue;
      }
    }
    dispatchEvent(AdsEvent.adDisplayFailed, arguments: adsBundle.arguments);
    Log.d("onAdDisplayFailed [$runtimeType] Not Found valid ads! requestLoad",
        tag: PropertyTags.strategyAds);
    requestLoad();
  }

  @override
  void onAdHidden(AdsBundle adsBundle) {
    dispatchEvent(AdsEvent.adHidden, arguments: adsBundle.arguments);
    Log.d("[${_showingAdUnit?.adUnitId}] onAdHidden!", tag: PropertyTags.strategyAds);
    _showingAdUnit = null;
    checkAndRetry(msg: "onAdHidden");
  }

  @override
  void onAdClicked(AdsBundle adsBundle) {
    dispatchEvent(AdsEvent.adClick, arguments: adsBundle.arguments);
  }

  void disposeLoadTimer() {
    for (var timer in loadTimers) {
      timer.cancel();
    }
    loadTimers.clear();
  }

  void disposeRetryAgent() {
    retryAgent?.dispose();
    retryAgent = null;
  }

  @override
  Future<AdCause> requestLoad() async {
    final loadAdUnits = <MaxInterstitialAdUnit>[];
    Log.d("[$runtimeType-$hashCode] requestLoad $strategyAdsState", tag: PropertyTags.strategyAds);
    switch (strategyAdsState) {
      case StrategyAdsState.init:
        loadAdUnits.addAll(adUnits);
        break;
      case StrategyAdsState.loaded:
        consumeUpcomingAdLoadedEvent();
        dispatchEvent(AdsEvent.adLoaded, arguments: _loadedAdsArguments);
        return AdCause.success;
      case StrategyAdsState.disposed:
        return AdCause.loadFailed;
      default:
        strategyAdsState = StrategyAdsState.idle;
        for (var adUnit in adUnits) {
          final state = await adUnit.getState();
          Log.d("[$runtimeType] requestLoad check ${adUnit.adUnitId} is $state",
              tag: PropertyTags.strategyAds);
          if (state == AdState.loaded) {
            strategyAdsState = StrategyAdsState.loaded;
            consumeUpcomingAdLoadedEvent();
            dispatchEvent(AdsEvent.adLoaded, arguments: _loadedAdsArguments);
            return AdCause.success;
          }
          if (state != AdState.loading && adUnit != _showingAdUnit) {
            loadAdUnits.add(adUnit);
          }
        }
    }

    if (loadAdUnits.isEmpty) {
      return AdCause.success;
    }
    disposeLoadTimer();
    disposeRetryAgent();
    final timers = <Timer>[];
    const gap = Duration(seconds: 5);
    Duration delay = Duration.zero;
    AdCause result = AdCause.requestFailed;
    for (var adUnit in loadAdUnits) {
      try {
        if (delay > Duration.zero) {
          timers.add(Timer(delay, () async {
            if (strategyAdsState != StrategyAdsState.disposed) {
              final r = await adUnit.load();
              Log.d("[$runtimeType] request load ${adUnit.adUnitId} $r",
                  tag: PropertyTags.strategyAds);
            }
          }));
          delay += gap;
        } else {
          result = await adUnit.load();
          if (result == AdCause.success) {
            Log.d("[$runtimeType] request load ${adUnit.adUnitId} success",
                tag: PropertyTags.strategyAds);
            delay += gap;
          }
        }
      } catch (error, stacktrace) {
        Log.w("requestLoad error! $error  $stacktrace", tag: PropertyTags.strategyAds);
        continue;
      }
    }
    loadTimers.addAll(timers);
    return result;
  }

  @override
  Future<AdCause> requestShow({required String scene}) async {
    final result = AdsManager.instance.canShowInterstitial(scene);
    if (result != AdCause.success) {
      return result;
    }
    if (strategyAdsState == StrategyAdsState.disposed) {
      Log.d("[$runtimeType] requestShow Ads is disposed!", tag: PropertyTags.strategyAds);
      return AdCause.displayFailed;
    }
    strategyAdsState = StrategyAdsState.idle;
    for (var adUnit in adUnits) {
      final state = await adUnit.getState();
      if (state != AdState.loaded) {
        Log.d("[$runtimeType] requestShow Check!! Ads [${adUnit.adUnitId}] state is $state",
            tag: PropertyTags.strategyAds);
        continue;
      }

      try {
        final result = await adUnit.show(scene: scene);
        Log.d("[$runtimeType] requestShow [${adUnit.adUnitId}]", tag: PropertyTags.strategyAds);
        if (result == AdCause.success) {
          _showingAdUnit = adUnit;
          return AdCause.success;
        } else {
          continue;
        }
      } catch (error, stacktrace) {
        Log.w("requestShow error ${adUnit.adUnitId}",
            error: error,
            stackTrace: stacktrace,
            syncFirebase: true,
            tag: PropertyTags.strategyAds);
        continue;
      }
    }
    Log.d("requestShow [$runtimeType] Not Found valid ads! requestLoad",
        tag: PropertyTags.strategyAds);
    requestLoad();
    return AdCause.requestFailed;
  }

  bool consumeUpcomingAdLoadedEvent() {
    final result = upcomingAdLoadedEvent;
    upcomingAdLoadedEvent = false;
    return result;
  }

  @override
  Future<bool> requestHide() async {
    return false;
  }

  @override
  Future<bool> requestDispose() async {
    for (var adUnit in adUnits) {
      try {
        Log.d("[$runtimeType] requestDispose [${adUnit.adUnitId}]", tag: PropertyTags.strategyAds);
        await adUnit.dispose();
      } catch (error, stacktrace) {
        Log.w("requestDispose error",
            error: error,
            stackTrace: stacktrace,
            syncFirebase: true,
            tag: PropertyTags.strategyAds);
      }
    }
    adUnits.clear();
    strategyAdsState = StrategyAdsState.disposed;
    return true;
  }

  @override
  Future<AdState> getState() async {
    final adStates = <AdState>[];
    for (var adUnit in adUnits) {
      final state = await adUnit.getState();
      adStates.add(state);
    }

    for (var state in adStates) {
      if (state == AdState.loaded) {
        return AdState.loaded;
      }
    }

    for (var state in adStates) {
      if (state == AdState.loading) {
        return AdState.loading;
      }
    }

    for (var state in adStates) {
      if (state != AdState.failed) {
        return AdState.created;
      }
    }
    return AdState.failed;
  }

  @override
  Future<AdCause> requestReset() async {
    return AdCause.internalError;
  }

  void dispatchAdLoadedEvent() {
    dispatchEvent(AdsEvent.adLoaded, arguments: _loadedAdsArguments);
  }

  Future checkAndRetry({String? msg}) async {
    if (strategyAdsState == StrategyAdsState.disposed) {
      Log.d("[${msg ?? runtimeType}] checkAndRetry Ads is disposed!",
          tag: PropertyTags.strategyAds);
      return;
    }
    bool loading = false;
    for (var adUnit in adUnits) {
      try {
        final state = await adUnit.getState();
        Log.d("[${msg ?? runtimeType}] checkAndRetry ad [${adUnit.adUnitId}] $state",
            tag: PropertyTags.strategyAds);
        if (state == AdState.loading) {
          loading = true;
          continue;
        }
        if (state == AdState.loaded) {
          if (consumeUpcomingAdLoadedEvent()) {
            dispatchEvent(AdsEvent.adLoaded, arguments: _loadedAdsArguments);
          }
          strategyAdsState = StrategyAdsState.loaded;
          continue;
        }
      } catch (error, stacktrace) {
        Log.w("check state error",
            error: error, stackTrace: stacktrace, tag: PropertyTags.strategyAds);
      }
    }
    if (loading) {
      Log.d("[${msg ?? runtimeType}] checkAndRetry loading! waiting ads...",
          tag: PropertyTags.strategyAds);
      strategyAdsState = StrategyAdsState.idle;
      return;
    }
    if (strategyAdsState != StrategyAdsState.loaded) {
      Log.d("[${msg ?? runtimeType}] Not Found loaded ads! requestLoad",
          tag: PropertyTags.strategyAds);
      strategyAdsState = StrategyAdsState.init;
      requestLoad();
    }
  }
}

class MaxStrategyInterstitialAds extends StrategyAds with AdsAudit {
  MaxStrategyInterstitialAds.create(List<AdId> adIds) : super() {
    strategy = MaxInterstitialStrategy(adIds, dispatchEvent);
  }

  @override
  @mustCallSuper
  Future dispose() async {
    bool result = false;
    try {
      result = await strategy.requestDispose();
    } catch (error, stacktrace) {
      Log.d("requestDispose error:$error $stacktrace", tag: PropertyTags.strategyAds);
    }
    onRequestDispose(AdsBundle.create(this));
    super.dispose();
    return result;
  }

  @override
  @mustCallSuper
  Future<bool> reset() async {
    AdCause result = AdCause.internalError;
    try {
      result = await strategy.requestReset();
    } catch (error, stacktrace) {
      Log.d("requestReset error:$error $stacktrace", tag: PropertyTags.strategyAds);
    }
    onRequestReset(AdsBundle.create(this));
    return result == AdCause.success;
  }

  @mustCallSuper
  void init() {}

  @override
  Future<AdCause> load() async {
    final adCause = await strategy.requestLoad().catchError((error, stacktrace) {
      Log.e("load error! ",
          tag: PropertyTags.strategyAds, error: error, stackTrace: stacktrace, syncFirebase: true);
      return AdCause.internalError;
    });
    Log.d("[$runtimeType]request load complete!! $adCause", tag: PropertyTags.strategyAds);
    onRequestLoad(AdsBundle.create(this, arguments: {"cause": adCause}));
    return adCause;
  }

  @override
  Future hide() async {
    onRequestHide(AdsBundle.create(this));
    return await strategy.requestHide();
  }

  @override
  Future<AdCause> show({required String scene, bool ignoreCheck = false}) async {
    final adCause = await strategy.requestShow(scene: scene).catchError((error, stacktrace) {
      Log.e("show error! $error", stackTrace: stacktrace, syncFirebase: true);
      return AdCause.internalError;
    });
    Log.d("[$runtimeType]show $scene complete!! $adCause",
        syncFirebase: true, tag: PropertyTags.strategyAds);
    onRequestShow(AdsBundle.create(this, arguments: {"scene": scene, "cause": adCause}));
    return adCause;
  }

  @override
  Future<AdState> getState() async {
    return await strategy.getState();
  }

  @override
  void preload() {
    if (AdsManager.instance.connectivityStatus != ConnectivityResult.none) {
      load();
    }
  }
}
