import assert from "assert";
import BigNumber from "bignumber.js";

import { BN, utils } from "@project-serum/anchor";
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import {
	Connection,
	LAMPORTS_PER_SOL,
	PublicKey,
	Signer,
	Transaction,
	VersionedTransaction,
} from "@solana/web3.js";

import { SOLMASH_SEEDS, TEN_BIGNUM } from "./constants";
import { AuctionInfo } from "./definitions/auctionInfo";
import { BuyerInfo } from "./definitions/buyerInfo";
import { SolmashWhitelistInstructions } from "./instructions";
import { SolmashWhitelistPayload } from "./payload";
import { getDecimals } from "./utils";

export class SolmashWhitelistService {
	private readonly _programId: PublicKey;

	static deriveAuctionAddress(auctionName: string, programId: string | PublicKey): PublicKey {
		const _programId = new PublicKey(programId);

		const [auction] = PublicKey.findProgramAddressSync(
			[
				Buffer.from(utils.bytes.utf8.encode(SOLMASH_SEEDS.auctionSeed)),
				Buffer.from(utils.bytes.utf8.encode(auctionName)),
			],
			_programId,
		);

		return auction;
	}

	static deriveAuctionVaultAddress(
		auction: string | PublicKey,
		programId: string | PublicKey,
	): PublicKey {
		const _auction = new PublicKey(auction);
		const _programId = new PublicKey(programId);

		const [auctionVault] = PublicKey.findProgramAddressSync(
			[Buffer.from(utils.bytes.utf8.encode(SOLMASH_SEEDS.auctionVaultSeed)), _auction.toBuffer()],
			_programId,
		);
		return auctionVault;
	}

	static deriveBuyerPdaAddress(
		auction: string | PublicKey,
		buyer: string | PublicKey,
		programId: string | PublicKey,
	): PublicKey {
		const _auction = new PublicKey(auction);
		const _buyer = new PublicKey(buyer);
		const _programId = new PublicKey(programId);

		const [buyerPda] = PublicKey.findProgramAddressSync(
			[
				Buffer.from(utils.bytes.utf8.encode(SOLMASH_SEEDS.buyerSeed)),
				_buyer.toBuffer(),
				_auction.toBuffer(),
			],
			_programId,
		);

		return buyerPda;
	}

	constructor(
		readonly instructions: SolmashWhitelistInstructions,
		readonly _connection: Connection,
		private readonly _signTransaction: <T extends Transaction | VersionedTransaction>(
			transaction: T,
		) => Promise<T>,
	) {
		this._programId = this.instructions.program.programId;
	}

	private _createPayload(tx: Transaction, signers: Signer[]) {
		const errorMap: Map<number, string> = new Map();
		for (let i = 0; i < this.instructions.program.idl.errors.length; i++) {
			const error = this.instructions.program.idl.errors[i];
			errorMap.set(error.code, error.msg);
		}

		return new SolmashWhitelistPayload(
			this._connection,
			this._signTransaction,
			errorMap,
			tx,
			signers,
		);
	}

	async initAuction(params: {
		creatorAddress: string | PublicKey;
		auctionTokenAddress: string | PublicKey;
		bidTokenAddress: string | PublicKey;
		claimTime: number;
		enabled: boolean;
		name: string;
		preSaleEndTime: number;
		preSaleStartTime: number;
		ticketPriceInBid: string;
		ticketPriceInSol: string;
		ticketsInPool: string;
		tokenQuantityPerTicket: string;
	}) {
		const creator = new PublicKey(params.creatorAddress);
		const auctionToken = new PublicKey(params.auctionTokenAddress);
		const bidToken = new PublicKey(params.bidTokenAddress);
		const auction = SolmashWhitelistService.deriveAuctionAddress(params.name, this._programId);
		console.debug("auction:", auction.toString());
		const auctionVault = SolmashWhitelistService.deriveAuctionVaultAddress(
			auction,
			this._programId,
		);
		console.debug("auction vault:", auctionVault.toString());
		const auctionVaultTokenAccount = getAssociatedTokenAddressSync(
			auctionToken,
			auctionVault,
			true,
		);

		const bidTokenDecimals = await getDecimals(this._connection, bidToken);
		const auctionTokenDecimals = await getDecimals(this._connection, auctionToken);

		const ticketPriceInSol = BigNumber(params.ticketPriceInSol).times(LAMPORTS_PER_SOL).toFixed(0);
		const ticketPriceInBid = BigNumber(params.ticketPriceInBid)
			.times(TEN_BIGNUM.pow(bidTokenDecimals))
			.toFixed(0);
		const tokenQuantityPerTicket = BigNumber(params.tokenQuantityPerTicket)
			.times(TEN_BIGNUM.pow(auctionTokenDecimals))
			.toFixed(0);

		const ix = await this.instructions.getInitAuctionInstruction(
			auction,
			auctionVault,
			auctionVaultTokenAccount,
			creator,
			{
				auctionToken,
				bidToken,
				claimTime: new BN(params.claimTime),
				enabled: params.enabled,
				name: params.name,
				preSaleEndTime: new BN(params.preSaleEndTime),
				preSaleStartTime: new BN(params.preSaleStartTime),
				ticketPriceInBid: new BN(ticketPriceInBid),
				ticketPriceInSol: new BN(ticketPriceInSol),
				ticketsInPool: new BN(params.ticketsInPool),
				tokenQuantityPerTicket: new BN(tokenQuantityPerTicket),
			},
		);

		const { blockhash, lastValidBlockHeight } = await this._connection.getLatestBlockhash();

		const tx = new Transaction({
			feePayer: creator,
			blockhash,
			lastValidBlockHeight,
		}).add(ix);

		return this._createPayload(tx, []);
	}

