import {Provider, constants, stark, ec, Account, provider, wallet, WebSocketChannel, logger} from "starknet";
import {calculateHash, getLogger, toHex} from "../../utils/Utils";
import {SignedStarknetTx, StarknetTransactions, StarknetTx} from "./modules/StarknetTransactions";
import {StarknetFees} from "./modules/StarknetFees";
import {StarknetAddresses} from "./modules/StarknetAddresses";
import {StarknetTokens} from "./modules/StarknetTokens";
import {StarknetEvents} from "./modules/StarknetEvents";
import {StarknetSignatures} from "./modules/StarknetSignatures";
import {StarknetAccounts} from "./modules/StarknetAccounts";
import {StarknetBlocks} from "./modules/StarknetBlocks";
import {BitcoinNetwork, ChainInterface, TransactionConfirmationOptions} from "@atomiqlabs/base";
import {StarknetSigner} from "../wallet/StarknetSigner";
import {Buffer} from "buffer";
import {StarknetKeypairWallet} from "../wallet/accounts/StarknetKeypairWallet";
import {StarknetBrowserSigner} from "../wallet/StarknetBrowserSigner";

/**
 * Configuration options for Starknet chain interface
 *
 * @category Chain Interface
 */
export type StarknetConfig = {
    /**
     * Limit of the number of events retrieved by a single `starknet_getEvents` RPC call.
     *
     * Defaults to 100 events
     */
    getLogChunkSize?: number, //100
    /**
     * When fetching events in the forward direction, sets the limit on the number of blocks
     *  to fetch in a single `starknet_getEvents` RPC call.
     *
     * Defaults to 2000 blocks
     */
    getLogForwardBlockRange?: number, //2000
    /**
     * Maximum numbers of keys allowed to be specified in a single `starknet_getEvents` RPC call
     *
     * Defaults to 64 keys
     */
    maxGetLogKeys?: number, //64
    /**
     * Maximum number of parallel contract calls to execute in batch functions
     */
    maxParallelCalls?: number, //10
};

/**
 * Main chain interface for interacting with Starknet blockchain
 *
 * @category Chain Interface
 */
export class StarknetChainInterface implements ChainInterface<StarknetTx, SignedStarknetTx, StarknetSigner, "STARKNET", Account> {

    public readonly chainId = "STARKNET";
    public readonly starknetChainId: constants.StarknetChainId;

    /**
     * Optional websocket channel for instant notifications
     * @internal
     */
    readonly wsChannel?: WebSocketChannel;
    /**
     * Underlying starknet.js provider
     * @internal
     */
    readonly provider: Provider;

    public Fees: StarknetFees;
    public readonly Tokens: StarknetTokens;
    public readonly Transactions: StarknetTransactions;
    public readonly Signatures: StarknetSignatures;
    public readonly Events: StarknetEvents;
    public readonly Accounts: StarknetAccounts;
    public readonly Blocks: StarknetBlocks;

    public readonly config: StarknetConfig;

    private readonly bitcoinNetwork?: BitcoinNetwork;

    constructor(
        chainId: constants.StarknetChainId,
        provider: Provider,
        wsChannel?: WebSocketChannel,
        feeEstimator: StarknetFees = new StarknetFees(provider),
        options?: StarknetConfig,
        bitcoinNetwork?: BitcoinNetwork
    ) {
        this.starknetChainId = chainId;
        this.provider = provider;
        this.config = options ?? {};
        this.config.getLogForwardBlockRange ??= 2000;
        this.config.getLogChunkSize ??= 100;
        this.config.maxGetLogKeys ??= 64;
        this.config.maxParallelCalls ??= 10;
        this.wsChannel = wsChannel;

        this.Fees = feeEstimator;
        this.Tokens = new StarknetTokens(this);
        this.Transactions = new StarknetTransactions(this);

        this.Signatures = new StarknetSignatures(this);
        this.Events = new StarknetEvents(this);
        this.Accounts = new StarknetAccounts(this);
        this.Blocks = new StarknetBlocks(this);

        this.bitcoinNetwork = bitcoinNetwork;
    }

    /**
     * @inheritDoc
     */
    async getBalance(signer: string, tokenAddress: string): Promise<bigint> {
        //TODO: For native token we should discount the cost of deploying an account if it is not deployed yet and the tx fee
        return await this.Tokens.getTokenBalance(signer, tokenAddress);
    }

    /**
     * @inheritDoc
     */
    getNativeCurrencyAddress(): string {
        return this.Tokens.getNativeCurrencyAddress();
    }

    /**
     * @inheritDoc
     */
    isValidToken(tokenIdentifier: string): boolean {
        return this.Tokens.isValidToken(tokenIdentifier);
    }

    /**
     * @inheritDoc
     */
    isValidAddress(address: string, lenient?: boolean): boolean {
        return StarknetAddresses.isValidAddress(address, lenient);
    }

    /**
     * @inheritDoc
     */
    normalizeAddress(address: string): string {
        return toHex(address);
    }

    ///////////////////////////////////
    //// Callbacks & handlers
    /**
     * @inheritDoc
     */
    offBeforeTxReplace(callback: (oldTx: string, oldTxId: string, newTx: string, newTxId: string) => Promise<void>): boolean {
        return true;
    }

    /**
     * @inheritDoc
     */
    onBeforeTxReplace(callback: (oldTx: string, oldTxId: string, newTx: string, newTxId: string) => Promise<void>): void {}

    /**
     * @inheritDoc
     */
    onBeforeTxSigned(callback: (tx: StarknetTx) => Promise<void>): void {
        this.Transactions.onBeforeTxSigned(callback);
    }

