import { ethers } from 'ethers';

declare enum Tier {
  One,
  Two,
  Three,
  Four,
  Five,
  Six,
  Seven,
}

declare enum Mode {
  Fizz,
  Provider,
}

interface OrderDetails {
  maxPrice: bigint;
  numOfBlocks: bigint;
  token: string;
  spec: string;
  version: number | bigint;
  mode: Mode;
  tier: Tier[];
}

declare enum OrderState {
  OPEN = 'open',
  PROVISIONED = 'provisioned',
  CLOSED = 'closed',
  MATCHED = 'matched',
}

interface OrderSpecs {
  specs: string;
  version: string;
  mode: string;
  tier: Tier[];
}

interface InitialOrder {
  id: number;
  name: string;
  region: string;
  maxPrice: number;
  numOfBlocks: number;
  token?: {
    symbol?: string;
    decimal?: number;
    address: string;
  };
  creator: string;
  state: OrderState;
  specs: OrderSpecs;
}

interface OrderMatchedEvent {
  orderId: string;
  providerAddress: string;
  providerId: string | number | bigint;
  acceptedPrice: string | number | bigint;
  creatorAddress: string;
}

interface OrderUpdatedEvent {
  orderId: string;
  providerAddress: string;
  tenantAddress: string;
  acceptedPrice: string | number | bigint;
}

interface OrderUpdateAcceptedEvent {
  orderId: string;
  providerAddress: string;
}

interface RpcProvider {
  HTTP_URL: string;
  WSS_URL: string;
}

type NetworkType = 'testnet' | 'mainnet';

declare class OrderModule {
  private provider: ethers.Provider;
  private websocketProvider?: ethers.WebSocketProvider;
  private createTimeoutId: NodeJS.Timeout | null;
  private updateTimeoutId: NodeJS.Timeout | null;
  private wallet: ethers.Wallet | undefined;

  constructor(
    provider: ethers.Provider,
    websocketProvider?: ethers.WebSocketProvider,
    wallet?: ethers.Wallet
  ) {
    this.provider = provider;
    this.websocketProvider = websocketProvider;
    this.createTimeoutId = null;
    this.updateTimeoutId = null;
    this.wallet = wallet;
  }

  async createOrder(orderDetails: OrderDetails): Promise<ethers.ContractTransactionReceipt | null> {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contract = new ethers.Contract(OrderRequest, OrderRequestAbi, signer);

      const tx: ethers.ContractTransactionResponse = await contract.createOrder(orderDetails);
      const receipt: ethers.ContractTransactionReceipt | null = await tx.wait();
      return receipt;
    } catch (error) {
      const errorMessage = handleContractError(error, OrderRequestAbi);
      throw errorMessage;
    }
  }

  async updateOrder(
    orderId: string,
    orderDetails: OrderDetails
  ): Promise<ethers.ContractTransactionReceipt | null> {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contract = new ethers.Contract(OrderRequest, OrderRequestAbi, signer);

      const tx: ethers.ContractTransactionResponse = await contract.updateInitialOrder(
        orderId,
        orderDetails
      );
      const receipt: ethers.ContractTransactionReceipt | null = await tx.wait();
      return receipt;
    } catch (error) {
      const errorMessage = handleContractError(error, OrderRequestAbi);
      throw errorMessage;
    }
  }

  async getOrderDetails(leaseId: string): Promise<InitialOrder> {
    const contractAbi = OrderRequestAbi;
    const contractAddress = OrderRequest;

    const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
    const response = await contract.getOrderById(leaseId);

    const specs = {
      specs: response.specs.specs,
      version: response.specs.version,
      mode: response.specs.mode,
      tier: response.specs.tier.map((t: bigint) => Number(t)) as Tier[],
    };

    const tokenDetails = getTokenDetails(response.token, 'testnet');
    const token = {
      symbol: tokenDetails?.symbol,
      decimal: tokenDetails?.decimal,
      address: tokenDetails?.address,
    };

    return {
      id: response.id.toString(),
      maxPrice: Number(response.maxPrice),
      numOfBlocks: Number(response.numOfBlocks),
      token,
      creator: response.creator,
      state: getOrderStateAsString(response.state),
      specs,
    } as InitialOrder;
  }

  async listenToOrderCreated(
    timeoutTime = 60000,
    onSuccessCallback: (
      orderId: string,
      providerAddress: string,
      providerId: string | number | bigint,
      acceptedPrice: string | number | bigint,
      creatorAddress: string
    ) => void,
    onFailureCallback: () => void
  ): Promise<OrderMatchedEvent> {
    if (!this.websocketProvider) {
      throw new Error('Please pass websocket provider in constructor');
    }
    const { signer } = await initializeSigner({ wallet: this.wallet });
    const account = await signer.getAddress();

    const contractAbi = BidAbi;
    const contractAddress = Bid;

    const contract = new ethers.Contract(contractAddress, contractAbi, this.websocketProvider);

    return new Promise((resolve, reject) => {
      this.createTimeoutId = setTimeout(() => {
        contract.off('OrderMatched');
        onFailureCallback();
        reject({ error: true, msg: 'Order creation failed' });
      }, timeoutTime);

      contract.on(
        'OrderMatched',
        (
          orderId: string,
          providerAddress: string,
          providerId: string | number | bigint,
          acceptedPrice: string | number | bigint,
          creatorAddress: string
        ) => {
          if (creatorAddress.toString().toLowerCase() === account.toString().toLowerCase()) {
            onSuccessCallback(orderId, providerAddress, providerId, acceptedPrice, creatorAddress);
            this.websocketProvider?.destroy();
            contract.off('OrderMatched');
            clearTimeout(this.createTimeoutId as NodeJS.Timeout);
            resolve({ orderId, providerAddress, providerId, acceptedPrice, creatorAddress });
          }
        }
      );
    });
  }

  async listenToOrderUpdated(
    timeoutTime = 60000,
    onSuccessCallback: (
      orderId: string,
      providerAddress: string,
      tenantAddress?: string,
      acceptedPrice?: string
    ) => void,
    onFailureCallback: () => void
  ): Promise<OrderUpdatedEvent> {
    if (!this.websocketProvider) {
      throw new Error('Please pass websocket provider in constructor');
    }

    const { signer } = await initializeSigner({ wallet: this.wallet });
    const account = await signer.getAddress();

    const contractAbi = BidAbi;
    const contractAddress = Bid;

    const contract = new ethers.Contract(contractAddress, contractAbi, this.websocketProvider);

    return new Promise((resolve, reject) => {
      this.updateTimeoutId = setTimeout(() => {
        contract.off('LeaseUpdated');
        onFailureCallback();
        reject({ error: true, msg: 'Order updation Failed' });
      }, timeoutTime);

      contract.on('LeaseUpdated', (orderId, providerAddress, tenantAddress, acceptedPrice) => {
        if (tenantAddress.toString().toLowerCase() === account.toString().toLowerCase()) {
          onSuccessCallback(orderId, providerAddress, tenantAddress, acceptedPrice?.toString());
          this.websocketProvider?.destroy();
          contract.off('LeaseUpdated');
          clearTimeout(this.updateTimeoutId as NodeJS.Timeout);
          resolve({ orderId, providerAddress, tenantAddress, acceptedPrice });
        }
      });
    });
  }

  async listenToOrderUpdateAccepted(
    timeoutTime = 60000,
    onSuccessCallback: (orderId: string, providerAddress: string) => void,
    onFailureCallback: () => void
  ): Promise<OrderUpdateAcceptedEvent> {
    if (!this.websocketProvider) {
      throw new Error('Please pass websocket provider in constructor');
    }

    const { signer } = await initializeSigner({ wallet: this.wallet });
    const account = await signer.getAddress();

    const contractAbi = BidAbi;
    const contractAddress = Bid;

    const contract = new ethers.Contract(contractAddress, contractAbi, this.websocketProvider);

    return new Promise((resolve, reject) => {
      this.updateTimeoutId = setTimeout(() => {
        contract.off('UpdateRequestAccepted');
        onFailureCallback();
        reject({ error: true, msg: 'Order updation Failed' });
      }, timeoutTime);

      contract.on('UpdateRequestAccepted', (orderId, providerAddress, tenantAddress) => {
        if (tenantAddress.toString().toLowerCase() === account.toString().toLowerCase()) {
          onSuccessCallback(orderId, providerAddress);
          this.websocketProvider?.destroy();
          contract.off('UpdateRequestAccepted');
          clearTimeout(this.updateTimeoutId as NodeJS.Timeout);
          resolve({ orderId, providerAddress });
        }
      });
    });
  }
}

