import { RouterABI, ViewABI, QuoterABI, ERC20ABI, ACMABI } from './abi'
import { parseConfig } from './config'
import {
  FutureInfo, GrantUserRoleMethodMap,
  LiquidityDistribution,
  LiquidityQuote,
  MakerLiquidityDistribution,
  MarginState,
  MarketInfo,
  MarketOraclePackages,
  MarketPortfolio,
  OraclePackage, RevokeUserRoleMethodMap,
  TradeQuote, UserRole
} from './typings'
import {
  DepositParams,
  ExecuteTradeParams,
  LiquidatePositionParams,
  LiquidityPositionQuoteParams,
  MarketUserOracleParams,
  PaginationParams,
  MarketFutureOraclePaginationParams,
  RhoSDKConfig,
  RhoSDKParams,
  TradeQuoteParams,
  TransferPositionsOwnershipParams,
  WithdrawParams,
  UserOraclePaginationParams,
  OraclePaginationParams,
  UserPaginationParams,
  MarketFutureOracleParams,
  LiquidityOperationParams
} from './sdk-typings'
import { OracleAPI } from './api/oracle'
import {
  BrowserProvider,
  Contract,
  ethers,
  JsonRpcProvider,
  JsonRpcSigner,
  TransactionReceipt,
  Wallet,
  TransactionRequest
} from 'ethers'
import { getUserRoleBytes } from './utils'
import { DataServiceAPI } from './api/dataservice'

const defaultLimit = 100

export default class RhoSDK {
  public config: RhoSDKConfig
  private router: Contract
  private view: Contract
  private quoter: Contract
  private acm?: Contract

  private signer?: Wallet | JsonRpcSigner
  public signerAddress: string = ''

  public readonly provider: JsonRpcProvider | BrowserProvider
  public oracleAPI: OracleAPI
  public dataServiceAPI: DataServiceAPI

  constructor(_config?: RhoSDKParams) {
    const config = parseConfig(_config)
    this.config = config

    if (config.provider) {
      this.provider = config.provider
    } else {
      this.provider = new JsonRpcProvider(config.rpcUrl)
    }

    if (config.privateKey) {
      this.signer = new ethers.Wallet(config.privateKey, this.provider)
    } else if (config.signer) {
      this.signer = config.signer
    }

    if (this.signer) {
      this.router = new ethers.Contract(config.routerAddress, RouterABI, this.signer)
      this.setSignerAddress(this.signer.address)
    } else {
      this.router = new ethers.Contract(config.routerAddress, RouterABI, this.provider)
    }

    this.view = new ethers.Contract(config.viewAddress, ViewABI, this.provider)
    this.quoter = new ethers.Contract(config.quoterAddress, QuoterABI, this.provider)

    this.oracleAPI = new OracleAPI({
      oracleServiceUrl: config.oracleServiceUrl,
    })

    this.dataServiceAPI = new DataServiceAPI({ network: config.network })
  }

  public setSignerAddress(address: string) {
    this.signerAddress = address
  }

  public setPrivateKey(privateKey: string) {
    const signer = new ethers.Wallet(privateKey, this.provider)
    this.setSigner(signer)
  }

  public setSigner(signer: Wallet | JsonRpcSigner) {
    this.signer = signer
    this.setSignerAddress(signer.address)
    this.router = new ethers.Contract(this.config.routerAddress, RouterABI, this.signer)
    this.acm = undefined // acm contract will be initialized on acm method call
  }

  public async getBalance(address: string): Promise<bigint> {
    return this.provider.getBalance(address)
  }

  public getNonce(): Promise<number> {
    return this.signer.getNonce()
  }

  public async getMarketsOraclePackages(): Promise<MarketOraclePackages[]> {
    const packages = await this.oracleAPI.getOraclePackages()
    return packages.map(oraclePackage => {
      return {
        marketId: oraclePackage.marketId,
        packages: [oraclePackage]
      }
    })
  }

  public async getOraclePackage(marketId: string): Promise<OraclePackage | undefined> {
    return await this.oracleAPI.getMarketOraclePackage(marketId)
  }

  public async getBalanceOf(contractAddress: string, userAddress: string): Promise<bigint> {
    const erc20Contract = new ethers.Contract(contractAddress, ERC20ABI, this.provider)
    return await erc20Contract.balanceOf(userAddress)
  }

  public async getAllowance(contractAddress: string, userAddress: string, spenderAddress: string): Promise<bigint> {
    const erc20Contract = new ethers.Contract(contractAddress, ERC20ABI, this.provider)
    return await erc20Contract.allowance(userAddress, spenderAddress)
  }

  public async setAllowance(
    contractAddress: string,
    spenderAddress: string,
    amount: bigint
  ): Promise<TransactionReceipt> {
    const erc20Contract = new ethers.Contract(contractAddress, ERC20ABI, this.signer)
    return await erc20Contract.approve(spenderAddress, amount)
  }

  public async getActiveMarketIds(params: PaginationParams = {}): Promise<string[]> {
    const { offset = 0, limit = defaultLimit } = params
    return await this.view.allActiveMarketsIds(offset, limit)
  }