	async addToken(params: {
		auctionAddress: string | PublicKey;
		creatorAddress: string | PublicKey;
	}) {
		const creator = new PublicKey(params.creatorAddress);
		const auction = new PublicKey(params.auctionAddress);
		const auctionData = await this.instructions.program.account.auction.fetch(auction, "confirmed");
		const auctionToken = auctionData.auctionToken;
		const auctionVault = SolmashWhitelistService.deriveAuctionVaultAddress(
			auction,
			this._programId,
		);
		const auctionVaultTokenAccount = getAssociatedTokenAddressSync(
			auctionToken,
			auctionVault,
			true,
		);
		const creatorAuctionTokenAccount = getAssociatedTokenAddressSync(auctionToken, creator);

		const ix = await this.instructions.getAddTokenInstruction(
			auction,
			auctionToken,
			auctionVault,
			auctionVaultTokenAccount,
			creator,
			creatorAuctionTokenAccount,
		);
		const { blockhash, lastValidBlockHeight } = await this._connection.getLatestBlockhash();

		const tx = new Transaction({
			feePayer: creator,
			blockhash,
			lastValidBlockHeight,
		}).add(ix);

		return this._createPayload(tx, []);
	}

	async withdrawFunds(params: {
		auctionAddress: string | PublicKey;
		creatorAddress: string | PublicKey;
		withdrawSol: boolean;
	}) {
		const { auctionAddress, creatorAddress, withdrawSol } = params;
		const auction = new PublicKey(auctionAddress);
		const auctionData = await this.instructions.program.account.auction.fetch(auction, "confirmed");
		const auctionToken = auctionData.auctionToken;
		const bidToken = auctionData.bidToken;
		const creator = new PublicKey(creatorAddress);
		const auctionVault = SolmashWhitelistService.deriveAuctionVaultAddress(
			auction,
			this._programId,
		);
		const auctionBidTokenAccount = getAssociatedTokenAddressSync(bidToken, auctionVault, true);
		const auctionVaultTokenAccount = getAssociatedTokenAddressSync(
			auctionToken,
			auctionVault,
			true,
		);
		const createAuctionTokenAccount = getAssociatedTokenAddressSync(auctionToken, creator);
		const creatorBidTokenAccount = getAssociatedTokenAddressSync(bidToken, creator);

		const ix = await this.instructions.getWithdrawFundsInstruction(
			auction,
			auctionBidTokenAccount,
			auctionToken,
			auctionVault,
			auctionVaultTokenAccount,
			bidToken,
			creator,
			createAuctionTokenAccount,
			creatorBidTokenAccount,
			withdrawSol,
		);

		const { blockhash, lastValidBlockHeight } = await this._connection.getLatestBlockhash();

		const tx = new Transaction({
			feePayer: creator,
			blockhash,
			lastValidBlockHeight,
		});

		tx.add(ix);

		return this._createPayload(tx, []);
	}

	async whitelist(params: {
		auctionAddress: string;
		creatorAddress: string;
		whitelistUserAddressList: string[];
	}) {
		const { auctionAddress, creatorAddress, whitelistUserAddressList } = params;

		const auction = new PublicKey(auctionAddress);
		const creator = new PublicKey(creatorAddress);

		assert(whitelistUserAddressList.length, "Empty whitelist user list");

		const ixs = await Promise.all(
			whitelistUserAddressList.map(async (user) => {
				const whitelistUser = new PublicKey(user);
				const buyerPda = SolmashWhitelistService.deriveBuyerPdaAddress(
					auction,
					whitelistUser,
					this._programId,
				);
				const ix = await this.instructions.getWhitelistInstruction(
					auction,
					buyerPda,
					creator,
					whitelistUser,
				);

				return ix;
			}),
		);

		const { blockhash, lastValidBlockHeight } = await this._connection.getLatestBlockhash();

		const tx = new Transaction({
			feePayer: creator,
			blockhash,
			lastValidBlockHeight,
		}).add(...ixs);

		return this._createPayload(tx, []);
	}

