contract SashimiInvestment is ISashimiInvestment, Ownable {
    using SafeERC20 for IERC20;
    using SafeMath for uint256;

    event AdminAdded(address admin);
    event AdminRemoved(address admin);
    event ProviderAdded(address indexed token, address provider, uint256 providerId);
    event ProviderSwitched(address indexed token, address provider, uint256 providerId);
    event ReservesRatiosChanged(address indexed token, uint256 reservesRatio);
    event ProviderDisabled(uint256 providerId);
    event ProviderEnabled(uint256 providerId);
    event ReBalancedReserves(address);
    event Harvested(address, address, uint256, uint256);

    address public router; // swap router
    address public routerForSashimi; // router for sashimi
    address public sashimi; // sashimi token
    address public wETH; // weth address
    mapping(address => bool) public isAdmin; // multi admins can chose providers

    address public beneficiary1; // 75% sashimi goes to
    address public beneficiary2; // 25% sashimi goes to

    bool public frozen;

    struct Provider {
        address vault;
        address token;
        bool enable;
        uint256 accProfit;
    }

    mapping(address => uint256) public deposits; // token => deposit amount
    mapping(address => uint256) public reservesRatios; // token => ratio
    mapping(address => uint256) public chosenProviders; // token => providerId

    uint256 public constant MAXIMAL_RATIO = 10000;
    uint256 public constant MINIMAL_RATIO = 1;
    uint256 public constant TOTAL_RATIO_SHARES = 10000;

    Provider[] public providers;

    modifier onlyRouter(){
        require(
            msg.sender == router,
            "Not router."
        );
        _;
    }

    modifier onlyAdmin(){
        require(
            isAdmin[msg.sender],
            "Not router."
        );
        _;
    }

    modifier validRatio(uint256 _reservesRatio){
        require(_reservesRatio >= MINIMAL_RATIO, "Invalid ratio.");
        require(_reservesRatio <= MAXIMAL_RATIO, "Invalid ratio.");
        _;
    }

    modifier notFrozen() {
        require(
            !frozen, "FROZEN."
        );
        _;
    }

    constructor (address _router, address _routerForSashimi, address _sashimi, address _wETH, address _beneficiary1) public {
        router = _router;
        routerForSashimi = _routerForSashimi;
        sashimi = _sashimi;
        wETH = _wETH;
        require(sashimi != wETH, "Invalid token input.");
        frozen = true;
        beneficiary1 = _beneficiary1;
        beneficiary2 = address(0xdead);

        // owner is also admin
        isAdmin[msg.sender] = true;

        // offset occupy
        providers.push(Provider({
        vault : address(0),
        token : address(0),
        enable : false,
        accProfit: 0
        }));
    }

    function reBalance(address _token) notFrozen public {
        uint256 providerId = chosenProviders[_token];
        require(providerId > 0, "Provider not chosen.");
        Provider memory provider = providers[providerId];

        uint256 reservesRatio = reservesRatios[_token];
        require(reservesRatio > 0, "ReservesRatios not exists.");

        uint256 beforeReserves = IERC20(_token).balanceOf(router);
        uint256 beforeDepositAmount = deposits[_token];
        uint256 afterReserves = (beforeReserves.add(beforeDepositAmount)).div(TOTAL_RATIO_SHARES).mul(reservesRatio);

        if (beforeReserves < afterReserves)
        {
            // more reserves needed
            uint256 amount = afterReserves.sub(beforeReserves);

            // withdraw
            ISashimiVault(provider.vault).withdraw(amount);

            // reserve to router
            IERC20(_token).safeTransfer(router, amount);
            deposits[_token] = beforeDepositAmount.sub(amount);
        }
        else if (beforeReserves > afterReserves)
        {
            // more deposit needed
            uint256 amount = beforeReserves.sub(afterReserves);
            IUniswapV2Router02(router).take(_token, amount);

            // be careful transfer deflationary tokens
            // approve and deposit
            ISashimiVault(provider.vault).deposit(amount);
            deposits[_token] = beforeDepositAmount.add(amount);
        }

        emit ReBalancedReserves(_token);
    }

    function harvest(address _token) notFrozen public returns (uint256 amount){
        uint256 chosen = chosenProviders[_token];
        require(chosen > 0, "Provider not chosen.");
        Provider storage provider = providers[chosen];
        amount = 0;
        uint256 profitAmount = ISashimiVault(provider.vault).harvest();
        if (profitAmount > 0)
        {
            address profit = ISashimiVault(provider.vault).profit();
            provider.accProfit += profitAmount;

            address[] memory path;
            if (profit == wETH)
            {
                path = new address[](2);
                path[0] = wETH;
                path[1] = sashimi;
            }
            else {
                path = new address[](3);
                path[0] = profit;
                path[1] = wETH;
                path[2] = sashimi;
            }

            // swap for sashimi, 3s is enough for deadline
            uint[] memory amounts = IUniswapV2Router01(routerForSashimi).swapExactTokensForTokens(profitAmount, 0, path, address(this), block.timestamp + 3);
            amount = amounts[path.length - 1];

            if (beneficiary1 != address(0))
                IERC20(sashimi).safeTransfer(beneficiary1, amount.mul(75).div(100));
            if (beneficiary2 != address(0))
                IERC20(sashimi).safeTransfer(beneficiary2, amount.mul(25).div(100));
            emit Harvested(_token, profit, profitAmount, amount);
        }
    }

    function withdraw(address _token, uint256 _amount) override onlyRouter notFrozen public {
        uint256 depositAmount = deposits[_token];
        require(depositAmount > _amount, "Deposit not enough.");
        uint256 providerId = chosenProviders[_token];
        //should be fine
        require(providerId > 0, "Provider not chosen.");

        Provider storage provider = providers[providerId];
        ISashimiVault(provider.vault).withdraw(_amount);
        IERC20(_token).safeTransfer(router, _amount);
        deposits[_token] = depositAmount.sub(_amount);
    }

    function withdrawWithReBalance(address _token, uint256 _amount) override onlyRouter notFrozen public{
        // reBalance
        reBalance(_token);
        uint256 depositAmount = deposits[_token];
        uint256 withdrawAmount = depositAmount > _amount ? _amount : depositAmount;
        withdraw(_token, withdrawAmount);
    }

    function disableProvider(uint256 _providerId) onlyOwner public {
        require(_providerId < providers.length, "Provider not exists.");
        Provider storage pro = providers[_providerId];
        require(pro.enable, "Provider already disabled.");
        pro.enable = false;
        emit ProviderDisabled(_providerId);
    }

    function enableProvider(uint256 _providerId) onlyOwner public {
        require(_providerId < providers.length, "Provider not exists.");
        Provider storage pro = providers[_providerId];
        require(!pro.enable, "Provider already enabled.");
        pro.enable = true;
        emit ProviderEnabled(_providerId);
    }

    function addProvider(address _token, address _vault) onlyOwner public returns (uint256) {
        providers.push(Provider({
        vault : _vault,
        token : _token,
        enable : true,
        accProfit: 0
        }));

        require(ISashimiVault(_vault).controller() == address (this), "Controller not matched.");
        require(ISashimiVault(_vault).token() == _token, "Controller not matched.");

        uint256 providerId = providers.length.sub(1);
        emit ProviderAdded(_token, _vault, providerId);
        return providerId;
    }

    function chooseProvider(address _token, uint256 _providerId) onlyAdmin public {
        require(_providerId < providers.length, "Provider not added.");
        Provider storage provider = providers[_providerId];

        require(provider.token == _token, "Token not matched.");
        require(provider.enable, "Provider disabled.");

        uint256 chosen = chosenProviders[_token];
        require(chosen != _providerId, "Provider already chosen.");

        // be careful max is enough
        if (IERC20(ISashimiVault(provider.vault).profit()).allowance(address(this), routerForSashimi) == 0)
            IERC20(ISashimiVault(provider.vault).profit()).safeApprove(routerForSashimi, type(uint256).max);

        if (IERC20(provider.token).allowance(address(this), provider.vault) == 0)
            IERC20(provider.token).safeApprove(provider.vault, type(uint256).max);

        if (chosen != 0) {
            // old provider exists and need harvest
            harvest(_token);
            Provider storage oldProvider = providers[chosen];

            // deposit on new
            uint256 withdrawAmount = ISashimiVault(oldProvider.vault).withdrawAll();
            uint256 depositAmount = deposits[_token];
            require(withdrawAmount >= depositAmount, "Insufficient withdraw amount.");
            ISashimiVault(provider.vault).deposit(depositAmount);
        }

        chosenProviders[_token] = _providerId;
        emit ProviderSwitched(_token, provider.vault, _providerId);
    }

    function changeReservesRatio(address _token, uint256 _reservesRatio) onlyOwner validRatio(_reservesRatio) public {
        reservesRatios[_token] = _reservesRatio;
        emit ReservesRatiosChanged(_token, _reservesRatio);
    }

    function addAdmin(address _admin) onlyOwner public {
        isAdmin[_admin] = true;
        emit AdminAdded(_admin);
    }

    function removeAdmin(address _admin) onlyOwner public {
        isAdmin[_admin] = false;
        emit AdminRemoved(_admin);
    }

    function changeBeneficiary1(address _beneficiary1) onlyOwner public {
        beneficiary1 = _beneficiary1;
    }

    function changeRouter(address _router) onlyOwner public {
        router = _router;
    }

    function changeSashimiRouter(address _sashimiRouter) onlyOwner public {
        routerForSashimi = _sashimiRouter;
    }

    function changeBeneficiary2(address _beneficiary2) onlyOwner public {
        beneficiary2 = _beneficiary2;
    }

    function start() onlyOwner public {
        frozen = false;
    }

    function emergencePause() onlyAdmin public {
        frozen = true;
    }

    // for token cannot withdraw with harvest
    function emergenceWithdraw(address _token, uint256 providerId, uint256 _amount, address _to) onlyOwner public{
        Provider memory provider = providers[providerId];
        ISashimiVault(provider.vault).transfer(_token, _to, _amount);
    }

    // for token cannot withdraw with harvest
    function emergenceWithdrawETH(uint256 providerId, uint256 _amount, address _to) onlyOwner public{
        Provider memory provider = providers[providerId];
        ISashimiVault(provider.vault).transferETH(_to, _amount);
    }

    function emergenceTriggerProvider(uint256 providerId, address target, uint value, string memory signature, bytes memory data) onlyOwner public{
        Provider memory provider = providers[providerId];
        ISashimiVault(provider.vault).executeTransaction(target, value, signature, data);
    }

    function earned(address _token) view public returns(address profit, uint256 amount){
        uint256 providerId = chosenProviders[_token];
        Provider memory provider = providers[providerId];
        profit = ISashimiVault(provider.vault).profit();
        amount = ISashimiVault(provider.vault).earned();
    }
}
function emergenceWithdraw(address _token, uint256 providerId, uint256 _amount, address _to) onlyOwner public{
    Provider memory provider = providers[providerId];
    ISashimiVault(provider.vault).transfer(_token, _to, _amount);
}

// for token cannot withdraw with harvest