import 'dart:io';

import 'package:adjust_sdk/adjust_event.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:guru_app/account/account_data_store.dart';
import 'package:guru_app/account/account_manager.dart';
import 'package:guru_app/account/model/account.dart';
import 'package:guru_app/account/model/credential.dart';
import 'package:guru_app/account/model/user.dart';
import 'package:guru_app/ads/ads_manager.dart';
import 'package:guru_app/analytics/abtest/abtest_model.dart';
import 'package:guru_app/analytics/guru_analytics.dart';
import 'package:guru_app/app/app_models.dart';
import 'package:guru_app/database/guru_db.dart';
import 'package:guru_app/financial/financial_manager.dart';
import 'package:guru_app/financial/iap/iap_manager.dart';
import 'package:guru_app/financial/manifest/manifest.dart';
import 'package:guru_app/financial/manifest/manifest_manager.dart';
import 'package:guru_app/financial/product/product_model.dart';
import 'package:guru_app/financial/reward/reward_manager.dart';
import 'package:guru_app/firebase/dxlinks/dxlink_manager.dart';
import 'package:guru_app/inventory/inventory_manager.dart';
import 'package:guru_applovin_flutter/guru_applovin_flutter.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_app/property/property_keys.dart';
import 'package:guru_utils/auth/auth_credential_manager.dart';
import 'package:guru_utils/collection/collectionutils.dart';
import 'package:guru_utils/controller/aware/ads/overlay/ads_overlay.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/device/device_utils.dart';
import 'package:guru_utils/lifecycle/lifecycle_manager.dart';
import 'package:guru_utils/network/network_utils.dart';
import 'package:guru_utils/property/app_property.dart';
import 'package:guru_app/property/settings/guru_settings.dart';
import 'package:guru_app/firebase/firebase.dart';
import 'package:guru_utils/http/http_ex.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_utils/packages/guru_package.dart';
import 'package:guru_utils/ads/ads.dart';
import 'package:guru_utils/guru_utils.dart';
import 'package:guru_utils/property/property_model.dart';
import 'package:logger/logger.dart' as Logger;
import 'package:guru_utils/aigc/bi/ai_bi.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:guru_popup/guru_popup.dart';
import 'package:guru_app/analytics/abtest/abtest_model.dart';

export 'package:firebase_core/firebase_core.dart';
export 'package:guru_app/app/app_models.dart';
export 'package:guru_utils/log/log.dart';
export 'package:guru_spec/guru_spec.dart';
export 'package:guru_app/analytics/guru_analytics.dart';
export 'package:guru_app/financial/product/product_model.dart';
export 'package:adjust_sdk/adjust_event.dart';
export 'package:guru_utils/ads/ads.dart';
export 'package:guru_utils/guru_utils.dart';
export 'dart:io';
export 'dart:math';
export 'package:guru_app/financial/manifest/manifest.dart';
export 'package:guru_app/firebase/messaging/remote_messaging_manager.dart';
export 'package:guru_app/analytics/abtest/abtest_model.dart';

/// Created by Haoyi on 2022/8/25

abstract class AppSpec {
  String get appName;

  AppCategory get appCategory;

  String get flavor;

  AppDetails get details;

  AdsProfile get adsProfile;

  ProductProfile get productProfile;

  AdjustProfile get adjustProfile;

  Deployment get deployment;

  Map<String, dynamic> get defaultRemoteConfig;

  Map<String, ABTestExperiment> get localABTestExperiments;

  String getRemoteConfigKey(String key);
}

class NotImplementationAppSpecCreatorException implements Exception {
  NotImplementationAppSpecCreatorException();

  @override
  String toString() {
    return 'NotImplementationAppSpecCreatorException';
  }
}

class AppEnv {
  final AppSpec spec;
  final RootPackage package;
  final IGuruSdkProtocol protocol;

  AppEnv(
      {required this.spec,
      required this.package,
      required this.protocol});
}

extension _GuruPackageExtension on GuruPackage {
  Iterable<Locale> _mergeSupportedLocales() {
    final Set<Locale> locales = supportedLocales.toSet();
    for (var child in children) {
      locales.addAll(child._mergeSupportedLocales());
    }
    return locales;
  }

  Iterable<LocalizationsDelegate<dynamic>> _mergeLocalizationsDelegates() {
    final Set<LocalizationsDelegate<dynamic>> delegates = localizationsDelegates.toSet();
    for (var child in children) {
      delegates.addAll(child._mergeLocalizationsDelegates());
    }
    return delegates;
  }