    /**
     * @inheritDoc
     */
    offBeforeTxSigned(callback: (tx: StarknetTx) => Promise<void>): boolean {
        return this.Transactions.offBeforeTxSigned(callback);
    }

    /**
     * @inheritDoc
     */
    randomAddress(): string {
        return toHex(stark.randomAddress());
    }

    /**
     * @inheritDoc
     */
    randomSigner(): StarknetSigner {
        const privateKey = "0x"+Buffer.from(ec.starkCurve.utils.randomPrivateKey()).toString("hex");
        const wallet = new StarknetKeypairWallet(this.provider, privateKey);
        return new StarknetSigner(wallet);
    }

    ////////////////////////////////////////////
    //// Transactions
    /**
     * @inheritDoc
     */
    sendAndConfirm(
        signer: StarknetSigner,
        txs: StarknetTx[],
        waitForConfirmation?: boolean,
        abortSignal?: AbortSignal,
        parallel?: boolean,
        onBeforePublish?: (txId: string, rawTx: string) => Promise<void>
    ): Promise<string[]> {
        return this.Transactions.sendAndConfirm(signer, txs, waitForConfirmation, abortSignal, parallel, onBeforePublish);
    }

    /**
     * @inheritDoc
     */
    sendSignedAndConfirm(
        signedTxs: SignedStarknetTx[],
        waitForConfirmation?: boolean,
        abortSignal?: AbortSignal,
        parallel?: boolean,
        onBeforePublish?: (txId: string, rawTx: string) => Promise<void>
    ): Promise<string[]> {
        return this.Transactions.sendSignedAndConfirm(signedTxs, waitForConfirmation, abortSignal, parallel, onBeforePublish);
    }

    /**
     * @inheritDoc
     */
    async prepareTxs(txs: StarknetTx[]): Promise<StarknetTx[]> {
        await this.Transactions.prepareTransactions(txs);
        return txs;
    }

    /**
     * @inheritDoc
     */
    serializeTx(tx: StarknetTx): Promise<string> {
        return Promise.resolve(StarknetTransactions.serializeTx(tx));
    }

    /**
     * @inheritDoc
     */
    deserializeTx(txData: string): Promise<StarknetTx> {
        return Promise.resolve(StarknetTransactions.deserializeTx(txData));
    }

    /**
     * @inheritDoc
     */
    serializeSignedTx(signedTx: SignedStarknetTx): Promise<string> {
        return Promise.resolve(StarknetTransactions.serializeTx(signedTx));
    }

    /**
     * @inheritDoc
     */
    deserializeSignedTx(txData: string): Promise<SignedStarknetTx> {
        return Promise.resolve(StarknetTransactions.deserializeTx(txData));
    }

    /**
     * @inheritDoc
     */
    getTxId(signedTX: SignedStarknetTx): Promise<string> {
        return Promise.resolve(signedTX.txId ?? calculateHash(signedTX));
    }

    /**
     * @inheritDoc
     */
    getTxIdStatus(txId: string): Promise<"not_found" | "pending" | "success" | "reverted"> {
        return this.Transactions.getTxIdStatus(txId);
    }

    /**
     * @inheritDoc
     */
    getTxStatus(tx: string): Promise<"not_found" | "pending" | "success" | "reverted"> {
        return this.Transactions.getTxStatus(tx);
    }

    /**
     * @inheritDoc
     */
    async getFinalizedBlock(): Promise<{ height: number; blockHash: string }> {
        const block = await this.Blocks.getBlock("l1_accepted");
        return {
            height: block.block_number as number,
            blockHash: block.block_hash as string
        }
    }

    /**
     * @inheritDoc
     */
    txsTransfer(signer: string, token: string, amount: bigint, dstAddress: string, feeRate?: string): Promise<StarknetTx[]> {
        return this.Tokens.txsTransfer(signer, token, amount, dstAddress, feeRate);
    }

    /**
     * @inheritDoc
     */
    async transfer(
        signer: StarknetSigner,
        token: string,
        amount: bigint,
        dstAddress: string,
        txOptions?: TransactionConfirmationOptions
    ): Promise<string> {
        const txs = await this.Tokens.txsTransfer(signer.getAddress(), token, amount, dstAddress, txOptions?.feeRate);
        const [txId] = await this.Transactions.sendAndConfirm(signer, txs, txOptions?.waitForConfirmation, txOptions?.abortSignal, false);
        return txId;
    }

    /**
     * @inheritDoc
     */
    wrapSigner(signer: Account): Promise<StarknetSigner> {
        if((signer as any).walletProvider!=null) {
            return Promise.resolve(new StarknetBrowserSigner(signer));
        } else {
            return Promise.resolve(new StarknetSigner(signer));
        }
    }

    async verifyNetwork(bitcoinNetwork: BitcoinNetwork): Promise<void> {
        if(this.bitcoinNetwork!=null && bitcoinNetwork!==this.bitcoinNetwork)
            throw new Error(`Network mismatch, the chain interface was not setup for ${BitcoinNetwork[bitcoinNetwork]}, chain interface network: ${BitcoinNetwork[this.bitcoinNetwork]}`);

        const chainId = await this.provider.getChainId();
        if(chainId!==constants.StarknetChainId.SN_MAIN && chainId!==constants.StarknetChainId.SN_SEPOLIA) {
            logger.warn(`verifyNetwork(): Using non-standard chainId ${chainId}, skipping network verfication!`);
            return;
        }

        if(this.starknetChainId!==chainId)
            throw new Error(`Network mismatch, the underlying RPC provider isn't using the correct chainId, expected: ${this.starknetChainId}, provider returned: ${chainId}`);
    }

}
