/*
 * Copyright (c)2009-2025 DemiVision, LLC. All Rights Reserved. The information
 * herein is the CONFIDENTIAL and PROPRIETARY information of DemiVision, LLC.
 */

using Plugin.InAppBilling;

using DXLib.UI.Alert;

using DXLib.Log;
using DXLib.Data;

namespace DXLib.Billing;

/*
 * Provides cross-platform access to the app store billing engines (iOS AppStore and Android Google Play). Methods are
 * provided for retrieving product lists and for purchasing products. The functionality applies to both normal and
 * subscription products.
 */
public static class DXBilling
{
	/* Constants */
	private const string PendingKey = "pending";

	// Available product types
	public enum ProductType
	{
		Consumable,
		NonConsumable,
		Subscription
	};

	// IAP success code
	public const int Success = 0;

	// Internal IAP error codes
	private enum ErrorCode
	{
		Network = 100,
		Connection,
		Product,
		Purchase,
		Pending,
		Consume
	};

	// Unknown error detail codes
	private enum ErrorUnknown
	{
		NA = 200,
		Error,
		ExceptionGeneral,
		ExceptionOther
	};

	/* Methods */

	// Handles connection and error handling common to all billing methods
	private static async Task<IInAppBilling> Connect()
	{
		// Validate network access
		if ( !DXData.HasConnection() )
		{
			HandleError( ErrorCode.Network );
			return null;
		}

		// Access app store engine
		IInAppBilling store = CrossInAppBilling.Current;

		// Connect to store
		bool connected = await store.ConnectAsync( true );

		// Could not connect
		if ( !connected )
		{
			HandleError( ErrorCode.Connection );
			return null;
		}
		
		return store;
	}

	// Returns list of all products from specified ID list (either normal or subscription)
	public static async Task<List<DXBillingProduct>> GetProducts( string[] productIds, ProductType type )
	{
		IInAppBilling store = null;
		List<DXBillingProduct> list = [];

		try
		{
			// Connect to app store
			store = await Connect();

			// Could not connect
			if ( store == null )
			{
				return null;
			}

			// Retrieve product list
			var products = await store.GetProductInfoAsync( GetItemType( type ), productIds );

			// Could not retrieve
			if ( products == null )
			{
				HandleError( ErrorCode.Product );
				return null;
			}

			// Convert to internal objects
			foreach ( InAppBillingProduct product in products )
			{
				list.Add( new DXBillingProduct( product ) );
			}

			// Error retrieving
			if ( list.Count == 0 )
			{
				HandleError( ErrorCode.Product );
				return null;
			}
			
			list = list.OrderBy( p => p.Name ).ToList();
		}
		// All exception handling
		catch ( Exception ex )
		{
			HandleException( ex );
			return null;
		}
		// Must disconnect cleanly
		finally
		{
			if ( store != null )
			{
				await store.DisconnectAsync();
			}
		}

		// Success
		return list;
	}

	// Asynchronously purchases specified product (either normal or subscription)
	public static async Task<DXBillingPurchase> Purchase( string productId, ProductType type )
	{
		IInAppBilling store = null;
		DXBillingPurchase result = null;

		try
		{
			// Connect to app store
			store = await Connect();

			// (store == null) handled in Connect()
			if ( store != null )
			{
				// Request purchase
				InAppBillingPurchase purchase = await store.PurchaseAsync( productId, GetItemType( type ) );

				// Validate
				if ( (purchase == null) || (purchase.ProductId != productId) )
				{
					HandleError( ErrorCode.Purchase );
					return null;
				}

				// Payment can be pending, save for later validation
				if ( purchase.State == PurchaseState.PaymentPending )
				{
					DXData.WriteCache( PendingKey, purchase );

					HandleError( ErrorCode.Pending );
					return null;
				}
				// Purchases must be acknowledged within 3 days
				else
				{
					// Consume and acknowledge
					if ( type == ProductType.Consumable )
					{
						bool consumed = await store.ConsumePurchaseAsync( productId, purchase.PurchaseToken );

						if ( !consumed )
						{
							HandleError( ErrorCode.Consume );
							return null;
						}
					}
					// Just acknowledge
					else
					{
						await store.FinalizePurchaseAsync( [ purchase.PurchaseToken ] );
					}
				}

				// Convert to internal object
				result = new DXBillingPurchase( purchase );
			}
		}
		// All exception handling
		catch ( Exception ex )
		{
			HandleException( ex );
		}
		// Must disconnect cleanly
		finally
		{
			if ( store != null )
			{
				await store.DisconnectAsync();
			}
		}

		return result;
	}