  Future _dispatchInitialize() async {
    await initialize();
    children.sort((p1, p2) {
      return p2.priority.compareTo(p1.priority);
    });
    for (var child in children) {
      if (flattenChildrenAsyncInit) {
        child._dispatchInitialize();
      } else {
        await child._dispatchInitialize();
      }
    }
  }

  Future _dispatchInitializeAsync() async {
    initializeAsync();
    for (var child in children) {
      child._dispatchInitializeAsync();
    }
  }

  Future _dispatchSwitchSession(String oldToken, String newToken) async {
    await switchSession(oldToken, newToken);
    children.sort((p1, p2) {
      return p2.priority.compareTo(p1.priority);
    });
    for (var child in children) {
      if (flattenChildrenAsyncInit) {
        child._dispatchSwitchSession(oldToken, newToken);
      } else {
        await child._dispatchSwitchSession(oldToken, newToken);
      }
    }
  }
}

enum AppCategory { game, app }

abstract class IGuruSdkProtocol {
  static void _unimplementedError(String name) {
    Log.e(
        "[$name] It is critically important that the GuruSDK protocol be implemented with precision. \n"
        "Failure to adhere to its standards will result in inaccuracies within our analytics data,\n"
        "thereby severely compromising our marketing strategies and the effectiveness of our user acquisition efforts.\n"
        "It is essential to understand that non-compliance is not an option,\n"
        "as it poses significant risks to our operational success and strategic objectives.");
    throw UnimplementedError("Please fully implement the IGuruSdkProtocol!");
  }

  InventoryDelegate? get inventoryDelegate => null;

  BackgroundMessageHandler? get backgroundMessageHandler => null;

  ToastDelegate? get toastDelegate => null;

  IAccountAuthDelegate? get accountAuthDelegate => null;

  String getLevelName() {
    if (GuruApp.instance.appSpec.appCategory == AppCategory.game) {
      _unimplementedError("getLevelName");
    }
    return "app";
  }

  int getCurrentLevel() {
    if (GuruApp.instance.appSpec.appCategory == AppCategory.game) {
      _unimplementedError("getCurrentLevel");
    }
    return 1;
  }
}

final Set<PropertyKey> _deviceSharedProperties = {
  PropertyKeys.deviceId,
  PropertyKeys.version,
  PropertyKeys.buildNumber,
  PropertyKeys.firstInstallTime,
  PropertyKeys.firstInstallVersion,
  PropertyKeys.prevInstallVersion,
  PropertyKeys.latestInstallVersion,
  PropertyKeys.previousInstalledVersion,
  PropertyKeys.latestLtDate,
  PropertyKeys.ltDays,
  PropertyKeys.appInstanceId,
  PropertyKeys.debugMode,
  PropertyKeys.keepOnScreenDuration,
  PropertyKeys.analyticsAdId,
  PropertyKeys.analyticsAdjustId,
  PropertyKeys.analyticsDeviceId,
  PropertyKeys.analyticsIdfa,
  PropertyKeys.analyticsFirebaseId,
  PropertyKeys.latestAnalyticsStrategy
};

class GuruApp {
  static late GuruApp _instance;

  static GuruApp get instance => _instance;

  final RootPackage rootPackage;

  final AppSpec appSpec;

  final IGuruSdkProtocol protocol;
  RemoteDeployment? _remoteDeployment;

  RemoteDeployment get remoteDeployment =>
      _remoteDeployment ??= RemoteConfigManager.instance.getRemoteDeployment();

  String get appName => appSpec.appName;

  String get flavor => appSpec.flavor;

  AppDetails get details => appSpec.details;

  AdsProfile get adsProfile => appSpec.adsProfile;

  AdjustProfile get adjustProfile => appSpec.adjustProfile;

  ProductProfile get productProfile => appSpec.productProfile;

  Map<String, dynamic> get defaultRemoteConfig => appSpec.defaultRemoteConfig;

  Set<String> get conversionEvents => appSpec.deployment.conversionEvents;

  GuruApp._(
      {required this.appSpec,
      required this.rootPackage,
      required this.protocol,
  }) {
    GuruUtils.toastDelegate = protocol.toastDelegate;
    AdsOverlay.bind(showBanner: GuruPopup.instance.showAdsBanner);
  }

