package jesse.keeblarcraft.BankMgr;

import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Map.Entry;

import jesse.keeblarcraft.BankMgr.BankAccountType.ACCOUNT_TYPE;
import jesse.keeblarcraft.ConfigMgr.ConfigManager;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;

// The bank manager takes care of routing any and all transactions throughout the server.
// It is a singleton object that is active throughout the mods lifetime and will cache players accounts
// when they log in to avoid constant look-ups through JSON.
public final class BankManager {
    private static BankManager static_inst;

    public static BankManager GetInstance() {
        if (static_inst == null) {
            static_inst = new BankManager();
        }
        return static_inst;
    }

    private class PlayerBankConfig {
        List<String> activeBanks = new ArrayList<String>(); // List of all banks a player has accounts in
        String defaultSelectedBank = "";
        String defaultSelectedAccount = "";
    }

    // Key = player uuid
    // Val = player config
    HashMap<String, PlayerBankConfig> playerConfigs = new HashMap<String, PlayerBankConfig>(); // Stores global detail information for bank mgr to use

    private static Integer KEEBLARCRAFT_SERVER_BANK_ID = 1000; // Server global bank (default bank on server)
    ConfigManager config = new ConfigManager();

    // KEY = Bank routing number
    // Val = Bank object
    private HashMap<Integer, IndividualBank> banks = new HashMap<Integer, IndividualBank>();

    // KEY = Bank name
    // Val = Bank routing number
    private HashMap<String, Integer> bankNameFastMap = new HashMap<String, Integer>();

    public BankManager() {
        if (playerConfigs == null) {
            playerConfigs = new HashMap<String, PlayerBankConfig>();
        }
    }

