import {HDPrivateKey, Message, Networkish, Networks, TransactionBuilder,} from "libnexa-ts";
import * as Bip39 from 'bip39'
import {rostrumProvider} from "../network/RostrumProvider";
import {AccountType, discoverWallet,} from "../utils/WalletUtils";
import {isBuffer, isNil, isString} from "lodash-es";
import WalletTransactionCreator from "./transactions/WalletTransactionCreator";
import AccountStore from "./accounts/AccountStore";
import {BaseAccount} from "./accounts/interfaces/BaseAccountInterface";
import ValidationUtils from "../utils/ValidationUtils";
import {AddressKey} from "../models/wallet.entities";

/**
 * Main Wallet class for managing Nexa blockchain wallet operations
 *
 * This class provides comprehensive wallet functionality including:
 * - Creating wallets from seed phrases or private keys
 * - Account discovery and management
 * - Transaction creation and signing
 * - Message signing and verification
 * - Multi-account support with different account types
 *
 * @example
 * ```typescript
 * // Create a new wallet with random seed phrase
 * const wallet = Wallet.create();
 *
 * // Restore wallet from existing seed phrase
 * const wallet = Wallet.fromSeedPhrase('your twelve word seed phrase here');
 *
 * // Initialize wallet (discovers accounts and balances)
 * await wallet.initialize();
 *
 * // Create a new account
 * const account = await wallet.newAccount('DefaultAccount');
 *
 * // Create and send a transaction
 * const tx = wallet.newTransaction(account)
 *   .to('nexa:address', 1000000) // 1 NEXA in satoshis
 *   .sign();
 *
 * const txId = await wallet.sendTransaction(tx.toHex());
 * ```
 */
export default class Wallet {

    /** The master HD private key derived from the seed phrase */
    private readonly masterKey!: HDPrivateKey;

    /** Store for managing wallet accounts */
    private _accountStore: AccountStore;

    /** The blockchain network this wallet operates on */
    private readonly _network: Networkish

    /** The BIP39 seed phrase used to generate this wallet (if created from phrase) */
    private readonly phrase?: string;

    /**
     * Creates a new Wallet instance
     *
     * @param data - Optional wallet data:
     *   - undefined: Generate new random seed phrase
     *   - string: Use as BIP39 seed phrase
     *   - HDPrivateKey: Use as master key directly
     * @param network - Network name ('mainnet', 'testnet', 'regtest'). Defaults to 'mainnet'
     *
     * @example
     * ```typescript
     * // Create new wallet with random seed
     * const wallet = new Wallet();
     *
     * // Create from seed phrase
     * const wallet = new Wallet('abandon abandon abandon...');
     *
     * // Create from master key
     * const masterKey = HDPrivateKey.fromString('xprv...');
     * const wallet = new Wallet(masterKey);
     *
     * // Create on testnet
     * const wallet = new Wallet(undefined, 'testnet');
     * ```
     */
    constructor(data?: string | HDPrivateKey | undefined, network?: string) {
        this._network = Networks.get(network) ?? Networks.mainnet
        this._accountStore = new AccountStore()
        if(isNil(data)) {
            this.phrase = Bip39.generateMnemonic(128, undefined, Bip39.wordlists.english)
            const seed = Bip39.mnemonicToSeedSync(this.phrase, '')

            const masterKey = HDPrivateKey.fromSeed(seed, this._network ?? Networks.mainnet)
            this.masterKey  = masterKey.deriveChild(44, true).deriveChild(29223, true)
        } else if(data instanceof HDPrivateKey) {
            this.masterKey = data
        } else if (isString(data)) {
            this.phrase = data
            const seed = Bip39.mnemonicToSeedSync(this.phrase, '')

            const masterKey = HDPrivateKey.fromSeed(seed, this._network ?? Networks.mainnet)
            this.masterKey  = masterKey.deriveChild(44, true).deriveChild(29223, true)
        }
    }

    /**
     * Create a new wallet with a randomly generated seed phrase
     *
     * This is the recommended way to create a new wallet for first-time users.
     * The generated seed phrase should be securely stored by the user.
     *
     * @returns A new Wallet instance with a random 12-word seed phrase
     *
     * @example
     * ```typescript
     * const wallet = Wallet.create();
     * console.log(wallet.export().phrase); // Store this securely!
     * ```
     */
    public static create(): Wallet {
        return new Wallet()
    }