declare enum LeaseState {
  ACTIVE = 'active',
  TERMINATED = 'terminated',
}

interface Lease {
  leaseId: string;
  fizzId: string;
  requestId: string;
  acceptedPrice: number;
  providerAddress: string;
  tenantAddress: string;
  startBlock: string;
  startTime: number;
  endTime: number;
  state: LeaseState;
}

interface LeaseWithOrderDetails extends Lease {
  name: string;
  region: string;
  tier: Tier[];
  token: {
    symbol?: string;
    decimal?: number;
  };
}

interface FizzParams {
  providerId: bigint;
  spec: string;
  walletAddress: string;
  paymentsAccepted: string[];
  rewardWallet: string;
}

type RawFizzNode = [
  bigint, // fizzId
  bigint, // providerId
  string, // spec
  string, // walletAddress
  string[], // paymentsAccepted
  bigint, // status
  bigint, // joinTimestamp
  string // rewardWallet
];

interface FizzNode {
  fizzId: bigint;
  providerId: bigint;
  region: string;
  spec: string;
  walletAddress: string;
  paymentsAccepted: string[];
  status: number;
  joinTimestamp: bigint;
  rewardWallet: string;
}

interface FizzDetails {
  region: string;
  providerId: bigint;
  spec: string;
  walletAddress: string;
  paymentsAccepted: string[];
  status: bigint;
  joinTimestamp: bigint;
  rewardWallet: string;
}

interface ResourceCategory {
  name: string;
  registry: string;
  baseReward: bigint;
}

interface Resource {
  name: string;
  tier: string;
  multiplier: bigint;
}

interface ResourceAttributes {
  cpuUnits: bigint;
  cpuAttributes: string[];
  ramUnits: bigint;
  ramAttributes: string[];
  gpuUnits: bigint;
  gpuAttributes: string[];
  endpointsKind: number;
  endpointsSequenceNumber: number;
}

interface FizzLease {
  leaseId: bigint;
  fizzId: bigint;
  requestId: bigint;
  resourceAttribute: ResourceAttributes;
  acceptedPrice: bigint;
  providerAddress: string;
  tenantAddress: string;
  startBlock: bigint;
  startTime: bigint;
  endTime: bigint;
  state: string;
}

declare enum FizzProviderStatus {
  Unregistered,
  Registered,
  Active,
  Maintenance,
  Suspended,
  Deactivated,
}

declare enum FizzProviderTrustTier {
  One,
  Two,
  Three,
  Four,
  Five,
  Six,
  Seven,
}

interface FizzProvider {
  providerId: bigint;
  name: string;
  region: string;
  walletAddress: string;
  paymentsAccepted: string[];
  spec: string;
  hostUri: string;
  certificate: string;
  status: FizzProviderStatus;
  tier: FizzProviderTrustTier;
  joinTimestamp: bigint;
  rewardWallet: string;
}

interface FizzAttribute {
  id: bigint | string;
  units: bigint | string;
}

type RawFizzAttribute = [id: string, units: string];

interface IProvider {
  spec: string;
  hostUri: string;
  certificate: string;
  paymentsAccepted: string[];
  status: string;
  trust: number;
  timestamp: number;
}

interface Attribute {
  id: bigint | string;
  units: bigint | string;
}

declare enum ProviderStatus {
  Unregistered,
  Registered,
  Active,
  Maintenance,
  Suspended,
  Deactivated,
}

declare enum ProviderTrustTier {
  One,
  Two,
  Three,
  Four,
  Five,
  Six,
  Seven,
}

interface Provider {
  providerId?: bigint;
  name: string;
  region: string;
  spec?: string;
  walletAddress: string;
  paymentsAccepted: string[];
  hostUri: string;
  certificate: string;
  status: ProviderStatus;
  tier: ProviderTrustTier;
  joinTimestamp: bigint;
  rewardWallet: string;
}

type Category = 'CPU' | 'GPU';

type RawProviderAttribute = [id: string, units: string];

declare class ProviderModule {
  private provider: ethers.Provider;

  constructor(provider: ethers.Provider) {
    this.provider = provider;
  }

