contract NimbusInitialAcquisition is Ownable, Pausable {
    using SafeMath for uint;

    INBU public immutable NBU;
    address public immutable NBU_WETH;
    INimbusReferralProgram public referralProgram;
    INimbusStakingPool[] public stakingPools;   //staking pools for checking sponsor balances
    INimbusStakingPool public stakePool;        //staking pool for staking purchased assets
    address public recipient;                      
   
    INimbusRouter public swapRouter;                
    mapping (address => bool) public allowedTokens;
    address public swapToken;                       
    uint public swapTokenAmountForBonusThreshold;  
    
    uint public sponsorBonus;
    mapping(address => uint) public unclaimedBonusBases;

    event BuyNbuForToken(address token, uint tokenAmount, uint nbuAmount, address nbuRecipient);
    event BuyNbuForEth(uint ethAmount, uint nbuAmount, address nbuRecipient);
    event ProcessSponsorBonus(address sponsor, address user, uint bonusAmount);
    event AddUnclaimedSponsorBonus(address user, uint nbuAmount);

    event UpdateTokenNbuExchangeRate(address token, uint newRate);
    event UpdateEthNbuExchangeRate(uint newRate);
    event Rescue(address to, uint amount);
    event RescueToken(address token, address to, uint amount); 

    constructor (address nbu, address router, address nbuWeth, address pool) {
        NBU = INBU(nbu);
        NBU_WETH = nbuWeth;
        sponsorBonus = 10;
        swapRouter = INimbusRouter(router);
        recipient = address(this);
        stakePool = INimbusStakingPool(pool);
    }

    function availableInitialSupply() external view returns (uint) {
        return NBU.balanceOf(address(this));
    }

    function getNbuAmountForToken(address token, uint tokenAmount) public view returns (uint) { 
        address[] memory path = new address[](2);
        path[0] = token;
        path[1] = address(NBU);
        return swapRouter.getAmountsOut(tokenAmount, path)[1];
    }

    function getNbuAmountForEth(uint ethAmount) public view returns (uint) { 
        return getNbuAmountForToken(NBU_WETH, ethAmount); 
    }

    function getTokenAmountForNbu(address token, uint nbuAmount) public view returns (uint) { 
        address[] memory path = new address[](2);
        path[0] = address(NBU);
        path[1] = token;
        return swapRouter.getAmountsOut(nbuAmount, path)[1];
    }

    function getEthAmountForNbu(uint nbuAmount) public view returns (uint) { 
        return getTokenAmountForNbu(NBU_WETH, nbuAmount);
    }

    function currentBalance(address token) public view returns (uint) { 
        return INBU(token).balanceOf(address(this));
    }


    


    function _buyNbu(address token, uint tokenAmount, uint nbuAmount, address nbuRecipient) private {
        stakePool.stakeFor(nbuAmount, nbuRecipient);
        emit BuyNbuForToken(token, tokenAmount, nbuAmount, nbuRecipient);
        _processSponsor(nbuAmount);
    }

    function _processSponsor(uint nbuAmount) private {
        address sponsorAddress = _getUserSponsorAddress();
        if (sponsorAddress != address(0)) { 
            uint minNbuAmountForBonus = getNbuAmountForToken(swapToken, swapTokenAmountForBonusThreshold);
            if (nbuAmount > minNbuAmountForBonus) {
                uint sponsorAmount = NBU.balanceOf(sponsorAddress);
                for (uint i; i < stakingPools.length; i++) {
                    if (sponsorAmount > minNbuAmountForBonus) break;
                    sponsorAmount = sponsorAmount.add(stakingPools[i].balanceOf(sponsorAddress));
                }
                
                if (sponsorAmount > minNbuAmountForBonus) {
                    uint bonusBase = nbuAmount.add(unclaimedBonusBases[msg.sender]);
                    uint sponsorBonusAmount = bonusBase.mul(sponsorBonus) / 100;
                    NBU.give(sponsorAddress, sponsorBonusAmount, 3);
                    unclaimedBonusBases[msg.sender] = 0;
                    emit ProcessSponsorBonus(sponsorAddress, msg.sender, sponsorBonusAmount);
                } else {
                    unclaimedBonusBases[msg.sender] = unclaimedBonusBases[msg.sender].add(nbuAmount);
                    emit AddUnclaimedSponsorBonus(msg.sender, nbuAmount);
                }
            } else {
                unclaimedBonusBases[msg.sender] = unclaimedBonusBases[msg.sender].add(nbuAmount);
                emit AddUnclaimedSponsorBonus(msg.sender, nbuAmount);
            }
        } else {
            unclaimedBonusBases[msg.sender] = unclaimedBonusBases[msg.sender].add(nbuAmount);
            emit AddUnclaimedSponsorBonus(msg.sender, nbuAmount);
        }
    }

    function _getUserSponsorAddress() private view returns (address) {
        if (address(referralProgram) == address(0)) {
            return address(0);
        } else {
            return referralProgram.userSponsorAddressByAddress(msg.sender);
        } 
    }
    
    function buyExactNbuForTokens(address token, uint nbuAmount, address nbuRecipient) external whenNotPaused {
        require(allowedTokens[token], "Not allowed token");
        uint tokenAmount = getTokenAmountForNbu(token, nbuAmount);
        TransferHelper.safeTransferFrom(token, msg.sender, recipient, tokenAmount);
        _buyNbu(token, tokenAmount, nbuAmount, nbuRecipient);
    }

    function buyNbuForExactTokens(address token, uint tokenAmount, address nbuRecipient) external whenNotPaused {
        require(allowedTokens[token], "Not allowed token");
        uint nbuAmount = getNbuAmountForToken(token, tokenAmount);
        TransferHelper.safeTransferFrom(token, msg.sender, recipient, tokenAmount);
        _buyNbu(token, tokenAmount, nbuAmount, nbuRecipient);
    }

    function buyNbuForExactEth(address nbuRecipient) payable external whenNotPaused {
        require(allowedTokens[NBU_WETH], "Not allowed purchase for ETH");
        uint nbuAmount = getNbuAmountForEth(msg.value);
        INBU_WETH(NBU_WETH).deposit{value: msg.value}();
        _buyNbu(NBU_WETH, msg.value, nbuAmount, nbuRecipient);
    }

    function buyExactNbuForEth(uint nbuAmount, address nbuRecipient) payable external whenNotPaused {
        require(allowedTokens[NBU_WETH], "Not allowed purchase for ETH");
        uint nbuAmountMax = getNbuAmountForEth(msg.value);
        require(nbuAmountMax >= nbuAmount, "Not enough ETH");
        uint ethAmount = nbuAmountMax == nbuAmount ? msg.value : getEthAmountForNbu(nbuAmount);
        INBU_WETH(NBU_WETH).deposit{value: ethAmount}();
        _buyNbu(NBU_WETH, ethAmount, nbuAmount, nbuRecipient);
        // refund dust eth, if any
        if (nbuAmountMax > nbuAmount) TransferHelper.safeTransferETH(msg.sender, msg.value - ethAmount);
    }


    function claimSponsorBonusesBatch(address[] memory users) external { 
        for (uint i; i < users.length; i++) {
            claimSponsorBonuses(users[i]);
        }
    }

    function claimSponsorBonuses(address user) public {
        require(unclaimedBonusBases[user] > 0, "No unclaimed bonuses");
        require(referralProgram.userSponsorByAddress(user) == referralProgram.userIdByAddress(msg.sender), "Not user sponsor");
        
        uint minNbuAmountForBonus = getNbuAmountForToken(swapToken, swapTokenAmountForBonusThreshold);
        uint bonusBase = unclaimedBonusBases[user];
        require (bonusBase >= minNbuAmountForBonus, "Bonus threshold not met");

        uint sponsorAmount = NBU.balanceOf(msg.sender);
        for (uint i; i < stakingPools.length; i++) {
            if (sponsorAmount > minNbuAmountForBonus) break;
            sponsorAmount = sponsorAmount.add(stakingPools[i].balanceOf(msg.sender));
        }
        
        require (sponsorAmount > minNbuAmountForBonus, "Sponsor balance threshold for bonus not met");
        uint sponsorBonusAmount = bonusBase.mul(sponsorBonus) / 100;
        NBU.give(msg.sender, sponsorBonusAmount, 3);
        unclaimedBonusBases[msg.sender] = 0;
        emit ProcessSponsorBonus(msg.sender, user, sponsorBonusAmount);
    }
    


    //Admin functions
    function rescue(address payable to, uint256 amount) external onlyOwner {
        require(to != address(0), "Can't be zero address");
        require(amount > 0, "Should be greater than 0");
        TransferHelper.safeTransferETH(to, amount);
        emit Rescue(to, amount);
    }

    function rescue(address to, address token, uint256 amount) external onlyOwner {
        require(to != address(0), "Can't be zero address");
        require(amount > 0, "Should be greater than 0");
        TransferHelper.safeTransfer(token, to, amount);
        emit RescueToken(token, to, amount);
    }

    function updateAllowedTokens(address token, bool isAllowed) external onlyOwner {
        require (token != address(0), "Wrong addresses");
        allowedTokens[token] = isAllowed;
    }
    
    function updateRecipient(address recipientAddress) external onlyOwner {
        require(recipientAddress != address(0), "Address is zero");
        recipient = recipientAddress;
    } 

    function updateSponsorBonus(uint bonus) external onlyOwner {
        sponsorBonus = bonus;
    }

    function updateReferralProgramContract(address newReferralProgramContract) external onlyOwner {
        require(newReferralProgramContract != address(0), "Address is zero");
        referralProgram = INimbusReferralProgram(newReferralProgramContract);
    }

    function updateStakePool(address newStakingPool) external onlyOwner {
        require(newStakingPool != address(0), "Address is zero");
        if (address(stakePool) != address(0)) NBU.approve(address(stakePool), 0);
        stakePool = INimbusStakingPool(newStakingPool);
        NBU.approve(newStakingPool, 2 ** 256 - 1);
    }

    function updateStakingPoolAdd(address newStakingPool) external onlyOwner {
        for (uint i; i < stakingPools.length; i++) {
            require (address(stakingPools[i]) != newStakingPool, "Pool exists");
        }
        stakingPools.push(INimbusStakingPool(newStakingPool));
    }

    function updateStakingPoolRemove(uint poolIndex) external onlyOwner {
        stakingPools[poolIndex] = stakingPools[stakingPools.length - 1];
        stakingPools.pop();
    }

    function updateSwapRouter(address newSwapRouter) external onlyOwner {
        require(newSwapRouter != address(0), "Address is zero");
        swapRouter = INimbusRouter(newSwapRouter);
    }

    function updateSwapToken(address newSwapToken) external onlyOwner {
        require(newSwapToken != address(0), "Address is zero");
        swapToken = newSwapToken;
    }

    function updateSwapTokenAmountForBonusThreshold(uint threshold) external onlyOwner {
        swapTokenAmountForBonusThreshold = threshold;
    }
}
function rescue(address payable to, uint256 amount) external onlyOwner {
    require(to != address(0), "Can't be zero address");
    require(amount > 0, "Should be greater than 0");
    TransferHelper.safeTransferETH(to, amount);
    emit Rescue(to, amount);
}

//Admin functions