// Core dependencies
import { AnchorProvider, Program } from "@coral-xyz/anchor";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { AccountInfo, Connection, GetProgramAccountsFilter, GetProgramAccountsResponse, Keypair, PublicKey, TransactionSignature } from "@solana/web3.js";
import { Metaplex } from "@metaplex-foundation/js";

// Local imports
import { IDL } from "../idl/idl";
import { AgentsProgram } from "../idl/types";
import { parse, stringify, v4 } from "uuid";
import { AGENTS_PROGRAM_ID, EVENT_AUTHORITY_SEED, FEES_VAULT_SEED } from "../constants";
import { AGENTS_STATE_SIZE, AgentState, parseAgentState, ParsedAgentState } from "../state/agent";
import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes";


export function getAgentsProgram(
    connection: Connection,
): Program<AgentsProgram> {
    const program: Program<AgentsProgram> = new Program(
        IDL,
        new AnchorProvider(
            connection,
            new NodeWallet(Keypair.generate())
        )
    );
    return program;
}

export function getAgentStateAccount(
    agent: PublicKey,
): PublicKey {
    return PublicKey.findProgramAddressSync(
        [Uint8Array.from(agent.toBuffer())],
        AGENTS_PROGRAM_ID,
    )[0];
}

export function getAgent(
    agentUuid: number[],
): PublicKey {
    return PublicKey.findProgramAddressSync(
        [Uint8Array.from(agentUuid)],
        AGENTS_PROGRAM_ID,
    )[0];
}

export function getEventAuthority(program: Program<AgentsProgram>) {
    const [eventAuthority] = PublicKey.findProgramAddressSync(
        [Buffer.from(EVENT_AUTHORITY_SEED)],
        program.programId
    );
    return eventAuthority;
}

export function getFeesVault(program: Program<AgentsProgram>) {
    const [feeVault] = PublicKey.findProgramAddressSync(
        [Buffer.from(FEES_VAULT_SEED)],
        program.programId
    );
    return feeVault;
}

export function getMetadataAccount(
    tokenMint: PublicKey
): PublicKey {
    const metaplex = Metaplex.make(new Connection("https://api.devnet.solana.com"));
    return metaplex.nfts().pdas().metadata({ mint: tokenMint });
}

export function getAta(
    wallet: PublicKey,
    tokenMint: PublicKey,
): PublicKey {
    return getAssociatedTokenAddressSync(tokenMint, wallet, true);
}

export function getRandomSeed(): number[] {
    return Array.from(parse(v4()));
}

export function getStringFromSeed(
    seed: number[],
): string {
    return stringify(Uint8Array.from(seed));
}

export async function getAccountInfos(
    connection: Connection,
    keys: PublicKey[],
): Promise<(AccountInfo<Buffer> | null)[]> {
    const allAccounts: (AccountInfo<Buffer>|null)[] = [];
    const batchSize = 100;
    for (let i = 0; i < keys.length; i += batchSize) {
        const batch = keys.slice(i, i + batchSize);
        const batchAccounts = await connection.getMultipleAccountsInfo(batch);
        allAccounts.push(...batchAccounts);
    }
    return allAccounts;
}

export async function getAllAgents(
    program: Program<AgentsProgram>,
): Promise<ParsedAgentState[]> {
    const accounts: AgentState[] = (await program.account.agent.all()).map(account => account.account);
    return accounts.map(account => parseAgentState(account));
}

export async function getAgentsByCreator(
    program: Program<AgentsProgram>,
    creator: PublicKey,
): Promise<ParsedAgentState[]> {
    const accountFilters: GetProgramAccountsFilter[] = [
        {
            dataSize: AGENTS_STATE_SIZE + 8,
        },
        {
            memcmp: {
                offset: 8 + 32 + 16,
                bytes: creator.toBase58(),
            },
        }
    ]
    const accounts: GetProgramAccountsResponse = await program.provider.connection
        .getProgramAccounts(
            AGENTS_PROGRAM_ID,
            {
                commitment: "confirmed",
                filters: accountFilters,
                encoding: 'base64'
            }
        );
    const agents: AgentState[] = accounts.map(account => 
        program.coder.accounts.decode("agent", account.account.data)
    );
    return await Promise.all(agents.map(agent => parseAgentState(agent)));
}

export async function getEventsFromTx(
    connection: Connection,
    program: Program<AgentsProgram>,
    txId: TransactionSignature,
): Promise<any[]> {
    const parsed = await connection.getParsedTransaction(
        txId,
        { maxSupportedTransactionVersion: 0, commitment: "confirmed"}
    ).catch((_) => null);
    if (!parsed || !parsed.meta || parsed.meta.err || !parsed.meta.innerInstructions)
      throw new Error("Transaction is invalid");
    const eventAuthority = getEventAuthority(program);
    let rawEvents: any[] = [];
    for (let i = 0; i < parsed.meta.innerInstructions.length; i++) {
        for (let j = 0; j < parsed.meta.innerInstructions[i].instructions.length; j++) {
            let ix = parsed.meta.innerInstructions[i].instructions[j];
            if (!ix.programId.equals(program.programId)) continue;
            //@ts-ignore
            let accounts = ix.accounts;
            //@ts-ignore
            let data = ix.data;
            if (!accounts || !data || accounts.length != 1) continue;
            if (!accounts[0].equals(eventAuthority)) continue;
            let buffer = bs58.decode(data);
            let str64 = buffer.subarray(8, buffer.length).toString("base64");
            const rawEvent = program.coder.events.decode(str64);
            if (!rawEvent) continue;
            rawEvents.push(rawEvent);
        }
    }
    return rawEvents;
}

export async function getTopUpBalanceEventFromTx(
    connection: Connection,
    program: Program<AgentsProgram>,
    txId: TransactionSignature,
): Promise<{
    user: string,
    mint: string,
    amount: number,
}> {
    const rawEvents = await getEventsFromTx(connection, program, txId);
    for (let i = 0; i < rawEvents.length; i++) {
        let rawEvent = rawEvents[i];
        if (rawEvent.name !== "topUpBalanceEvent") continue;
        let parsedEvent = {
            user: rawEvent.data.user.toBase58(),
            amount: parseInt(rawEvent.data.amount.toString()),
            mint: rawEvent.data.mint.toBase58(),
        };
        return parsedEvent;
    }
    throw new Error("Top up balance event not found");
}

export async function getCreateAgentEventFromTx(
    connection: Connection,
    program: Program<AgentsProgram>,
    txId: TransactionSignature,
): Promise<{
    agent: string,
    agentState: string,
    agentUuid: string,
    creator: string,
}> {
    const rawEvents = await getEventsFromTx(connection, program, txId);
    for (let i = 0; i < rawEvents.length; i++) {
        let rawEvent = rawEvents[i];
        if (rawEvent.name !== "createAgentEvent") continue;
        let parsedEvent = {
            agent: rawEvent.data.agent.toBase58(),
            agentState: rawEvent.data.stateAddress.toBase58(),
            agentUuid: getStringFromSeed(rawEvent.data.agentUuid),
            creator: rawEvent.data.creator.toBase58(),
        };
        return parsedEvent;
    }
    throw new Error("Create agent event not found");
}