    // TODO: THIS NEEDS TO READ IN FROM A FILE TO STOP NUKING BANKS ON REBOOT
    /////////////////////////////////////////////////////////////////////////////
    /// @fn         InitializeBanks
    ///
    /// @brief      Initializes all the banks on the server at construction time
    /////////////////////////////////////////////////////////////////////////////
    public void InitializeBanks() {
        banks.put(KEEBLARCRAFT_SERVER_BANK_ID, new IndividualBank(Integer.toString(KEEBLARCRAFT_SERVER_BANK_ID), "KeeblarcraftGlobal"));

        // Initialize fast map
        for (Entry<Integer, IndividualBank> bank : banks.entrySet()) {
            bankNameFastMap.put(bank.getValue().GetBankName(), bank.getValue().GetRoutingNumber());
        }
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         GetAllBankNames
    ///
    /// @return     List of all the banks that exist on a server
    /////////////////////////////////////////////////////////////////////////////
    public List<String> GetAllBankNames() {
        List<String> names = new ArrayList<String>();

        // Iterate through all banks in the list to get their names
        for (Entry<Integer, IndividualBank> bank : banks.entrySet()) {
            names.add(bank.getValue().GetBankName());
        }
        return names;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         GetBankByRoutingNumber
    ///
    /// @return     The IndividualBank object by routing number if the bank exists
    /////////////////////////////////////////////////////////////////////////////
    public IndividualBank GetBankByRoutingNumber(Integer number) {
        IndividualBank bank = null;
        if (banks.containsKey(number)) {
            bank = banks.get(number);
        }
        return bank;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         GetBankByName
    ///
    /// @return     The Individualbank object by name if the bank exists
    /////////////////////////////////////////////////////////////////////////////
    public IndividualBank GetBankByName(String name) {
        IndividualBank bank = null;
        System.out.println("GetBankByName called with value " + name);
        if (bankNameFastMap.containsKey(name)) {
            System.out.println("Value of bank with name is " + bankNameFastMap.get(name));
            System.out.println("Banks map size is " + banks.size());
            bank = banks.get(bankNameFastMap.get(name));
        }

        System.out.println("Returning bank information");
        return bank;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         ChangeDefaultPlayerAccount
    ///
    /// @param[in]  player Player object to change default accounts of
    ///
    /// @param[in]  The new default account global account identifier
    ///
    /// @return     Changes the players default account at a selected bank
    /////////////////////////////////////////////////////////////////////////////
    public void ChangeDefaultPlayerAccount(ServerPlayerEntity player, String newDefaultAccount) {
        String bankName = AccountNumberGenerator.GetFinancialSymbolFromId(newDefaultAccount);

        // Verify bank exists first
        System.out.println(bankNameFastMap);

        for(Entry<String, Integer> entry : bankNameFastMap.entrySet()) {
            System.out.println("KEY: " + entry.getKey());
        }

        if (bankNameFastMap.containsKey(bankName)) {
            Integer routNum = bankNameFastMap.get(bankName);
            IndividualBank bank = banks.get(routNum);

            // Verify this person has access to this account
            if(bank.IsAccountHolder(newDefaultAccount, player.getUuidAsString())) {
                // Finally update config to this account since checks pass
                playerConfigs.get(player.getUuidAsString()).defaultSelectedAccount = newDefaultAccount;
            }
        } else {
            player.sendMessage(Text.of("Could not change default selected account. Bank does not exist!"));
        }
    }

    // This guarentees a player config exists; and so can be called from many areas. Does not erase pre-existing config
    public boolean EnsurePlayerConfigExists(String uuid) {
        if (!playerConfigs.containsKey(uuid)) {
            playerConfigs.put(uuid, new PlayerBankConfig());
        }
        return true;
    }

    // Allows the player to select a bank
    public void ChangeDefaultSelectedBank(ServerPlayerEntity player, String bankName) {
        if (bankNameFastMap.containsKey(bankName) && EnsurePlayerConfigExists(player.getUuidAsString())) {
            playerConfigs.get(player.getUuidAsString()).defaultSelectedBank = bankName;
            player.sendMessage(Text.of("You have successfully selected the following financial institution: " + bankName));
            player.sendMessage(Text.of("Please be aware the context of commands following this will likely be done under this financial institution"));
        } else {
            player.sendMessage(Text.of("That bank does not exist."));
        }
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         AdminChangeFunds
    ///
    /// @param[in]  initiator The player object who is initiating this call
    ///
    /// @param[in]  accountId The account to change funds of
    ///
    /// @param[in]  amount The amount to change funds of in changeType
    ///
    /// @param[in]  changeType The type of funds change being initiated
    ///
    /// @param[in]  optionalReason The optional reason of changing funds
    ///
    /// @brief      Command manager to initiate a funds change from an admins
    ///             perspective (safe guards dropped). Valid changeTypes are
    ///             found inside the switch-case statement in the below function
    /////////////////////////////////////////////////////////////////////////////
    public void AdminChangeFunds(ServerPlayerEntity initiator, String accountId, Integer amount, String changeType, String optionalReason) {
        // Check to make sure account id exists
        Integer routingNum = AccountNumberGenerator.GetRoutingNumberFromId(accountId);
        IndividualBank bankFromRout = GetBankByRoutingNumber(routingNum);

        System.out.println("Is bank null? " + (bankFromRout == null ? "YES" : "NO"));
        System.out.println("Bank specified: " + bankFromRout);
        System.out.println("Routing number: " + routingNum);
        // Verify bank exists
        if (bankFromRout != null) {
            // Verify account exists
            System.out.println("accountNumber is " + accountId);
            String accountNumber = AccountNumberGenerator.GetAccountNumberFromId(accountId);
            System.out.println("changeType is " + changeType);

            switch (changeType) {
                case "add":
                    bankFromRout.AddMoneyToAccount(accountNumber, amount);
                    break;
                case "subtract":
                    bankFromRout.SubtractMoneyFromAccount(accountNumber, amount);
                    break;
                case "set":
                    bankFromRout.SetMoneyOnAccount(accountNumber, amount);
                    break;
                default:
                    System.out.println("The operation that was specified by the developer does not exist. Valid operations are add/subtract/set");
                    break;
            }
        } else {
            initiator.sendMessage(Text.of("That bank does not exist!"));
        }
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         InitiateBankFundsTransfer
    ///
    /// @param[in]  fromPlayer is the player funds are coming out of
    ///
    /// @param[in]  toAccount is the account the funds are going to
    ///
    /// @param[in]  amount is the amount of money coming from the player
    ///
    /// @brief      Initiate a funds transfer between accounts or banks
    /////////////////////////////////////////////////////////////////////////////
    public void InitiateBankFundsTransfer(ServerPlayerEntity fromPlayer, String toAccount, Integer amount, String reason) {
        // Get player default selection
        String fromAccount = playerConfigs.get(fromPlayer.getUuidAsString()).defaultSelectedAccount;

        String fromAccountSymbol = AccountNumberGenerator.GetFinancialSymbolFromId(fromAccount);
        String toAccountSymbol = AccountNumberGenerator.GetFinancialSymbolFromId(toAccount);

        System.out.println("InitiateBankFundsTransfer: FROM_ACCOUNT, FROM_ACCOUNT_SYMBOL, TO_ACCOUNT_SYMBOL: " + fromAccount + ", " + fromAccountSymbol + ", " + toAccountSymbol);

        Integer destRoutingNumber = bankNameFastMap.get(toAccountSymbol);
        Integer fromRoutingNumber = bankNameFastMap.get(fromAccountSymbol);
        IndividualBank destBank = banks.get(destRoutingNumber);
        IndividualBank fromBank = banks.get(fromRoutingNumber);

        // Verify banks exist
        if (destBank != null && fromBank != null) {
            if (fromBank.IsValidWithdrawal(amount, fromAccount)) {
                fromBank.SubtractMoneyFromAccount(fromAccount, amount);
                destBank.AddMoneyToAccount(toAccount, amount);

                fromPlayer.sendMessage(Text.of("[" + fromAccountSymbol + "]: Your wire has processed."));
            } else {
                fromPlayer.sendMessage(Text.of("[" + fromAccountSymbol + "]: You are not allowed to make this withdrawal."));
            }
        } else {
            fromPlayer.sendMessage(Text.of("Something went wrong! Either your bank or their bank does not exist. You shouldn't get this error!"));
        }
    }

    public void InitiateBankAccountClosure(String bankIdentifier, ServerPlayerEntity player, String bankAccountId) {

    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         GetDefaultSelectedAccount
    ///
    /// @param[in]  playerUuid is the player to get their default account
    ///
    /// @param[in]  bankIdentifier is the bank the default account is at
    ///
    /// @brief      Gets a players default account
    ///
    /// @return     The global account identifier of the default selected account
    /////////////////////////////////////////////////////////////////////////////
    public String GetDefaultSelectedAccount(String playerUuid, String bankIdentifier) {
        String account = "";

        if (playerConfigs.containsKey(playerUuid)) {
            account = playerConfigs.get(playerUuid).defaultSelectedAccount;
        }

        return account;
    }

    public String GetPlayerSelectedBank(String playerUuid) {
        String bank = "";
        if (playerConfigs.containsKey(playerUuid)) {
            bank = playerConfigs.get(playerUuid).defaultSelectedBank;
        }

        return bank;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         InitiateBankAccountCreation
    ///
    /// @param[in]  bankIdentifier is the bank routing number
    ///
    /// @param[in]  player is the player object trying to create account
    ///
    /// @paran[in]  accountType Is the type of account the player wants to make
    ///
    /// @brief      Initiates a bank account creation with a bank
    /////////////////////////////////////////////////////////////////////////////
    public void InitiateBankAccountCreation(String bankIdentifier, ServerPlayerEntity player, ACCOUNT_TYPE accountType) {
        Boolean success = false;
        IndividualBank bank = GetBankByName(bankIdentifier);

        if (bank != null) {
            success = bank.CreateAccount(player.getUuidAsString(), player.getEntityName(), accountType);
        }

        if (success) {
            player.sendMessage(Text.of("The banking operation was successful and your banking information has been updated"));
        } else {
            player.sendMessage(Text.of("The banking operating FAILED. Make sure you selected this bank before creating it!"));
        }
    }
}
