import { Contract, ContractRunner } from 'ethers';
import KAMI721CABI from '../abis/KAMI721C/KAMI721C.json';
import { colorLog, logStyles, formatKeyValue } from '../utils/console-colors';
import { ethers } from 'ethers';

/**
 * Royalty data structure
 */
export interface RoyaltyData {
	receiver: string;
	feeNumerator: bigint;
}

/**
 * KAMI721C contract wrapper class
 */
export class KAMI721C {
	private contract: Contract;

	/**
	 * Creates a new instance of the KAMI721C contract wrapper
	 * @param runner An ethers.js ContractRunner (Provider or Signer)
	 * @param contractAddress The address of the KAMI721C contract
	 */
	constructor(runner: ContractRunner, contractAddress: string) {
		// Use the ABI directly from the imported JSON
		this.contract = new Contract(contractAddress, KAMI721CABI.abi, runner);
	}

	/**
	 * Connect a signer to the contract
	 * @param signer The signer to connect
	 * @returns A new instance of the KAMI721C contract with the connected signer
	 */
	connect(signer: ContractRunner): KAMI721C {
		return new KAMI721C(signer, this.contract.target as string);
	}

	/**
	 * Get the contract address
	 * @returns The contract address
	 */
	getAddress(): string {
		return this.contract.target as string;
	}

	/**
	 * Get the contract name
	 * @returns The name of the NFT collection
	 */
	async name(): Promise<string> {
		return await this.contract.name();
	}

	/**
	 * Get the contract symbol
	 * @returns The symbol of the NFT collection
	 */
	async symbol(): Promise<string> {
		return await this.contract.symbol();
	}

	/**
	 * Get the total supply of tokens
	 * @returns The total number of tokens minted
	 */
	async totalSupply(): Promise<bigint> {
		try {
			// Try to call totalSupply directly first
			return await this.contract.totalSupply();
		} catch (error) {
			try {
				// If totalSupply is not available, estimate using token counter
				// In the new contract, we can check the current token ID counter
				const tokenIdCounter = await this.contract._tokenIdCounter();
				return tokenIdCounter;
			} catch (error) {
				// If both methods fail, return 0 as no tokens have been minted yet
				return 0n;
			}
		}
	}

	/**
	 * Get the token URI for a specific token ID
	 * @param tokenId The ID of the token to query
	 * @returns The token URI
	 */
	async tokenURI(tokenId: number | bigint): Promise<string> {
		return await this.contract.tokenURI(tokenId);
	}

	/**
	 * Get the owner of a specific token
	 * @param tokenId The ID of the token to query
	 * @returns The address of the token owner
	 */
	async ownerOf(tokenId: number | bigint): Promise<string> {
		return await this.contract.ownerOf(tokenId);
	}

	/**
	 * Get the balance of an address
	 * @param owner The address to query
	 * @returns The number of tokens owned by the address
	 */
	async balanceOf(owner: string): Promise<bigint> {
		return await this.contract.balanceOf(owner);
	}

	/**
	 * Get the current mint price
	 * @returns The mint price in USDC (with 6 decimals)
	 */
	async mintPrice(): Promise<bigint> {
		return await this.contract.mintPrice();
	}

	/**
	 * Set the mint price (requires OWNER_ROLE)
	 * @param newMintPrice The new mint price in USDC (with 6 decimals)
	 * @returns The transaction
	 */
	async setMintPrice(newMintPrice: bigint | string) {
		return await this.contract.setMintPrice(newMintPrice);
	}

	/**
	 * Mint a new token (requires USDC approval)
	 * @returns The transaction
	 */
	async mint() {
		return await this.contract.mint();
	}

	/**
	 * Set royalties for all newly minted tokens (requires OWNER_ROLE)
	 * @param royalties Array of royalty receivers and fee numerators
	 * @returns The transaction
	 */
	async setMintRoyalties(royalties: RoyaltyData[]) {
		return await this.contract.setMintRoyalties(royalties);
	}

	/**
	 * Set royalties for a specific token's mint event (requires OWNER_ROLE)
	 * @param tokenId The ID of the token to set royalties for
	 * @param royalties Array of royalty receivers and fee numerators
	 * @returns The transaction
	 */
	async setTokenMintRoyalties(tokenId: number | bigint, royalties: RoyaltyData[]) {
		return await this.contract.setTokenMintRoyalties(tokenId, royalties);
	}