  Iterable<Locale> get supportedLocales => rootPackage._mergeSupportedLocales();

  Iterable<LocalizationsDelegate<dynamic>> get localizationsDelegates =>
      rootPackage._mergeLocalizationsDelegates();

  String getRemoteConfigKey(String key) => appSpec.getRemoteConfigKey(key);

  bool? _check;

  @visibleForTesting
  static void setMockInstance(GuruApp app) {
    _instance = app;
  }

  Future _initialize() async {
    try {
      await GuruDB.instance.initDatabase();
      AppProperty.initialize(GuruDB.instance, cacheSize: appSpec.deployment.propertyCacheSize);
      await GuruSettings.instance.refresh();
      Paint.enableDithering = appSpec.deployment.enableDithering; // 3.16 default enabled
      await _dispatchInitializeSync();
      _dispatchInitializeAsync();
    } catch (error, stacktrace) {
      Log.w("initialize error:$error, $stacktrace");
    }
  }

  Future _migrateDeviceSharedData(PropertyBundle latestData) async {
    final PropertyBundle bundle = PropertyBundle();
    final keys = {
      ..._deviceSharedProperties,
      ...(protocol.accountAuthDelegate?.deviceSharedProperties ?? {})
    };
    for (var key in keys) {
      final value = latestData.getString(key);
      if (value != null) {
        bundle.setString(key, value);
      }
    }
    await AppProperty.getInstance().setProperties(bundle);
  }

  RemoteDeployment refreshRemoteDeployment() {
    try {
      return _remoteDeployment ??= RemoteConfigManager.instance.getRemoteDeployment();
    } catch (error, stacktrace) {
      Log.w("refreshRemoteDeployment error:$error, $stacktrace");
    }
    return RemoteDeployment.fromJson({});
  }

  Future<bool> switchAccount(GuruUser user, Credential credential, {GuruUser? oldUser}) async {
    final String oldToken = oldUser?.uid ?? "";
    final String newToken = user.uid;
    try {
      final previousUserProperties =
          PropertyBundle(map: await AppProperty.getInstance().loadAllValues());

      final result = await GuruDB.instance.switchSession(oldToken, newToken);
      if (!result) {
        Log.w("switchSession failed");
        return false;
      }
      AppProperty.reload(cacheSize: appSpec.deployment.propertyCacheSize);
      await _migrateDeviceSharedData(previousUserProperties);
      await GuruSettings.instance.refresh();
      await DeviceUtils.reload();
      FinancialManager.instance.switchSession(oldToken, newToken);
      GuruAnalytics.instance.switchSession(oldToken, newToken);
      await AccountManager.instance.processLogin(user, credential);
      await rootPackage._dispatchSwitchSession(oldToken, newToken);
      return true;
    } catch (error, stacktrace) {
      Log.w("switchSession error:$error, $stacktrace");
      return false;
    }
  }

  Future<bool> _checkApp() async {
    try {
      final pkgName = (await PackageInfo.fromPlatform()).appName;
      final result = _check ??= (pkgName != GuruApp.instance.details.appId);
      GuruAnalytics.instance.logGuruEvent(
          "dev_audit",
          CollectionUtils.filterOutNulls({
            "item_category": "pkg",
            "result": result == true ? 1 : 0,
            "err_info": result != true ? pkgName : null,
          }));
      return result == true;
    } catch (error, stacktrace) {
      Log.w("checkApp error:$error, $stacktrace");
      GuruAnalytics.instance.logException(error, stacktrace: stacktrace);
      GuruAnalytics.instance.logGuruEvent(
          "dev_audit",
          CollectionUtils.filterOutNulls({
            "item_category": "pkg",
            "result": 0,
            "err_info": error.runtimeType.toString(),
          }));
      return false;
    }
  }

  Future _dispatchInitializeSync() async {
    await RemoteConfigManager.instance.init(appSpec.defaultRemoteConfig);
    refreshRemoteDeployment();
    await DeviceUtils.initialize();
    await GuruAnalytics.instance.prepare();
    await rootPackage._dispatchInitialize();
    try {
      GuruUtils.isTablet = (await GuruApplovinFlutter.instance.isTablet()) ?? false;
      Log.d("isTablet: ${GuruUtils.isTablet}");
    } catch (error, stacktrace) {
      Log.w("invoke isTablet error:$error, $stacktrace");
    }
  }

