// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

// This file is strictly provided as-is
// without any express or implied guarantees, representations, or warranties
// as to this file or any deployments hereof.

// Any users of these files are doing so at their own risk.

/// @notice timelocks $ENSHROUD tokens. The admin can send tokens to this contract to timelock them.
/// These tokens will then unlock to their recipient linearly, starting from releaseStart and ending at releaseEnd of the respective timelock.
/// Alternatively, the token recipients can transfer their locked tokens from TimelockManager to the DAOPool or the MVOStaking contract.
/// These tokens will remain timelocked (i.e., will not be withdrawable) until they are unlocked
/// according to their respective schedule but can be otherwise used in the respective contracts.

// Solbase ReentrancyGuard
import {ReentrancyGuard} from "./ReentrancyGuard.sol";

// Solbase SafeTransferLib
import {SafeTransferLib} from "./SafeTransferLib.sol";

interface IDAOPool_Timelock {
    function deposit(
        address depositor,
        address beneficiary,
        uint256 withdrawable
    ) external;

    function depositWithLocking(
        address depositor,
        address beneficiary,
        uint256 timelocked,
        uint128 unlockStart,
        uint128 unlockEnd
    ) external;
}

interface IMVOStaking_Timelock {
    function stakeTimelock(
        string calldata mvoID,
        uint256 amount,
        address staker,
        uint128 releaseStart,
        uint128 releaseEnd
    ) external;
}

interface IEnshroudToken_Timelock {
    function approve(address spender, uint256 amount) external returns (bool);
}