	// async preSaleBuyUsingSpl(params: {
	// 	auctionAddress: string;
	// 	bidTokenAddress: string;
	// 	buyerAddress: string;
	// }) {
	// 	const { auctionAddress, bidTokenAddress, buyerAddress } = params;

	// 	const auction = new PublicKey(auctionAddress);
	// 	const bidToken = new PublicKey(bidTokenAddress);
	// 	const buyer = new PublicKey(buyerAddress);
	// 	const auctionVault = SolmashWhitelistService.deriveAuctionVaultAddress(
	// 		auction,
	// 		this._programId,
	// 	);
	// 	const buyerPda = SolmashWhitelistService.deriveBuyerPdaAddress(auction, buyer, this._programId);
	// 	const auctionVaultBidTokenAccount = getAssociatedTokenAddressSync(bidToken, auctionVault, true);
	// 	const buyerBidTokenAccount = getAssociatedTokenAddressSync(bidToken, buyer);

	// 	const auctionVaultBidTokenAccountInfo = await this._connection.getAccountInfo(
	// 		auctionVaultBidTokenAccount,
	// 		"confirmed",
	// 	);

	// 	const ix = await this.instructions.getPreSaleBuyUsingSplInstruction(
	// 		auction,
	// 		auctionVault,
	// 		auctionVaultBidTokenAccount,
	// 		bidToken,
	// 		buyer,
	// 		buyerBidTokenAccount,
	// 		buyerPda,
	// 	);

	// 	const { blockhash, lastValidBlockHeight } = await this._connection.getLatestBlockhash();

	// 	const tx = new Transaction({
	// 		feePayer: buyer,
	// 		blockhash,
	// 		lastValidBlockHeight,
	// 	});

	// 	if (!auctionVaultBidTokenAccountInfo) {
	// 		tx.add(
	// 			createAssociatedTokenAccountInstruction(
	// 				buyer,
	// 				auctionVaultBidTokenAccount,
	// 				auctionVault,
	// 				bidToken,
	// 			),
	// 		);
	// 	}

	// 	tx.add(ix);

	// 	return this._createPayload(tx, []);
	// }

	async preSaleBuyUsingSol(params: { auctionAddress: string; buyerAddress: string }) {
		const { auctionAddress, buyerAddress } = params;
		const auction = new PublicKey(auctionAddress);
		const buyer = new PublicKey(buyerAddress);

		const auctionVault = SolmashWhitelistService.deriveAuctionVaultAddress(
			auction,
			this._programId,
		);
		const buyerPda = SolmashWhitelistService.deriveBuyerPdaAddress(auction, buyer, this._programId);

		const ix = await this.instructions.getPreSaleBuyUsingSolInstruction(
			auction,
			auctionVault,
			buyer,
			buyerPda,
		);

		const { blockhash, lastValidBlockHeight } = await this._connection.getLatestBlockhash();

		const tx = new Transaction({
			feePayer: buyer,
			blockhash,
			lastValidBlockHeight,
		}).add(ix);

		return this._createPayload(tx, []);
	}

	async claimTokens(params: { auctionAddress: string; buyerAddress: string }) {
		const { auctionAddress, buyerAddress } = params;

		const auction = new PublicKey(auctionAddress);
		const auctionData = await this.instructions.program.account.auction.fetch(auction, "confirmed");
		const auctionToken = auctionData.auctionToken;
		const buyer = new PublicKey(buyerAddress);
		const auctionVault = SolmashWhitelistService.deriveAuctionVaultAddress(
			auction,
			this._programId,
		);
		const buyerPda = SolmashWhitelistService.deriveBuyerPdaAddress(auction, buyer, this._programId);
		const auctionVaultTokenAccount = getAssociatedTokenAddressSync(
			auctionToken,
			auctionVault,
			true,
		);
		const buyerAuctionTokenAccount = getAssociatedTokenAddressSync(auctionToken, buyer);

		const ix = await this.instructions.getClaimTokensInstruction(
			auction,
			auctionToken,
			auctionVault,
			auctionVaultTokenAccount,
			buyer,
			buyerAuctionTokenAccount,
			buyerPda,
		);

		const { blockhash, lastValidBlockHeight } = await this._connection.getLatestBlockhash();

		const tx = new Transaction({
			feePayer: buyer,
			blockhash,
			lastValidBlockHeight,
		});

		tx.add(ix);

		return this._createPayload(tx, []);
	}

