import dayjs, { OpUnitType, QUnitType } from 'dayjs';
import mongoose, { Connection } from 'mongoose';
import PoolSchema from './models/Pool.schema';
import { ERRORS, PoolConfig, PoolStatus, Reward } from './types';
import { randomInt } from './utils';
//@ts-ignore;
import bezier from 'cubic-bezier';

const init = async ({
  mongoUrl,
  debug,
  cleanUpPools,
}: {
  mongoUrl: string;
  debug?: boolean;
  cleanUpPools?: boolean;
}) => {
  let db: Connection;
  const RATE_DEMICAL_PLACES = 2;
  const RATE_DEMICAL_PLACES_MULTIPLIER = Math.pow(RATE_DEMICAL_PLACES, 10);

  try {
    db = mongoose.createConnection(mongoUrl);
  } catch (e) {
    throw new Error(ERRORS.ERR_DB_CONN);
  }

  const Pool = db.model('Pool', PoolSchema, 'Pool');
  if (cleanUpPools) {
    await Pool.deleteMany({});
  }

  const listPools = async () => {
    const pools = await Pool.find();
    return pools;
  };

  const createPool = async ({
    poolKey,
    name,
    startTime,
    endTime,
    period,
    initialAmount,
    config,
    data,
  }: {
    poolKey: string;
    name: string;
    startTime: Date;
    endTime: Date;
    period?: QUnitType | OpUnitType;
    initialAmount: number;
    config?: PoolConfig;
    data?: { [key: string]: any };
  }) => {
    const pool = await Pool.create({
      poolKey,
      name,
      startTime,
      endTime,
      period,
      initialAmount,
      unclaimed: initialAmount,
      status: PoolStatus.ACTIVE,
      config,
      data,
    });
    return pool;
  };

  const disablePool = async (poolKey: string) => {
    await Pool.updateOne({ poolKey }, { $set: { status: PoolStatus.DISABLED, disabledAt: Date.now() } });
  };
  const enablePool = async (poolKey: string) => {
    await Pool.updateOne({ poolKey }, { $set: { status: PoolStatus.ACTIVE } });
  };

  const draw = async ({
    poolKeys,
    accessRate,
    timestamp,
  }: {
    poolKeys: string[];
    accessRate?: number;
    timestamp?: Date;
  }) => {
    const handlingData: { [key: string]: any } = {};
    handlingData.accessRate = 100;
    if (accessRate || accessRate === 0) {
      handlingData.accessRate = accessRate;
    }
    handlingData.accessRollResult = randomInt(0, 100 * RATE_DEMICAL_PLACES_MULTIPLIER - 1);
    handlingData.poolsAccessed =
      handlingData.accessRollResult < handlingData.accessRate * RATE_DEMICAL_PLACES_MULTIPLIER;

    if (!handlingData.poolsAccessed) {
      if (debug) {
        console.log(handlingData);
      }
      return;
    }

    const currentTime = dayjs(timestamp);
    let reward: Reward | undefined = undefined;
    const pools = await Pool.find({ poolKey: { $in: poolKeys }, status: PoolStatus.ACTIVE });

    handlingData.pools = [];
    for (const pool of pools) {
      const handlingPoolData: { [key: string]: any } = { poolKey: pool.poolKey };
      let startTime = dayjs(pool.startTime);
      let endTime = dayjs(pool.endTime);
      let maxRate = 100;
      let minRate = 0;
      if (pool.config?.maxRate !== undefined) {
        maxRate = pool.config.maxRate;
      }
      if (pool.config?.minRate !== undefined) {
        minRate = pool.config.minRate;
      }

      handlingPoolData.burntAmount = pool.initialAmount - pool.unclaimed;
      handlingPoolData.rate = 0;
      const unit: OpUnitType | QUnitType = (pool.period as any) || 'ms';
      handlingPoolData.expectedBurnSpeed = endTime.diff(startTime, unit, true) / pool.initialAmount;
      handlingPoolData.expectedBurnAmount =
        (currentTime.diff(startTime, unit) + 1) / handlingPoolData.expectedBurnSpeed;

      handlingPoolData.remaining = handlingPoolData.expectedBurnAmount - handlingPoolData.burntAmount;

      // console.log('duration', endTime.diff(startTime, unit, true));
      // console.log('burn speed', handlingPoolData.expectedBurnSpeed);
      // console.log(currentTime.diff(startTime, unit));
      // console.log('expected', handlingPoolData.expectedBurnAmount);
      // console.log(handlingPoolData.remaining);

      if (handlingPoolData.remaining <= 0) {
        handlingData.pools.push(handlingPoolData);
        continue;
      }

      handlingPoolData.controlPoints = pool.config?.controlPoints || [
        [0, 0],
        [1, 1],
      ];
      let rateFormula = bezier(
        handlingPoolData.controlPoints[0][0],
        handlingPoolData.controlPoints[0][1],
        handlingPoolData.controlPoints[1][0],
        handlingPoolData.controlPoints[1][1],
        1000 / 60 / handlingPoolData.expectedBurnSpeed / 4
      );
      const rate =
        ((maxRate - minRate) * rateFormula(handlingPoolData.expectedBurnAmount - handlingPoolData.burntAmount) +
          minRate) *
        RATE_DEMICAL_PLACES_MULTIPLIER;

      handlingPoolData.rate = rate / RATE_DEMICAL_PLACES_MULTIPLIER;
      handlingPoolData.rollResult = randomInt(0, 100 * RATE_DEMICAL_PLACES_MULTIPLIER - 1);
      handlingPoolData.poolWinned = handlingPoolData.rollResult < rate;

      if (!handlingPoolData.poolWinned) {
        handlingData.pools.push(handlingPoolData);
        continue;
      }

      try {
        const updatedPool = await Pool.findOneAndUpdate(
          {
            poolKey: pool.poolKey,
            unclaimed: { $gte: 1, $eq: pool.unclaimed },
          },
          { $inc: { unclaimed: -1 } },
          { new: true }
        );
        if (!updatedPool) {
          handlingData.pools.push(handlingPoolData);
          continue;
        } else {
          reward = {
            poolName: updatedPool.name,
            poolKey: updatedPool.poolKey,
            poolData: updatedPool.data,
          };
          handlingData.pools.push(handlingPoolData);
          break;
        }
      } catch (e: any) {
        handlingPoolData.error = e.message;
        handlingData.pools.push(handlingPoolData);
      }
    }
    if (debug) {
      console.log(JSON.stringify(handlingData, null, 2));
    }
    return reward;
  };

  return { listPools, createPool, enablePool, disablePool, draw };
};

export { init };
export default init;