    /**
     * Create a wallet from an existing BIP39 seed phrase
     *
     * Use this method to restore a wallet from a previously generated seed phrase.
     * The seed phrase should be a valid BIP39 mnemonic.
     *
     * @param phrase - The BIP39 seed phrase (12 or 24 words)
     * @param network - Optional network name ('mainnet', 'testnet', 'regtest')
     * @returns A new Wallet instance restored from the seed phrase
     * @throws {Error} If the seed phrase is invalid or not provided
     *
     * @example
     * ```typescript
     * const wallet = Wallet.fromSeedPhrase(
     *   'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
     *   'testnet'
     * );
     * ```
     */
    public static fromSeedPhrase(phrase: string, network?: string): Wallet {
        ValidationUtils.validateArgument(isString(phrase), 'seedphrase must be provided')
        return new Wallet(phrase, network)
    }

    /**
     * Create a wallet from an extended private key (xpriv)
     *
     * Use this method to create a wallet from a master private key in extended format.
     * This is useful for advanced users who want to use a specific key derivation.
     *
     * @param xpriv - The extended private key string (starts with 'xprv')
     * @param network - Optional network name ('mainnet', 'testnet', 'regtest')
     * @returns A new Wallet instance using the provided master key
     * @throws {Error} If the private key is invalid or not provided
     *
     * @example
     * ```typescript
     * const wallet = Wallet.fromXpriv(
     *   'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi'
     * );
     * ```
     */
    public static fromXpriv(xpriv: string, network?: string): Wallet {
        ValidationUtils.validateArgument(isString(xpriv), 'private key must be provided')
        const masterKey = HDPrivateKey.fromString(xpriv)
        return new Wallet(masterKey, network)
    }

    /**
     * Initialize the wallet by discovering accounts and loading balances
     *
     * This method performs account discovery using the BIP44 derivation path
     * and scans for existing accounts with transaction history or balances.
     * Must be called before using the wallet's accounts.
     *
     * @returns Promise that resolves when initialization is complete
     *
     * @example
     * ```typescript
     * const wallet = Wallet.fromSeedPhrase('your seed phrase');
     * await wallet.initialize();
     *
     * // Now you can access discovered accounts
     * const accounts = wallet.accountStore.listAccounts();
     * ```
     */
    public async initialize(): Promise<void> {
        const walletAccounts: BaseAccount[] = await discoverWallet(this.masterKey)
        for(const account of walletAccounts){
            this._accountStore.importAccount(account)
        }
    }

    /**
     * Create a new transaction builder for this wallet
     *
     * @param fromAccount - The account to send the transaction from
     * @param x - Optional existing transaction data:
     *   - TransactionBuilder: Use existing transaction builder
     *   - string: Parse from hex string
     *   - Buffer: Parse from binary buffer
     *   - undefined: Create new empty transaction
     * @returns A new WalletTransactionCreator instance
     *
     * @example
     * ```typescript
     * const account = wallet.accountStore.getAccount(0);
     * const tx = wallet.newTransaction(account)
     *   .to('nexa:address', 1000000) // 1 NEXA
     *   .sign();
     *
     * // Or from existing transaction hex
     * const tx = wallet.newTransaction(account, 'raw_tx_hex')
     *   .sign();
     * ```
     */
    public newTransaction(fromAccount: BaseAccount, x?: TransactionBuilder | string | Buffer): WalletTransactionCreator {
        let tx: WalletTransactionCreator;

        if (x instanceof TransactionBuilder) {
            tx = new WalletTransactionCreator(fromAccount, x);
        } else if (isString(x)) {
            tx = new WalletTransactionCreator(fromAccount).parseTxHex(x);
        } else if (isBuffer(x) && !isNil(x)) {
            tx = new WalletTransactionCreator(fromAccount).parseTxBuffer(x);
        } else {
            tx = new WalletTransactionCreator(fromAccount);
        }

        return tx.onNetwork(this._network);
    }

    /**
     * Create a new account for this wallet
     *
     * @param accountType - The type of account to create:
     *   - 'DefaultAccount': Standard account for general use
     *   - 'VaultAccount': Secured account with additional protection
     *   - 'DappAccount': Account optimized for dApp interactions
     * @returns Promise that resolves to the newly created account
     *
     * @example
     * ```typescript
     * const defaultAccount = await wallet.newAccount('DefaultAccount');
     * const vaultAccount = await wallet.newAccount('VaultAccount');
     * const dappAccount = await wallet.newAccount('DappAccount');
     * ```
     */
    public async newAccount(accountType: AccountType): Promise<BaseAccount>{
        return await this.accountStore.createAccount(accountType, this.masterKey)
    }