  public async getPortfolioMarketIds(params: UserPaginationParams): Promise<string[]> {
    const { offset = 0, limit = defaultLimit } = params
    return await this.view.portfolioMarketIds(params.userAddress, offset, limit)
  }

  public async getActiveMarkets(params: OraclePaginationParams = {}): Promise<MarketInfo[]> {
    const { offset = 0, limit = defaultLimit } = params
    return await this.view.activeMarketsInfo(offset, limit, [])
  }

  public async getPortfolio(params: Omit<UserOraclePaginationParams, 'oraclePackages'> & {
    oraclePackages?: MarketOraclePackages[]
  }): Promise<MarketPortfolio[]> {
    const { userAddress, offset = 0, limit = defaultLimit } = params
    const oraclePackages = params.oraclePackages || await this.getMarketsOraclePackages()

    return await this.view.portfolio(userAddress, offset, limit, oraclePackages)
  }

  public async getMarketPortfolio(params: MarketUserOracleParams): Promise<MarketPortfolio> {
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(params.marketId)]
    return await this.view.marketPortfolio(params.marketId, params.userAddress, oraclePackages)
  }

  public async getMarginDetails(params: MarketUserOracleParams): Promise<MarginState> {
    const { marketId, userAddress } = params
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(marketId)]

    return this.view.marginDetails(marketId, userAddress, oraclePackages)
  }

  public async getWithdrawableMargin(params: MarketUserOracleParams): Promise<bigint> {
    const { marketId, userAddress } = params
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(marketId)]

    return await this.view.withdrawableMargin(marketId, userAddress, oraclePackages)
  }

  public async getPoolLiquidityDistribution(
    params: MarketFutureOraclePaginationParams
  ): Promise<LiquidityDistribution> {
    const { marketId, futureId, offset = 0, limit = defaultLimit } = params

    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(marketId)]
    const [provisionDistribution, currentFutureRate, intervalLiquidity] = await this.view.poolLiquidityDistribution(
      futureId,
      oraclePackages,
      offset,
      limit
    )

    return {
      provisionDistribution,
      currentFutureRate,
      intervalLiquidity
    }
  }

  public async getMakerLiquidityDistribution(
    params: {futureId: string} & UserPaginationParams
  ): Promise<MakerLiquidityDistribution> {
    const { userAddress, futureId, offset = 0, limit = defaultLimit } = params

    const [currentFutureRate, intervalLiquidity] = await this.view.makerLiquidityDistribution(
      futureId,
      userAddress,
      offset,
      limit
    )

    return {
      currentFutureRate,
      intervalLiquidity
    }
  }

  public async futuresInfoCloseToMaturityWithoutIndex(params: {
    marketId: string,
    maturityBufferSeconds: number,
  }): Promise<FutureInfo[]> {
    const { marketId, maturityBufferSeconds } = params
    return await this.view.futuresInfoCloseToMaturityWithoutIndex(marketId, maturityBufferSeconds)
  }

  public async isLiquidatable(params: MarketUserOracleParams): Promise<boolean> {
    const { marketId, userAddress } = params
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(marketId)]
    return await this.quoter.isLiquidatable(marketId, userAddress, oraclePackages)
  }

  public async isProvisionCancellable(params: MarketUserOracleParams): Promise<boolean> {
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(params.marketId)]
    return await this.quoter.isProvisionCancellable(params.marketId, params.userAddress, oraclePackages)
  }

  public async cancelProvisions(params: MarketUserOracleParams): Promise<TransactionReceipt> {
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(params.marketId)]
    return await this.router.cancelProvisions(params.marketId, params.userAddress, oraclePackages)
  }

  public async getTradeQuote(params: TradeQuoteParams): Promise<TradeQuote> {
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(params.marketId)]
    return await this.quoter.quoteTrade(params.futureId, params.notional, params.userAddress, oraclePackages)
  }

  public async getLiquidityProvisionQuote(params: LiquidityPositionQuoteParams): Promise<LiquidityQuote> {
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(params.marketId)]
    return await this.quoter.quoteLiquidityProvision(
      params.futureId,
      params.notional,
      params.userAddress,
      params.operation,
      params.lowerBound,
      params.upperBound,
      oraclePackages
    )
  }

  // Router methods
  public async liquidatePositions(params: LiquidatePositionParams): Promise<TransactionReceipt> {
    const { marketId, futureIds, positionsPercentage, userAddress, settleMaturedPositions = false } = params
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(marketId)]
    return await this.router.liquidatePositions(marketId, futureIds, positionsPercentage, userAddress, settleMaturedPositions, oraclePackages)
  }

  public async persistIndexAtMaturity(
    params: {
      futureId: string,
      oraclePackage: OraclePackage
    }
  ): Promise<TransactionReceipt> {
    return await this.router.persistIndexAtMaturity(params.futureId, params.oraclePackage)
  }

  public async executeTrade(params: ExecuteTradeParams, txRequestParams?: TransactionRequest): Promise<TransactionReceipt> {
    const { marketId, deadline = Date.now() + 5 * 60 * 1000, settleMaturedPositions = true } = params
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(marketId)]

    const args: any[] = [
      params.futureId,
      params.riskDirection,
      params.notional,
      params.futureRateLimit,
      params.depositAmount,
      deadline,
      settleMaturedPositions,
      oraclePackages
    ]

    if(txRequestParams) {
      args.push(txRequestParams)
    }

    return await this.router.executeTrade(...args)
  }

  public async executeTradeEstimateGas(params: ExecuteTradeParams, txRequestParams?: TransactionRequest): Promise<bigint> {
    const { marketId, deadline = Date.now() + 5 * 60 * 1000, settleMaturedPositions = true } = params
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(marketId)]

    const args: any[] = [
      params.futureId,
      params.riskDirection,
      params.notional,
      params.futureRateLimit,
      params.depositAmount,
      deadline,
      settleMaturedPositions,
      oraclePackages
    ]

    if(txRequestParams) {
      args.push(txRequestParams)
    }

    return await this.router.executeTrade.estimateGas(...args)
  }

  public async deposit(params: DepositParams): Promise<TransactionReceipt> {
    const { marketId, userAddress, amount, settleMaturedPositions = true } = params
    const oraclePackages = params.oraclePackages || []
    return await this.router.deposit(marketId, userAddress, amount, settleMaturedPositions, oraclePackages)
  }

  public async withdraw(params: WithdrawParams): Promise<TransactionReceipt> {
    const { marketId, amount, unwrapNativeToken = false, settleMaturedPositions = true } = params
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(marketId)]

    return await this.router.withdraw(marketId, unwrapNativeToken, amount, settleMaturedPositions, oraclePackages)
  }

  public async provideLiquidity(params: LiquidityOperationParams): Promise<TransactionReceipt> {
    const { marketId, deadline = Date.now() + 5 * 60 * 1000, settleMaturedPositions = true } = params
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(marketId)]

    return await this.router.provideLiquidity(
      params.futureId,
      params.notional,
      params.collateral,
      params.lowerBound,
      params.upperBound,
      deadline,
      settleMaturedPositions,
      oraclePackages
    )
  }

  public async removeLiquidity(params: LiquidityOperationParams): Promise<TransactionReceipt> {
    const { marketId, deadline = Date.now() + 5 * 60 * 1000, settleMaturedPositions = true } = params
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(marketId)]

    return await this.router.removeLiquidity(
      params.futureId,
      params.notional,
      params.collateral,
      params.lowerBound,
      params.upperBound,
      deadline,
      settleMaturedPositions,
      oraclePackages
    )
  }

  public async quotePositionsOwnershipTransfer(
    params: MarketUserOracleParams & { liquidator: string }
  ): Promise<{ transferAmount: bigint; depositAmount: bigint }> {
    const { marketId, userAddress, liquidator } = params
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(marketId)]

    return await this.quoter.quotePositionsOwnershipTransfer(
      marketId,
      userAddress,
      liquidator,
      oraclePackages
    )
  }

  public async transferPositionsOwnership(params: TransferPositionsOwnershipParams): Promise<TransactionReceipt> {
    const { marketId, userAddress, depositAmount, settleMaturedPositions = true } = params
    const oraclePackages = params.oraclePackages || [await this.getOraclePackage(marketId)]

    return await this.router.transferPositionsOwnership(
      marketId,
      userAddress,
      depositAmount,
      settleMaturedPositions,
      oraclePackages
    )
  }

  private async getAcmContract() {
    if(!this.acm) {
      const acmAddress = await this.router.getAcm()
      this.acm = new ethers.Contract(acmAddress, ACMABI, this.signer || this.provider)
    }
    return this.acm
  }

  public async getUsers(params: { role: UserRole } & PaginationParams): Promise<string[]> {
    const { role, offset = 0, limit = 100 } = params
    const acm = await this.getAcmContract()
    return await acm.getRoleAddresses(getUserRoleBytes(role), offset, limit)
  }

  public async getUsersCount(params: { role: UserRole }): Promise<bigint> {
    const { role } = params
    const acm = await this.getAcmContract()
    return await acm.getRoleAddressesCount(getUserRoleBytes(role))
  }

  public async hasRole(params: { address: string, role: UserRole }): Promise<boolean> {
    const { address, role } = params
    const acm = await this.getAcmContract()
    return await acm.hasRole(getUserRoleBytes(role), address)
  }

  public async grantRole(params: { address: string, role: UserRole }): Promise<TransactionReceipt> {
    const { address, role } = params

    const acm = await this.getAcmContract()
    const grantContractMethod = GrantUserRoleMethodMap[role]
    return await acm[grantContractMethod](address)
  }

  public async revokeRole(params: { address: string, role: UserRole }): Promise<TransactionReceipt> {
    const { address, role } = params

    const acm = await this.getAcmContract()
    const revokeContractMethod = RevokeUserRoleMethodMap[role]
    return await acm[revokeContractMethod](address)
  }
}

export * from './typings'
export * from './sdk-typings'
export * from './utils'
export * from './api/oracle'
export * from './api/subgraph'
export * from './api/dataservice'
