// File: contracts/S9Strategy.sol



import "@openzeppelin/contracts/access/Ownable.sol";
import "./interfaces/ISwapRouter.sol";
import "./interfaces/IWETH.sol";
import "./interfaces/IERC20.sol";
import "./interfaces/IFees.sol";
import "./interfaces/ISwapHelper.sol";
import "./interfaces/IRibbonVault.sol";
import "./proxies/S9Proxy.sol";

pragma solidity ^0.8.17;

// SPDX-License-Identifier: MIT

contract S9Strategy is Ownable {
    address swapRouter;
    address feeContract;
    address wethAddress = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    constructor(address swapRouter_, address feeContract_) {
        swapRouter = swapRouter_;
        feeContract = feeContract_;
    }

    uint256 public constant strategyId = 14;


    //modifiers
    modifier whitelistedToken(address token) {
        require(
            IFees(feeContract).whitelistedDepositCurrencies(strategyId, token),
            "whitelistedToken: Invalid token"
        );
        _;
    }

    //mappings
    //user => user proxy
    mapping(address => address) public depositors;

    //vault address => whitelist status
    mapping(address=>bool) public vaultWhitelist;

    //valutAsset => vaultAssetSwapHelper
    mapping(address=>address) public swapHelper;

    //events
    event Deposit(
        address user,
        address tokenIn,
        address vault,
        uint256 amountIn
    );

    event QueueWithdraw(address user, address vault, uint256 amount);

    event Withdraw(
        address user,
        address tokenOut,
        address vault,
        uint256 amount,
        uint256 fee
    );

    //getters

    //returns the amounts in the different deposit states
    //@dev return amounts are in shares except avaliableInstant, multiply with pricePerShare to get USDC amount

    //locked amount currently generating yield
    //pending amount not generating yield, waiting to be available
    //avaliableClaim is amount that has gone through an epoch and has been initiateWithdraw
    //availableInstant amount are deposits that have not gone through an epoch that can be withdrawn
    function getDepositData(address user, address vault)
        public
        view
        returns (
            address,
            uint256,
            uint256,
            uint256,
            uint256,
            uint256,
            uint256,
            uint
        )
    {
        address vaultAsset;
        uint256 locked;
        uint256 pending;
        uint256 avaliableInstant;
        uint256 avaliableClaim;
        uint256 pricePerShare;
        uint256 withdrawPricePerShare;
        uint256 vaultRound;
        uint256 currentLoanTermLength;        

        IRibbonVault rv = IRibbonVault(vault);
        (,vaultAsset,,)=rv.vaultParams();
        //depositReciept is only updated when initiateWithdraw is called
        //In native token
        (vaultRound, , , , , , , , ) = getVaultState(vault);
        avaliableInstant = _getDepositReciepts(
            vault,
            vaultRound,
            depositors[user]
        );
        (, pending, avaliableClaim, withdrawPricePerShare) = _getWithdrawReciepts(
            vault,
            vaultRound,
            depositors[user]
        );
        pricePerShare=rv.pricePerShare();
        (uint heldByAccount,uint heldByVault)=rv.shareBalances(depositors[user]);
        locked=heldByAccount+heldByVault;
        (, , currentLoanTermLength, , , , , ) = rv.allocationState();
        return (
            //asset token used by the vault
            vaultAsset,
            //generating yield denotes shares are currently locked in round
            //shares generating yield, before initateWithdraw
            locked,
            //shares generating yield, after initiateWithdraw before completeWithdraw
            pending,
            //token not generating yield, pending instantWithdraw
            avaliableInstant,
            //shares not generating yield, after round end before comepleteWithdraw
            //@dev to only take this parameter for v1                 
            avaliableClaim,
            //the price per share of the last round, used to calculate token value for locked
            pricePerShare,
            //the price per share of the initiate withdraw round, used to calculate token value for avaliableClaim
            withdrawPricePerShare,
            currentLoanTermLength
        );
    }

    //direct proxy method to vaultState
    function getVaultState(address vault)
        public
        view
        returns (
            uint16,
            uint104,
            uint104,
            uint128,
            uint128,
            uint64,
            uint64,
            uint128,
            uint256
        )
    {
        (
            uint16 round,
            uint104 lockedAmount,
            uint104 lastLockedAmount,
            uint128 totalPending,
            uint128 queuedWithdrawShares,
            uint64 lastEpochTime,
            uint64 lastOptionPurchaseTime,
            uint128 optionsBoughtInRound,
            uint256 amtFundsReturned
        ) = IRibbonVault(vault).vaultState();
        return (
            round,
            lockedAmount,
            lastLockedAmount,
            totalPending,
            queuedWithdrawShares,
            lastEpochTime,
            lastOptionPurchaseTime,
            optionsBoughtInRound,
            amtFundsReturned
        );
    }

    //write
    function depositToken(
        address tokenIn,
        address vault,
        uint256 amount,
        uint256 minAmountOut
    ) public payable whitelistedToken(tokenIn){
        require(
            IFees(feeContract).depositStatus(strategyId),
            "depositToken: depositsStopped"
        );
        require(vaultWhitelist[vault], "depositToken: vaultWhitelist");
        address proxy = depositors[msg.sender];
        if (proxy == address(0)) {
            //mint proxy if not exists
            S9Proxy newProxy = new S9Proxy(msg.sender);
            proxy = address(newProxy);
            depositors[msg.sender] = proxy;
        }
        address vaultAsset;
        (,vaultAsset,,)=IRibbonVault(vault).vaultParams();
        //swap
        if (tokenIn != vaultAsset) {
            if (msg.value == 0) {
                IERC20(tokenIn).transferFrom(msg.sender, address(this), amount);
            } else {
                //convert eth to weth
                (bool success, ) = payable(wethAddress).call{value: msg.value}(
                    ""
                );
                require(success, "depositToken: Send ETH fail");
                tokenIn = wethAddress;
            }
            IERC20(tokenIn).approve(swapRouter, amount);
            //swap to weth            
            amount=ISwapRouter(swapRouter).swapTokenForToken(
                tokenIn,
                vaultAsset,
                amount,
                minAmountOut,
                proxy
            );            
        } else {
            IERC20(vaultAsset).transferFrom(
                msg.sender,
                proxy,
                amount
            );
        }
        S9Proxy(depositors[msg.sender]).deposit(vault, vaultAsset, amount);
        emit Deposit(msg.sender, tokenIn, vault, amount);
    }
    //@dev only the amount here should be in shares
    function queueWithdraw(address vault, uint256 amount) external {
        S9Proxy(depositors[msg.sender]).queueWithdraw(vault, amount);
        emit QueueWithdraw(msg.sender, vault, amount);
    }
    
    //@dev pass address(0) for ETH
    function withdrawToken(
        address tokenOut,
        address vault,
        uint256 requestAmtToken,
        uint256 minAmountOut,
        address feeToken
    ) external whitelistedToken(tokenOut) {
        address _tokenOut = tokenOut != address(0) ? tokenOut : wethAddress;
        address vaultAsset;

        (,vaultAsset,,)=IRibbonVault(vault).vaultParams();
        (uint256 vaultRound, , , , , , , , ) = getVaultState(vault);
        uint256 instantAmt=_getDepositReciepts(vault, vaultRound, depositors[msg.sender]);
        S9Proxy(depositors[msg.sender]).withdraw(
            vault,
            vaultAsset,
            requestAmtToken,
            instantAmt
        );
        uint result=IERC20(vaultAsset).balanceOf(address(this));
        //We redeposit if there is excess
        if (result > requestAmtToken) {
            uint redepositAmt=result-requestAmtToken;
            //We transfer to proxy
            IERC20(vaultAsset).transfer(depositors[msg.sender], redepositAmt);
            S9Proxy(depositors[msg.sender]).deposit(vault, vaultAsset, redepositAmt);
            result = requestAmtToken;
        }
        uint256 fee = (IFees(feeContract).calcFee(
            strategyId,
            msg.sender,
            feeToken
        ) * result) / 1000;
        IERC20(vaultAsset).transfer(
            IFees(feeContract).feeCollector(strategyId),
            fee
        );
        result=IERC20(vaultAsset).balanceOf(address(this));
        if(swapHelper[vaultAsset]!=address(0)){
            //expected to always swap to weth
            IERC20(vaultAsset).approve(swapHelper[vaultAsset], result);
            result=ISwapHelper(swapHelper[vaultAsset]).swap(vaultAsset, wethAddress, result, minAmountOut, address(this));
            vaultAsset=wethAddress;
        }
        if (_tokenOut != vaultAsset) {
            //swap
            IERC20(vaultAsset).approve(swapRouter, result);
            result = ISwapRouter(swapRouter).swapTokenForToken(
                vaultAsset,
                _tokenOut,
                result,
                1,
                address(this)
            );
        }
        require(result>=minAmountOut, "withdrawToken: minAmountOut");
        if (tokenOut != address(0)) {
            IERC20(tokenOut).transfer(msg.sender, IERC20(tokenOut).balanceOf(address(this)));
        } else {
            IWETH(wethAddress).withdraw(result);
            (bool success, ) = payable(msg.sender).call{value: result}("");
            require(success, "withdrawToken: Send ETH fail");
        }
        emit Withdraw(msg.sender, tokenOut, vault, result, fee);
    }

    function toggleVaultWhitelist(address[] calldata vaults, bool state) external onlyOwner{
        for(uint i=0; i<vaults.length; i++){
            vaultWhitelist[vaults[i]]=state;
        }
    }

    function setHelper(address token, address helper) external onlyOwner{
        swapHelper[token]=helper;
    }

    //internal reads
    function _getDepositReciepts(
        address vault,
        uint256 vaultRound,
        address user
    ) internal view returns (uint256) {
        uint256 available;
        (uint256 round, uint256 depositAmount, ) = IRibbonVault(vault)
            .depositReceipts(user);
        if (vaultRound == round) {
            available += depositAmount;
        }
        return available;
    }

    function _getWithdrawReciepts(
        address vault,
        uint256 vaultRound,
        address user
    ) internal view returns (uint, uint256, uint, uint) {
        uint pending;
        uint256 avaliableClaim;
        IRibbonVault rv = IRibbonVault(vault);
        (uint256 round, uint256 withdrawAmount) = rv.withdrawals(user);
        //only pending or avaliableClaim will be possible
        //initiating withdraw when avaliableClaim > 0 will revert
        if (vaultRound > round) {
            avaliableClaim += withdrawAmount;
        }
        if(vaultRound==round){
            pending+=withdrawAmount;
        }
        uint withdrawPricePerShare=rv.roundPricePerShare(round);
        return (round, pending, avaliableClaim, withdrawPricePerShare);
    }
    receive() external payable {}
}
// File: contracts/interfaces/IERC20.sol