  async getProviderDetails(providerAddress: string): Promise<IProvider> {
    if (!providerAddress) {
      throw new Error('Pass Provider Address');
    }

    if (!isValidEthereumAddress(providerAddress)) {
      throw new Error('Pass Valid Address');
    }
    try {
      const contractAbi = ProviderRegistryAbi;
      const contractAddress = ProviderRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
      const response = await contract.getProviderByAddress(providerAddress);

      const providerDetailsData: IProvider = {
        spec: response[0],
        hostUri: response[1],
        certificate: response[2],
        paymentsAccepted: response[3],
        status: response[4].toString(),
        trust: Number(response[5].toString()) + 1,
        timestamp: Number(response[6].toString()),
      };

      return providerDetailsData;
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderRegistryAbi);
      throw errorMessage;
    }
  }

  async getProviderPendingAttributes(providerAddress: string, category: Category) {
    if (!providerAddress) {
      throw new Error('Pass Provider Address');
    }

    if (!isValidEthereumAddress(providerAddress)) {
      throw new Error('Pass Valid Address');
    }

    if (!category) {
      throw new Error('Please pass a category');
    }
    try {
      const contractAbi = ProviderAttributeRegistryAbi;
      const contractAddress = ProviderAttributeRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
      const response = await contract.getProviderPendingAttributes(providerAddress, category);

      return response;
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderAttributeRegistryAbi);
      throw errorMessage;
    }
  }

  async getProviderAttributes(providerAddress: string, category: Category) {
    if (!providerAddress) {
      throw new Error('Pass Provider Address');
    }

    if (!isValidEthereumAddress(providerAddress)) {
      throw new Error('Pass Valid Address');
    }

    if (!category) {
      throw new Error('Please pass a category');
    }
    try {
      const contractAbi = ProviderAttributeRegistryAbi;
      const contractAddress = ProviderAttributeRegistry;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
      const response = await contract.getProviderAttributes(providerAddress, category);

      return response;
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderAttributeRegistryAbi);
      throw errorMessage;
    }
  }

  async getProvider(providerId: bigint): Promise<Provider> {
    try {
      const contractAddress = ProviderRegistry;
      const contractAbi = ProviderRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
      const providerData = await contract.getProvider(providerId);

      let name, region;
      try {
        const { Name, Region } = JSON.parse(providerData.spec);
        name = Name;
        region = Region;
      } catch {
        try {
          const { Name, Region } = decompressProviderSpec(providerData.spec) as {
            Name: string;
            Region: string;
          };
          name = Name;
          region = Region;
        } catch {
          name = '';
          region = '';
        }
      }

      return {
        name,
        region,
        spec: providerData.spec,
        hostUri: providerData.hostUri,
        certificate: providerData.certificate,
        paymentsAccepted: providerData.paymentsAccepted,
        status: providerData.status,
        tier: providerData.tier,
        joinTimestamp: providerData.joinTimestamp,
        walletAddress: providerData.walletAddress,
        rewardWallet: providerData.rewardWallet,
      };
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderRegistryAbi);
      throw errorMessage;
    }
  }

  async getProviderByAddress(walletAddress: string): Promise<Provider> {
    try {
      const contractAddress = ProviderRegistry;
      const contractAbi = ProviderRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const providerData = await contract.getProviderByAddress(walletAddress);

      let name, region;
      try {
        const { Name, Region } = JSON.parse(providerData.spec);
        name = Name;
        region = Region;
      } catch {
        try {
          const { Name, Region } = decompressProviderSpec(providerData.spec) as {
            Name: string;
            Region: string;
          };
          name = Name;
          region = Region;
        } catch {
          name = '';
          region = '';
        }
      }

      return {
        name,
        region,
        spec: providerData.spec,
        hostUri: providerData.hostUri,
        certificate: providerData.certificate,
        paymentsAccepted: providerData.paymentsAccepted,
        status: providerData.status,
        tier: providerData.tier,
        joinTimestamp: providerData.joinTimestamp,
        walletAddress,
        rewardWallet: providerData.rewardWallet,
      };
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderRegistryAbi);
      throw errorMessage;
    }
  }

  async getAllProviders(): Promise<Provider[]> {
    try {
      const contractAddress = ProviderRegistry;
      const contractAbi = ProviderRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
      const providersData = await contract.getAllProviders();

      const providers: Provider[] = providersData.map((provider: Provider) => {
        let name, region;
        try {
          const { Name, Region } = JSON.parse(provider.spec as string);
          name = Name;
          region = Region;
        } catch {
          try {
            const { Name, Region } = decompressProviderSpec(provider.spec as string) as {
              Name: string;
              Region: string;
            };
            name = Name;
            region = Region;
          } catch {
            name = '';
            region = '';
          }
        }
        return {
          name,
          region,
          providerId: (provider.providerId as bigint).toString(),
          walletAddress: provider.walletAddress,
          paymentsAccepted: provider.paymentsAccepted,
          spec: provider.spec,
          hostUri: provider.hostUri,
          certificate: provider.certificate,
          status: ProviderStatus[provider.status],
          tier: Number(provider.tier.toString()),
          // tier: ProviderTrustTier[provider.tier],
          joinTimestamp: Number(provider.joinTimestamp.toString()),
          rewardWallet: provider.rewardWallet,
        };
      });

      return providers;
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderRegistryAbi);
      throw errorMessage;
    }
  }

  async getAttributes(providerAddress: string, category: string): Promise<Attribute[]> {
    try {
      const contractAddress = ProviderAttributeRegistry;
      const contractAbi = ProviderAttributeRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const attributes: RawProviderAttribute[] = await contract.getAttributes(
        providerAddress,
        category
      );

      const decoratedAttributes = attributes.map((attr: RawProviderAttribute) => ({
        id: attr[0],
        units: attr[1],
      }));
      return decoratedAttributes;
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderRegistryAbi);
      throw errorMessage;
    }
  }

  async getPendingAttributes(providerAddress: string, category: string): Promise<Attribute[]> {
    try {
      const contractAddress = ProviderAttributeRegistry;
      const contractAbi = ProviderAttributeRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const attributes: RawProviderAttribute[] = await contract.getPendingAttributes(
        providerAddress,
        category
      );

      const decoratedAttributes = attributes.map((attr: RawProviderAttribute) => ({
        id: attr[0],
        units: attr[1],
      }));
      return decoratedAttributes;
    } catch (error) {
      const errorMessage = handleContractError(error, ProviderRegistryAbi);
      throw errorMessage;
    }
  }
}

declare class FizzModule {
  private provider: ethers.Provider;
  private webSocketProvider: ethers.WebSocketProvider | undefined;
  private timeoutId: NodeJS.Timeout | undefined;
  private wallet: ethers.Wallet | undefined;
  private providerModule: ProviderModule;

  constructor(
    provider: ethers.Provider,
    webSocketProvider?: ethers.WebSocketProvider,
    wallet?: ethers.Wallet
  ) {
    this.provider = provider;
    this.webSocketProvider = webSocketProvider;
    this.wallet = wallet;
    this.providerModule = new ProviderModule(provider);
  }

