import { SDK } from 'rm-sdk/build/';
import { Token } from 'rm-sdk/build/interfaces/token.interface';
import {
  Environment,
  FrontendOperator,
  Market,
  Policy,
  Pool,
  PoolParticipant,
} from 'rm-sdk/build/types';
import BigNumber from "bignumber.js";
import { ContractTransaction, providers } from "ethers";
import { QueryService } from 'rm-sdk';
import { EntityValue } from 'rm-sdk/build/services/api.service';
import { GraphVersionEnum } from 'rm-sdk/build/types';

const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';

export class QuoteLoader {
  public sdk: SDK;
  public market?: Market;
  public premiumToken?: Token;
  public capitalToken?: Token;
  public insuredToken?: Token;
  public monthlyQuote?: BigNumber;
  public minDeposit?: BigNumber;
  public topUpValue?: BigNumber;
  public quote?: BigNumber;
  public rate?: BigNumber;
  public policy?: Policy;
  public policyList: Policy[] | undefined;
  public frontendOperator?: FrontendOperator;
  public quoteCoefficient?: number;
  public bonusCoefficient?: number;
  public bonus?: BigNumber;
  public total?: BigNumber;
  public duration?: number;
  public marketCoverage?: [string, string];
  public queryService: QueryService = new QueryService();
  public environment: Environment | string;
  public uiEntities?: EntityValue[][];

  constructor(env: Environment | string) {
    this.environment = env;

    this.sdk = new SDK(env);
  }

  public async load(
    provider: providers.Web3Provider,
    duration: number,
    marketId: string,
    desiredCoverage: string,
    frontendOperator?: string,
  ): Promise<boolean> {
    if (!marketId || !desiredCoverage) {
      return false;
    }

    this.market = await this.sdk.getMarketById(marketId);

    if (!this.market) {
      return false;
    }

    if (frontendOperator) {
      this.frontendOperator = await this.sdk.getFrontendOperatorById(frontendOperator.toLowerCase());

      if (this.frontendOperator) {
        this.quoteCoefficient = new BigNumber(this.frontendOperator.frontendOperatorFee).div('1e18').plus(1).toNumber();
        this.bonusCoefficient = new BigNumber(this.frontendOperator.policyBuyerReferralBonus).div('1e18').toNumber();
      } else {
        throw new Error('Specified frontend operator not found');
      }
    }

    this.premiumToken = await this.sdk.getTokenInfo(this.market.premiumToken);
    this.capitalToken = this.market.premiumToken === this.market.capitalToken ?
      this.premiumToken :
      await this.sdk.getTokenInfo(this.market.capitalToken);

    if (this.market.insuredToken && this.market.insuredToken !== ZERO_ADDRESS) {
      this.insuredToken = await this.sdk.getTokenInfo(this.market.insuredToken);
    }

    if (!this.premiumToken) {
      throw new Error(`Cannot find premium token (${this.market.premiumToken}) in supported token lists.`);
    }

    if (!this.capitalToken) {
      throw new Error(`Cannot find capital token (${this.market.capitalToken}) in supported token lists.`);
    }

    this.initMarketRate(this.market, this.frontendOperator);

    provider && await this.calcQuote(provider, desiredCoverage, duration);

    this.uiEntities = await this.sdk.getProductUiEntities(this.market.product.id);

    return true;
  }

  public initMarketRate(
    market: Market,
    frontendOperator: FrontendOperator | undefined,
  ) {
    const { currentApr } = this.sdk.getApr(market, frontendOperator);

    this.rate = new BigNumber(currentApr.toFixed());
    this.marketCoverage = [market.exposure ?? "0", market.actualCover ?? "0"];
  }