pragma solidity 0.8.17;
// SPDX-License-Identifier: MIT

interface IERC20 {
    function balanceOf(address _owner) external view returns (uint256);

    function allowance(address owner, address spender) external view returns (uint256);

    function approve(address spender, uint256 amount) external;

    function transfer(address recipient, uint256 amount) external;

    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external;

    function decimals() external view returns (uint256);
}
// File: contracts/interfaces/ISwapRouter.sol



pragma solidity >=0.8.17;
// SPDX-License-Identifier: MIT

interface ISwapRouter {
    function swapTokenForToken(address _tokenIn, address _tokenOut, uint256 _amount, uint256 _amountOutMin, address _to) external returns(uint256);
    function swapTokenForETH(address _tokenIn, uint256 _amount, uint256 _amountOutMin, address _to) external returns(uint256);
    function swapETHForToken(address _tokenOut, uint256 _amount, uint256 _amountOutMin, address _to) external payable returns(uint256);
    // function swap(address tokenIn, address tokenOut, uint amount, uint minAmountOut, address to) external;
    function _swapV2(address _router, address _tokenIn, uint256 _amount, address _to) external returns(uint256);
}
// File: contracts/interfaces/IWETH.sol



pragma solidity 0.8.17;
// SPDX-License-Identifier: MIT
interface IWETH {
    function withdraw(uint wad) external;
}
// File: contracts/interfaces/IFees.sol