    /**
     * Broadcast a signed transaction to the Nexa network
     *
     * @param transaction - The signed transaction in hex format
     * @returns Promise that resolves to the transaction ID (txid)
     * @throws {Error} If the transaction is invalid or broadcast fails
     *
     * @example
     * ```typescript
     * const tx = wallet.newTransaction(account)
     *   .to('nexa:address', 1000000)
     *   .sign();
     *
     * const txId = await wallet.sendTransaction(tx.toHex());
     * console.log('Transaction sent:', txId);
     * ```
     */
    public async sendTransaction(transaction: string): Promise<string> {
        ValidationUtils.validateArgument(isString(transaction), 'transaction must be present and valid')
        return rostrumProvider.broadcast(transaction)
    }

    /**
     * Sign a message using a specific address from this wallet
     *
     * The message is signed using the private key associated with the given address.
     * This can be used for authentication or to prove ownership of an address.
     *
     * @param message - The message to sign
     * @param addressToUse - The address whose private key should sign the message
     * @returns The signature as a base64-encoded string
     * @throws {Error} If the address is not owned by this wallet
     *
     * @example
     * ```typescript
     * const account = wallet.accountStore.getAccount(0);
     * const address = account.getReceiveAddress();
     * const signature = wallet.signMessage('Hello World', address);
     * ```
     */
    public signMessage(message: string, addressToUse: string): string {
        let msg = new Message(message);
        const addressKey = this.accountStore.findKeyForAddress(addressToUse)
        ValidationUtils.validateArgument(isNil(addressKey), "You dont own this private key")
        return msg.sign(addressKey?.key.privateKey!)
    }

    /**
     * Verify a message signature against an address
     *
     * This method can verify signatures created by any address, not just addresses
     * owned by this wallet. It's useful for verifying messages from other parties.
     *
     * @param message - The original message that was signed
     * @param signature - The signature to verify (base64-encoded)
     * @param address - The address that supposedly signed the message
     * @returns true if the signature is valid, false otherwise
     * @throws {Error} If any parameters are missing or invalid
     *
     * @example
     * ```typescript
     * const isValid = wallet.verifyMessage(
     *   'Hello World',
     *   'signature_string',
     *   'nexa:address'
     * );
     * console.log('Signature valid:', isValid);
     * ```
     */
    public verifyMessage(message: string, signature: string, address: string): boolean {
        ValidationUtils.validateArgument(!isNil(message), 'message is required')
        ValidationUtils.validateArgument(!isNil(signature), 'signature is required')
        ValidationUtils.validateArgument(!isNil(address), 'address is required ')
        let msg = new Message(message);
        const addressKey = this.accountStore.findKeyForAddress(address)
        ValidationUtils.validateArgument(isNil(addressKey), "You dont own this private key")
        return msg.verify(address, signature)
    }

    /**
     * Export the wallet data for backup or storage
     *
     * Returns an object containing the wallet's seed phrase, master key, and accounts.
     * This data can be used to restore the wallet later. The seed phrase should be
     * stored securely as it provides full access to the wallet.
     *
     * @returns Object containing wallet data
     * @property {string} phrase - The BIP39 seed phrase (if available)
     * @property {HDPrivateKey} masterKey - The master private key
     * @property {BaseAccount[]} accounts - Array of discovered accounts
     *
     * @example
     * ```typescript
     * const walletData = wallet.export();
     *
     * // Store the seed phrase securely
     * const seedPhrase = walletData.phrase;
     *
     * // Later, restore the wallet
     * const restoredWallet = Wallet.fromSeedPhrase(seedPhrase);
     * ```
     */
    public export(): any {
        return {
            phrase: this.phrase,
            masterKey: this.masterKey,
            accounts: this._accountStore.listAccounts(),
            accountIndexes: this.accountStore.listAccounts().keys()
        }
    }

    /**
     * Get the account store for managing wallet accounts
     *
     * The account store provides methods to create, import, and manage accounts
     * within this wallet. Each account has its own set of addresses and keys.
     *
     * @returns The wallet's account store
     *
     * @example
     * ```typescript
     * const accountStore = wallet.accountStore;
     * const accounts = accountStore.listAccounts();
     * const firstAccount = accountStore.getAccount(0);
     * ```
     */
    get accountStore(): AccountStore {
        return this._accountStore;
    }

    /**
     * Get the network this wallet is operating on
     *
     * @returns The network object (mainnet, testnet, or regtest)
     *
     * @example
     * ```typescript
     * const network = wallet.network;
     * console.log('Network:', network.name);
     * ```
     */
    get network(): Networkish {
        return this._network;
    }
}
