import * as anchor from "@coral-xyz/anchor";
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import idl from "../idl/iswap.json";
import type { Iswap } from "../types/iswap";
import { AnchorWallet, ETFCreateParams, ETFBurnParams, MintETFTokenParams, ETFCreateResult, MintETFResult } from "../types/params";
import { deriveEtfTokenMintAccount } from "../utils/getAddress";
import { ETFQueries } from "../utils/queries";
import {
    createAssociatedTokenAccountInstruction,
    getAccount,
    getAssociatedTokenAddressSync,
    TokenAccountNotFoundError,
} from '@solana/spl-token';
import { checkATAExists, checkBalance, checkETFExists, validateETFCreateParams, validateMintETFParams } from '../utils/checks';

const DEFAULT_PROGRAM_ID = "dXgZyuguvD2m5G5385XkdokZBryUoSE6LbNJeWiFiN5";

export class DexClient {
    public readonly program: anchor.Program<Iswap>;

    public readonly queries: ETFQueries;
    private readonly wallet: AnchorWallet;

    constructor(
        public connection: Connection,
        wallet: AnchorWallet,
        provider: anchor.AnchorProvider,
    ) {
        this.connection = connection;
        this.wallet = wallet;

        this.program = new anchor.Program(idl as Iswap, provider)
        this.queries = new ETFQueries(this);
    }

    public async createETF(params: ETFCreateParams): Promise<ETFCreateResult> {
        try {
            validateETFCreateParams(params);

            const { name, symbol, description, url, assets } = params;

            const [etfAddress] = deriveEtfTokenMintAccount(this.program as unknown as anchor.Program, ["etf_token_v3", symbol]);
            const [etfCoreAddress] = deriveEtfTokenMintAccount(this.program as unknown as anchor.Program, ["etf_token_v3", etfAddress]);

            const etfInfo = await this.queries.getETFInfo(etfAddress);
            if (typeof etfInfo !== 'string') {
                return {
                    success: false,
                    error: 'etf already exists',
                    data: {
                        etfAddress,
                        etfCoreAddress: etfInfo.etfCoreAddress,
                        symbol,
                        description: etfInfo.description,
                        creator: etfInfo.creator,
                        assets: etfInfo.assets
                    }
                };
            }

            const latestBlockhash = await this.connection.getLatestBlockhash('confirmed');
            console.log('get the latest blockhash:', latestBlockhash.blockhash);


            let tx = new Transaction();

            for (const { token } of assets) {

                const tokenKey = new PublicKey(token);
                const address = getAssociatedTokenAddressSync(tokenKey, etfCoreAddress, true);

                try {
                    await getAccount(this.connection, address);
                } catch (e) {
                    if (e instanceof TokenAccountNotFoundError) {
                        tx.add(
                            createAssociatedTokenAccountInstruction(
                                this.wallet.publicKey,
                                address,
                                etfCoreAddress,
                                tokenKey,
                            ),
                        );
                    }
                }
            }

            const ix = await this.program.methods
                .etfCreate({
                    name,
                    symbol,
                    description,
                    url,
                    assets: assets.map(c => ({
                        token: new PublicKey(c.token),
                        weight: c.weight
                    }))
                })
                .transaction();

            tx.add(ix);

            tx.recentBlockhash = latestBlockhash.blockhash;
            tx.feePayer = this.wallet.publicKey;

            console.log('transaction created:', {
                recentBlockhash: tx.recentBlockhash,
                feePayer: tx.feePayer.toBase58(),
                instructions: tx.instructions.length
            });

            try {

                const signedTx = await this.wallet.signTransaction(tx);
                console.log('transaction signed');

                const txid = await this.connection.sendRawTransaction(signedTx.serialize(), {
                    skipPreflight: false,
                    preflightCommitment: 'confirmed',
                    maxRetries: 5
                });
                console.log('transaction sent, signed:', txid);


                const confirmation = await this.connection.confirmTransaction({
                    signature: txid,
                    blockhash: latestBlockhash.blockhash,
                    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
                }, 'confirmed');

                if (confirmation.value.err) {
                    throw new Error(`transaction confirm failed: ${JSON.stringify(confirmation.value.err)}`);
                }

                console.log('transaction confirmed');

                // get the confirmed etf info
                const confirmedEtfInfo = await this.queries.getETFInfo(etfAddress);
                if (typeof confirmedEtfInfo === 'string') {
                    throw new Error('etf create success but not found etf info');
                }

                return {
                    success: true,
                    txid,
                    data: {
                        etfAddress,
                        etfCoreAddress,
                        symbol,
                        name,
                        description,
                        creator: confirmedEtfInfo.creator,
                        assets: confirmedEtfInfo.assets
                    }
                };
            } catch (error) {
                console.error("createETF error:", error);
                throw error;
            }
        } catch (error) {
            console.error("Error details:", error);
            if (error instanceof Error) {
                console.error("Error stack:", error.stack);
            }
            return {
                success: false,
                txid: '',
                error: error instanceof Error ? error.message : 'meet an unknown error when create ETF'
            };
        }
    }