	// Returns locally cached pending purchase if any
	public static DXBillingPurchase GetPending()
	{
		InAppBillingPurchase pending = DXData.ReadCache<InAppBillingPurchase>( PendingKey );

		return (pending == null) ? null : new DXBillingPurchase( pending );
	}

	// Completes specified pending purchase if applicable
	public static async Task<DXBillingPurchase> CompletePurchase( DXBillingPurchase pending, ProductType type )
	{
		DXBillingPurchase result = null;
		IInAppBilling store = null;

		try
		{
			// Connect to app store
			store = await Connect();

			if ( store != null )
			{
				// Fetch all purchases for user
				var purchases = await store.GetPurchasesAsync( GetItemType( type ) );

				// Find matching purchase
				foreach ( InAppBillingPurchase purchase in purchases )
				{
					if ( purchase.Id == pending.PurchaseId )
					{
						switch ( purchase.State )
						{
							// Still pending
							case PurchaseState.PaymentPending:
							{
								HandleError( ErrorCode.Pending );
								break;
							}
							// Payment complete
							case PurchaseState.Purchased:
							{
								var ack = await store.FinalizePurchaseAsync( [ purchase.PurchaseToken ] );

								// Purchase acknowledged
								if ( ack != null )
								{
									result = new DXBillingPurchase( purchase );
									DXData.DeleteCache( PendingKey );
								}

								break;
							}
						}
					}
				}
			}
		}
		// All exception handling
		catch ( Exception ex )
		{
			HandleException( ex );
		}
		// Must disconnect cleanly
		finally
		{
			if ( store != null )
			{
				await store.DisconnectAsync();
			}
		}

		return result;
	}

	// Returns billing item type for specified subscription/consumable type
	private static ItemType GetItemType( ProductType type )
	{
		return (type == ProductType.Subscription) ? ItemType.Subscription : ItemType.InAppPurchase;
	}

	// Shows error alert for specified internal error code
	private static void HandleError( ErrorCode code )
	{
		string msg;
		ErrorUnknown unknownCode = ErrorUnknown.NA;

		// Map to error message
		switch ( code )
		{
			case ErrorCode.Network: msg = "network"; break;
			case ErrorCode.Connection: msg = "connect"; break;
			case ErrorCode.Product: msg = "product"; break;
			case ErrorCode.Purchase: msg = "purchase"; break;
			case ErrorCode.Pending: msg = "pending"; break;

			default:
			{
				msg = "unknown";
				unknownCode = ErrorUnknown.Error;
				break;
			}
		}

		string resource = $"billing.err.{msg}";

		// Alert user
		DXAlert.ShowErrorCode( "billing.err", resource, (int)unknownCode );

		// Local debug
		DXLog.Exception(  $"billing.err:{msg}", null, (int)unknownCode );
	}

	// Shows error alert for specified billing engine exception
	private static void HandleException( Exception ex )
	{
		string msg;
		ErrorUnknown unknownCode = ErrorUnknown.NA;

		// Purchase exception
		if ( ex is InAppBillingPurchaseException exception )
		{
			switch ( exception.PurchaseError )
			{
				case PurchaseError.ServiceUnavailable:
				{
					msg = "network";
					break;
				}
				case PurchaseError.AppStoreUnavailable:
				case PurchaseError.BillingUnavailable:
				{
					msg = "available";
					break;
				}
				case PurchaseError.ProductRequestFailed:
				case PurchaseError.ItemUnavailable:
				case PurchaseError.InvalidProduct:
				{
					msg = "product";
					break;
				}
				case PurchaseError.PaymentNotAllowed:
				case PurchaseError.PaymentInvalid:
				{
					msg = "payment";
					break;
				}
				case PurchaseError.UserCancelled:
				{
					msg = "cancel";
					break;
				}
				case PurchaseError.DeveloperError:
				case PurchaseError.GeneralError:
				default:
				{
					msg = "unknown";
					unknownCode = ErrorUnknown.ExceptionGeneral;
					break;
				}
			}
		}
		// Generic exception
		else
		{
			msg = "unknown";
			unknownCode = ErrorUnknown.ExceptionOther;
		}

		string resource = $"billing.err.{msg}";

		// Alert user
		DXAlert.ShowErrorCode( "billing.err", resource, (int)unknownCode );

		// Local debug
		DXLog.Exception( $"billing.ex:{msg}", ex, (int)unknownCode );
	}
}

//
