contract U3ReProxyUpgradablity is Ownable {
    /// @notice LockableToken struct for storing token lockable details
    struct LockableTokens {
        uint256 lockableDays;
        bool optionableStatus;
    }
    /// @notice U3 Instance
    U3Legacy public uniV3;

    /// @notice U3Proxy Instance
    U3Proxy public uniV3Proxy;

    /// @notice ReProxy Instance
    V3Reproxy public u3Reproxy;

    /// @notice SafeMath using for Arthmetic Operations.
    using SafeMath for uint256;

    /// @notice intervalDays for reward calculation x days.
    uint256[] public intervalDays = [1, 8, 15, 22, 29, 36];

    /// @notice Days (86400 seconds)
    uint256 public constant DAYS = 1 days;

    /// @notice Hours (3600 seconds)
    uint256 public constant HOURS = 1 hours;

    /// @notice old Paid Address
    address public oldPaidAddress;

    /// @notice new Paid Address
    address public newPaidAddress;

    /// @notice poolStartTime
    uint256 public poolStartTime;

    /// @notice store total unstaking of u3Upgrade
    mapping(address => uint256) public totalStaking;

    /// @notice lockable token mapping
    mapping(address => LockableTokens) public u3UpgradeLockableDetails;

    /// @notice mapping for storing the unStaking status of a user.
    mapping(address => mapping(uint256 => bool)) public u3UnstakeStatus;

    event IntervalDaysDetails(uint256[] updatedIntervals, uint256 time);

    event Claim(
        address indexed userAddress,
        address indexed stakedTokenAddress,
        address indexed tokenAddress,
        uint256 claimRewards,
        uint256 time
    );

    event UnStake(
        address indexed userAddress,
        address indexed unStakedtokenAddress,
        uint256 unStakedAmount,
        uint256 time,
        uint256 stakeID
    );

    event ReferralEarn(
        address indexed userAddress,
        address indexed callerAddress,
        address indexed rewardTokenAddress,
        uint256 rewardAmount,
        uint256 time
    );

    event LockableTokenDetails(
        address indexed tokenAddress,
        uint256 lockableDys,
        bool optionalbleStatus,
        uint256 updatedTime
    );

    event WithdrawDetails(
        address indexed tokenAddress,
        uint256 withdrawalAmount,
        uint256 time
    );

    constructor() Ownable(msg.sender) {}

    function cohortConfig() external onlyOwner {
        poolStartTime = uniV3.poolStartTime();
        oldPaidAddress = uniV3Proxy.oldPaidAddress();
        newPaidAddress = uniV3Proxy.newPaidAddress();
    }

    function updateTotalStaking(
        address[] memory tokenAddresses,
        uint256[] memory overAllTotalStake
    ) external onlyOwner returns (bool) {
        require(
            tokenAddresses.length == overAllTotalStake.length,
            "U3ReProxyUpgradablity: Invalid Inputs"
        );
        for (uint8 n = 0; n < tokenAddresses.length; n++) {
            require(
                tokenAddresses[n] != address(0),
                "U3ReProxyUpgradablity: invalid poolAddress"
            );
            require(
                overAllTotalStake[n] > 0,
                "U3ReProxyUpgradablity: emptied overAllStaked"
            );
            totalStaking[tokenAddresses[n]] = overAllTotalStake[n];
        }
        return true;
    }

    function updatePoolStartTime(uint256 _newPoolStartTime)
        external
        onlyOwner
        returns (bool)
    {
        poolStartTime = _newPoolStartTime;
        return true;
    }

    function init(address[] memory tokenAddress)
        external
        onlyOwner
        returns (bool)
    {
        for (uint256 i = 0; i < tokenAddress.length; i++) {
            transferFromContractV3(tokenAddress[i]);
        }
        return true;
    }

    function transferFromContractV3(address tokenAddress) internal {
        uint256 bal = IERC20(tokenAddress).balanceOf(address(u3Reproxy));
        if (bal > 0) u3Reproxy.safeWithdraw(tokenAddress, bal);
    }

    /**
     * @notice Get rewards for one day
     * @param stakedAmount Stake amount of the user
     * @param stakedToken Staked token address of the user
     * @param rewardToken Reward token address
     * @return reward One dayh reward for the user
     */

    function getOneDayReward(
        uint256 stakedAmount,
        address stakedToken,
        address rewardToken,
        uint256 totalStake
    ) public view returns (uint256 reward) {
        reward = (
            stakedAmount.mul(
                uniV3.tokenDailyDistribution(stakedToken, rewardToken)
            )
        ).div(totalStake);
    }

    /**
     * @notice send rewards
     * @param stakedToken Stake amount of the user
     * @param tokenAddress Reward token address
     * @param amount Amount to be transferred as reward
     */
    function sendToken(
        address user,
        address stakedToken,
        address tokenAddress,
        uint256 amount
    ) internal {
        // Checks
        if (tokenAddress != address(0)) {
            require(
                IERC20(tokenAddress).balanceOf(address(this)) >= amount,
                "SEND: Insufficient Balance in Contract"
            );

            IERC20(tokenAddress).transfer(user, amount);

            emit Claim(
                user,
                stakedToken,
                tokenAddress,
                amount,
                block.timestamp
            );
        }
    }

    function getTotalStaking(address tokenAddress)
        public
        view
        returns (uint256)
    {
        (, uint256 _totalStaking) = swapPaidToken(tokenAddress);
        return _totalStaking;
    }

    /**
     * @notice Unstake and claim rewards
     * @param stakeId Stake ID of the user
     */
    function unStake(address user, uint256 stakeId) external whenNotPaused {
        require(
            msg.sender == user || msg.sender == _owner,
            "UNSTAKE: Invalid User Entry"
        );

        (
            ,
            address[] memory tokenAddress,
            bool[] memory activeStatus,
            ,
            ,
            uint256[] memory stakedAmount,
            uint256[] memory startTime
        ) = (uniV3.viewStakingDetails(user));

        bool isAlreadyUnstaked = uniV3Proxy.unstakeStatus(user, stakeId);
        bool isAlreadyUnstakedFromProxy = u3Reproxy.u3UnstakeStatus(
            user,
            stakeId
        );

        // lockableDays check
        require(
            u3UpgradeLockableDetails[tokenAddress[stakeId]].lockableDays <=
                block.timestamp,
            "Token Locked"
        );

        // optional lock check
        if (
            u3UpgradeLockableDetails[tokenAddress[stakeId]].optionableStatus ==
            true
        ) {
            require(
                poolStartTime.add(uniV3.stakeDuration()) <= block.timestamp,
                "Locked in optional lock"
            );
        }

        // Checks
        if (
            u3UnstakeStatus[user][stakeId] == false &&
            isAlreadyUnstaked == false &&
            isAlreadyUnstakedFromProxy == false &&
            activeStatus[stakeId] == true
        ) u3UnstakeStatus[user][stakeId] = true;
        else revert("UNSTAKE : Unstaked Already");

        (address stakedToken, uint256 _totalStaking) = swapPaidToken(
            tokenAddress[stakeId]
        );

        // Balance check
        require(
            IERC20(stakedToken).balanceOf(address(this)) >=
                stakedAmount[stakeId],
            "UNSTAKE : Insufficient Balance"
        );

        IERC20(stakedToken).transfer(user, stakedAmount[stakeId]);

        // precaution for overflow.
        uint256 endTime = poolStartTime.add(uniV3.stakeDuration());

        if (endTime > startTime[stakeId]) {
            claimRewards(user, stakeId, _totalStaking);
        }

        // Emit state changes
        emit UnStake(
            user,
            stakedToken,
            stakedAmount[stakeId],
            block.timestamp,
            stakeId
        );
    }

    function claimRewards(
        address user,
        uint256 stakeId,
        uint256 _totalStaking
    ) internal {
        (
            address[] memory referrerAddress,
            address[] memory tokenAddress,
            ,
            ,
            ,
            uint256[] memory stakedAmount,
            uint256[] memory startTime
        ) = (uniV3.viewStakingDetails(user));

        // Local variables
        uint256 interval;
        uint256 endOfProfit;

        interval = poolStartTime.add(uniV3.stakeDuration());

        if (interval > block.timestamp) endOfProfit = block.timestamp;
        else endOfProfit = poolStartTime.add(uniV3.stakeDuration());

        interval = endOfProfit.sub(startTime[stakeId]);

        // Reward calculation
        if (interval >= HOURS)
            _rewardCalculation(
                user,
                referrerAddress[stakeId],
                tokenAddress[stakeId],
                stakedAmount[stakeId],
                interval,
                _totalStaking
            );
    }

    function _rewardCalculation(
        address user,
        address referrerAddress,
        address stakedToken,
        uint256 stakedAmount,
        uint256 interval,
        uint256 totalStake
    ) internal {
        uint256 rewardsEarned;
        uint256 noOfDays;
        uint256 noOfHours;

        noOfHours = interval.div(HOURS);
        noOfDays = interval.div(DAYS);

        rewardsEarned = noOfHours.mul(
            getOneDayReward(stakedAmount, stakedToken, stakedToken, totalStake)
        );

        (address stakedToken1, ) = swapPaidToken(stakedToken);

        // Referrer Earning
        if (referrerAddress != address(0)) {
            uint256 refEarned = (rewardsEarned.mul(uniV3.refPercentage())).div(
                100 ether
            );
            rewardsEarned = rewardsEarned.sub(refEarned);
            require(
                IERC20(stakedToken1).transfer(referrerAddress, refEarned),
                "Transfer Failed"
            );

            emit ReferralEarn(
                referrerAddress,
                user,
                stakedToken1,
                refEarned,
                block.timestamp
            );
        }

        //  Rewards Send
        sendToken(user, stakedToken1, stakedToken1, rewardsEarned);

        uint8 i = 1;
        while (i < intervalDays.length) {
            if (noOfDays >= intervalDays[i]) {
                uint256 balHours = noOfHours.sub(
                    (intervalDays[i].sub(1)).mul(24)
                );

                address rewardToken = uniV3.tokensSequenceList(stakedToken, i);

                if (
                    rewardToken != stakedToken &&
                    uniV3.tokenBlockedStatus(stakedToken, rewardToken) == false
                ) {
                    rewardsEarned = balHours.mul(
                        getOneDayReward(
                            stakedAmount,
                            stakedToken,
                            rewardToken,
                            totalStake
                        )
                    );

                    (address rewardToken1, ) = swapPaidToken(rewardToken);

                    // Referrer Earning

                    if (referrerAddress != address(0)) {
                        uint256 refEarned = (
                            rewardsEarned.mul(uniV3.refPercentage())
                        ).div(100 ether);
                        rewardsEarned = rewardsEarned.sub(refEarned);

                        require(
                            IERC20(rewardToken1).transfer(
                                referrerAddress,
                                refEarned
                            ),
                            "Transfer Failed"
                        );

                        emit ReferralEarn(
                            referrerAddress,
                            user,
                            rewardToken1,
                            refEarned,
                            block.timestamp
                        );
                    }

                    //  Rewards Send
                    sendToken(user, stakedToken, rewardToken1, rewardsEarned);
                }
                i = i + 1;
            } else {
                break;
            }
        }
    }

    function swapPaidToken(address tokenAddress)
        internal
        view
        returns (address, uint256)
    {
        if (tokenAddress == oldPaidAddress) {
            return (newPaidAddress, totalStaking[oldPaidAddress]);
        } else {
            return (tokenAddress, totalStaking[tokenAddress]);
        }
    }

    function updateIntervalDays(uint256[] memory _interval) public onlyOwner {
        intervalDays = new uint256[](0);
        for (uint8 i = 0; i < _interval.length; i++) {
            uint256 noD = uniV3.stakeDuration().div(DAYS);
            require(noD > _interval[i], "Invalid Interval Day");
            intervalDays.push(_interval[i]);
        }
        emit IntervalDaysDetails(intervalDays, block.timestamp);
    }

    function lockableToken(
        address tokenAddress,
        uint8 lockableStatus,
        uint256 lockedDays,
        bool optionableStatus
    ) external onlyOwner {
        require(
            lockableStatus == 1 || lockableStatus == 2 || lockableStatus == 3,
            "Invalid Lockable Status"
        );

        (bool tokenExist, , , , , , ) = uniV3.tokenDetails(tokenAddress);

        require(tokenExist == true, "Token Not Exist");

        if (lockableStatus == 1) {
            u3UpgradeLockableDetails[tokenAddress].lockableDays = block
                .timestamp
                .add(lockedDays);
        } else if (lockableStatus == 2)
            u3UpgradeLockableDetails[tokenAddress].lockableDays = 0;
        else if (lockableStatus == 3)
            u3UpgradeLockableDetails[tokenAddress]
                .optionableStatus = optionableStatus;

        emit LockableTokenDetails(
            tokenAddress,
            u3UpgradeLockableDetails[tokenAddress].lockableDays,
            u3UpgradeLockableDetails[tokenAddress].optionableStatus,
            block.timestamp
        );
    }

    function transferV3ProxyOwnership(address newOwner) external onlyOwner {
        u3Reproxy.transferOwnership(newOwner);
    }

    function safeWithdraw(address tokenAddress, uint256 amount)
        external
        onlyOwner
    {
        require(
            IERC20(tokenAddress).balanceOf(address(this)) >= amount,
            "SAFEWITHDRAW: Insufficient Balance"
        );

        require(
            IERC20(tokenAddress).transfer(_owner, amount) == true,
            "SAFEWITHDRAW: Transfer failed"
        );

        emit WithdrawDetails(tokenAddress, amount, block.timestamp);
    }

    function setAllLegacyCohortU3(address[] memory u3Addresses)
        external
        onlyOwner
        returns (bool)
    {
        uniV3 = U3Legacy(u3Addresses[0]);
        uniV3Proxy = U3Proxy(u3Addresses[1]);
        u3Reproxy = V3Reproxy(u3Addresses[2]);
        return true;
    }

    function updateOldPaidAddress(address oldPaid)
        external
        onlyOwner
        returns (bool)
    {
        oldPaidAddress = oldPaid;
        return true;
    }

    function updateNewPaidAddress(address newPaid)
        external
        onlyOwner
        returns (bool)
    {
        newPaidAddress = newPaid;
        return true;
    }

    function emergencyUnstake(
        uint256 stakeId,
        address userAddress,
        address[] memory rewardtokens,
        uint256[] memory amount
    ) external onlyOwner {
        (
            address[] memory referrerAddress,
            address[] memory tokenAddress,
            bool[] memory activeStatus,
            ,
            ,
            uint256[] memory stakedAmount,

        ) = (uniV3.viewStakingDetails(userAddress));

        bool isAlreadyUnstaked = uniV3Proxy.unstakeStatus(userAddress, stakeId);
        // Checks
        if (
            u3UnstakeStatus[userAddress][stakeId] == false &&
            isAlreadyUnstaked == false &&
            activeStatus[stakeId] == true &&
            u3Reproxy.u3UnstakeStatus(userAddress,stakeId) == false
        ) u3UnstakeStatus[userAddress][stakeId] = true;
        else revert("EMERGENCY: Unstaked Already");

        (address stakedToken, ) = swapPaidToken(tokenAddress[stakeId]);

        // Balance check
        require(
            IERC20(stakedToken).balanceOf(address(this)) >=
                stakedAmount[stakeId],
            "EMERGENCY : Insufficient Balance"
        );

        IERC20(stakedToken).transfer(userAddress, stakedAmount[stakeId]);

        for (uint256 i = 0; i < rewardtokens.length; i++) {
            require(
                IERC20(rewardtokens[i]).balanceOf(address(this)) >= amount[i],
                "EMERGENCY : Insufficient Reward Balance"
            );
            uint256 rewardsEarned = amount[i];

            if (referrerAddress[stakeId] != address(0)) {
                uint256 refEarned = (rewardsEarned.mul(uniV3.refPercentage()))
                    .div(100 ether);
                rewardsEarned = rewardsEarned.sub(refEarned);

                require(
                    IERC20(rewardtokens[i]).transfer(
                        referrerAddress[stakeId],
                        refEarned
                    ),
                    "EMERGENCY : Transfer Failed"
                );

                emit ReferralEarn(
                    referrerAddress[stakeId],
                    userAddress,
                    rewardtokens[i],
                    refEarned,
                    block.timestamp
                );
            }
            sendToken(userAddress, stakedToken, rewardtokens[i], rewardsEarned);
        }

        // Emit state changes
        emit UnStake(
            userAddress,
            stakedToken,
            stakedAmount[stakeId],
            block.timestamp,
            stakeId
        );
    }

    function lockContract(bool pauseStatus) external onlyOwner {
        if (pauseStatus == true) _pause();
        else if (pauseStatus == false) _unpause();
    }
}
function getOneDayReward(
    uint256 stakedAmount,
    address stakedToken,
    address rewardToken,
    uint256 totalStake
) public view returns (uint256 reward) {
    reward = (
        stakedAmount.mul(
            uniV3.tokenDailyDistribution(stakedToken, rewardToken)
        )
    ).div(totalStake);
}

/**
 * @notice Get rewards for one day
 * @param stakedAmount Stake amount of the user
 * @param stakedToken Staked token address of the user
 * @param rewardToken Reward token address
 * @return reward One dayh reward for the user
 */