    public async purchaseETF(params: MintETFTokenParams): Promise<MintETFResult> {
        try {

            const etfAddress = new PublicKey(params.etfAddress);
            validateMintETFParams(params);

            const etfInfo = await checkETFExists(this, etfAddress);

            // check if the user has the required tokens
            await checkATAExists(this, etfInfo.assets.map(item => item.token), this.wallet.publicKey);

            // check if the contract has the required tokens, if the token is pda, need to pass the true 
            await checkATAExists(this, etfInfo.assets.map(item => item.token), etfInfo.etfCoreAddress, true);

            for (const item of etfInfo.assets) {
                const requiredAmount = (params.lamports * item.weight) / 10000;
                await checkBalance(this, item.token, this.wallet.publicKey, requiredAmount);
            }


            const latestBlockhash = await this.connection.getLatestBlockhash('confirmed');
            console.log('get the latest blockhash:', latestBlockhash.blockhash);

            const remainingAccounts = etfInfo.assets.flatMap((item) => {
                const userATA = getAssociatedTokenAddressSync(item.token, this.wallet.publicKey);
                const contractATA = getAssociatedTokenAddressSync(item.token, etfInfo.etfCoreAddress, true);
                return [userATA, contractATA];
            });

            const ix = await this.program.methods
                .etfMint(new anchor.BN(params.lamports))
                .accounts({
                    etfTokenMintAccount: params.etfAddress,
                    authority: this.wallet.publicKey,
                })
                .remainingAccounts(
                    remainingAccounts.map((item) => ({ pubkey: item, isSigner: false, isWritable: true })),
                )
                .transaction();

            const modifyComputeUnits = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
                units: 400_000,
            });

            let tx = new Transaction().add(ix).add(modifyComputeUnits);

            tx.recentBlockhash = latestBlockhash.blockhash;
            tx.feePayer = this.wallet.publicKey;

            console.log('transaction created:', {
                recentBlockhash: tx.recentBlockhash,
                feePayer: tx.feePayer.toBase58(),
                instructions: tx.instructions.length
            });

