using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace CurrencyManager
{
    /// <summary>
    /// Manages the player's wallet, including currency balances and transaction history.
    /// </summary>
    public class Wallet : MonoBehaviour
    {
        private static Wallet _instance;

        public event Action<Transaction> OnTransactionAdded;

        /// <summary>
        /// Represents a currency balance, with a currency type and an amount.
        /// </summary>
        [Serializable]
        public class CurrencyBalance
        {
            public Currency currency;
            public float amount;
        }

        /// <summary>
        /// Represents a currency and an amount to add or subtract from the wallet.
        /// </summary>
        [Serializable]
        public struct CurrencyAmount
        {
            public Currency currency;
            public float amount;
        }

        /// <summary>
        /// Represents a transaction, including the type of transaction, the currencies and amounts involved,
        /// and the date of the transaction.
        /// </summary>
        [Serializable]
        public class Transaction
        {
            public enum TransactionType { Add, Subtract, Exchange };
            public TransactionType type;
            public List<CurrencyAmount> fromCurrency;
            public List<CurrencyAmount> toCurrency;
            public DateTime date;
        }

        /// <summary>
        /// Represents the wallet's data for serialization and deserialization.
        /// </summary>
        [Serializable]
        public class WalletData
        {
            public List<CurrencyBalance> currencyBalances;
            public List<Transaction> transactionHistory;
        }

        [SerializeField] private List<CurrencyBalance> currencyBalances;
        [SerializeField] private List<Transaction> transactionHistory;

        /// <summary>
        /// Gets the transaction history.
        /// </summary>
        public List<Transaction> TransactionHistory
        {
            get { return transactionHistory; }
        }

        /// <summary>
        /// Gets the singleton instance of the wallet.
        /// </summary>
        public static Wallet Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = FindObjectOfType<Wallet>();

                    if (_instance == null)
                    {
                        GameObject obj = new GameObject();
                        obj.name = typeof(Wallet).Name;
                        _instance = obj.AddComponent<Wallet>();
                    }
                }
                return _instance;
            }
        }

        private void Awake()
        {
            if (currencyBalances == null)
                currencyBalances = new List<CurrencyBalance>();

            if (transactionHistory == null)
                transactionHistory = new List<Transaction>();
        }

        /// <summary>
        /// This method adds a specified amount of a given currency to the wallet.
        /// If the currency does not already exist in the wallet, a new CurrencyBalance object is created.
        /// If saveTransaction is true, the transaction is recorded in the transaction history.
        /// </summary>
        /// <param name="currency">The currency to be added to the wallet.</param>
        /// <param name="amount">The amount of the currency to be added.</param>
        /// <param name="saveTransaction">Whether or not to save the transaction in the transaction history.</param>
        public void AddCurrency(Currency currency, float amount, bool saveTransaction = true)
        {
            CurrencyBalance balance = GetCurrencyBalanceObject(currency);

            if (balance == null)
            {
                balance = new CurrencyBalance { currency = currency, amount = 0 };
                currencyBalances.Add(balance);
            }

            balance.amount += amount;

            if (saveTransaction)
            {
                Transaction transaction = new Transaction
                {
                    type = Transaction.TransactionType.Add,
                    //fromCurrency = new List<CurrencyAmount> { new CurrencyAmount { currency = null, amount = 0 } },
                    toCurrency = new List<CurrencyAmount> { new CurrencyAmount { currency = currency, amount = amount } },
                    date = DateTime.Now
                };
                AddTransaction(transaction);
            }
        }

        /// <summary>
        /// This method adds a list of currency amounts to their respective balances in the wallet.
        /// </summary>
        /// <param name="currencyAmounts">The list of CurrencyAmount objects representing the currencies and amounts to be added.</param>
        public void AddCurrencies(List<CurrencyAmount> currencyAmounts)
        {
            foreach (CurrencyAmount currencyAmount in currencyAmounts)
                AddCurrency(currencyAmount.currency, currencyAmount.amount);
        }

        /// <summary>
        /// This method subtracts a given amount from the balance of a specified currency in the wallet.
        /// If there is sufficient balance for the currency, the transaction is recorded in the transaction history.
        /// If there is insufficient balance for the currency, a warning message is logged and the transaction is not recorded.
        /// </summary>
        /// <param name="currency">The Currency object representing the currency to be subtracted from.</param>
        /// <param name="amount">The amount to be subtracted.</param>
        /// <param name="saveTransaction">A boolean indicating whether the transaction should be recorded in the transaction history.</param>
        /// <returns>A boolean indicating whether the transaction was successful (true) or not due to insufficient funds (false).</returns>
        public bool SubtractCurrency(Currency currency, float amount, bool saveTransaction = true)
        {
            CurrencyBalance balance = GetCurrencyBalanceObject(currency);

            if (balance != null && balance.amount >= amount)
            {
                balance.amount -= amount;

                if (saveTransaction)
                {
                    Transaction transaction = new Transaction
                    {
                        type = Transaction.TransactionType.Subtract,
                        toCurrency = new List<CurrencyAmount> { new CurrencyAmount { currency = currency, amount = amount } },
                        date = DateTime.Now
                    };
                    AddTransaction(transaction);
                }

                return true;
            }
            else
            {
                Debug.LogWarning("Insufficient balance");
                return false;
            }
        }

        /// <summary>
        /// This method subtracts a list of currency amounts from their respective balances in the wallet.
        /// If there is sufficient balance for each currency, the transaction is recorded in the transaction history
        /// and returns true. If there is insufficient balance for any currency, a warning message is logged, 
        /// the transaction is not recorded, and returns false.
        /// </summary>
        /// <param name="currencyAmounts">The list of CurrencyAmount objects representing the currencies and amounts to be subtracted.</param>
        public bool SubtractCurrencies(List<CurrencyAmount> currencyAmounts)
        {
            var insufficientCurrencies = GetInsufficientCurrencies(currencyAmounts);

            if (insufficientCurrencies.Count == 0)
            {
                var toCurrencies = new List<CurrencyAmount>();
                foreach (CurrencyAmount currencyAmount in currencyAmounts)
                {
                    SubtractCurrency(currencyAmount.currency, currencyAmount.amount, false);

                    toCurrencies.Add(new CurrencyAmount { currency = currencyAmount.currency, amount = currencyAmount.amount });
                }

                Transaction transaction = new Transaction
                {
                    type = Transaction.TransactionType.Subtract,
                    toCurrency = toCurrencies,
                    date = DateTime.Now
                };
                AddTransaction(transaction);

                return true;
            }
            else
            {
                string insufficientCurrencyNames = string.Join(", ", insufficientCurrencies.Select(c => c.name));
                Debug.LogWarning($"Insufficient balance for the following currencies: {insufficientCurrencyNames}");
                return false;
            }
        }


        /// <summary>
        /// This method checks if there are insufficient balances for a list of currency amounts,
        /// and returns a list of the currencies with insufficient balances.
        /// </summary>
        /// <param name="currencyAmounts">The list of currency amounts to check</param>
        /// <returns>A list of currencies with insufficient balances</returns>
        public List<Currency> GetInsufficientCurrencies(List<CurrencyAmount> currencyAmounts)
        {
            return currencyAmounts
                .Where(ca => GetCurrencyBalance(ca.currency) < ca.amount)
                .Select(ca => ca.currency)
                .ToList();
        }

        /// <summary>
        /// Gets the balance of a specific currency in the wallet.
        /// </summary>
        /// <param name="currency">The currency to get the balance of.</param>
        /// <returns>The balance of the specified currency.</returns>
        public float GetCurrencyBalance(Currency currency)
        {
            CurrencyBalance balance = GetCurrencyBalanceObject(currency);
            return balance != null ? balance.amount : 0;
        }

        /// <summary>
        /// Gets the currency balances for the specified list of currencies.
        /// </summary>
        /// <param name="currencies">The list of currencies to retrieve balances for.</param>
        /// <returns>A list of CurrencyAmount objects representing the balances of the specified currencies in the wallet.</returns>
        public List<CurrencyAmount> GetCurrencyBalances(List<Currency> currencies)
        {
            List<CurrencyAmount> currencyAmounts = new List<CurrencyAmount>();

            foreach (Currency currency in currencies)
            {
                float balance = GetCurrencyBalance(currency);
                currencyAmounts.Add(new CurrencyAmount { currency = currency, amount = balance });
            }

            return currencyAmounts;
        }

        /// <summary>
        /// Gets a list of all currency balances in the wallet.
        /// </summary>
        /// <returns>A list of CurrencyAmount objects representing the balances of all currencies in the wallet.</returns>
        public List<CurrencyAmount> GetAllBalances()
        {
            List<CurrencyAmount> currencyAmounts = new List<CurrencyAmount>();

            foreach (CurrencyBalance balance in currencyBalances)
                currencyAmounts.Add(new CurrencyAmount { currency = balance.currency, amount = balance.amount });

            return currencyAmounts;
        }

        /// <summary>
        /// Exchanges a certain amount of currency from one type to another, if the balance is sufficient.
        /// </summary>
        /// <param name="fromCurrency">The currency to exchange from.</param>
        /// <param name="toCurrency">The currency to exchange to.</param>
        /// <param name="amount">The amount of currency to exchange.</param>
        /// <returns>True if the exchange is successful, false otherwise.</returns>
        public bool ExchangeCurrency(Currency fromCurrency, Currency toCurrency, float amount)
        {
            if (GetCurrencyBalance(fromCurrency) >= amount)
            {
                float amountInBaseCurrency = amount * fromCurrency.ExchangeRateToBaseCurrency;
                float amountToCurrency = amountInBaseCurrency / toCurrency.ExchangeRateToBaseCurrency;

                SubtractCurrency(fromCurrency, amount, false);
                AddCurrency(toCurrency, amountToCurrency, false);

                Transaction transaction = new Transaction
                {
                    type = Transaction.TransactionType.Exchange,
                    fromCurrency = new List<CurrencyAmount> { new CurrencyAmount { currency = fromCurrency, amount = amount } },
                    toCurrency = new List<CurrencyAmount> { new CurrencyAmount { currency = toCurrency, amount = amountToCurrency } },
                    date = DateTime.Now
                };
                AddTransaction(transaction);

                return true;
            }
            else
            {
                Debug.LogWarning("Insufficient balance for currency exchange");
                return false;
            }
        }

        /// <summary>
        /// Returns the JSON string representation of the wallet's currency balances and transaction history data for saving.
        /// </summary>
        public string GetSaveData()
        {
            var walletData = new WalletData
            {
                currencyBalances = currencyBalances,
                transactionHistory = transactionHistory
            };
            return JsonUtility.ToJson(walletData);
        }

        /// <summary>
        /// Sets the wallet's currency balances and transaction history based on serialized JSON data.
        /// </summary>
        /// <param name="json">The serialized JSON data representing the wallet's data.</param>
        public void SetSaveData(string json)
        {
            var walletData = JsonUtility.FromJson<WalletData>(json);
            currencyBalances = walletData.currencyBalances;
            transactionHistory = walletData.transactionHistory;
        }

        /// <summary>
        /// Clears the transaction history.
        /// </summary>
        public void ClearTransactionHistory()
        {
            transactionHistory.Clear();
        }

        /// <summary>
        /// Finds and returns the CurrencyBalance object associated with the specified currency.
        /// </summary>
        /// <param name="currency">The currency to search for.</param>
        /// <returns>The CurrencyBalance object associated with the specified currency, or null if it cannot be found.</returns>
        private CurrencyBalance GetCurrencyBalanceObject(Currency currency)
        {
            return currencyBalances.Find(b => b.currency == currency);
        }

        /// <summary>
        /// Adds a transaction to the transaction history list.
        /// </summary>
        /// <param name="transaction">The transaction to add.</param>
        private void AddTransaction(Transaction transaction)
        {
            transactionHistory.Add(transaction);

            OnTransactionAdded?.Invoke(transaction);
        }
    }
}