import {Networkish, Networks, TransactionBuilder} from "libnexa-ts";
import WatchOnlyTransactionCreator from "./transactions/WatchOnlyTransactionCreator";
import {rostrumProvider} from "../network/RostrumProvider";
import {WatchOnlyAddress} from "../models/wallet.entities";
import {isArray, isBuffer, isNil, isObject, isString} from "lodash-es";
import ValidationUtils from "../utils/ValidationUtils";
import {isValidNexaAddress} from "../utils/WalletUtils";
import {SubscribeCallback} from "@vgrunner/electrum-cash";
import {isNullOrEmpty} from "../utils/CommonUtils";

/**
 * WatchOnlyWallet provides functionality for monitoring and creating transactions
 * for addresses without storing private keys. This allows users to track balances
 * and create unsigned transactions that can be signed elsewhere.
 */
export default class WatchOnlyWallet {
    /** The blockchain network this wallet operates on */
    private readonly _network: Networkish
    /** Array of addresses to monitor without their private keys */
    private readonly _addressesToWatch: WatchOnlyAddress[];

    /**
     * Creates a new WatchOnlyWallet instance
     * @param addressesToWatch Array of addresses to monitor
     * @param network Optional network name (defaults to mainnet)
     * @throws Error if validation fails
     */
    constructor(addressesToWatch: WatchOnlyAddress[], network?: string) {
        // Validate network parameter
        if (network !== undefined && !isString(network)) {
            throw new Error('Network must be a string');
        }

        if (network !== undefined && isString(network)) {
            const n = Networks.get(network)
            if (n === undefined) {
                throw new Error(`Invalid network: ${network}`);
            }
        }

        this._network = Networks.get(network) ?? Networks.mainnet;
        // Validate addressesToWatch parameter
        this._addressesToWatch = this.validateAddressesToWatch(addressesToWatch);
    }

    /**
     * Validates the addresses to watch array
     * @param addressesToWatch Array of addresses to validate
     * @returns Validated array of WatchOnlyAddress objects
     * @throws Error if validation fails
     */
    private validateAddressesToWatch(addressesToWatch: WatchOnlyAddress[]): WatchOnlyAddress[] {
        if (addressesToWatch === null || addressesToWatch === undefined) {
            throw new Error('addresesToWatch is required');
        }
        
        if (!isArray(addressesToWatch)) {
            throw new Error('addressesToWatch must be an array');
        }

        // Check if array is not empty
        if (addressesToWatch.length === 0) {
            throw new Error('addressesToWatch cannot be empty');
        }

        // Validate each address object
        const validatedAddresses: WatchOnlyAddress[] = [];

        for (let i = 0; i < addressesToWatch.length; i++) {
            const addr = addressesToWatch[i];

            // Check if address is an object
            if (!isObject(addr) || isArray(addr)) {
                throw new Error(`addressesToWatch[${i}] must be an object`);
            }

            // Check if address property exists and is a string
            if (!addr.hasOwnProperty('address') || !isString(addr.address)) {
                throw new Error(`addressesToWatch[${i}].address must be a string`);
            }

            // Check if address is not empty
            if (addr.address.trim() === '') {
                throw new Error(`addressesToWatch[${i}].address cannot be empty`);
            }

            // Validate address format
            if (!isValidNexaAddress(addr.address, this._network)) {
                throw new Error(`addressesToWatch[${i}].address is not a valid NEXA address: ${addr.address}`);
            }

            // Validate optional xPub property
            if (addr.xPub !== undefined && !isObject(addr.xPub)) {
                throw new Error(`addressesToWatch[${i}].xPub must be a PublicKey object`);
            }

            // Validate optional derivationPath property
            if (addr.derivationPath !== undefined && !isString(addr.derivationPath)) {
                throw new Error(`addressesToWatch[${i}].derivationPath must be a string`);
            }

            // Check for duplicate addresses
            const isDuplicate = validatedAddresses.some(existingAddr =>
                existingAddr.address === addr.address
            );
            if (isDuplicate) {
                throw new Error(`Duplicate address found: ${addr.address}`);
            }

            validatedAddresses.push({
                address: addr.address.trim(),
                xPub: addr.xPub,
                derivationPath: addr.derivationPath
            });
        }

        return validatedAddresses;
    }

    /**
     * Creates a new transaction creator for this watch-only wallet
     * @param x Optional transaction data - can be a TransactionBuilder, hex string, or Buffer
     * @returns WatchOnlyTransactionCreator configured with wallet's addresses and network
     */
    public newTransaction(x?: TransactionBuilder | string | Buffer): WatchOnlyTransactionCreator {
        let tx: WatchOnlyTransactionCreator;

        // Handle different input types for creating transactions
        if (x instanceof TransactionBuilder) {
            // Use existing TransactionBuilder instance
            tx = new WatchOnlyTransactionCreator(x);
        } else if (isString(x)) {
            // Parse transaction from hex string
            tx = new WatchOnlyTransactionCreator().parseTxHex(x);
        } else if (isBuffer(x) && !isNil(x)) {
            // Parse transaction from buffer
            tx = new WatchOnlyTransactionCreator().parseTxBuffer(x);
        } else {
            // Create new empty transaction
            tx = new WatchOnlyTransactionCreator();
        }

        // Configure transaction with wallet's addresses and network
        return tx.from(this._addressesToWatch).onNetwork(this._network);
    }

    /**
     * Broadcasts a signed transaction to the network
     * @param transaction Hex-encoded signed transaction
     * @returns Promise resolving to transaction ID
     * @throws Error if transaction is invalid or broadcast fails
     */
    public async sendTransaction(transaction: string): Promise<string> {
        ValidationUtils.validateArgument(isString(transaction), 'transaction must be present and valid')
        return rostrumProvider.broadcast(transaction)
    }

    public async subscribeToAddressNotifications(callback: SubscribeCallback): Promise<void>{
        await rostrumProvider.subscribeToAddresses(this._addressesToWatch.map(addr => addr.address), callback)
    }

    /**
     * Gets the list of addresses being watched
     * @returns Array of watched addresses (copy to prevent mutation)
     */
    public getWatchedAddresses(): WatchOnlyAddress[] {
        return [...this._addressesToWatch];
    }
}