  public async calcQuote(
    provider: providers.Web3Provider,
    desiredCoverage: string,
    duration: number,
  ): Promise<boolean> {
    if (!await this.isWalletChainCorrect(provider)) {
      return false;
    }

    const account = provider && provider.listAccounts && (await provider.listAccounts())[0];

    if (!provider || !this.premiumToken || !this.capitalToken || !account || !this.market) {
      return false;
    }

    try {
      const coverage = new BigNumber(desiredCoverage).times(`1e${this.capitalToken.decimals}`).toFixed(0);
      const rq = await this.sdk.getRawQuote(
        this.market.id,
        coverage,
        provider,
      );

      const monthLong = 30;
      const monthlyQuoteWei = this.sdk.calcQuoteFromRaw(coverage, rq, monthLong);

      let p2cRate = 1;

      if (this.premiumToken.address !== this.capitalToken.address) {
        const r = await this.sdk.getExchangeRate(
          this.market.id,
          this.premiumToken.address,
          this.capitalToken.address,
        );

        if (!r) {
          p2cRate = 0;
        } else {
          p2cRate = new BigNumber(r).toNumber();
        }
      }

      const r = this.sdk.calcRateFromRaw(rq, p2cRate);

      const rate = new BigNumber(r).times(100);

      const minDepositWei = await this.sdk.getMinPolicyDeposit(
        this.market.id,
        provider,
      );

      this.monthlyQuote = new BigNumber(monthlyQuoteWei).div(`1e${this.premiumToken.decimals}`).times(this.quoteCoefficient ?? 1);
      this.minDeposit = new BigNumber(minDepositWei).div(`1e${this.premiumToken.decimals}`).times(this.quoteCoefficient ?? 1);
      this.rate = rate;
      this.marketCoverage = [rq[1], rq[2]];

      const k = duration / monthLong;
      this.updateTopUpValue(this.monthlyQuote.times(k).toFixed(2, BigNumber.ROUND_UP));

      return true;
    } catch (e) {
      console.error(e);
    }

    return false;
  }

  public updateTopUpValue(value: string) {
    const t = new BigNumber(new BigNumber(value)
      .toFixed(this.premiumToken?.decimals || 2, BigNumber.ROUND_UP));
    const k = this.monthlyQuote && !this.monthlyQuote.isZero() ? t.div(this.monthlyQuote) : new BigNumber(0);

    this.topUpValue = new BigNumber(t.toFixed(2, BigNumber.ROUND_UP));
    this.bonus = new BigNumber(this.topUpValue.times(this.bonusCoefficient ?? 0).toFixed(2));

    this.topUpValue = t;
    this.total = t.plus(this.minDeposit ?? 0);
    this.quote = t.plus(this.minDeposit ?? 0).minus(this.bonus);

    this.duration = k.times(30).toNumber();
  }

  public async getExchangeRate(fromToken: string, toToken: string): Promise<number | undefined> {
    if (!this.market) {
      return;
    }

    const r = await this.sdk.getExchangeRate(
      this.market.id,
      fromToken,
      toToken,
    );

    if (r) {
      return +r;
    }
  }

  public async getPremiumTokenAllowance(provider: providers.Web3Provider): Promise<string | null> {
    if (!this.premiumToken || !await this.isWalletChainCorrect(provider)) {
      return null;
    }

    const account = provider?.listAccounts && (await provider.listAccounts())[0];

    if (this.market && provider && account) {
      const a = await this.sdk.getTokenAllowance(
        this.market.premiumToken,
        account,
        this.getAddressToApprove(this.sdk.envConfig?.graphVersion ?? GraphVersionEnum.latest)!,
        provider,
      );

      return new BigNumber(a).div(`1e${this.premiumToken.decimals}`).toFixed();
    }

    return null;
  }

  getAddressToApprove(version: GraphVersionEnum): string | undefined {
    if (version === GraphVersionEnum.v2) {
      return this.market?.product.id;
    }

    return this.market?.riskPoolsControllerAddress;
  }

  public async getPremiumTokenBalance(provider: providers.Web3Provider): Promise<string | null> {
    if (!this.premiumToken || !await this.isWalletChainCorrect(provider)) {
      return null;
    }

    const account = provider && (await provider.listAccounts())[0];

    if (this.market && provider && account) {
      const a = await this.sdk.getTokenBalance(
        this.market.premiumToken,
        account,
        provider,
      );

      return new BigNumber(a).div(`1e${this.premiumToken.decimals}`).toFixed();
    }

    return null;
  }

  public async approvePremiumToken(provider: providers.Web3Provider): Promise<false | ContractTransaction> {
    if (!await this.isWalletChainCorrect(provider)) {
      return false;
    }

    if (this.market) {
      const tx = await this.sdk.setTokenAllowance(
        this.market.premiumToken,
        this.getAddressToApprove(this.sdk.envConfig?.graphVersion ?? GraphVersionEnum.latest)!,
        provider,
      );

      return tx;
    }

    return false;
  }