            try {

                const signedTx = await this.wallet.signTransaction(tx);
                console.log('transaction signed');

                // 发送交易
                const txid = await this.connection.sendRawTransaction(signedTx.serialize(), {
                    skipPreflight: false,
                    preflightCommitment: 'confirmed',
                    maxRetries: 5
                });
                console.log('transaction sent, signed:', txid);

                const confirmation = await this.connection.confirmTransaction({
                    signature: txid,
                    blockhash: latestBlockhash.blockhash,
                    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
                }, 'confirmed');

                if (confirmation.value.err) {
                    throw new Error(`transaction confirm failed: ${JSON.stringify(confirmation.value.err)}`);
                }

                console.log('transaction confirmed');

                // get the mint tokenata account
                const mintTokenataAccount = getAssociatedTokenAddressSync(etfAddress, this.wallet.publicKey);
                const balance = await this.queries.getETFBalance(etfAddress, this.wallet.publicKey);

                return {
                    success: true,
                    txid,
                    data: {
                        mintTokenataAccount,
                        balance,
                        etfAddress: params.etfAddress
                    }
                };
            } catch (error) {
                console.error("purchaseETF error:", error);
                throw error;
            }
        } catch (error) {
            console.error("Error in purchaseETF:");
            console.error("Error details:", error);
            if (error instanceof Error) {
                console.error("Error name:", error.name);
                console.error("Error message:", error.message);
                console.error("Error stack:", error.stack);
            }
            if (error instanceof anchor.web3.SendTransactionError) {
                console.error("Transaction error logs:", error.logs);
            }
            return {
                success: false,
                txid: '',
                error: error instanceof Error ? error.message : 'Mint ETF Token 时发生未知错误'
            };
        }
    }

    public async burnETF(params: ETFBurnParams): Promise<MintETFResult> {
        try {
            const etfAddress = new PublicKey(params.etfAddress);
            const etfInfo = await checkETFExists(this, etfAddress);

            const tokensToCheck = [...etfInfo.assets.map(item => item.token), etfAddress];
            await checkATAExists(this, tokensToCheck, this.wallet.publicKey);
            await checkBalance(this, etfAddress, this.wallet.publicKey, params.lamports as number);


            const latestBlockhash = await this.connection.getLatestBlockhash('confirmed');
            console.log('get the latest blockhash:', latestBlockhash.blockhash);

            const remainingAccounts = etfInfo.assets.flatMap((item) => {
                const userATA = getAssociatedTokenAddressSync(item.token, this.wallet.publicKey);
                const contractATA = getAssociatedTokenAddressSync(item.token, etfInfo.etfCoreAddress, true);
                return [
                    userATA,
                    contractATA,
                ];
            });

            const ix = await this.program.methods
                .etfBurn(new anchor.BN(params.lamports as number))
                .accounts({
                    etfTokenMintAccount: params.etfAddress,
                    authority: this.wallet.publicKey,
                })
                .remainingAccounts(
                    remainingAccounts.map((item) => ({ pubkey: item, isSigner: false, isWritable: true })),
                )
                .transaction();

            const modifyComputeUnits = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
                units: 400_000,
            });

            let tx = new Transaction().add(ix).add(modifyComputeUnits);

            tx.recentBlockhash = latestBlockhash.blockhash;
            tx.feePayer = this.wallet.publicKey;

            console.log('transaction created:', {
                recentBlockhash: tx.recentBlockhash,
                feePayer: tx.feePayer.toBase58(),
                instructions: tx.instructions.length
            });

            try {

                const signedTx = await this.wallet.signTransaction(tx);
                console.log('transaction signed');

                const txid = await this.connection.sendRawTransaction(signedTx.serialize(), {
                    skipPreflight: false,
                    preflightCommitment: 'confirmed',
                    maxRetries: 5
                });
                console.log('transaction sent, signed:', txid);

                const confirmation = await this.connection.confirmTransaction({
                    signature: txid,
                    blockhash: latestBlockhash.blockhash,
                    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
                }, 'confirmed');

                if (confirmation.value.err) {
                    throw new Error(`transaction confirm failed: ${JSON.stringify(confirmation.value.err)}`);
                }

                console.log('transaction confirmed');

                const mintTokenataAccount = getAssociatedTokenAddressSync(etfAddress, this.wallet.publicKey);
                const balance = await this.queries.getETFBalance(etfAddress, this.wallet.publicKey);

                return {
                    success: true,
                    txid,
                    data: {
                        mintTokenataAccount,
                        balance,
                        etfAddress: params.etfAddress
                    }
                };
            } catch (error) {
                console.error("burnETF error:", error);
                throw error;
            }
        } catch (error) {
            console.error("Error in burnETF:");
            console.error("Error details:", error);
            if (error instanceof Error) {
                console.error("Error name:", error.name);
                console.error("Error message:", error.message);
                console.error("Error stack:", error.stack);
            }
            if (error instanceof anchor.web3.SendTransactionError) {
                console.error("Transaction error logs:", error.logs);
            }
            return {
                success: false,
                txid: '',
                error: error instanceof Error ? error.message : 'Burn ETF Token 时发生未知错误'
            };
        }
    }

}

