package jesse.keeblarcraft.BankMgr;

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

import static java.util.Map.entry;

import java.io.File;

import jesse.keeblarcraft.ConfigMgr.ConfigManager;
import jesse.keeblarcraft.Keeblarcraft;
import jesse.keeblarcraft.BankMgr.BankAccountType.ACCOUNT_TYPE;

// Contains the information of an individual bank
//
// The bank will keep track of all accounts within its facilities. In addition to accounts, the bank
// maintains its own identifier which is unique and other misc things.
public class IndividualBank {
    private Map<ACCOUNT_TYPE, Integer> ACCOUNT_TYPES = Map.ofEntries(
        entry(ACCOUNT_TYPE.CHECKING, 0),
        entry(ACCOUNT_TYPE.SAVINGS, 1)
    );

    private ConfigManager config = new ConfigManager();
    private Integer routingNumber; // this is the banks unique identifier
    private Integer numberOfAccounts = 0; // Total number of accounts the bank has. This includes only active accounts inside accountsList.
    private Integer maxBankAccounts = 100_000_000; // Making this simple for myself any one type of account has 8 random numbers genereated so 10^8 possible accounts
    private String bankFourLetterIdentifier;
    private String registeredBankName;

    private static String CONFIG_LOCATION = "config/keeblarcraft/bank/";

    // Think FDIC but from the servers account (keeblarcraft insurance corporation)
    // KBIC will ensure an amount of money based on its trustworthiness to a bank and the number of holders it has.
    private Integer kbicInsuredAmount = 0;
    private Boolean kbicInsured = false;

    // bankMoney is the total amount of money the bank possesses itself. The bank itself is personally responsible
    // for backing the amount of money it claims it has and this is the balance that is withdrawn from for credits.
    // A bank can have a sum of money that is less than the total amount of money of all its account holders
    private Integer bankMoney = 0;

    // Key   = ACCOUNT NUMBER
    // Value = ACCOUNT
    private class Accounts {
        // Key = account identifier
        // Val = account object
        private HashMap<String, IndividualAccount> accountsList = new HashMap<String, IndividualAccount>();

        // Key = user uuid
        // Val = List of account identifiers
        private HashMap<String, List<String>> accountsListFromName = new HashMap<String, List<String>>(); // This is a list that just points to a list of account numbers by person. USEFUL
    }