	/**
	 * Set global transfer royalties for all tokens (requires OWNER_ROLE)
	 * @param royalties Array of royalty receivers and fee numerators
	 * @returns The transaction
	 */
	async setTransferRoyalties(royalties: RoyaltyData[]) {
		return await this.contract.setTransferRoyalties(royalties);
	}

	/**
	 * Set transfer royalties for a specific token (requires OWNER_ROLE)
	 * @param tokenId The ID of the token to set royalties for
	 * @param royalties Array of royalty receivers and fee numerators
	 * @returns The transaction
	 */
	async setTokenTransferRoyalties(tokenId: number | bigint, royalties: RoyaltyData[]) {
		return await this.contract.setTokenTransferRoyalties(tokenId, royalties);
	}

	/**
	 * Get the current royalty percentage for transfers
	 * @returns The royalty percentage in basis points (e.g., 1000 = 10%)
	 */
	async royaltyPercentage(): Promise<number> {
		return await this.contract.royaltyPercentage();
	}

	/**
	 * Set the royalty percentage for transfers (requires OWNER_ROLE)
	 * @param newRoyaltyPercentage New royalty percentage in basis points (e.g., 1000 = 10%)
	 * @returns The transaction
	 */
	async setRoyaltyPercentage(newRoyaltyPercentage: number) {
		return await this.contract.setRoyaltyPercentage(newRoyaltyPercentage);
	}

	/**
	 * Get the platform commission details
	 * @returns The platform commission percentage and address
	 */
	async getPlatformCommission(): Promise<{ percentage: number; address: string }> {
		const percentage = await this.contract.platformCommissionPercentage();
		const address = await this.contract.platformAddress();
		return { percentage, address };
	}

	/**
	 * Set the platform commission details (requires OWNER_ROLE)
	 * @param newPercentage New commission percentage in basis points (e.g., 500 = 5%)
	 * @param newAddress New platform address to receive commission
	 * @returns The transaction
	 */
	async setPlatformCommission(newPercentage: number, newAddress: string) {
		return await this.contract.setPlatformCommission(newPercentage, newAddress);
	}

	/**
	 * Sell a token to another address with royalties handled automatically
	 * @param to The buyer address
	 * @param tokenId The token ID to sell
	 * @param salePrice The sale price in USDC
	 * @returns The transaction
	 */
	async sellToken(to: string, tokenId: number | bigint, salePrice: bigint | string) {
		try {
			// Verify the caller owns the token
			const owner = await this.ownerOf(tokenId);
			const signerAddress = await (this.contract.runner as ethers.Signer).getAddress();

			if (owner.toLowerCase() !== signerAddress.toLowerCase()) {
				colorLog.error(
					`Error: Only the token owner can sell. Current owner: ${logStyles.address(owner)}, Caller: ${logStyles.address(
						signerAddress
					)}`
				);
				throw new Error('Only the token owner can sell this token');
			}

			// Verify that the contract is approved to transfer the token
			const isApproved = await this.contract.isApprovedForAll(signerAddress, this.getAddress());
			if (!isApproved) {
				colorLog.warning(`Contract is not approved to transfer tokens. Please call setApprovalForAll first.`);
			}

			// Verify USDC allowance for the buyer
			try {
				const usdcAddress = await this.contract.usdcToken();
				const usdcABI = [
					'function allowance(address owner, address spender) external view returns (uint256)',
					'function balanceOf(address owner) external view returns (uint256)',
				];
				const usdc = new ethers.Contract(usdcAddress, usdcABI, this.contract.runner);

				const allowance = await usdc.allowance(to, this.getAddress());
				if (allowance < salePrice) {
					colorLog.warning(
						`Buyer has insufficient USDC allowance. Required: ${ethers.formatUnits(
							salePrice,
							6
						)}, Current: ${ethers.formatUnits(allowance, 6)}`
					);
				}

				const balance = await usdc.balanceOf(to);
				if (balance < salePrice) {
					colorLog.warning(
						`Buyer has insufficient USDC balance. Required: ${ethers.formatUnits(salePrice, 6)}, Current: ${ethers.formatUnits(
							balance,
							6
						)}`
					);
				}
			} catch (error) {
				colorLog.warning(`Could not verify USDC allowance: ${error}`);
			}

			// Now call the sellToken function
			colorLog.info(
				`Calling sellToken with parameters: to=${logStyles.address(to)}, tokenId=${logStyles.value(
					tokenId.toString()
				)}, salePrice=${logStyles.value(salePrice.toString())}`
			);
			return await this.contract.sellToken(to, tokenId, salePrice);
		} catch (error: any) {
			colorLog.error(`sellToken failed: ${error.message}`);
			throw error;
		}
	}

