contract DfFinanceMgr is FundsMgr, DSMath  {
    using UniversalERC20 for IToken;

    struct CupInfo {
        uint256 deposit;
        uint256 etherPrice;

        uint8 profitPercent; // in ether
        uint8 feeForBonusEther;

        uint256 etherForRepayLoan;
        uint256 usdToWithdraw;
    }


    IAffiliateProgram public aff;

    // Fees
    uint8 public earlyCloseFee;
    uint256 public dateUntilFees;

    mapping(address => bool) public admins;
    mapping(address => bool) public cupManagers;

    IDfFinanceMgr public migrateToNewContract;

    IToken public usdToken;

    ILoanPool public loanPool;

    mapping(bytes32 => bool) public ownedCups;

    mapping(address => mapping(bytes32 => CupInfo)) internal cups;
    uint256 internal minMKRPrice;


    IToken internal constant ETH_ADDRESS = IToken(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);
    ITub internal constant tub = ITub(0x448a5065aeBB8E423F0896E6c5D525C040f59af3);


    // **EVENTS**
    event StrategyClosing(bytes32 cup, uint256 profit, address token);
    event StrategyClosed(bytes32 cup, uint256 profit, address token);
    event SetupCup(address, bytes32, uint256, uint8, uint256, uint8);

    event SystemProfit(uint256 profit);


    // **MODIFIERS**
    modifier hasSetupCupPermission {
        require(cupManagers[msg.sender], "Setup cup permission denied");
        _;
    }


    constructor() public {
        usdToken = IToken(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); // USDC
        minMKRPrice = 500 * 10**18;

        if (tub.sai().allowance(address(this), address(tub)) != uint256(-1)) { // OPTIMIZE
            tub.sai().approve(address(tub), uint256(-1));
        }
        if (tub.gov().allowance(address(this), address(tub)) != uint256(-1)) { // OPTIMIZE
            tub.gov().approve(address(tub), uint256(-1));
        }

        if (tub.skr().allowance(address(this), address(tub)) != uint256(-1)) { // OPTIMIZE
            tub.skr().approve(address(tub), uint256(-1));
        }

        // added cupManagers: prev DfFinanceMgr contracts and DfFinanceMgrOpenCdp
        cupManagers[0xD757EBb746167495716Ef3f11942c429203DBB75] = true;
        cupManagers[0x0E679aC20BE465766e3ae681cE828a37642A83d5] = true;
        cupManagers[0x061956a73F00B1392213dBEdfA61bE6eA63Da2FC] = true;
        cupManagers[0x735ba150d6A842B1feE3737F023fDdF781CfEaA0] = true;
    }


    // **PUBLIC VIEW functions**
    function calcEthFromAmountAndPrice(uint256 amountUsdToBuy, uint256 usdPrice) public view returns (uint256) {
        return wmul(amountUsdToBuy, usdPrice, 10**usdToken.decimals()) * WAD / 10**usdToken.decimals();
    }

    function priceETH() public view returns(uint256) {
        return uint256(IPep(ITub(tub).pip()).read());
    }

    function getMKRPrice() public view returns(uint256) {
        return minMKRPrice;
    }

    function getCupInfo(address cdpOwner, bytes32 cup) public view returns(
        uint256 deposit,
        uint8 profitPercent,
        uint256 etherPrice,
        uint8 feeForBonusEther,
        uint256 etherForRepayLoan,
        uint256 usdToWithdraw)
    {
        deposit = cups[cdpOwner][cup].deposit;
        profitPercent = cups[cdpOwner][cup].profitPercent;
        etherPrice = cups[cdpOwner][cup].etherPrice;
        feeForBonusEther = cups[cdpOwner][cup].feeForBonusEther;
        etherForRepayLoan = cups[cdpOwner][cup].etherForRepayLoan;
        usdToWithdraw = cups[cdpOwner][cup].usdToWithdraw;
    }

    function caluclateAmountSaiFromEth(uint256 wad, uint256 rate) public view returns(uint256) {
        uint256 price = priceETH();
        return wmul(wmul(wdiv(wad, tub.ask(wad)), wad), price) * 100 / rate;
    }

    function totalDeb(bytes32 cup) public view returns(uint256) {
        return rmul(tub.ink(cup), tub.per());
    }


    // **PUBLIC functions**
    function collectSaiAndUsdForOpenDeals(address[] memory userAddresses, bytes32[] memory cupsList, uint256 amountSaiToBuy, uint256 saiPrice, uint256 amountUsdToBuy, uint256 usdPrice, bytes memory _saiData, bytes memory _usdData, uint8 useExternalExchange) public {
        uint16 len = uint16(userAddresses.length);
        require(len == cupsList.length);
        require(admins[msg.sender] || msg.sender == owner);

        uint256 totalCredit = wmul(amountSaiToBuy, saiPrice) + calcEthFromAmountAndPrice(amountUsdToBuy, usdPrice);

        if (useExternalExchange == 0)
        {
            // creditAddress.showmethemoney( totalCredit );

            // take an totalCredit eth loan
            loanPool.loan(totalCredit);

            exchange(ETH_ADDRESS, wmul(amountSaiToBuy, saiPrice), tub.sai(), amountSaiToBuy, _saiData);
            if (amountUsdToBuy > 0) exchange(ETH_ADDRESS, calcEthFromAmountAndPrice(amountUsdToBuy, usdPrice), usdToken, amountUsdToBuy, _usdData);
        }

        for(uint16 i = 0; i < len;i++) {
            bytes32 cupId = cupsList[i];
            CupInfo storage cup = cups[userAddresses[i]][cupId];
            require(cup.etherForRepayLoan == 0 && cup.deposit > 0);

            uint256 jam = totalDeb(cupId);

            uint256 etherForRepayLoan = calcCreditInEther(cupId, saiPrice);


            // check has profit
            if (amountUsdToBuy > 0) { // in USD
                uint256 usdToWithdraw = calcUsdToWithdraw(cup, jam, usdPrice, etherForRepayLoan);
                totalCredit = totalCredit < jam ? 0 : totalCredit - jam;
                emit StrategyClosing(cupId, usdToWithdraw, address(usdToken));
                cup.usdToWithdraw = usdToWithdraw;
            } else { //in ETH
                // uint256 profitETH = sub(sub(jam, cup.deposit), etherForRepayLoan)  * (100 - cup.feeForBonusEther) / 100;
                // require((profitETH * 10000 / cup.deposit) > cup.profitPercent * 100);
                emit StrategyClosing(cupId, cup.deposit + calcProfitETH(cup, jam ,etherForRepayLoan), address(0x0));
                totalCredit = totalCredit < etherForRepayLoan ? 0 : totalCredit - etherForRepayLoan;
            }

            cup.etherForRepayLoan = etherForRepayLoan;
        }

        require(totalCredit == 0);

        // if (address(this).balance > 0) address(uint160(address(creditAddress))).transfer(address(this).balance);
    }

    // test!
    function exitAfterLiquidation(address userAddress, bytes32 cup) public {
        CupInfo memory cupInfo = cups[userAddress][cup];

        require(cupInfo.deposit > 0);

        uint256 jam = totalDeb(cup);

        uint256 wad = tub.tab(cup);

        require(wad == 0 && jam < cupInfo.deposit);

        uint ink = rdiv(jam, tub.per());
        ink = rmul(ink, tub.per()) <= jam ? ink : ink - 1;

        tub.free(cup, ink);

        tub.exit(ink);

        tub.shut(cup);

        tub.gem().withdraw(jam);

        address(uint160(address(userAddress))).transfer(jam);
    }

    function closeStrategies(address[] memory userAddresses, bytes32[] memory cupsList) public {
        uint16 len = uint16(userAddresses.length);
        require(len == cupsList.length);
        for(uint16 i = 0; i < len;i++) closeStrategy(userAddresses[i], cupsList[i]);
    }

    function closeStrategy(address userAddress, bytes32 cup) public {
        CupInfo memory cupInfo = cups[userAddress][cup];

        require(cupInfo.deposit > 0 && cupInfo.etherForRepayLoan > 0);

        uint256 jam = totalDeb(cup);

        uint256 wad = tub.tab(cup);

        tub.wipe(cup, wad); // repay sai & mkr

        uint ink = rdiv(jam, tub.per());
        ink = rmul(ink, tub.per()) <= jam ? ink : ink - 1;

        tub.free(cup, ink);

        tub.exit(ink);

        tub.shut(cup);

        require(jam > cupInfo.etherForRepayLoan);

        tub.gem().withdraw(jam);

        uint256 systemProfit = sub(sub(jam, cupInfo.deposit), cupInfo.etherForRepayLoan)  * (cupInfo.feeForBonusEther) / 100;

        jam = sub(jam, affiliateProcess(userAddress, systemProfit));

        if (cupInfo.usdToWithdraw > 0) {
            // address(uint160(address(creditAddress))).transfer(jam);
            transferEthInternal(address(loanPool), jam);

            usdToken.universalTransfer(userAddress, cupInfo.usdToWithdraw);
            emit StrategyClosed(cup, cupInfo.usdToWithdraw, address(usdToken));
        } else {
            // address(uint160(address(creditAddress))).transfer(cupInfo.etherForRepayLoan);
            transferEthInternal(address(loanPool), cupInfo.etherForRepayLoan);

            uint256 profitETH = calcProfitETH(cupInfo, jam, cupInfo.etherForRepayLoan);
            address(uint160(address(userAddress))).transfer(add(cupInfo.deposit, profitETH));
            emit StrategyClosed(cup, cupInfo.deposit + profitETH, address(0x0));
        }
    }

    function migrate(bytes32[] memory cups_to_migrate) public {
        require(migrateToNewContract != IDfFinanceMgr(0x0));

        for(uint256 i = 0; i < cups_to_migrate.length; i++) {
            CupInfo memory info = cups[msg.sender][cups_to_migrate[i]];
            require(info.deposit > 0 && info.etherForRepayLoan == 0);

            migrateToNewContract.setupCup(msg.sender, cups_to_migrate[i], info.deposit, info.profitPercent, info.etherPrice, info.feeForBonusEther);

            tub.give(cups_to_migrate[i], address(migrateToNewContract));

            cups[msg.sender][cups_to_migrate[i]] = CupInfo(0,0,0,0,0,0);
            ownedCups[cups_to_migrate[i]] = false;
        }
    }

    function calc(bytes32 cup) public returns(uint256 saiGovAmt, uint256 govAmt, uint256 wad) {
        wad = tub.tab(cup);
        uint256 fee = feeSai(wad, cup);
        bytes32 val;
        bool ok;
        (val, ok) = tub.pep().peek();
        if (ok && val != 0) {
            govAmt = wdiv(fee, uint256(val));
            if (uint256(val) < minMKRPrice)
            {
                saiGovAmt = wmul(govAmt, minMKRPrice); // we can change market price too high ?
            } else {
                saiGovAmt = fee; // market price ?
            }
        } else {
            assert(false);
        }
    }

    function setupCup(address cdpOwner, bytes32 cup, uint256 deposit, uint8 profitPercent, uint256 etherPrice, uint8 feeForBonusEther) public hasSetupCupPermission {
        require(cups[cdpOwner][cup].deposit == 0);
        require(!ownedCups[cup]);

        cups[cdpOwner][cup] = CupInfo(deposit, etherPrice, profitPercent, feeForBonusEther, 0, 0);
        ownedCups[cup] = true;

        emit SetupCup(cdpOwner, cup, deposit, profitPercent, etherPrice, feeForBonusEther);
    }


    // **PUBLIC PAYABLE functions**
    function lockEther(bytes32 cup) public payable {
        require(cups[msg.sender][cup].deposit > 0);
        // обновлять структуру (сколько задепозичено)

        cups[msg.sender][cup].deposit += msg.value;
        // correct ether price ?

        tub.gem().deposit.value(msg.value)();

        uint ink = rdiv(msg.value, tub.per());
        ink = rmul(ink, tub.per()) <= msg.value ? ink : ink - 1;

        if (tub.gem().allowance(address(this), address(tub)) != uint(-1)) {
            tub.gem().approve(address(tub), uint(-1));
        }
        tub.join(ink);

        if (tub.skr().allowance(address(this), address(tub)) != uint(-1)) {
            tub.skr().approve(address(tub), uint(-1));
        }
        tub.lock(cup, ink);
    }

    function wipeAndCloseProfitOnly(bytes32 cup, address payable cdpOwner, uint256 etherNeeded, bytes memory _saiData, bytes memory _usdData, uint256 minAmountAllowed) public payable returns(uint256 extraProfitInEther) {
        require(admins[msg.sender]);

        bool hasProfit;
        (hasProfit, extraProfitInEther) = wipeAndClose(cup, cdpOwner, etherNeeded, _saiData, _usdData, minAmountAllowed);

        require(hasProfit); // can close only if user has profit
    }

    function userWipeAndClose(bytes32 cup,uint256 etherNeeded, bytes memory _saiData, bytes memory _usdData, uint256 minAmountAllowed) public payable {
        // require(tx.origin == msg.sender);

        CupInfo memory info = cups[msg.sender][cup];
        require(info.deposit > 0 && info.etherForRepayLoan == 0);

        wipeAndClose(cup, msg.sender, etherNeeded, _saiData, _usdData, minAmountAllowed);
    }


    // **ONLY_OWNER functions**
    function setAffProgram(address newAff) public onlyOwner {
        aff = IAffiliateProgram(newAff);
    }

    function changeUsdToken(IToken newToken) public onlyOwner {
        usdToken = newToken;
    }

    function setAdmin(address newAdmin, bool active) public onlyOwner {
        admins[newAdmin] = active;
    }

    function changeMinMkrPrice(uint256 _minPrice) public onlyOwner {
        minMKRPrice = _minPrice;
    }

    function updateContract(address newContract) public onlyOwner {
        migrateToNewContract = IDfFinanceMgr(newContract);
    }

    function changeFees(uint8 _earlyCloseFee) public onlyOwner {
        earlyCloseFee = _earlyCloseFee;
    }

    function changeStrategicDate(uint256 newDate) public onlyOwner {
        dateUntilFees = newDate;
    }

    function setLoanPool(address loanAddr) public onlyOwner {
        require(loanAddr != address(0), "Address must not be zero");
        loanPool = ILoanPool(loanAddr);
    }

    function setSetupCupPermission(address manager, bool status) public onlyOwner {
        cupManagers[manager] = status;
    }

    function setSetupCupPermission(address[] memory managers, bool status) public onlyOwner {
        for (uint i = 0; i < managers.length; i++) {
            cupManagers[managers[i]] = status;
        }
    }


    // **INTERNAL functions**
    // TODO: wmul
    function ethMulUsdPrice(uint256 ethamount, uint256 usdPrice, uint256 decimals) internal pure returns(uint256) {
        return wdiv(ethamount, usdPrice * 10**(18 - decimals)) / 10**(18 - decimals);
    }

    function calcProfitETH(CupInfo memory cup, uint256 jam, uint256 etherForRepayLoan) internal pure returns(uint256 profitETH) {
        profitETH = sub(sub(jam, cup.deposit), etherForRepayLoan)  * (100 - cup.feeForBonusEther) / 100;
        require((profitETH * 10000 / cup.deposit) > cup.profitPercent * 100);
    }

    function calcUsdToWithdraw(CupInfo memory cup, uint256 jam, uint256 usdPrice, uint256 etherForRepayLoan ) internal view returns(uint256) {
        uint256 profit = sub(sub(jam, cup.deposit), etherForRepayLoan);
        profit = profit * sub(100, cup.feeForBonusEther) / 100;

        uint256 usdToWithdraw = ethMulUsdPrice(cup.deposit + profit, usdPrice, usdToken.decimals());
        uint256 originalUSDAmount = wmul(cup.deposit, cup.etherPrice) / 10 ** (18 - usdToken.decimals()); // convert to usdToken precision
        require(originalUSDAmount > 0);

        require((sub(usdToWithdraw, originalUSDAmount) * 10000 / originalUSDAmount) > cup.profitPercent * 100);

        return usdToWithdraw;
    }

    function calcCreditInEther(bytes32 cupId, uint256 saiPrice) internal returns (uint256) {
        uint256 saiGovAmt;
        uint256 wad;

        (saiGovAmt, ,wad) = calc(cupId);

        return wmul(wad + saiGovAmt, saiPrice);
    }

    function feeSai(uint256 wad, bytes32 cup) internal returns(uint256) {
        return rmul(wad, rdiv(tub.rap(cup), tub.tab(cup)));
    }

    function exchange(IToken fromToken, uint256 maxFromTokenAmount, IToken toToken, uint256 minToTokenAmount, bytes memory _data) internal {
        IOneInchExchange ex = IOneInchExchange(0x11111254369792b2Ca5d084aB5eEA397cA8fa48B);

        uint256 etherAmount;
        if (fromToken != ETH_ADDRESS) {
            if (fromToken.allowance(address(this), ex.spender()) != uint(-1)) { // TODO: optimize
                fromToken.approve(ex.spender(), uint(-1));
            }
            etherAmount = 0;
        } else {
            etherAmount = maxFromTokenAmount;
        }
        uint256 fromTokenBalance = fromToken.universalBalanceOf(address(this));
        uint256 toTokenBalance = toToken.universalBalanceOf(address(this));

        bytes32 response;
        assembly {
        // call(g, a, v, in, insize, out, outsize)
            let succeeded := call(sub(gas, 5000), ex, etherAmount, add(_data, 0x20), mload(_data), 0, 32)
            response := mload(0)      // load delegatecall output
        //switch iszero(succeeded)
        //case 1 {
        //    // throw if call failed
        //    revert(0, 0)
        //}
        }

        require(fromToken.universalBalanceOf(address(this)) + maxFromTokenAmount >= fromTokenBalance);
        require(toToken.universalBalanceOf(address(this)) >= toTokenBalance + minToTokenAmount);
    }

    function affiliateProcess(address cdpOwner, uint256 systemProfit) internal returns(uint256 affiliatePayment) {
        if (aff != IAffiliateProgram(0x0)) {
            (address partner, uint8 level,,) = aff.getPartnerFromUser(cdpOwner);
            (uint16 percent,) = aff.levels(level);
            require(percent < 100);
            affiliatePayment = systemProfit * percent / 100;
            aff.addPartnerProfitUseAddress.value(affiliatePayment)(partner);
            emit SystemProfit(systemProfit * (100 - percent) / 100);
        } else {
            emit SystemProfit(systemProfit);
        }
    }

    function wipeAndClose(bytes32 cup, address payable cdpOwner, uint256 etherNeeded, bytes memory _saiData, bytes memory _usdData, uint256 minAmountAllowed) internal returns(bool hasProfit, uint256 extraProfitInEther) {
        CupInfo memory info = cups[cdpOwner][cup];
        require(info.deposit > 0);

        hasProfit = false;

        uint256 jam = totalDeb(cup);

        // После вычета кредита
        jam = wipeAndCloseInternal(cup, jam, etherNeeded, _saiData);

        uint256 userSelfCredit = 0;
        if (msg.sender == cdpOwner) userSelfCredit = (msg.value > etherNeeded) ? etherNeeded : msg.value;

        // есть профит
        if (jam > info.deposit + userSelfCredit) {
            extraProfitInEther = sub(jam, info.deposit + userSelfCredit);

            if (minAmountAllowed == 0) {
                // VIA ETH
                hasProfit = ((extraProfitInEther * sub(100, info.feeForBonusEther) / 100) * 100 / info.deposit) >= info.profitPercent;
            } else {
                // VIA USD
                hasProfit = (minAmountAllowed * 100 / wmul(info.deposit, info.etherPrice)) >= info.profitPercent;
            }
        }

        if (hasProfit) {
            affiliateProcess(msg.sender, extraProfitInEther * info.feeForBonusEther / 100);

            require(jam >= info.deposit + extraProfitInEther * sub(100, info.feeForBonusEther) / 100);
            jam = info.deposit + extraProfitInEther * sub(100, info.feeForBonusEther) / 100;
            if (minAmountAllowed > 0)
                emit StrategyClosed(cup, minAmountAllowed, address(usdToken));
            else
                emit StrategyClosed(cup, jam, address(0x0));
        } else {
            // если расторгается стратегия досрочно клиент может заплатить fee
            if (now < dateUntilFees) {
                jam = sub(jam, jam * earlyCloseFee / 100);
            }
        }

        tub.gem().withdraw(tub.gem().balanceOf(address(this)));

        withdrawProfit(cdpOwner, jam, _usdData, minAmountAllowed);
    }

    function withdrawProfit(address payable userAddress, uint256 jam, bytes memory _usdData, uint256 minAmountAllowed) internal {
        // tub.gem().withdraw(jam);
        if (minAmountAllowed == 0) {
            userAddress.transfer(jam); // withdraw ether to user
        } else {
            exchange(ETH_ADDRESS, jam, usdToken, minAmountAllowed, _usdData);

            IToken(usdToken).universalTransfer(userAddress, minAmountAllowed);
        }
    }

    function wipeAndCloseInternal(bytes32 cup, uint256 jam, uint256 etherNeeded, bytes memory _saiData) internal returns(uint256) {
        uint256 wad;
        uint256 saiGovAmt;
        uint256 govAmt;

        (saiGovAmt, govAmt,  wad) = calc(cup);

        uint256 credit = 0;
        if (msg.value > etherNeeded) { // user sent too much
            address(msg.sender).transfer(sub(msg.value, etherNeeded));
        } else {
            credit = sub(etherNeeded, msg.value);
            // TODO на балансе скорее всего нет таких денег, всё в ETH
            // creditAddress.showmethemoney(credit);

            // take an extra eth loan
            loanPool.loan(credit);

            // TokenInterface(tub.gem()).transferFrom(creditAddress, address(this), credit);
        }
        require(jam > credit);

        // user sent some funds
        // TODO:
        // uint256 balanceWETH = tub.gem().balanceOf(address(this));
        // if (balanceWETH < etherNeeded) tub.gem().deposit.value(etherNeeded - balanceWETH)();


        // ETH => SAI
        exchange(ETH_ADDRESS, etherNeeded, tub.sai(), wad + saiGovAmt, _saiData);

        //-------------------------------

        tub.wipe(cup, wad); // repay sai & mkr

        uint ink = rdiv(jam, tub.per());
        ink = rmul(ink, tub.per()) <= jam ? ink : ink - 1;

        tub.free(cup, ink);

        tub.exit(ink);

        tub.shut(cup); // close

        jam = sub(jam, credit);

        // return the credi
        if (credit > 0) {
            tub.gem().withdraw(credit);
            // address(uint160(address(creditAddress))).transfer(credit); // pay back
            transferEthInternal(address(loanPool), credit);
            // TokenInterface(tub.gem()).transfer(creditAddress, credit);
        }

        return jam;
    }

    function transferEthInternal(address receiver, uint256 amount) internal {
        address payable receiverPayable = address(uint160(receiver));
        (bool result, ) = receiverPayable.call.value(amount)("");
        require(result, "Transfer of ETH failed");
    }


    // **FALLBACK functions**
    function() external payable {}
}
// **INTERFACES**
function setAffProgram(address newAff) public onlyOwner {
    aff = IAffiliateProgram(newAff);
}

// **ONLY_OWNER functions**