    Accounts accounts;
    private List<String> lockedUsers; // A list of users who are locked out of the bank and are incapable of performing more actions within it

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         IndividualBank
    ///
    /// @param[in]  routingNumber is the routing number this bank is constructed
    ///             with
    ///
    /// @param[in]  nameOfBank Will be the display name of this bank to players
    ///
    /// @brief      Constructor for this bank
    /////////////////////////////////////////////////////////////////////////////
    public IndividualBank(String routingNumber, String nameOfBank) {
        accounts = new Accounts();
        lockedUsers = new ArrayList<String>();
        registeredBankName = nameOfBank.toUpperCase();
        bankFourLetterIdentifier = nameOfBank.substring(0, 4).toLowerCase();
        this.routingNumber = Integer.parseInt(routingNumber);

        System.out.println("CREATING BANK ACCOUNT WITH ROUTING NUMBER " + routingNumber + " AND NAME " + nameOfBank);

        boolean existingFile = false;
        try {
            // Read in the global accounts list
            String accountsListDir = CONFIG_LOCATION + routingNumber.toString() + "/accounts/";
            System.out.println("accountsListDir + bankName is " + accountsListDir + nameOfBank);
            accounts = config.GetJsonObjectFromFile(accountsListDir + nameOfBank, Accounts.class);
            existingFile = true;

            // TODO: REPLACE WITH SQL SERVER. DIRTY ITERATE OVER ALL FILES IN DIRECTORY TO LOAD STRUCTURE
            File dir = new File(accountsListDir);
            File[] allFiles = dir.listFiles();
            if (accounts != null && allFiles != null) {
                for (File file : allFiles ) {
                    // First grab file identifier as KEY
                    String accountIdentifier = file.getName();
                    String accountFromFile = accountsListDir + "/" + accountIdentifier;
                    System.out.println("accountIdentifier found in file is " + accountIdentifier);
                    System.out.println("account identifier with dir path is " + accountFromFile);
                    accounts.accountsList.put(accountIdentifier, config.GetJsonObjectFromFile(accountFromFile, IndividualAccount.class));
                }
            }
        } catch (Exception e) {
            System.out.println("The try-catch in IndividualBank.java failed to complete. Printing stack trace");
            // e.printStackTrace();
            // Falling into this catch just means the file needs to be made
        }

        if (!existingFile)
        {
            try {
                // We assume the bank dir is created by server. Create this banks dir
                // config.CreateDirectory("bank/" + routingNumber);
                // Create this banks initial accounts dir
                config.CreateDirectory(CONFIG_LOCATION + routingNumber + "/accounts");

                // Flash initial account configuration file for this bank
                FlashConfig("accounts");
            } catch (Exception e) {
                Keeblarcraft.LOGGER.error("Could not write to file in IndividualBank");
            }
        }

        // A modified config reader is needed here for when each IndividualAccount is read in - the name is taken from that and is attached to the
        // 'accountsListFromName' structure. This makes it no worse than O(n) to fill these two structures in.
        // NOTE: This is an *EXPENSIVE* operation! Future us might need to update this. Also note a method is needed for everytime a player opens a new account
        // or gets put on one to update the map every time
        if (accounts != null) {
            for (Entry<String, IndividualAccount> account : accounts.accountsList.entrySet()) {
                // We must loop over the string of holders for each account as well to make the flattened accountsListFromName map
                List<String> accountHolders = account.getValue().GetAccountHolders();

                // Match each user to the secondary map & add to list-value if not existing
                for (Integer holderIndex = 0; holderIndex < accountHolders.size(); holderIndex++) {
                    if (accounts.accountsListFromName.containsKey(accountHolders.get(holderIndex))) {
                        // Case 1: User exists, update map entry
                        accounts.accountsListFromName.get(accountHolders.get(holderIndex)).add(account.getKey()); // Add a new account id to this person in the new flat map
                    } else {
                        // Case 2: User does not already exist; add a new map entry
                        accounts.accountsListFromName.put(accountHolders.get(holderIndex), List.of(account.getKey())); // Store name as key, and new List with the value of ACCOUNT #
                    }
                }
            }
            numberOfAccounts = accounts.accountsList.size();
        } else {
            accounts = new Accounts();
            numberOfAccounts = 0;
        }
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         GetBankName
    ///
    /// @return     Returns this banks name
    /////////////////////////////////////////////////////////////////////////////
    public String GetBankName() {
        return registeredBankName;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         GetAccountsOfUser
    ///
    /// @param[in]  uuid is the players UUID to check
    ///
    /// @brief      Gets all the accounts a user has at this bank by UUID
    ///
    /// @return     List of all bank accounts. List will be EMPTY if no accounts
    /////////////////////////////////////////////////////////////////////////////
    public List<IndividualAccount> GetAccountsOfUser(String uuid) {
        List<IndividualAccount> accountsFromUser = new ArrayList<IndividualAccount>();
        List<String> listOfAccounts = accounts.accountsListFromName.get(uuid);

        if (listOfAccounts != null && listOfAccounts.size() > 0) {
            for (int i = 0; i < listOfAccounts.size(); i++) {
                accountsFromUser.add(accounts.accountsList.get(listOfAccounts.get(i)));
            }
        }
        return accountsFromUser;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         GetBankBalance
    ///
    /// @return     The banks balance
    /////////////////////////////////////////////////////////////////////////////
    public Integer GetBankBalance() {
        return bankMoney;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         AddBankBalance
    ///
    /// @param[in]  amount Amount to add to the banks balance
    ///
    /// @brief      Adds to the banks balance
    /////////////////////////////////////////////////////////////////////////////
    public void AddBankBalance(Integer amount) {
        bankMoney += amount;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         SubtractBankBalance
    ///
    /// @param[in]  amount Amount to subtract from banks balance
    ///
    /// @brief      Subtracts from the banks balance
    /////////////////////////////////////////////////////////////////////////////
    public void SubtractBankBalance(Integer amount) {
        bankMoney -= amount;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         SetBankBalance
    ///
    /// @param[in]  amount Amount to give the bank
    ///
    /// @brief      Set the banks balance
    /////////////////////////////////////////////////////////////////////////////
    public void SetBankBalance(Integer amount) {
        bankMoney = amount;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         IsBankInsured
    ///
    /// @return     True if bank is insured, false if not
    /////////////////////////////////////////////////////////////////////////////
    public Boolean IsBankInsured() {
        return kbicInsured;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         InsuranceAmount
    ///
    /// @brief      Get the insurance amount at this bank
    ///
    /// @return     Insurance amount
    /////////////////////////////////////////////////////////////////////////////
    public Integer InsuranceAmount() {
        Integer insuredAmnt = 0;
        if (kbicInsured) {
            insuredAmnt = kbicInsuredAmount;
        } else {
            insuredAmnt = 0;
        }
        return insuredAmnt;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         UpdateBankAccounts
    ///
    /// @brief      Flashes bank account information to disk; updates volatile
    ///             memory of banking information for this bank
    /////////////////////////////////////////////////////////////////////////////
    public void UpdateBankAccounts(String newHolderName, String newHolderUuid, String accountIdentifier, IndividualAccount newAccountOnly) {
        // Update the fast-access map first
        System.out.println("UpdateBankAccounts called with information " + newHolderName + " " + newHolderUuid + " " + accountIdentifier);
        if (accounts.accountsListFromName.containsKey(newHolderUuid)) {
            // Check if user is already in map
            accounts.accountsListFromName.get(newHolderUuid).add(accountIdentifier);
        } else {
            // Add new entry to map
            List<String> userAccountList = new ArrayList<String>(); // Lists are immutable; must make ArrayList
            userAccountList.add(accountIdentifier);
            accounts.accountsListFromName.put(newHolderUuid, userAccountList);
        }

        // Update regular account list
        if (accounts.accountsList.containsKey(accountIdentifier)) {
            // This path assumes we are adding a holder as opposed to adding an account (else, how else would this work?)
            System.out.println("Account found in accounts list, adding this person as a holder instead");
            accounts.accountsList.get(accountIdentifier).AddAccountHolder(newHolderName, newHolderUuid);
        } else {
            // Non-existent account means a new one!
            System.out.println("Brand new account creation, adding!");
            accounts.accountsList.put(accountIdentifier, newAccountOnly);
            numberOfAccounts++;
        }

        System.out.println("Flashing configuration file");
        FlashConfig("bank/" + routingNumber + "/accounts");
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         GetRoutingNumber
    ///
    /// @return     Routing number
    /////////////////////////////////////////////////////////////////////////////
    public Integer GetRoutingNumber() {
        return this.routingNumber;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         AddMoneyToAccount
    ///
    /// @param[in]  accountId is the account to add money to at this bank
    ///
    /// @param[in]  amount is the amount of money to add to this account
    ///
    /// @brief      Adds money to an account at this bank if it exists
    /////////////////////////////////////////////////////////////////////////////
    public void AddMoneyToAccount(String accountId, Integer amount) {
        IndividualAccount account = accounts.accountsList.get(accountId);
        System.out.println("Received account # " + accountId + " and money amnt " + amount);

        if (account != null) {
            account.Deposit(amount);
            FlashConfig("bank/" + routingNumber + "/accounts");
        }
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         SubtractMoneyFromAccount
    ///
    /// @param[in]  accountId is the account to subtract money to at this bank
    ///
    /// @param[in]  amount is the amount of money to subtract to this account
    ///
    /// @brief      Subtracts money from an account at this bank if it exists
    /////////////////////////////////////////////////////////////////////////////
    public void SubtractMoneyFromAccount(String accountId, Integer amount) {
        IndividualAccount account = accounts.accountsList.get(accountId);

        for (Entry<String, IndividualAccount> debug : accounts.accountsList.entrySet()) {
            System.out.println("ACCOUNT ID: " + debug.getKey());
            System.out.println("ACCOUNT NUM: " + debug.getValue().GetAccountNumber());
        }

        if (account != null) {
            account.Withdraw(amount);
            FlashConfig("bank/" + routingNumber + "/accounts");
        }
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         SetMoneyOnAccount
    ///
    /// @param[in]  accountId is the account number
    ///
    /// @param[in]  amount is the new balance to give this account
    ///
    /// @brief      Sets a balance on an account if it exists
    /////////////////////////////////////////////////////////////////////////////
    public void SetMoneyOnAccount(String accountId, Integer amount) {
        IndividualAccount account = accounts.accountsList.get(accountId);
        System.out.println("Is account null? " + (account == null ? "YES" : "NO"));
        System.out.println("Received account # " + accountId + " and money amnt " + amount);

        for (Entry<String, IndividualAccount> debug : accounts.accountsList.entrySet()) {
            System.out.println("ACCOUNT ID: " + debug.getKey());
            System.out.println("ACCOUNT NUM: " + debug.getValue().GetAccountNumber());
        }

        if (account != null) {
            account.SetMoney(amount);
            FlashConfig("bank/" + routingNumber + "/accounts");
        }
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         CreateAccount
    ///
    /// @param[in]  holderUuid is the new holders UUID of this account
    ///
    /// @param[in]  holderName is the display name of the holder
    ///
    /// @param[in]  accountTypeStr is the account type as a string (will be number)
    ///
    /// @brief      Create a new account at this bank
    ///
    /// @return     True if account can be created, false if it fails
    /////////////////////////////////////////////////////////////////////////////
    public Boolean CreateAccount(String holderUuid, String holderName, ACCOUNT_TYPE accountType) {
        Boolean success = false;
        if (accounts.accountsList.size() <= maxBankAccounts) {
            // Verify this isn't a blacklisted user
            System.out.println("Is user bank locked? " + lockedUsers.contains(holderName));
            if (!lockedUsers.contains(holderName)) {
                Integer maxAttempts = 15; // Reasonably a unique bank account should pop up within 1000 generations. If not, the user may try again.
                String accountId = AccountNumberGenerator.GenerateNewAccountNumber(bankFourLetterIdentifier, routingNumber, ACCOUNT_TYPES.get(accountType), holderName);

                System.out.println("Account generator came back with bank account id { " + accountId + " }");
                System.out.println("4 letter bank: " + AccountNumberGenerator.GetFinancialSymbolFromId(accountId));
                System.out.println("Routing: " + AccountNumberGenerator.GetRoutingNumberFromId(accountId));
                System.out.println("Account type: " + AccountNumberGenerator.GetAccountTypeFromId(accountId));
                System.out.println("RNG Account number: " + AccountNumberGenerator.GetAccountNumberFromId(accountId));

                // TODO: Fix in future with a method that will guarentee a one-time necessary number generator. Statistically speaking; this will be okay for the
                // entire life time of the server. BUT, you never know!
                while (maxAttempts != 0 && !accounts.accountsList.containsKey(AccountNumberGenerator.GetAccountNumberFromId(accountId))) {
                    accountId = AccountNumberGenerator.GenerateNewAccountNumber(bankFourLetterIdentifier, routingNumber, ACCOUNT_TYPES.get(accountType), holderName);
                    System.out.println("Account generator came back with bank account id { " + accountId + " }");
                    maxAttempts--;
                }

                // Final check to add the account
                String actualAccountNumber = AccountNumberGenerator.GetAccountNumberFromId(accountId);
                System.out.println("Bank account identifier is { " + actualAccountNumber + " }. Is this already an existing account? " + accounts.accountsList.containsKey(actualAccountNumber));
                if (!accounts.accountsList.containsKey(actualAccountNumber)) {
                    IndividualAccount newAccount = new IndividualAccount(actualAccountNumber, this.routingNumber, List.of(holderName),
                                                                        List.of(holderUuid), false, 0,
                                                                        "", accountType, bankFourLetterIdentifier);
                    System.out.println("Updating accounts list for this bank");
                    UpdateBankAccounts(holderName, holderUuid, actualAccountNumber, newAccount);
                    success = true;
                }
            }
        }
        return success;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         AliasAccount
    ///
    /// @param[in]  accountId to alias
    ///
    /// @param[in]  newAlias is name to give this account
    ///
    /// @brief      Alias an account
    /////////////////////////////////////////////////////////////////////////////
    public void AliasAccount(String accountId, String newAlias) {
        String accountNumber = AccountNumberGenerator.GetAccountNumberFromId(accountId);
        if (accounts.accountsList.containsKey(accountNumber)) {
            accounts.accountsList.get(accountNumber).AliasAccount(newAlias);
        }
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         LockAccountHolder
    ///
    /// @param[in]  holderName is player to lock account
    ///
    /// @brief      Locks all accounts under a name for this user
    ///
    /// @return     True if lock succeeds, false if not
    /////////////////////////////////////////////////////////////////////////////
    public Boolean LockAccountHolder(String holderName) {
        Boolean success = false;

        Integer accountIter = 0;
        for (Entry<String, List<String>> holderAccounts : accounts.accountsListFromName.entrySet()) {
            accounts.accountsList.get(holderAccounts.getValue().get(accountIter++)).LockAccount();
        }
        return success;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         CloseAccount
    ///
    /// @param[in]  accountId is id of account to be closed
    ///
    /// @brief      Closes an account
    ///
    /// @note       Balance of account must be 0 to close successfully
    ///
    /// @return     True if can close, false if not
    /////////////////////////////////////////////////////////////////////////////
    public Boolean CloseAccount(String accountId) {
        Boolean success = false;

        String accountNumber = AccountNumberGenerator.GetAccountNumberFromId(accountId);

        if (accounts.accountsList.get(accountNumber).GetAccountBalance() == 0) {
            accounts.accountsList.remove(accountNumber);
            success = true;
        }
        return success;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         HasAccount
    ///
    /// @param[in]  accountIdentifier account number to check to see if it exists
    ///             at this bank
    ///
    /// @brief      See if an account identifier belongs to this bank
    ///
    /// @return     True if this bank has this account identifier, false if not
    /////////////////////////////////////////////////////////////////////////////
    public Boolean HasAccount(String accountIdentifier) {
        Boolean containsAccount = false;
        if (accounts.accountsList.containsKey(accountIdentifier)) {
            containsAccount = true;
        }
        return containsAccount;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         IsValidWithdrawal
    ///
    /// @param[in]  withdrawalAmount The amount to be withdrawn
    ///
    /// @param[in]  accountIdentifier account to withdraw from
    ///
    /// @brief      Verifies if a withdrawal will succeed on an account prior to
    ///             the actual withdrawal itself
    ///
    /// @return     True if this account can afford this withdrawal. False if not
    /////////////////////////////////////////////////////////////////////////////
    public Boolean IsValidWithdrawal(Integer withdrawalAmount, String accountIdentifier) {
        Boolean isValid = false;

        if (accounts.accountsList.containsKey(accountIdentifier)) {
            IndividualAccount account = accounts.accountsList.get(accountIdentifier);

            if (account.CanWithdraw(withdrawalAmount)) {
                isValid = true;
            }
        }

        return isValid;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         IsAccountHolder
    ///
    /// @param[in]  accountIdentifier Account to check
    ///
    /// @param[in]  uuid Is players UUID to see if they are on this account
    ///
    /// @brief      Check if a player is on this account
    ///
    /// @return     True if player is on this account. False if not or account
    ///             does not exist
    /////////////////////////////////////////////////////////////////////////////
    public Boolean IsAccountHolder(String accountIdentifier, String uuid) {
        Boolean isHolder = false;

        // Verify account exists first
        if (accounts.accountsList.containsKey(accountIdentifier)) {
            isHolder = accounts.accountsList.get(accountIdentifier).IsHolder(uuid);
        }

        return isHolder;
    }

    /////////////////////////////////////////////////////////////////////////////
    /// @fn         FlashConfig
    ///
    /// @param[in]  dirName is config to flash to. Banking is not trivial and will
    ///             require separate files to be updated at different locations!
    ///
    /// @brief      Flashes the config to the disk
    ///
    /// @note       dirName should be the relative full path to dir
    /// @note       Function will be removed in near future for SQL but is
    ///             expensive to run as it flashes everything even if un-updated
    /////////////////////////////////////////////////////////////////////////////
    public void FlashConfig(String dirName) {
        for (Entry<String, IndividualAccount> singleAccount : accounts.accountsList.entrySet()) {

            // Iterate over each one & verify if a file exists inside the dir. if it does;
            // nuke it and
            // replace it with the new contents in memory
            String accountNum = singleAccount.getKey().toString();

            // delete file
            File file = new File(dirName + "/" + accountNum + ".json");
            if (file.exists()) {
                file.delete();
            }

            // Re-flash file
            config.WriteToJsonFile(dirName + "/" + accountNum + ".json", singleAccount.getValue());
        }
    }
}