	/**
	 * Get royalty information for a token sale
	 * @param tokenId The ID of the token being sold
	 * @param salePrice The sale price
	 * @returns The royalty receiver address and amount
	 */
	async royaltyInfo(tokenId: number | bigint, salePrice: bigint | string): Promise<{ receiver: string; royaltyAmount: bigint }> {
		const [receiver, royaltyAmount] = await this.contract.royaltyInfo(tokenId, salePrice);
		return { receiver, royaltyAmount };
	}

	/**
	 * Get mint royalty receivers for a token
	 * @param tokenId The ID of the token
	 * @returns Array of royalty data
	 */
	async getMintRoyaltyReceivers(tokenId: number | bigint): Promise<RoyaltyData[]> {
		return await this.contract.getMintRoyaltyReceivers(tokenId);
	}

	/**
	 * Get transfer royalty receivers for a token
	 * @param tokenId The ID of the token
	 * @returns Array of royalty data
	 */
	async getTransferRoyaltyReceivers(tokenId: number | bigint): Promise<RoyaltyData[]> {
		return await this.contract.getTransferRoyaltyReceivers(tokenId);
	}

	/**
	 * Check if an address has a specific role
	 * @param role The role to check (OWNER_ROLE, PLATFORM_ROLE, or RENTER_ROLE)
	 * @param address The address to check
	 * @returns True if the address has the role
	 */
	async hasRole(role: string, address: string): Promise<boolean> {
		return await this.contract.hasRole(role, address);
	}

	/**
	 * Grant a role to an address (requires DEFAULT_ADMIN_ROLE)
	 * @param role The role to grant (OWNER_ROLE, PLATFORM_ROLE, or RENTER_ROLE)
	 * @param address The address to grant the role to
	 * @returns The transaction
	 */
	async grantRole(role: string, address: string) {
		return await this.contract.grantRole(role, address);
	}

	/**
	 * Revoke a role from an address (requires DEFAULT_ADMIN_ROLE)
	 * @param role The role to revoke (OWNER_ROLE, PLATFORM_ROLE, or RENTER_ROLE)
	 * @param address The address to revoke the role from
	 * @returns The transaction
	 */
	async revokeRole(role: string, address: string) {
		return await this.contract.revokeRole(role, address);
	}

	/**
	 * Get the OWNER_ROLE constant
	 * @returns The OWNER_ROLE bytes32 value
	 */
	async OWNER_ROLE(): Promise<string> {
		return await this.contract.OWNER_ROLE();
	}

	/**
	 * Get the PLATFORM_ROLE constant
	 * @returns The PLATFORM_ROLE bytes32 value
	 */
	async PLATFORM_ROLE(): Promise<string> {
		return await this.contract.PLATFORM_ROLE();
	}

	/**
	 * Get the RENTER_ROLE constant
	 * @returns The RENTER_ROLE bytes32 value
	 */
	async RENTER_ROLE(): Promise<string> {
		return await this.contract.RENTER_ROLE();
	}

	/**
	 * Set the base URI for tokens (requires OWNER_ROLE)
	 * @param baseURI The new base URI
	 * @returns The transaction
	 */
	async setBaseURI(baseURI: string) {
		return await this.contract.setBaseURI(baseURI);
	}

	/**
	 * Burn a token (requires token owner permission)
	 * @param tokenId The ID of the token to burn
	 * @returns The transaction
	 */
	async burn(tokenId: number | bigint) {
		return await this.contract.burn(tokenId);
	}

	/**
	 * Set the security policy for the contract (requires OWNER_ROLE)
	 * @param securityLevel The security level
	 * @param operatorWhitelistId The operator whitelist ID
	 * @param permittedContractReceiversAllowlistId The permitted contract receivers allowlist ID
	 * @returns The transaction
	 */
	async setSecurityPolicy(securityLevel: number, operatorWhitelistId: number, permittedContractReceiversAllowlistId: number) {
		return await this.contract.setSecurityPolicy(securityLevel, operatorWhitelistId, permittedContractReceiversAllowlistId);
	}
}