contract TimelockManager is ReentrancyGuard {
    using SafeTransferLib for address;

    /// totalAmount: total amount of timelocked tokens
    /// remainingAmount: remaining amount of 'totalAmount' in this timelock (i.e. 'totalAmount' - withdrawn amount)
    struct Timelock {
        uint256 totalAmount;
        uint256 remainingAmount;
        uint128 releaseStart;
        uint128 releaseEnd;
    }

    IEnshroudToken_Timelock public immutable iEnshroudToken;

    IDAOPool_Timelock public daoPool;
    IMVOStaking_Timelock public mvoStaking;

    /// @notice controller of this contract. One admin is sufficient as this contract only locks tokens;
    /// it does not have isMinter[] status for the $ENSHROUD token, nor can it change an existing timelock, so admin security vectors are minimal
    address public admin;

    /// @notice address of $ENSHROUD token to be timelocked, immutable
    address public immutable enshroudToken;

    /// @notice timelocks in this contract; has an automatic getter as a public mapping
    mapping(address => Timelock) public timelocks;

    /*///////////////////////////////////////////////////////////////
                            MODIFIERS
    //////////////////////////////////////////////////////////////*/

    modifier onlyAdmin() {
        if (msg.sender != admin) revert Timelock_NotAdmin();
        _;
    }

    modifier onlyIfRecipientHasRemainingTokens(address recipient) {
        if (timelocks[recipient].remainingAmount == 0)
            revert Timelock_NoAvailableTokens();
        _;
    }

    /*///////////////////////////////////////////////////////////////
                            ERRORS
    //////////////////////////////////////////////////////////////*/

    error Timelock_AmountTooLarge();
    error Timelock_NoAvailableTokens();
    error Timelock_NotAdmin();
    error Timelock_StartMustPrecedeEnd();
    error Timelock_StartNotInFuture();
    error TimelockAlreadyExists();
    error ZeroAddress();

    /*///////////////////////////////////////////////////////////////
                            EVENTS
    //////////////////////////////////////////////////////////////*/

    event AdminUpdated(address newAdmin);

    event DAOPoolUpdated(address daoPoolAddress);

    event MVOStakingContractUpdated(address mvoStakingContractAddress);

    event TransferredAndLocked(
        address source,
        address recipient,
        uint256 amount,
        uint128 releaseStart,
        uint128 releaseEnd
    );

    event Withdrawn(address recipient, uint256 withdrawable);

    event WithdrawnToMVOStaking(
        address recipient,
        address mvoStakingContract,
        address beneficiary
    );

    event WithdrawnToPool(
        address recipient,
        address daoPoolAddress,
        address beneficiary
    );

    /*///////////////////////////////////////////////////////////////
                            FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @dev MVO staking contract is not initialized in the constructor because this contract will be deployed before it, so updateMVOStakingContract() *MUST* be called post-deploy
    /// @param _enshroudTokenAddress: address of the $ENSHROUD token contract
    /// @param _admin: address to control this contract as admin
    /// @param _daoPoolAddress: address of the DAO pool contract
    constructor(
        address _enshroudTokenAddress,
        address _admin,
        address _daoPoolAddress
    ) payable {
        enshroudToken = _enshroudTokenAddress;
        iEnshroudToken = IEnshroudToken_Timelock(_enshroudTokenAddress);
        daoPool = IDAOPool_Timelock(_daoPoolAddress);
        admin = _admin;
    }

    /// @notice called by the owner to set the address of the MVO staking contract, to which token recipients can transfer their locked $ENSHROUD
    /// @param _mvoStakingAddress: address of the MVO Staking contract
    function updateMVOStakingContract(
        address _mvoStakingAddress
    ) external onlyAdmin {
        mvoStaking = IMVOStaking_Timelock(_mvoStakingAddress);
        emit MVOStakingContractUpdated(_mvoStakingAddress);
    }

    /// @notice transfer admin role to a new admin address
    /// @param _admin: address of new admin. Pass address(0) to burn admin role, which will remove ability to call the onlyAdmin-modified functions
    function updateAdmin(address _admin) external onlyAdmin {
        admin = _admin;
        emit AdminUpdated(_admin);
    }

    /// @notice called by the owner to update the address of daoPool, which token recipients can transfer their tokens to for staking
    /// @param _daoPoolAddress: address of the DAO pool contract
    function updateDAOPool(address _daoPoolAddress) external onlyAdmin {
        daoPool = IDAOPool_Timelock(_daoPoolAddress);
        emit DAOPoolUpdated(_daoPoolAddress);
    }

    /// @notice called by the recipient to withdraw their unlocked tokens
    function withdraw()
        external
        onlyIfRecipientHasRemainingTokens(msg.sender)
        nonReentrant
    {
        uint256 _withdrawable = getWithdrawable(msg.sender);
        if (_withdrawable == 0) revert Timelock_NoAvailableTokens();

        timelocks[msg.sender].remainingAmount -= _withdrawable;

        enshroudToken.safeTransfer(msg.sender, _withdrawable);

        emit Withdrawn(msg.sender, _withdrawable);
    }

    /// @notice used by the recipient to withdraw their (locked and unlocked) tokens to the DAO pool
    /// @param _beneficiary: address that the tokens will be deposited to the pool contract on behalf of
    function withdrawToPool(
        address _beneficiary
    ) external onlyIfRecipientHasRemainingTokens(msg.sender) nonReentrant {
        address _daoPoolAddress = address(daoPool);
        if (_beneficiary == address(0) || _daoPoolAddress == address(0))
            revert ZeroAddress();

        uint256 _withdrawable = getWithdrawable(msg.sender);
        uint256 _remaining = timelocks[msg.sender].remainingAmount;
        uint256 _timelocked = _remaining - _withdrawable;

        // approve the amount that remains in this timelock to be withdrawn to pool
        iEnshroudToken.approve(_daoPoolAddress, _remaining);

        // deposit the funds that are withdrawable without locking
        if (_withdrawable != 0) {
            daoPool.deposit(address(this), _beneficiary, _withdrawable);
        }

        // deposit in the daoPool the tokens that are still timelocked
        // unlocking will continue linearly at the DAO pool; '_beneficiary' should call updateTimelockStatus() in the daoPool contract after successful deposit
        uint128 _block = uint128(block.number);
        if (_timelocked != 0) {
            daoPool.depositWithLocking(
                address(this),
                _beneficiary,
                _timelocked,
                _block > timelocks[msg.sender].releaseStart
                    ? _block
                    : timelocks[msg.sender].releaseStart,
                timelocks[msg.sender].releaseEnd
            );
        }

        // delete the timelock in this contract, as it is now relocated to DAOPool
        delete timelocks[msg.sender];

        emit WithdrawnToPool(msg.sender, _daoPoolAddress, _beneficiary);
    }

    /// @notice Used by the recipient (an MVO operator) to move their timelocked tokens to stake in the MVO staking contract
    /// @param _beneficiary: address that the tokens will be staked in the MVO staking contract on behalf of
    /// @param _mvoID: MVO ID of msg.sender
    function withdrawTimelockToMVOStaking(
        address _beneficiary,
        string calldata _mvoID
    ) external onlyIfRecipientHasRemainingTokens(msg.sender) nonReentrant {
        address _mvoStakingContract = address(mvoStaking);
        if (_beneficiary == address(0) || _mvoStakingContract == address(0))
            revert ZeroAddress();
        address _recipient = msg.sender;
        uint256 _withdrawable = getWithdrawable(_recipient);

        // withdraw any unlocked tokens for simplicity, msg.sender can stake them in the MVO Staking contract separately if desired
        if (_withdrawable != 0) {
            timelocks[_recipient].remainingAmount -= _withdrawable;
            enshroudToken.safeTransfer(_recipient, _withdrawable);
            emit Withdrawn(_recipient, _withdrawable);
        }
        uint256 _timelocked = timelocks[_recipient].remainingAmount;

        // stake the tokens that are still timelocked
        // unlocking will continue linearly at the MVO staking contract
        uint128 _block = uint128(block.number);
        if (_timelocked != 0) {
			// approve the timelocked amount for MVOStaking, so it may transfer tokens from address(this) to itself as they unlock
			iEnshroudToken.approve(_mvoStakingContract, _timelocked);

            mvoStaking.stakeTimelock(
                _mvoID,
                _timelocked,
                _beneficiary,
                _block > timelocks[_recipient].releaseStart
                    ? _block
                    : timelocks[_recipient].releaseStart,
                timelocks[_recipient].releaseEnd
            );
        }

        // delete the timelock in this contract, as it is now relocated to MVOstaking
        delete timelocks[_recipient];

        emit WithdrawnToMVOStaking(
            _recipient,
            _mvoStakingContract,
            _beneficiary
        );

    }

    /// @notice transfers $ENSHROUD tokens to this contract and timelocks them
    /// @dev _source must first approve() address(this) in the enshroudToken contract for '_amount'. A recipient cannot have multiple timelocks.
    /// @param _source: source of tokens
    /// @param _recipient: recipient of tokens
    /// @param _amount: amount of tokens
    /// @param _releaseStart: block number start of release time
    /// @param _releaseEnd: block number end of release time
    function transferAndLock(
        address _source,
        address _recipient,
        uint256 _amount,
        uint128 _releaseStart,
        uint128 _releaseEnd
    ) public onlyAdmin {
        if (timelocks[_recipient].remainingAmount != 0)
            revert TimelockAlreadyExists();
        if (_releaseEnd <= _releaseStart) revert Timelock_StartMustPrecedeEnd();
        if (_releaseStart <= uint128(block.number))
            revert Timelock_StartNotInFuture();
        timelocks[_recipient] = Timelock({
            totalAmount: _amount,
            remainingAmount: _amount,
            releaseStart: _releaseStart,
            releaseEnd: _releaseEnd
        });
        enshroudToken.safeTransferFrom(_source, address(this), _amount);
        emit TransferredAndLocked(
            _source,
            _recipient,
            _amount,
            _releaseStart,
            _releaseEnd
        );
    }

    /// @notice returns the amount of tokens a recipient can currently withdraw
    /// @param _recipient: address of the recipient
    /// @return withdrawable amount of tokens withdrawable by the recipient (unlocked - already withdrawn)
    function getWithdrawable(
        address _recipient
    ) public view returns (uint256 withdrawable) {
        withdrawable =
            _getUnlocked(_recipient) -
            (timelocks[_recipient].totalAmount -
                timelocks[_recipient].remainingAmount);
    }

    /// @notice returns 'unlocked', the amount of tokens that was unlocked for the recipient to date. Includes both withdrawn and non-withdrawn tokens.
    /// @param _recipient: address of the recipient
	/// @return unlocked number of unlocked tokens
    function _getUnlocked(
        address _recipient
    ) internal view returns (uint256 unlocked) {
        Timelock storage timelock = timelocks[_recipient];
        uint256 _releaseStart = uint256(timelock.releaseStart);
        uint256 _releaseEnd = uint256(timelock.releaseEnd);
        if (block.number <= _releaseStart) {
            unlocked = 0;
        } else if (block.number >= _releaseEnd) {
            unlocked = timelock.totalAmount;
        } else {
            // unlocked = (total amount * passed time since start) / total time of timelock
            unlocked =
                (timelock.totalAmount * (block.number - _releaseStart)) /
                (_releaseEnd - _releaseStart);
        }
    }
}