  Future _dispatchInitializeAsync() async {
    _initCommon();
    _initRemoteConfig();
    _initRemoteMessaging();
    _initAnalytics();
    if (appSpec.adsProfile != AdsProfile.invalid) {
      _initAds();
    }
    _initFinancial();
    _initAccount();
    _initDxLink();
    rootPackage._dispatchInitializeAsync();
    Future.delayed(const Duration(seconds: 15), () async {
      await _checkApp();
    });
  }

  static Future initialize({required AppEnv appEnv}) async {
    final backgroundMessageHandler = appEnv.protocol.backgroundMessageHandler;
    if (backgroundMessageHandler != null) {
      FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
    }
    WidgetsFlutterBinding.ensureInitialized();
    try {
      await Firebase.initializeApp();
    } catch (error, stacktrace) {
      Log.e("Firebase.initializeApp() error!", error: error, stackTrace: stacktrace);
    }
    GuruUtils.flavor = appEnv.spec.flavor;

    /// 这里不用担心重复初始化，因为initialize会把对应的 AuthType 重新赋值
    /// 如果传入的 AuthType 有重复，会覆盖之前的 AuthType
    AuthCredentialManager.initialize([
      ...AccountManager.defaultSupportedAuthCredentialDelegates,
      ...appEnv.protocol.accountAuthDelegate?.supportedAuthCredentialDelegates ?? []
    ]);
    try {
      _instance = GuruApp._(
          appSpec: appEnv.spec,
          rootPackage: appEnv.package,
          protocol: appEnv.protocol);
      Log.init(_instance.appName,
          persistentLogFileSize: appEnv.spec.deployment.logFileSizeLimit,
          persistentLogCount: appEnv.spec.deployment.logFileCount,
          persistentLevel: appEnv.spec.deployment.persistentLogLevel);
      AiBi.instance.init();
      AdsManager.instance.ensureInitialize();
      await _instance._initialize();
      LifecycleManager.instance.init();
    } catch (error, stacktrace) {
      Log.e("GuruApp initialize error!", error: error, stackTrace: stacktrace);
      rethrow;
    }
  }

  void showToast(String message, {Duration duration = const Duration(seconds: 3)}) {
    GuruUtils.showToast(message, duration: duration);
  }
}

extension GuruAppInitializerExt on GuruApp {
  Future _initCommon() async {
    await NetworkUtils.init();
  }

  Future _initRemoteConfig() async {
    await RemoteConfigManager.instance.fetchAndActivate();
    final cdnConfig = RemoteConfigManager.instance.getCdnConfig();
    HttpEx.init(cdnConfig, GuruApp.instance.appSpec.details.storagePrefix);
    refreshRemoteDeployment();
    Settings.get()
        .keepOnScreenDuration
        .set(remoteDeployment.keepScreenOnDuration * DateTimeUtils.minuteInMillis);
  }

  void _initAnalytics() {
    GuruAnalytics.instance.init();
  }

  void _initRemoteMessaging() async {
    RemoteMessagingManager.instance.init();
  }

  void _initDxLink() {
    Future.delayed(const Duration(seconds: 2), () {
      DxLinkManager.instance.init();
    });
  }

  void _initAds() async {
    try {
      await AccountDataStore.instance.observableSaasUser
          .firstWhere((saasUser) => saasUser?.isValid == true)
          .timeout(const Duration(seconds: 3));
    } catch (error, stacktrace) {
      Log.w("wait account error! $error", stackTrace: stacktrace);
    } finally {
      await AdsManager.instance.initialize(saasUser: AccountDataStore.instance.user);
    }
  }

  Future _initFinancial() async {
    ManifestManager.instance.addBuilders(GuruApp.instance.productProfile.manifestBuilders);

    FinancialManager.instance.init();
  }

  Future _initAccount() async {
    await AccountManager.instance.init();
  }
}

extension GuruAppFinancialExt on GuruApp {
  ProductId defineProductId(String sku, int attr, TransactionMethod method) {
    return productProfile.define(sku, attr, method);
  }

  ProductId? findProductId({String? sku, int? attr}) {
    return productProfile.find(sku: sku, attr: attr);
  }

  Set<ProductId> offerProductIds(ProductId productId) {
    return productProfile.offerProductIds(productId);
  }
}

extension GuruRemoteConfigExt on GuruApp {
  String getDefaultRemoteConfig(String key, {String defaultValue = ""}) {
    return defaultRemoteConfig[key] ?? defaultValue;
  }
}
