import bigDecimal from "js-big-decimal";
import {BaseAccount} from "./interfaces/BaseAccountInterface";
import {
    AccountType,
    discoverNexaAccount,
    generateAccountKey,
    generateKeyAndAddress, generateKeysAndAddresses,
    getNextAccountIndex
} from "../../utils/WalletUtils";
import {HDPrivateKey} from "libnexa-ts";
import DAppAccount from "./models/DappAccount";
import VaultAccount from "./models/VaultAccount";
import DefaultAccount from "./models/DefaultAccount";
import {AccountIndexes, AddressKey} from "../../models/wallet.entities";

/**
 * AccountStore manages a collection of wallet accounts of different types.
 * It provides functionality to create, import, export, and manage accounts
 * including DApp accounts, Vault accounts, and Default NEXA accounts.
 */
export default class AccountStore {

    /** Map storing all accounts indexed by their unique store key */
    private readonly _accounts: Map<string, BaseAccount>

    /**
     * Creates a new AccountStore instance
     * Initializes an empty map to store accounts
     */
    public constructor() {
        this._accounts = new Map<string, BaseAccount>()
    }

    /**
     * Generates a unique store key for an account based on its type and index
     * @param accountType The type of account (DAPP, VAULT, or DEFAULT)
     * @param index The account index
     * @returns Unique string key for storing the account
     */
    private getAccountStoreKey(accountType: AccountType, index: number): String {
        switch (accountType){
            case AccountType.DAPP_ACCOUNT:
                // DApp accounts use format: "2.index"
                return String(accountType + '.' + index);
            case AccountType.VAULT_ACCOUNT:
                // Vault accounts use format: "1.index"
                return String(accountType + '.' + index);
            default:
                // Default accounts use just the index
                return String(index)
        }
    }

    /**
     * Creates a new account of the specified type
     * @param accountType Type of account to create (DAPP, VAULT, or DEFAULT)
     * @param masterKey Master HD private key for deriving account keys
     * @returns Promise resolving to the created account
     */
    async createAccount(accountType: AccountType, masterKey: HDPrivateKey): Promise<BaseAccount> {
        // Get the next available index for this account type
        const nextIndex = await getNextAccountIndex(accountType, masterKey);
        const accountStoreKey = this.getAccountStoreKey(accountType, nextIndex)
        
        // Check if account already exists
        const indexExists = this._accounts.get(String(accountStoreKey))
        if(indexExists) {
            return indexExists
        }
        
        switch (accountType){
            case AccountType.DAPP_ACCOUNT:
                // Create DApp account (purpose 2)
                let dappAccountKey = generateAccountKey(masterKey, 2);
                const dAppAccount = new DAppAccount(2, nextIndex, generateKeyAndAddress(dappAccountKey, nextIndex))
                await dAppAccount.loadBalances();
                this._accounts.set(dAppAccount.getAccountStoreKey(), dAppAccount)
                return dAppAccount
            case AccountType.VAULT_ACCOUNT:
                // Create Vault account (purpose 1)
                let vaultAccountKey = generateAccountKey(masterKey, 1);
                const vaultAccount = new VaultAccount(1, nextIndex, generateKeyAndAddress(vaultAccountKey, nextIndex))
                await vaultAccount.loadBalances();
                this._accounts.set(vaultAccount.getAccountStoreKey(), vaultAccount)
                return vaultAccount
            default:
                // Create default NEXA account with receive and change addresses
                let nexaAccountKey = generateAccountKey(masterKey, nextIndex);
                const nexaAccountIndexes: AccountIndexes = { rIndex: 0, cIndex: 0 };
                const nexaAccount = new DefaultAccount(nextIndex, nexaAccountIndexes, generateKeysAndAddresses(nexaAccountKey, nexaAccountIndexes.rIndex + 1, nexaAccountIndexes.rIndex + 20, nexaAccountIndexes.cIndex + 1, nexaAccountIndexes.cIndex + 20))
                await nexaAccount.loadBalances()
                this._accounts.set(nexaAccount.getAccountStoreKey(), nexaAccount)
                return nexaAccount;
        }
    }

    /**
     * Finds the private key associated with a given address across all accounts
     * @param address The address to search for
     * @returns The AddressKey containing the private key, or null if not found
     */
    findKeyForAddress(address: string): AddressKey | null {
        // Search through all accounts
        for (const [_, account] of this._accounts.entries()) {
            // Combine receive and change keys for this account
            const allKeys = account.accountKeys.receiveKeys.concat(account.accountKeys.changeKeys)
            
            // Check each key for a matching address
            for(const key of allKeys){
                if(key.address == address) {
                    return key
                }
            }
        }
        return null
    }

    /**
     * Imports an existing account into the store
     * @param accountData The account data to import
     * @throws Error if an account with the same key already exists
     */
    importAccount(accountData: BaseAccount): void {
        let index: string = accountData.getAccountStoreKey()
        if(this._accounts.get(index)) {
            throw Error('Account already exists!')
        }
        this._accounts.set(String(index), accountData)
    }

    /**
     * Exports account data by index
     * @param accountIndex The account index to export
     * @returns The account data
     * @throws Error if the account doesn't exist
     */
    exportAccount(accountIndex: string): BaseAccount {
        if(!this._accounts.get(accountIndex)) {
            throw Error('Cannot find account!')
        }

        return this._accounts.get(accountIndex)!
    }

    /**
     * Removes an account from the store
     * @param accountIndex The account index to remove
     * @throws Error if the account doesn't exist
     */
    removeAccount(accountIndex: string): void {
        if(!this._accounts.get(accountIndex)) {
            throw Error('Cannot find account!')
        }
        this._accounts.delete(accountIndex)
    }

    /**
     * Returns all accounts in the store
     * @returns Map of account store keys to BaseAccount objects
     */
    listAccounts(): Map<string, BaseAccount> {
        return this._accounts
    }

    /**
     * Retrieves a specific account by its index
     * @param index The account index to retrieve
     * @returns The account if found, undefined otherwise
     */
    getAccount(index:string): BaseAccount | undefined {
        return this._accounts.get(index)
    }
}