  async addFizzNode(fizzParams: FizzParams): Promise<unknown> {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = FizzRegistryTestnet;
      const abi = FizzRegistryAbi;
      const contract = new ethers.Contract(contractAddress, abi, signer);

      const tx = await contract.addFizzNode(fizzParams);
      const receipt = await tx.wait();

      return { tx, receipt };
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async updateFizzName(newName: string): Promise<unknown> {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      // Contract address (hardcoded or retrieved from an environment variable)
      const contractAddress = FizzRegistryTestnet;
      const abi = FizzRegistryAbi;
      const contract = new ethers.Contract(contractAddress, abi, signer);

      const tx = await contract.updateFizzName(newName);

      const receipt = await tx.wait();

      return { tx, receipt };
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async getFizzById(fizzId: bigint): Promise<FizzDetails> {
    try {
      const contractAddress = FizzRegistryTestnet;
      const contractAbi = FizzRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const fizzDetails = await contract.getFizz(fizzId);

      const {
        providerId,
        spec,
        paymentsAccepted,
        status,
        joinTimestamp,
        walletAddress,
        rewardWallet,
      } = fizzDetails;

      return {
        region: spec?.split(',')?.[7] ?? '',
        providerId,
        spec,
        paymentsAccepted,
        status,
        joinTimestamp,
        walletAddress,
        rewardWallet,
      };
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async getFizzNodeByAddress(walletAddress: string) {
    try {
      const contractAddress = FizzRegistryTestnet;
      const contractAbi = FizzRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const fizzId = await contract.addressToFizzId(walletAddress);
      const fizzNode: FizzDetails = await this.getFizzById(fizzId);

      const result: FizzNode = {
        region: fizzNode.spec?.split(',')?.[7] ?? '',
        fizzId,
        providerId: fizzNode.providerId,
        spec: fizzNode.spec,
        walletAddress: fizzNode.walletAddress,
        paymentsAccepted: fizzNode.paymentsAccepted,
        status: Number(fizzNode.status.toString()),
        joinTimestamp: fizzNode.joinTimestamp,
        rewardWallet: fizzNode.rewardWallet,
      };

      return result;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async getTotalFizzNodes(): Promise<bigint> {
    try {
      const contractAddress = FizzRegistryTestnet;
      const contractAbi = FizzRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
      const fizzCounter = await contract.nextFizzId();
      return fizzCounter;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async getAllFizzNodes(): Promise<FizzNode[]> {
    try {
      const contractAddress = FizzRegistryTestnet;
      const contractAbi = FizzRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const allFizzNodes = await contract.getAllFizzNodes();

      const fizzNodes: FizzNode[] = allFizzNodes.map((fizzNode: RawFizzNode) => ({
        fizzId: fizzNode[0],
        providerId: fizzNode[1],
        spec: fizzNode[2],
        walletAddress: fizzNode[3],
        paymentsAccepted: fizzNode[4],
        status: fizzNode[5],
        joinTimestamp: fizzNode[6],
        rewardWallet: fizzNode[7],
      }));

      return fizzNodes;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async getAttributes(providerAddress: string, category: string): Promise<FizzAttribute[]> {
    try {
      const contractAddress = FizzAttributeRegistryTestnet;
      const contractAbi = FizzAttributeRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const attributes: RawFizzAttribute[] = await contract.getAttributes(
        providerAddress,
        category
      );

      const decoratedAttributes = attributes.map((attr: RawFizzAttribute) => ({
        id: attr[0],
        units: attr[1],
      }));
      return decoratedAttributes;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzAttributeRegistryAbi);
      throw errorMessage;
    }
  }

  async getPendingAttributes(providerAddress: string, category: string): Promise<FizzAttribute[]> {
    try {
      const contractAddress = FizzAttributeRegistryTestnet;
      const contractAbi = FizzAttributeRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const attributes: RawFizzAttribute[] = await contract.getPendingAttributes(
        providerAddress,
        category
      );

      const decoratedAttributes = attributes.map((attr: RawFizzAttribute) => ({
        id: attr[0],
        units: attr[1],
      }));
      return decoratedAttributes;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzAttributeRegistryAbi);
      throw errorMessage;
    }
  }

  async getResource(resourceID: bigint, category: string): Promise<Resource> {
    try {
      const contractAbi = ResourceRegistryAbi;
      const contractAddress =
        category === 'CPU' ? ResourceRegistryCPUTestnet : ResourceRegistryGPUTestnet;

      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const [name, tier, multiplier]: [string, string, bigint] = await contract.getResource(
        resourceID
      );

      const resource: Resource = { name, tier, multiplier };

      return resource;
    } catch (error) {
      const errorMessage = handleContractError(error, ResourceRegistryAbi);
      throw errorMessage;
    }
  }

  async getFizzLeases(
    fizzId: bigint,
    providerId: bigint,
    state?: string
  ): Promise<FizzLease[] | unknown> {
    try {
      const providerData = await this.providerModule.getProvider(providerId);
      const walletAddress = providerData.walletAddress;

      const leaseContractAddress = ComputeLeaseTestnet;
      const leaseContractAbi = ComputeLeaseAbi;
      const leaseContract = new ethers.Contract(
        leaseContractAddress,
        leaseContractAbi,
        this.provider
      );
      const [activeLeases, allLeases] = await leaseContract.getProviderLeases(walletAddress);

      const selectedLeases = state === 'ACTIVE' ? activeLeases : allLeases;
      const leases: FizzLease[] = [];

      for (const leaseId of selectedLeases) {
        const leaseData = await leaseContract.leases(leaseId);

        // Filter by fizzId
        if (leaseData.fizzId === fizzId) {
          const lease: FizzLease = {
            leaseId: leaseData.leaseId,
            fizzId: leaseData.fizzId,
            requestId: leaseData.requestId,
            resourceAttribute: leaseData.resourceAttributes,
            acceptedPrice: leaseData.acceptedPrice,
            providerAddress: leaseData.providerAddress,
            tenantAddress: leaseData.tenantAddress,
            startBlock: leaseData.startBlock,
            startTime: leaseData.startTime,
            endTime: leaseData.endTime,
            state: leaseData.state,
          };

          leases.push(lease);
        }
      }

      return leases;
    } catch (error) {
      const errorMessage = handleContractError(error, ComputeLeaseAbi);
      throw errorMessage;
    }
  }

  async listenToFizzCreated(
    onSuccessCallback: (fizzId: bigint, walletAddress: string) => void,
    onFailureCallback: () => void,
    timeoutTime = 60000
  ) {
    const contractAddress = FizzRegistryTestnet;
    const contractAbi = FizzRegistryAbi;

    try {
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const contract = new ethers.Contract(contractAddress, contractAbi, this.webSocketProvider);

      return new Promise((resolve, reject) => {
        this.timeoutId = setTimeout(() => {
          contract.off('FizzNodeAdded');
          onFailureCallback();
          reject({ error: true, msg: 'Fizz creation failed' });
        }, timeoutTime);

        contract.on('FizzNodeAdded', (fizzId: bigint, walletAddress: string) => {
          if (walletAddress.toString().toLowerCase() === accounts[0].toString().toLowerCase()) {
            onSuccessCallback(fizzId, walletAddress);
            this.webSocketProvider?.destroy();
            contract.off('FizzNodeAdded');
            clearTimeout(this.timeoutId as NodeJS.Timeout);
            resolve({ fizzId, walletAddress });
          }
        });
      });
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async updateFizzSpecs(specs: string) {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = FizzRegistryTestnet;
      const contractAbi = FizzRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, signer);

      const tx = await contract.updateFizzSpec(specs);

      const receipt = await tx.wait();

      return receipt;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async listenSpecUpdated(
    onSuccessCallback: (fizzId: bigint, specs: string, walletAddress: string) => void,
    onFailureCallback: () => void,
    timeoutTime = 60000
  ) {
    const contractAddress = FizzRegistryTestnet;
    const contractAbi = FizzRegistryAbi;

    try {
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const contract = new ethers.Contract(contractAddress, contractAbi, this.webSocketProvider);

      let timeoutId: NodeJS.Timeout | undefined;

      return new Promise((resolve, reject) => {
        timeoutId = setTimeout(() => {
          contract.off('FizzNodeSpecUpdated');
          onFailureCallback();
          reject({ error: true, msg: 'Fizz update failed' });
        }, timeoutTime);

        contract.on('FizzNodeSpecUpdated', async (fizzId: bigint, specs: string) => {
          const fizz: FizzDetails = await this.getFizzById(fizzId);
          if (
            fizz.walletAddress.toString().toLowerCase() === accounts[0].toString().toLowerCase()
          ) {
            onSuccessCallback(fizzId, specs, fizz.walletAddress);
            this.webSocketProvider?.destroy();
            contract.off('FizzNodeSpecUpdated');
            clearTimeout(timeoutId as NodeJS.Timeout);
            resolve({ fizzId, specs, walletAddress: fizz.walletAddress });
          }
        });
      });
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async updateFizzRegion(region: string) {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = FizzRegistryTestnet;
      const contractAbi = FizzRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, signer);

      const tx = await contract.updateFizzRegion(region);

      const receipt = await tx.wait();

      return receipt;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async listenRegionUpdated(
    onSuccessCallback: (fizzId: bigint, region: string, walletAddress: string) => void,
    onFailureCallback: () => void,
    timeoutTime = 60000
  ) {
    const contractAddress = FizzRegistryTestnet;
    const contractAbi = FizzRegistryAbi;

    try {
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const contract = new ethers.Contract(contractAddress, contractAbi, this.webSocketProvider);

      let timeoutId: NodeJS.Timeout | undefined;

      return new Promise((resolve, reject) => {
        timeoutId = setTimeout(() => {
          contract.off('FizzNodeRegionUpdated');
          onFailureCallback();
          reject({ error: true, msg: 'Fizz update failed' });
        }, timeoutTime);

        contract.on('FizzNodeRegionUpdated', async (fizzId: bigint, region: string) => {
          const fizz: FizzDetails = await this.getFizzById(fizzId);
          if (
            fizz.walletAddress.toString().toLowerCase() === accounts[0].toString().toLowerCase()
          ) {
            onSuccessCallback(fizzId, region, fizz.walletAddress);
            this.webSocketProvider?.destroy();
            contract.off('FizzNodeRegionUpdated');
            clearTimeout(timeoutId as NodeJS.Timeout);
            resolve({ fizzId, region, walletAddress: fizz.walletAddress });
          }
        });
      });
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async updateFizzProvider(providerId: bigint) {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = FizzRegistryTestnet;
      const contractAbi = FizzRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, signer);

      const tx = await contract.updateFizzProviderId(providerId);

      const receipt = await tx.wait();

      return receipt;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async listenProviderUpdated(
    onSuccessCallback: (fizzId: bigint, providerId: bigint, walletAddress: string) => void,
    onFailureCallback: () => void,
    timeoutTime = 60000
  ) {
    const contractAddress = FizzRegistryTestnet;
    const contractAbi = FizzRegistryAbi;

    try {
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const contract = new ethers.Contract(contractAddress, contractAbi, this.webSocketProvider);

      let timeoutId: NodeJS.Timeout | undefined;

      return new Promise((resolve, reject) => {
        timeoutId = setTimeout(() => {
          contract.off('FizzNodeProviderIdUpdated');
          onFailureCallback();
          reject({ error: true, msg: 'Fizz update failed' });
        }, timeoutTime);

        contract.on('FizzNodeProviderIdUpdated', async (fizzId: bigint, providerId: bigint) => {
          const fizz: FizzDetails = await this.getFizzById(fizzId);
          if (
            fizz.walletAddress.toString().toLowerCase() === accounts[0].toString().toLowerCase()
          ) {
            onSuccessCallback(fizzId, providerId, fizz.walletAddress);
            this.webSocketProvider?.destroy();
            contract.off('FizzNodeProviderIdUpdated');
            clearTimeout(timeoutId as NodeJS.Timeout);
            resolve({ fizzId, providerId, walletAddress: fizz.walletAddress });
          }
        });
      });
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async addAcceptedPayment(tokenAddress: string) {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = FizzRegistryTestnet;
      const contractAbi = FizzRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, signer);

      const tx = await contract.addAcceptedPayment(tokenAddress);

      const receipt = await tx.wait();

      return receipt;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async listenToAddAcceptedPayment(
    onSuccessCallback: (fizzId: bigint, tokenAddress: string, walletAddress: string) => void,
    onFailureCallback: () => void,
    timeoutTime = 60000
  ) {
    const contractAddress = FizzRegistryTestnet;
    const contractAbi = FizzRegistryAbi;

    try {
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const contract = new ethers.Contract(contractAddress, contractAbi, this.webSocketProvider);

      let timeoutId: NodeJS.Timeout | undefined;

      return new Promise((resolve, reject) => {
        timeoutId = setTimeout(() => {
          contract.off('PaymentAdded');
          onFailureCallback();
          reject({ error: true, msg: 'Fizz update failed' });
        }, timeoutTime);

        contract.on('PaymentAdded', async (fizzId: bigint, tokenAddress: string) => {
          const fizz: FizzDetails = await this.getFizzById(fizzId);
          if (
            fizz.walletAddress.toString().toLowerCase() === accounts[0].toString().toLowerCase()
          ) {
            onSuccessCallback(fizzId, tokenAddress, fizz.walletAddress);
            this.webSocketProvider?.destroy();
            contract.off('PaymentAdded');
            clearTimeout(timeoutId as NodeJS.Timeout);
            resolve({ fizzId, tokenAddress, walletAddress: fizz.walletAddress });
          }
        });
      });
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async removeAcceptedPayment(tokenAddress: string) {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractAddress = FizzRegistryTestnet;
      const contractAbi = FizzRegistryAbi;

      const contract = new ethers.Contract(contractAddress, contractAbi, signer);

      const tx = await contract.removeAcceptedPayment(tokenAddress);

      const receipt = await tx.wait();

      return receipt;
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }

  async listenToRemoveAcceptedPayment(
    onSuccessCallback: (fizzId: bigint, tokenAddress: string, walletAddress: string) => void,
    onFailureCallback: () => void,
    timeoutTime = 60000
  ) {
    const contractAddress = FizzRegistryTestnet;
    const contractAbi = FizzRegistryAbi;

    try {
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const contract = new ethers.Contract(contractAddress, contractAbi, this.webSocketProvider);

      let timeoutId: NodeJS.Timeout | undefined;

      return new Promise((resolve, reject) => {
        timeoutId = setTimeout(() => {
          contract.off('PaymentRemoved');
          onFailureCallback();
          reject({ error: true, msg: 'Fizz update failed' });
        }, timeoutTime);

        contract.on('PaymentRemoved', async (fizzId: bigint, tokenAddress: string) => {
          const fizz: FizzDetails = await this.getFizzById(fizzId);
          if (
            fizz.walletAddress.toString().toLowerCase() === accounts[0].toString().toLowerCase()
          ) {
            onSuccessCallback(fizzId, tokenAddress, fizz.walletAddress);
            this.webSocketProvider?.destroy();
            contract.off('PaymentRemoved');
            clearTimeout(timeoutId as NodeJS.Timeout);
            resolve({ fizzId, tokenAddress, walletAddress: fizz.walletAddress });
          }
        });
      });
    } catch (error) {
      const errorMessage = handleContractError(error, FizzRegistryAbi);
      throw errorMessage;
    }
  }
}

declare class LeaseModule {
  private provider: ethers.Provider;
  private orderModule: OrderModule;
  private fizzModule: FizzModule;
  private providerModule: ProviderModule;
  private websocketProvider?: ethers.WebSocketProvider;
  private leaseCloseTimeoutId: NodeJS.Timeout | null;
  private wallet: ethers.Wallet | undefined;

  constructor(
    provider: ethers.Provider,
    websocketProvider?: ethers.WebSocketProvider,
    wallet?: ethers.Wallet
  ) {
    this.provider = provider;
    this.websocketProvider = websocketProvider;
    this.getLeaseDetails = this.getLeaseDetails.bind(this);
    this.orderModule = new OrderModule(provider);
    this.fizzModule = new FizzModule(provider, websocketProvider);
    this.providerModule = new ProviderModule(provider);
    this.leaseCloseTimeoutId = null;
    this.wallet = wallet;
  }

  async getLeaseDetails(leaseId: string) {
    const contractAbi = ComputeLeaseAbi;
    const contractAddress = ComputeLease;

    const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
    const response = await contract.leases(leaseId);

    const lease: Lease = {
      leaseId: response.leaseId.toString(),
      fizzId: response.fizzId.toString(),
      requestId: response.requestId.toString(),
      acceptedPrice: Number(response.acceptedPrice),
      providerAddress: response.providerAddress.toString(),
      tenantAddress: response.tenantAddress.toString(),
      startBlock: response.startBlock.toString(),
      startTime: Number(response.startTime),
      endTime: Number(response.endTime),
      state: getLeaseStateAsString(response.state.toString()) as LeaseState,
    };

    return lease;
  }

  async getLeaseIds(address: string) {
    const contractAbi = ComputeLeaseAbi;
    const contractAddress = ComputeLease;

    const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);
    const response = await contract.getTenantLeases(address);

    const activeLeaseIds = response[0].map((id: bigint) => id.toString()) as string[];
    const allLeaseIds = response[1].map((id: bigint) => id.toString()) as string[];

    const terminatedLeaseIds = allLeaseIds.filter((lId) => {
      return !activeLeaseIds.includes(lId);
    });

    return {
      activeLeaseIds,
      allLeaseIds,
      terminatedLeaseIds,
    };
  }

  async getLeasesByState(
    address: string,
    options?: { state?: LeaseState; page?: number; pageSize?: number }
  ) {
    const { activeLeaseIds, terminatedLeaseIds, allLeaseIds } = await this.getLeaseIds(address);

    let leaseIds = allLeaseIds;
    const totalCount = allLeaseIds.length;
    const terminatedCount = terminatedLeaseIds.length;
    const activeCount = activeLeaseIds.length;

    if (options?.state) {
      switch (options.state) {
        case LeaseState.ACTIVE:
          leaseIds = activeLeaseIds;
          break;
        case LeaseState.TERMINATED:
          leaseIds = terminatedLeaseIds;
          break;
      }
    }

    leaseIds.sort((a, b) => Number(b) - Number(a));

    if (options?.page) {
      const pageSize = options.pageSize || DEFAULT_PAGE_SIZE;
      leaseIds = leaseIds.slice((options.page - 1) * pageSize, options.page * pageSize);
    }

    const filteredLeases = await Promise.all(leaseIds.map((lId) => this.getLeaseDetails(lId)));
    const orderDetails = await Promise.all(
      leaseIds.map((lId) => this.orderModule.getOrderDetails(lId))
    );

    const leaseWithToken: LeaseWithOrderDetails[] = await Promise.all(
      filteredLeases.map(async (lease, index) => {
        const order = orderDetails[index];
        let tokenDetails;
        if (order.token?.address) tokenDetails = getTokenDetails(order.token.address, 'testnet');

        let region;
        if (lease.fizzId.toString() !== '0') {
          const fizz: FizzDetails = await this.fizzModule.getFizzById(BigInt(lease.fizzId));
          region = fizz?.region;
        } else {
          const provider: Provider = await this.providerModule.getProviderByAddress(
            lease.providerAddress
          );
          region = provider?.region;
        }

        return {
          ...lease,
          name: order.name,
          tier: order.specs.tier,
          region: region,
          token: {
            symbol: tokenDetails?.symbol,
            decimal: tokenDetails?.decimal,
          },
        };
      })
    );

    return {
      leases: leaseWithToken,
      activeCount,
      terminatedCount,
      totalCount,
    };
  }

  async closeLease(leaseId: string) {
    const contractAbi = ComputeLeaseAbi;
    const contractAddress = ComputeLease;
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });
      const contract = new ethers.Contract(contractAddress, contractAbi, signer);
      const tx = await contract.closeLease(leaseId);
      const receipt = await tx.wait();
      return receipt;
    } catch (error) {
      const errorMessage = handleContractError(error, contractAbi);
      throw errorMessage;
    }
  }

  async listenToLeaseClosedEvent(
    onSuccessCallback: ({
      orderId,
      providerAddress,
      tenantAddress,
    }: {
      orderId: string;
      providerAddress: string;
      tenantAddress: string;
    }) => void,
    onFailureCallback: () => void,
    timeout = 60000
  ) {
    if (!this.websocketProvider) {
      throw new Error('Please pass websocket provider in constructor');
    }
    const { signer } = await initializeSigner({ wallet: this.wallet });
    const account = await signer.getAddress();
    const contractAbi = ComputeLeaseAbi;
    const contractAddress = ComputeLease;

    const contract = new ethers.Contract(contractAddress, contractAbi, this.websocketProvider);

    return new Promise((resolve, reject) => {
      this.leaseCloseTimeoutId = setTimeout(() => {
        contract.off('LeaseClosed');
        onFailureCallback();
        reject({ error: true, msg: 'Lease Close Failed' });
      }, timeout);

      contract.on(
        'LeaseClosed',
        (orderId: string, providerAddress: string, tenantAddress: string) => {
          if (
            providerAddress.toString().toLowerCase() === account.toString().toLowerCase() ||
            tenantAddress.toString().toLowerCase() === account.toString().toLowerCase()
          ) {
            onSuccessCallback({ orderId, providerAddress, tenantAddress });
            this.websocketProvider?.destroy();
            contract.off('LeaseClosed');
            clearTimeout(this.leaseCloseTimeoutId as NodeJS.Timeout);
            resolve({ orderId, providerAddress, tenantAddress });
          }
        }
      );
    });
  }
}

interface ProviderDetails {
  name: string;
  region: string;
  attributes: string;
  hostUri: string;
  certificate: string;
  paymentsAccepted: string[];
  status: string;
  trust: number;
  timestamp: number;
}

declare enum TransactionStatus {
  SUCCESS = 'success',
  FAILURE = 'failure',
}

interface TransactionData {
  rewardWallet: string;
  tokenAddress: string;
  amount: number;
  decimals: number;
  onSuccessCallback?: (data: unknown) => void;
  onFailureCallback?: (data: unknown) => void;
}

interface DepositData {
  token: string;
  amount: number;
  onSuccessCallback?: (data: unknown) => void;
  onFailureCallback?: (data: unknown) => void;
}

interface TokenDetails {
  name: string;
  symbol: string;
  decimal: number;
}

interface UserBalance {
  lockedBalance: string;
  unlockedBalance: string;
  token: TokenDetails;
}

declare class EscrowModule {
  private provider: ethers.Provider;
  private wallet: ethers.Wallet | undefined;

  constructor(provider: ethers.Provider, wallet?: ethers.Wallet) {
    this.provider = provider;
    this.wallet = wallet;
  }

  async getUserBalance(token: string, walletAddress?: string, isOperator: boolean = false) {
    try {
      const contractAbi = EscrowAbi;
      const contractAddress = Escrow;
      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const tokenDetails = tokenMap[networkType].find(
        (eachToken) => eachToken.symbol.toLowerCase() === token.toLowerCase()
      );
      if (!tokenDetails) {
        throw new Error('Provided token symbol is invalid.');
      }
      const tokenAddress: string =
        tokenDetails?.address || '0x0000000000000000000000000000000000000000';

      let userWalletAddress;
      if (walletAddress) {
        userWalletAddress = walletAddress;
      } else {
        if (this.wallet) {
          userWalletAddress = await this.wallet.getAddress();
        } else {
          throw new Error('No wallet address provided');
        }
      }

      const response = await contract.getUserData(userWalletAddress, tokenAddress, isOperator);

      const userData: UserBalance = {
        lockedBalance: response[0].toString(),
        unlockedBalance: response[1].toString(),
        token: {
          name: tokenDetails?.name,
          symbol: tokenDetails?.symbol,
          decimal: tokenDetails?.decimal,
        },
      };

      return userData;
    } catch (error) {
      const errorMessage = handleContractError(error, EscrowAbi);
      throw errorMessage;
    }
  }

  // write operations
  async depositBalance({ token, amount, onSuccessCallback, onFailureCallback }: DepositData) {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractABI = EscrowAbi;
      const contractAddress = Escrow;
      const tokenABI = TokenAbi;

      const tokenDetails = tokenMap[networkType].find(
        (eachToken) => eachToken.symbol.toLowerCase() === token.toLowerCase()
      );
      if (!tokenDetails) {
        throw new Error('Provided token symbol is invalid.');
      }
      const decimals = tokenDetails?.decimal ?? 18;
      const tokenAddress: string = tokenDetails?.address;

      const contract = new ethers.Contract(contractAddress, contractABI, signer);
      const tokenContract = new ethers.Contract(tokenAddress, tokenABI, signer);

      const finalAmount = Number(amount.toString());
      const depositAmount = ethers.parseUnits(finalAmount.toFixed(decimals), decimals);

      const approvalTxn = await tokenContract.approve(contractAddress, depositAmount);
      await approvalTxn.wait();

      const result = await contract.deposit(tokenAddress, depositAmount);
      const receipt = await result.wait();
      if (onSuccessCallback) onSuccessCallback(receipt);
      return receipt;
    } catch (error) {
      if (onFailureCallback) onFailureCallback(error);
      const errorMessage = handleContractError(error, EscrowAbi);
      throw errorMessage;
    }
  }

  async withdrawBalance({ token, amount, onSuccessCallback, onFailureCallback }: DepositData) {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });
      const contractABI = EscrowAbi;
      const contractAddress = Escrow;

      const tokenDetails = tokenMap[networkType].find(
        (eachToken) => eachToken.symbol.toLowerCase() === token.toLowerCase()
      );
      if (!tokenDetails) {
        throw new Error('Provided token symbol is invalid.');
      }
      const decimals = tokenDetails?.decimal ?? 18;
      const tokenAddress: string = tokenDetails?.address;

      const contract = new ethers.Contract(contractAddress, contractABI, signer);

      const finalAmount = (Number(amount.toString()) * 10 ** decimals - 1) / 10 ** decimals;
      const withdrawAmount = ethers.parseUnits(finalAmount.toFixed(decimals), decimals);

      const result = await contract.withdraw(tokenAddress, withdrawAmount);
      const receipt = await result.wait();
      if (onSuccessCallback) onSuccessCallback(receipt);
      return receipt;
    } catch (error) {
      if (onFailureCallback) onFailureCallback(error);
      const errorMessage = handleContractError(error, EscrowAbi);
      throw errorMessage;
    }
  }

  async withdrawProviderEarnings({
    rewardWallet,
    tokenAddress,
    amount,
    decimals,
    onSuccessCallback,
    onFailureCallback,
  }: TransactionData) {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });
      const contractABI = EscrowAbi;
      const contractAddress = Escrow;

      const contract = new ethers.Contract(contractAddress, contractABI, signer);

      const finalAmount = (Number(amount.toString()) - 1) / 10 ** decimals;
      const withdrawAmount = ethers.parseUnits(finalAmount.toFixed(decimals), decimals);

      const result = await contract.withdrawProviderEarnings(
        rewardWallet,
        tokenAddress,
        withdrawAmount
      );
      const receipt = await result.wait();
      if (onSuccessCallback) onSuccessCallback(receipt);
      return receipt;
    } catch (error) {
      if (onFailureCallback) onFailureCallback(error);
      const errorMessage = handleContractError(error, EscrowAbi);
      throw errorMessage;
    }
  }

  async withdrawFizzEarnings({
    rewardWallet,
    tokenAddress,
    amount,
    decimals,
    onSuccessCallback,
    onFailureCallback,
  }: TransactionData) {
    try {
      const { signer } = await initializeSigner({ wallet: this.wallet });

      const contractABI = EscrowAbi;
      const contractAddress = Escrow;
      const contract = new ethers.Contract(contractAddress, contractABI, signer);

      const finalAmount = (Number(amount.toString()) - 1) / 10 ** decimals;
      const withdrawAmount = ethers.parseUnits(finalAmount.toFixed(decimals), decimals);

      const result = await contract.withdrawFizzNodeEarnings(
        rewardWallet,
        tokenAddress,
        withdrawAmount
      );
      const receipt = await result.wait();
      if (onSuccessCallback) onSuccessCallback(receipt);
      return receipt;
    } catch (error) {
      if (onFailureCallback) onFailureCallback(error);
      const errorMessage = handleContractError(error, EscrowAbi);
      throw errorMessage;
    }
  }

  // read operations
  async getProviderEarnings(providerAddress: string, tokenAddress: string) {
    try {
      const contractAbi = EscrowAbi;
      const contractAddress = Escrow;
      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const response = await contract.getProviderEarnings(providerAddress, tokenAddress);

      const providerEarnings: { earned: string; withdrawn: string; balance: string } = {
        earned: response[0].toString(),
        withdrawn: response[1].toString(),
        balance: response[2].toString(),
      };

      return providerEarnings;
    } catch (error) {
      const errorMessage = handleContractError(error, EscrowAbi);
      throw errorMessage;
    }
  }
  async getFizzEarnings(fizzAddress: string, tokenAddress: string) {
    try {
      const contractAbi = EscrowAbi;
      const contractAddress = Escrow;
      const contract = new ethers.Contract(contractAddress, contractAbi, this.provider);

      const response = await contract.getFizzNodeEarnings(fizzAddress, tokenAddress);

      const fizzEarnings: { earned: string; withdrawn: string; balance: string } = {
        earned: response[0].toString(),
        withdrawn: response[1].toString(),
        balance: response[2].toString(),
      };

      return fizzEarnings;
    } catch (error) {
      const errorMessage = handleContractError(error, EscrowAbi);
      throw errorMessage;
    }
  }
}

interface CreateDeploymentResponse {
  leaseId: string;
  transaction: ethers.ContractTransactionReceipt | null;
}

interface DeploymentResponse {
  services: Record<string, ServiceDetails> | null;
  forwarded_ports: Record<string, ForwardedPort[]> | null;
  ips: string[] | null;
}

interface ServiceDetails {
  name: string;
  available: number;
  total: number;
  uris: string[] | null;
  observed_generation: number;
  replicas: number;
  updated_replicas: number;
  ready_replicas: number;
  available_replicas: number;
  container_statuses: ContainerStatus[];
  creationTimestamp: string;
}

interface ContainerStatus {
  name: string;
  state: ContainerState;
  lastState: Record<string, unknown>;
  ready: boolean;
  restartCount: number;
  image: string;
  imageID: string;
  containerID: string;
  started: boolean;
}

interface ContainerState {
  running?: { startedAt: string };
  terminated?: { exitCode: number; reason: string; finishedAt: string };
  waiting?: { reason: string; message: string };
}

interface ForwardedPort {
  host: string;
  port: number;
  externalPort: number;
  proto: string;
  name: string;
}

declare class DeploymentModule {
  private wallet: ethers.Wallet | undefined;
  private escrowModule: EscrowModule;
  private orderModule: OrderModule;
  private leaseModule: LeaseModule;
  private providerModule: ProviderModule;

  constructor(
    provider: ethers.Provider,
    websocketProvider: ethers.WebSocketProvider,
    wallet?: ethers.Wallet
  ) {
    this.wallet = wallet;
    this.escrowModule = new EscrowModule(provider);
    this.orderModule = new OrderModule(provider, websocketProvider, wallet);
    this.leaseModule = new LeaseModule(provider, websocketProvider, wallet);
    this.providerModule = new ProviderModule(provider);
  }

  async createDeployment(
    iclYaml: string,
    providerProxyUrl: string,
    createOrderMatchedCallback?: (transactionHash: string) => void,
    createOrderFailedCallback?: (transactionHash: string) => void,
    isOperator: boolean = false
  ): Promise<CreateDeploymentResponse> {
    try {
      const { error, orderDetails: details } = yamlToOrderDetails(iclYaml);
      if (error || typeof details === 'undefined') {
        throw new Error('Please verify YAML format');
      }
      const sdlManifest = getManifestIcl(iclYaml);
      const { token, maxPrice, numOfBlocks } = details;
      const tokenDetails = getTokenDetails(token, networkType as NetworkType);
      const decimal = 18;
      const totalCost =
        Number(Number(maxPrice.toString()) / 10 ** (decimal || 0)) * Number(numOfBlocks);

      if (!this.wallet) {
        throw new Error('Unable to access wallet');
      }
      const account = await this.wallet.getAddress();

      const tokenBalance = await this.escrowModule.getUserBalance(
        tokenDetails?.symbol || '',
        account,
        isOperator
      );
      if (Number(tokenBalance.unlockedBalance) / 10 ** (tokenDetails?.decimal || 0) < totalCost) {
        throw new Error('Insufficient Balance');
      }
      try {
        const transaction = await this.orderModule.createOrder(details);
        const newOrderEvent: Promise<OrderMatchedEvent> = this.orderModule.listenToOrderCreated(
          60_000,
          () => {
            createOrderMatchedCallback?.(transaction?.hash || '');
          },
          () => {
            createOrderFailedCallback?.(transaction?.hash || '');
          }
        );
        const { orderId, providerAddress } = await newOrderEvent;

        const providerDetails: IProvider = await this.providerModule.getProviderDetails(
          providerAddress
        );
        const { certificate, hostUri } = providerDetails;
        const authToken = await createAuthorizationToken(this.wallet);
        const port = details.mode === 0 ? 8543 : 8443;
        const spheronProvider = new SpheronProviderModule(
          `https://${hostUri}:${port}`,
          providerProxyUrl
        );
        try {
          await spheronProvider.submitManfiest(
            certificate,
            authToken,
            orderId.toString(),
            sdlManifest
          );
          return { leaseId: orderId, transaction };
        } catch (error) {
          throw new Error('Error occured in sending manifest');
        }
      } catch (error) {
        throw error;
      }
    } catch (error) {
      throw error;
    }
  }

  async updateDeployment(
    leaseId: string,
    iclYaml: string,
    providerProxyUrl: string,
    updatedOrderLeaseCallback?: (orderId: string, providerAddress: string) => void,
    updatedOrderLeaseFailedCallback?: () => void,
    updateOrderAcceptedCallback?: (orderId: string) => void,
    updateOrderFailedCallback?: () => void,
    isOperator: boolean = false
  ) {
    try {
      const { error, orderDetails: details } = yamlToOrderDetails(iclYaml);
      if (error || typeof details === 'undefined') {
        throw new Error('Please verify YAML format');
      }
      const sdlManifest = getManifestIcl(iclYaml);
      const { token, maxPrice, numOfBlocks } = details;
      const tokenDetails = getTokenDetails(token, networkType as NetworkType);
      const decimal = 18;
      const totalCost =
        Number(Number(maxPrice.toString()) / 10 ** (decimal || 0)) * Number(numOfBlocks);

      if (!this.wallet) {
        throw new Error('Unable to access wallet');
      }
      const account = await this.wallet.getAddress();

      const tokenBalance = await this.escrowModule.getUserBalance(
        tokenDetails?.symbol || '',
        account,
        isOperator
      );
      if (Number(tokenBalance.unlockedBalance) / 10 ** (tokenDetails?.decimal || 0) < totalCost) {
        throw new Error('Insufficient Balance');
      }

      const updatedOrderLease: Promise<OrderUpdatedEvent> = this.orderModule.listenToOrderUpdated(
        120_000,
        (orderId, providerAddress) => {
          updatedOrderLeaseCallback?.(orderId, providerAddress);
        },
        () => {
          updatedOrderLeaseFailedCallback?.();
        }
      );
      const updateOrderAcceptance: Promise<OrderUpdateAcceptedEvent> =
        this.orderModule.listenToOrderUpdateAccepted(
          120_000,
          (orderId) => {
            updateOrderAcceptedCallback?.(orderId);
          },
          () => {
            updateOrderFailedCallback?.();
          }
        );

      await this.orderModule.updateOrder(leaseId, details);
      const updateOrderAcceptanceResponse = await updateOrderAcceptance;

      const { orderId, providerAddress } = updateOrderAcceptanceResponse;
      const providerDetails: IProvider = await this.providerModule.getProviderDetails(
        providerAddress
      );
      const { certificate, hostUri } = providerDetails;
      const port = details.mode === 0 ? 8543 : 8443;
      const spheronProvider = new SpheronProviderModule(
        `https://${hostUri}:${port}`,
        providerProxyUrl
      );
      const authToken = await createAuthorizationToken(this.wallet);
      const updateOrder = await spheronProvider.submitManfiest(
        certificate,
        authToken,
        orderId as string,
        sdlManifest
      );
      const updateOrderLeaseResponse = await updatedOrderLease;
      return { ...updateOrderLeaseResponse };
    } catch (error) {
      throw error;
    }
  }

  async getDeployment(leaseId: string, providerProxyUrl: string): Promise<DeploymentResponse> {
    try {
      if (!this.wallet) {
        throw new Error('Unable to access wallet');
      }

      if (!leaseId) {
        throw new Error('Provider Lease Id');
      }
      const { providerAddress, fizzId } = await this.leaseModule.getLeaseDetails(leaseId);
      const port = fizzId?.toString() !== '0' ? 8543 : 8443;
      const providerDetails: IProvider = await this.providerModule.getProviderDetails(
        providerAddress
      );
      const { certificate, hostUri } = providerDetails;

      const spheronProvider = new SpheronProviderModule(
        `https://${hostUri}:${port}`,
        providerProxyUrl
      );
      const authToken = await createAuthorizationToken(this.wallet);
      const leaseInfo = await spheronProvider.getLeaseStatus(certificate, authToken, leaseId);
      return leaseInfo;
    } catch (error) {
      throw error;
    }
  }

  async getDeploymentLogs(
    leaseId: string,
    providerProxyUrl: string,
    logsOptions?: { service?: string; tail?: number; startup?: boolean }
  ): Promise<string[]> {
    try {
      if (!this.wallet) {
        throw new Error('Unable to access wallet');
      }

      if (!leaseId) {
        throw new Error('Provider Lease Id');
      }
      const { providerAddress, fizzId } = await this.leaseModule.getLeaseDetails(leaseId);
      const port = fizzId?.toString() !== '0' ? 8543 : 8443;
      const providerDetails: IProvider = await this.providerModule.getProviderDetails(
        providerAddress
      );
      const { certificate, hostUri } = providerDetails;

      const spheronProvider = new SpheronProviderModule(
        `https://${hostUri}:${port}`,
        `${providerProxyUrl}/ws-data`
      );
      const authToken = await createAuthorizationToken(this.wallet);
      const leaseLogs = await spheronProvider.getLeaseLogs(
        certificate,
        authToken,
        leaseId,
        logsOptions?.service,
        logsOptions?.tail,
        logsOptions?.startup
      );
      return leaseLogs;
    } catch (error) {
      throw error;
    }
  }

  async closeDeployment(leaseId: string) {
    try {
      if (!this.wallet) {
        throw new Error('Unable to access wallet');
      }

      if (!leaseId) {
        throw new Error('Provider Lease Id');
      }

      const closeLeaseResponse = await this.leaseModule.closeLease(leaseId);

      return closeLeaseResponse;
    } catch (error) {
      throw error;
    }
  }
}

declare class SpheronSDK {
    leases: LeaseModule;
    orders: OrderModule;
    escrow: EscrowModule;
    provider: ProviderModule;
    fizz: FizzModule;
    deployment: DeploymentModule;
    constructor(networkType: NetworkType, privateKey?: string, rpcProvider?: RpcProvider);
}

export { type Attribute, type Category, type DepositData, type FizzAttribute, type FizzDetails, type FizzLease, type FizzNode, type FizzParams, type FizzProvider, FizzProviderStatus, FizzProviderTrustTier, type IProvider, type InitialOrder, type Lease, LeaseState, type LeaseWithOrderDetails, Mode, type OrderDetails, type OrderMatchedEvent, OrderState, type OrderUpdateAcceptedEvent, type OrderUpdatedEvent, type Provider, type ProviderDetails, ProviderStatus, ProviderTrustTier, type RawFizzAttribute, type RawFizzNode, type RawProviderAttribute, type Resource, type ResourceAttributes, type ResourceCategory, SpheronSDK, Tier, type TokenDetails, type TransactionData, TransactionStatus, type UserBalance };