pragma solidity >=0.8.17;

// SPDX-License-Identifier: MIT

interface IFees {
    struct FeeTokenData {
        uint256 minBalance;
        uint256 fee;
    }

    //read functions

    function defaultFee() external view returns (uint256);

    function feeCollector(uint256 strategyId) external view returns (address);

    function feeTokenMap(uint256 strategyId, address feeToken)
        external
        view
        returns (FeeTokenData memory);    

    function depositStatus(uint256 strategyId) external view returns (bool);

    function whitelistedDepositCurrencies(uint256, address)
        external
        view
        returns (bool);

    function calcFee(
        uint256 strategyId,
        address user,
        address feeToken
    ) external view returns (uint256);

    //write functions    

    function setTokenFee(
        uint256 strategyId,
        address feeToken,
        uint256 minBalance,
        uint256 fee
    ) external;

    function setTokenMulti(
        uint256 strategyId,
        address[] calldata feeTokens,
        uint256[] calldata minBalance,
        uint256[] calldata fee) external;

    function setDepositStatus(uint256 strategyId, bool status) external;

    function setFeeCollector(address newFeeCollector) external;

    function setDefaultFee(uint newDefaultFee) external;

    function toggleWhitelistTokens(
        uint256 strategyId,
        address[] calldata tokens,
        bool state
    ) external;
}

// File: contracts/interfaces/ISwapHelper.sol



pragma solidity 0.8.17;
// SPDX-License-Identifier: MIT

interface ISwapHelper {
        function swap(
        address tokenIn,
        address tokenOut,
        uint256 amount,
        uint256 minAmountOut,
        address to
    ) external returns (uint256 result);
}
// File: contracts/interfaces/IRibbonVault.sol



pragma solidity >=0.8.4;