	async changeTime(params: {
		auctionAddress: string;
		creatorAddress: string;
		newStartTime: number;
		newEndTime: number;
	}) {
		const auction = new PublicKey(params.auctionAddress);
		const creator = new PublicKey(params.creatorAddress);
		const newStartTime = new BN(params.newStartTime);
		const newEndTime = new BN(params.newEndTime);

		const ix = await this.instructions.getChangeTimeInstruction(
			auction,
			creator,
			newStartTime,
			newEndTime,
		);
		const { blockhash, lastValidBlockHeight } = await this._connection.getLatestBlockhash();

		const tx = new Transaction({
			feePayer: creator,
			blockhash,
			lastValidBlockHeight,
		}).add(ix);

		return this._createPayload(tx, []);
	}

	async getAuctionInfo(auctionAddress: string): Promise<AuctionInfo> {
		const auction = new PublicKey(auctionAddress);
		const decoded = await this.instructions.program.account.auction.fetch(auction);

		const bidTokenDecimals = await getDecimals(this._connection, decoded.bidToken);
		const auctionTokenDecimals = await getDecimals(this._connection, decoded.auctionToken);

		const accumulatedBidToken = BigNumber(decoded.accumulatedBid.toString())
			.div(TEN_BIGNUM.pow(bidTokenDecimals))
			.toFixed();
		const accumulatedSol = BigNumber(decoded.accumulatedSol.toString())
			.div(LAMPORTS_PER_SOL)
			.toFixed();
		const ticketPriceInBid = BigNumber(decoded.ticketPriceInBid.toString())
			.div(TEN_BIGNUM.pow(bidTokenDecimals))
			.toFixed();
		const ticketPriceInSol = BigNumber(decoded.ticketPriceInSol.toString())
			.div(LAMPORTS_PER_SOL)
			.toFixed();
		const tokenQuantityPerTicket = BigNumber(decoded.tokenQuantityPerTicket.toString())
			.div(TEN_BIGNUM.pow(auctionTokenDecimals))
			.toFixed();

		return {
			accumulatedBidToken,
			accumulatedSol,
			auctionToken: decoded.auctionToken.toString(),
			bidToken: decoded.bidToken.toString(),
			enabled: decoded.enabled,
			name: decoded.name,
			owner: decoded.owner.toString(),
			preSaleEndTime: decoded.preSaleEndTime.toNumber(),
			preSaleStartTime: decoded.preSaleStartTime.toNumber(),
			ticketPriceInBid,
			ticketPriceInSol,
			ticketsInPool: decoded.ticketsInPool.toString(),
			tokenQuantityPerTicket,
			claimTime: decoded.claimTime.toNumber(),
			remainingTickets: decoded.remainingTickets.toString(),
			ticketsClaimed: decoded.ticketsClaimed.toString(),
		};
	}

	async getBuyerInfo(auctionAddress: string, buyerAddress: string): Promise<BuyerInfo> {
		const auction = new PublicKey(auctionAddress);
		const buyer = new PublicKey(buyerAddress);
		const buyerPda = SolmashWhitelistService.deriveBuyerPdaAddress(auction, buyer, this._programId);

		const decoded = await this.instructions.program.account.buyer.fetch(buyerPda);
		return {
			auctionAddress: decoded.auctionAddress.toString(),
			buyerAddress: decoded.buyerAddress.toString(),
			claimed: decoded.claimed,
			participated: decoded.participated,
			whitelisted: decoded.whitelisted,
			blacklisted: decoded.blacklisted,
		};
	}

	async isBuyerWhitelisted(auctionAddress: string, buyerAddress: string) {
		try {
			const buyerPdaData = await this.getBuyerInfo(auctionAddress, buyerAddress);
			return buyerPdaData.whitelisted;
		} catch (err) {
			return false;
		}
	}

	async buyerParticipated(auctionAddress: string, buyerAddress: string) {
		try {
			const info = await this.getBuyerInfo(auctionAddress, buyerAddress);
			return info.participated;
		} catch (err) {
			return false;
		}
	}

	async buyerClaimed(auctionAddress: string, buyerAddress: string) {
		try {
			const info = await this.getBuyerInfo(auctionAddress, buyerAddress);
			return info.claimed;
		} catch (err) {
			return false;
		}
	}
}
