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

import 'package:flutter/foundation.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/subscription.keys.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/purchasable_product.model.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:url_launcher/url_launcher.dart';

// Gives the option to override in tests.
class IAPConnection {
  static InAppPurchase? _instance;
  static set instance(InAppPurchase value) {
    _instance = value;
  }

  static InAppPurchase get instance {
    _instance ??= InAppPurchase.instance;
    return _instance!;
  }
}

enum StoreState { loading, available, notAvailable }

Timer? globalForceIpaCheck;

class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
  PurchasesProvider() {
    _subscription = iapConnection.purchaseStream.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );

    _planSub = apiService.onPlanUpdated.listen(updatePlan);
    _connSub = apiService.onConnectionStateUpdated.listen((_) async {
      try {
        if (userService.isUserCreated) {
          updatePlan(planFromString(userService.currentUser.subscriptionPlan));
        }
      } catch (e) {
        Log.error(e);
      }
    });

    if (userService.isUserCreated) {
      updatePlan(planFromString(userService.currentUser.subscriptionPlan));
    }

    loadPurchases();
    Log.info('PurchasesProvider: constructor finished');
  }

  SubscriptionPlan plan = SubscriptionPlan.Free;
  StoreState storeState = StoreState.loading;
  List<PurchasableProduct> products = [];

  late StreamSubscription<List<PurchaseDetails>> _subscription;
  final InAppPurchase iapConnection = IAPConnection.instance;

  late StreamSubscription<SubscriptionPlan> _planSub;
  late StreamSubscription<bool> _connSub;
  bool _userTriggeredBuyButton = false;

  void updatePlan(SubscriptionPlan newPlan) {
    plan = newPlan;
    notifyListeners();
  }

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    Log.info('PurchasesProvider: IAP available: $available');
    if (!available) {
      storeState = StoreState.notAvailable;
      Log.warn('Store is not available');
      notifyListeners();
      return;
    }
    const ids = <String>{
      SubscriptionKeys.proMonthly,
      SubscriptionKeys.proYearly,
      SubscriptionKeys.familyYearly,
    };
    final response = await iapConnection.queryProductDetails(ids);
    if (response.notFoundIDs.isNotEmpty) {
      Log.warn(response.notFoundIDs);
    }
    products = response.productDetails.map(PurchasableProduct.new).toList();
    if (products.isEmpty) {
      Log.warn('Could not load any products from the store!');
    }
    storeState = StoreState.available;
    notifyListeners();

    try {
      if (userService.isUserCreated &&
          isPayingUser(
            planFromString(userService.currentUser.subscriptionPlan),
          )) {
        globalForceIpaCheck = Timer(const Duration(seconds: 5), () async {
          Log.info(
            'Force Ipa check was not stopped. Requesting forced check...',
          );
          await apiService.forceIpaCheck();
        });
      }

      await iapConnection.restorePurchases();
    } catch (e) {
      Log.error(e);
    }
  }

  Future<void> buy(PurchasableProduct product) async {
    final purchaseParam = PurchaseParam(productDetails: product.productDetails);
    switch (product.id) {
      // case storeKeyConsumable:
      //   await iapConnection.buyConsumable(purchaseParam: purchaseParam);
      case SubscriptionKeys.proMonthly:
      case SubscriptionKeys.proYearly:
      case SubscriptionKeys.familyYearly:
        _userTriggeredBuyButton = true;
        Log.info('User wants to buy ${product.id}');

        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
      default:
        throw ArgumentError.value(
          product.productDetails,
          '${product.id} is not a known product',
        );
    }
  }

  Future<void> _onPurchaseUpdate(
    List<PurchaseDetails> purchaseDetailsList,
  ) async {
    for (final purchaseDetails in purchaseDetailsList) {
      await _handlePurchase(purchaseDetails);
    }
    notifyListeners();
  }

  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
    Log.info('verifying _verifyPurchase');
    if (Platform.isIOS) {
      try {
        var b64Data = purchaseDetails.verificationData.serverVerificationData
            .split('.')[1];

        final paddingNeeded = (4 - (b64Data.length % 4)) % 4;
        b64Data += '=' * paddingNeeded;

        final jsonData = base64Decode(b64Data);
        final data = jsonDecode(utf8.decode(jsonData)) as Map<String, dynamic>;
        final expiresDate = data['expiresDate'] as int;
        final dt = DateTime.fromMillisecondsSinceEpoch(
          expiresDate,
          isUtc: true,
        );
        if (dt.isBefore(DateTime.now())) {
          Log.warn('ExpiresDate is in the past: $dt');
          if (_userTriggeredBuyButton && Platform.isIOS) {
            await launchUrl(
              Uri.parse('https://apps.apple.com/account/subscriptions'),
              mode: LaunchMode.externalApplication,
            );
          }
          return false;
        }
      } catch (e) {
        Log.error(e);
      }
    }
    if (kDebugMode) {
      Log.info(purchaseDetails.productID);
      Log.info(purchaseDetails.verificationData.source);
    }
    final res = await apiService.ipaPurchase(
      purchaseDetails.productID,
      purchaseDetails.verificationData.source,
      purchaseDetails.verificationData.serverVerificationData,
    );
    // plan is updated in the apiProvider, as the server updates its states and responses with
    // an ok authenticated which is processed in the apiProvider...
    if (res.isSuccess) {
      if (Platform.isAndroid) {
        await UserService.update((u) {
          u.subscriptionPlanIdStore = purchaseDetails.productID;
        });
      }
    }
    if (res.isError) {
      if (res.error == ErrorCode.IPAPaymentExpired &&
          _userTriggeredBuyButton &&
          Platform.isIOS) {
        await launchUrl(
          Uri.parse('https://apps.apple.com/account/subscriptions'),
          mode: LaunchMode.externalApplication,
        );
      }
    }
    return res.isSuccess;
  }

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
    Log.info(
      '_handlePurchase: ${purchaseDetails.productID}, ${purchaseDetails.status}',
    );
    if (purchaseDetails.status == PurchaseStatus.purchased ||
        (purchaseDetails.status == PurchaseStatus.restored &&
            _userTriggeredBuyButton)) {
      await _verifyPurchase(purchaseDetails);
    }
    if (purchaseDetails.status == PurchaseStatus.restored &&
        purchaseDetails.error == null) {
      globalForceIpaCheck?.cancel();

      final currentPlan = userService.currentUser.subscriptionPlan;

      if (currentPlan != SubscriptionPlan.Family.name &&
          currentPlan != SubscriptionPlan.Pro.name) {
        for (var i = 0; i < 100; i++) {
          if (apiService.isAuthenticated) {
            Log.info(
              'current user does not have a sub: ${purchaseDetails.productID}',
            );
            await _verifyPurchase(purchaseDetails);
            break;
          }
          await Future.delayed(const Duration(seconds: 1));
        }
      }
    }

    if (purchaseDetails.status == PurchaseStatus.error) {
      await iapConnection.restorePurchases();
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await iapConnection.completePurchase(purchaseDetails);
    }
  }

  @override
  void dispose() {
    _planSub.cancel();
    _connSub.cancel();
    _subscription.cancel();
    super.dispose();
  }

  void _updateStreamOnDone() {
    _subscription.cancel();
  }

  void _updateStreamOnError(dynamic error) {
    // Handle error here
  }
}