// SPDX-License-Identifier: MIT

interface IRibbonVault {
    //read
    //returns heldByAccount, heldByValut
    //heldByAccount: locked
    //heldByVault: avaliable
    function shareBalances(address account)
        external
        view
        returns (uint256 heldByAccount, uint256 heldByVault);

    //shares: amount pending
    function withdrawals(address account)
        external
        view
        returns (uint256 round, uint256 shares);

    //amount in native token available for withdraw
    function depositReceipts(address account)
        external
        view
        returns (
            uint16 round,
            uint104 amount,
            uint128 unredeemedShares
        );

    //multiplied on UI to show amount staked
    function pricePerShare() external view returns (uint256);

    function vaultState()
        external
        view
        returns (
            uint16 round,
            uint104 lockedAmount,
            uint104 lastLockedAmount,
            uint128 totalPending,
            uint128 queuedWithdrawShares,
            uint64 lastEpochTime,
            uint64 lastOptionPurchaseTime,
            uint128 optionsBoughtInRound,
            uint256 amtFundsReturned
        );

    function allocationState()
        external
        view
        returns (
            uint32 nextLoanTermLength,
            uint32 nextOptionPurchaseFreq,
            uint32 currentLoanTermLength,
            uint32 currentOptionPurchaseFreq,
            uint32 loanAllocationPCT,
            uint32 optionAllocationPCT,
            uint256 loanAllocation,
            uint256 optionAllocation
        );
    
    function vaultParams() external view returns(
        uint8 decimals,
        address asset, 
        uint56 minimumSupply,
        uint104 cap
    );

    function roundPricePerShare(uint round) external view returns(uint pricePerShare);
    function balanceOf(address user) external view returns(uint balanace);
    function decimals() external view returns(uint decimals);
    //write functions
    function deposit(uint256 amount) external;

    function initiateWithdraw(uint256 numShares) external;

    function withdrawInstantly(uint256 numShares) external;

    function maxRedeem() external;

    //Used for tests
    function rollToNextRound() external;

    function buyOption() external;

    function completeWithdraw() external;
}

// File: contracts/proxies/S9Proxy.sol



import "../interfaces/IERC20.sol";
import "../interfaces/IRibbonVault.sol";

pragma solidity ^0.8.17;

// SPDX-License-Identifier: MIT

contract S9Proxy {
    address deployer;
    address user;

    constructor(address user_) {
        deployer = msg.sender;
        user = user_;
    }

    modifier onlyDeployer() {
        require(msg.sender == deployer, "onlyDeployer: Unauthorized");
        _;
    }

    function deposit(
        address vault,
        address inputToken,
        uint256 amount
    ) external onlyDeployer {
        IERC20(inputToken).approve(vault, amount);
        IRibbonVault(vault).deposit(amount);
    }

    function queueWithdraw(address vault, uint256 amount)
        external
        onlyDeployer
    {
        //max redeem done internally
        IRibbonVault(vault).initiateWithdraw(amount);
    }

    function withdraw(
        address vault,
        address vaultAsset,
        //in token
        uint256 requestAmtToken,
        uint256 instantAvaliable
    ) external onlyDeployer returns (uint256) {
        uint256 balance;
        //Prioritize withdrawing non yielding funds
        //withdraws vaultAsset that was previously initiateWithdraw
        IRibbonVault(vault).completeWithdraw();
        balance = IERC20(vaultAsset).balanceOf(address(this));
        //Withdraw from current deposit that has not been sent to vault
        if (balance < requestAmtToken && instantAvaliable > 0) {
            //withdraw the difference, up to the requested amount
            //will revert R32 if withdrawInstantly round is current round
            //will revert R33 if insufficient
            uint256 withdrawInstantAmt = instantAvaliable >
                requestAmtToken - balance
                ? requestAmtToken - balance
                : instantAvaliable;
            IRibbonVault(vault).withdrawInstantly(withdrawInstantAmt);
            // balance += requestAmtToken - balance;
            balance = IERC20(vaultAsset).balanceOf(address(this));
        }
        //We transfer everything to strategy contract
        IERC20(vaultAsset).transfer(deployer, balance);
        return balance;
    }

    function emergencyWithdraw(
        address vault,
        address vaultAsset,
        uint256 instantAmt
    ) external {
        require(msg.sender == user);
        IRibbonVault(vault).completeWithdraw();
        if (instantAmt > 0) {
            IRibbonVault(vault).withdrawInstantly(instantAmt);
        }
        IERC20(vaultAsset).transfer(
            user,
            IERC20(vaultAsset).balanceOf(address(this))
        );
    }
}

// File: @openzeppelin/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: @openzeppelin/contracts/utils/Context.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}