  public async isUserHasPolicies(provider: providers.Web3Provider): Promise<boolean> {
    const account = provider?.listAccounts && (await provider.listAccounts())[0];

    if (!account || !this.market) {
      return false;
    }

    const list = await this.sdk.getUserPolicies(account, this.market.id);

    list.sort((p1: Policy, p2: Policy) => +p2.coverage - +p1.coverage);

    this.policy = (list && list.find((p: Policy) => +p.coverage > 0));
    this.policyList = list;

    const hasPolicy = this.policy !== undefined;

    return hasPolicy;
  }

  public async getPolicy(policyId: string): Promise<Policy | undefined> {
    const p = await this.sdk.getPolicyById(policyId);

    return p;
  }

  public async isWalletChainCorrect(
    provider: providers.Web3Provider,
  ): Promise<boolean> {
    const chainId = (await provider.getNetwork()).chainId;

    if (!chainId || chainId !== this.sdk.envConfig?.chainId) {
      return false;
    }

    return true;
  }

  public async buyPolicy(
    provider: providers.Web3Provider,
    desiredCoverage: string,
    deposit: string,
    frontendOperator?: string,
    referrer?: string,
  ): Promise<false | ContractTransaction> {
    if (!await this.isWalletChainCorrect(provider)) {
      return false;
    }

    if (this.market && this.capitalToken && this.premiumToken && this.quote) {
      const tx = await this.sdk.buyPolicy(
        new BigNumber(desiredCoverage).times(`1e${this.capitalToken.decimals}`).toFixed(0),
        new BigNumber(deposit).times(`1e${this.premiumToken.decimals}`).toFixed(0),
        this.market.product.id,
        this.market.id,
        provider,
        frontendOperator,
        referrer,
      );

      return tx;
    }

    return false;
  }

  public async cancelPolicy(provider: providers.Web3Provider, policy: Policy): Promise<false | ContractTransaction> {
    if (!await this.isWalletChainCorrect(provider)) {
      return false;
    }

    if (policy) {
      const tx = await this.sdk.deactivatePolicy(
        policy.id,
        provider,
      );

      return tx;
    }

    return false;
  }

  public async isMarketOpened(): Promise<boolean> {
    return this.market?.id != null && this.sdk.isMarketOpened(this.market.id);
  }

  public async getMarketPools(): Promise<Pool[] | undefined> {
    return this.market && this.sdk.getMarketPools(this.market.id);
  }

  public async getMarketCapacity(): Promise<BigNumber> {
    const pools = await this.getMarketPools();
    let totalCapacity = new BigNumber(0);

    if (pools) {
      await Promise.all(pools.map(async (p) => {
        let rate = 1;

        if (!this.capitalToken) {
          rate = 0;
        } else if (p.capitalTokenAddress.toLowerCase() !== this.capitalToken.address.toLowerCase()) {
          rate = (await this.getExchangeRate(p.capitalTokenAddress, this.capitalToken.address)) || 0;
        }

        totalCapacity = totalCapacity.plus(new BigNumber(p.capacity).times(rate));
      }));
    }

    return totalCapacity;
  }

  protected async getPoolLPs(poolList: string[]): Promise<PoolParticipant[]> {
    const r: PoolParticipant[] = [];
    let d: PoolParticipant[];
    let skip = 0;
    let take = 500;

    do {
      d = await this.sdk.getPoolParticipants(poolList, skip, take);

      r.push(...d);
      skip += take;

    } while (d.length === take);

    return r;
  }

  public async getMarketEntities(): Promise<Array<EntityValue | undefined>> {
    if (!this.market) {
      return [];
    }
    const entityList = await this.queryService.getMarketEntities(this.environment,  this.market.product.id, this.market.entityList);

    return entityList;
  }

  public async getMarketLPCount(): Promise<number> {
    const pools = await this.getMarketPools();

    if (!pools) {
      return 0;
    }

    const lps = await this.getPoolLPs(pools?.map(p => p.id));
    const lpSet = new Set<string>();

    lps.forEach(lp => +lp.tokenBalance && lpSet.add(lp.user));

    return lpSet.size;
  }

  public async waitUntilGraphIsSynced(blockNumber: number): Promise<boolean> {
    return this.sdk.waitForBlockSync(blockNumber);
  }

  public uiUrl(): string | undefined {
    return this.sdk.envConfig?.uiUrl;
  }
}