contract Distributor is SafeTransfer, ReentrancyGuard, TokenWithdrawer {
    address payable private owner;  // Contract deployer can modify parameters
    address private signer; // Signer can only distribute Starterpacks

    // Defines the Starterpack parameters
    struct Pack {
        StickerMarket stickerMarket; // Sticker market contract for minting the sticker pack ids
        uint256 ethAmount; // The Amount of ETH to transfer to the recipient
        address[] tokens; // Array of ERC20 Contract Addresses
        uint256[] tokenAmounts; // Array of ERC20 amounts corresponding to cells in tokens[]
        uint256[] stickerPackIds; // Array of NFT's
    }

    Pack private defaultPack;
    Pack private promoPack;
    uint private promoAvailable;

    ERC20Token public sntToken;

    bool public pause = true;
    
    uint ineligibleVersion;
    mapping(bytes32 => bool) public ineligible; // Used to make addresses ineligible for subsequent starterpacks, after successful transaction

    event RequireApproval(address attribution);
    
    mapping(address => uint) public pendingAttributionCnt;
    mapping(address => uint) public attributionCnt;
    
    struct Attribution {
        bool enabled;
        uint256 ethAmount; // The Amount of ETH to transfer to the referrer
        address[] tokens; // Array of ERC20 Contract Addresses
        uint256[] tokenAmounts; // Array of ERC20 amounts corresponding to cells in tokens[]
        uint maxThreshold;
    }
    
    mapping(address => Attribution) defaultAttributionSettings;
    mapping(address => Attribution) promoAttributionSettings;

    // Modifiers --------------------------------------------------------------------------------------------

    // Functions only Owner can call
    modifier onlyOwner {
        require(msg.sender == owner, "Unauthorized");
        _;
    }

    // Logic ------------------------------------------------------------------------------------------------

    /// @notice Check if an address is eligible for a starterpack
    /// @dev will return false if a transaction of distributePack for _recipient has been successfully executed
    /// @param _recipient The address to be checked for eligibility
    function eligible(address _recipient) public view returns (bool){
        return !ineligible[keccak256(abi.encodePacked(ineligibleVersion, _recipient))];
    }

    /// @notice Get the starter pack configuration
    /// @return stickerMarket address Stickermarket contract address
    /// @return ethAmount uint256 ETH amount in wei that will be sent to a recipient
    /// @return tokens address[] List of tokens that will be sent to a recipient
    /// @return tokenAmounts uint[] Amount of tokens that will be sent to a recipient
    /// @return stickerPackIds uint[] List of sticker packs to send to a recipient
    function getDefaultPack() external view returns(address stickerMarket, uint256 ethAmount, address[] memory tokens, uint[] memory tokenAmounts, uint[] memory stickerPackIds) {
        stickerMarket = address(defaultPack.stickerMarket);
        ethAmount = defaultPack.ethAmount;
        tokens = defaultPack.tokens;
        tokenAmounts = defaultPack.tokenAmounts;
        stickerPackIds = defaultPack.stickerPackIds;
    }

    /// @notice Get the promo pack configuration
    /// @return stickerMarket address Stickermarket contract address
    /// @return ethAmount uint256 ETH amount in wei that will be sent to a recipient
    /// @return tokens address[] List of tokens that will be sent to a recipient
    /// @return tokenAmounts uint[] Amount of tokens that will be sent to a recipient
    /// @return stickerPackIds uint[] List of sticker packs to send to a recipient
    /// @return available uint number of promo packs available
    function getPromoPack() external view returns(address stickerMarket, uint256 ethAmount, address[] memory tokens, uint[] memory tokenAmounts, uint[] memory stickerPackIds, uint256 available) {
        stickerMarket = address(promoPack.stickerMarket);
        ethAmount = promoPack.ethAmount;
        tokens = promoPack.tokens;
        tokenAmounts = promoPack.tokenAmounts;
        stickerPackIds = promoPack.stickerPackIds;
        available = promoAvailable;
    }

    event Distributed(address indexed recipient, address indexed attribution);

    /// @notice Distributes a starterpack to an eligible address. Either a promo pack or a default will be distributed depending on availability
    /// @dev Can only be called by signer, assumes signer has validated an IAP receipt, owner can block calling by pausing.
    /// @param _recipient A payable address that is sent a starterpack after being checked for eligibility
    /// @param _attribution A payable address who referred the starterpack purchaser 
    function distributePack(address payable _recipient, address payable _attribution) external reentrancyGuard {
        require(!pause, "Paused");
        require(msg.sender == signer, "Unauthorized");
        require(eligible(_recipient), "Recipient is not eligible.");
        require(_recipient != _attribution, "Recipient should be different from Attribution address");

        Pack memory pack;
        if(promoAvailable > 0){
            pack = promoPack;
            promoAvailable--;
        } else {
            pack = defaultPack;
        }

        // Transfer Tokens
        // Iterate over tokens[] and transfer the an amount corresponding to the i cell in tokenAmounts[]
        for (uint256 i = 0; i < pack.tokens.length; i++) {
            ERC20Token token = ERC20Token(pack.tokens[i]);
            uint256 amount = pack.tokenAmounts[i];
            _safeTransfer(token, _recipient, amount);
        }

        // NFT Sticker Packs
        StickerType stickerType = StickerType(pack.stickerMarket.stickerType());
        for (uint256 i = 0; i < pack.stickerPackIds.length; i++) {
            uint256 packId = pack.stickerPackIds[i];
            (, bool mintable, uint256 price,) = stickerType.getPaymentData(packId);
            if(mintable && price > 0){
                sntToken.approve(address(pack.stickerMarket),price);
                pack.stickerMarket.buyToken(
                    packId,
                    _recipient,
                    price
                ); 
            }
        }

        // The recipient is no longer eligible for a pack, unless clearPurchases is called.
        ineligible[keccak256(abi.encodePacked(ineligibleVersion, _recipient))] = true;

        // Transfer ETH
        // .transfer bad post Istanbul fork :|
        (bool success, ) = _recipient.call.value(pack.ethAmount)("");
        require(success, "ETH Transfer failed");

        emit Distributed(_recipient, _attribution);

        if (_attribution == address(0)) return;
        
        // Determine if attribution is valid


        bool isPromo;
        Attribution memory attr;
        if(promoAttributionSettings[_attribution].maxThreshold > 0){
            isPromo = true;
            promoAttributionSettings[_attribution].maxThreshold--;
            attr = promoAttributionSettings[_attribution];
        } else {
            attr = defaultAttributionSettings[_attribution];
        }

        if (!attr.enabled) {
           attr = defaultAttributionSettings[address(0)];
        }
        
        if(!isPromo && (attributionCnt[_attribution] + 1) > attr.maxThreshold){
            emit RequireApproval(_attribution);
            pendingAttributionCnt[_attribution]++;
        } else {
            if(!isPromo){
                attributionCnt[_attribution]++;
            }

            if (attr.ethAmount != 0){
                (bool success, ) = _attribution.call.value(attr.ethAmount)("");
                require(success, "ETH Transfer failed");
            }

            for (uint256 i = 0; i < attr.tokens.length; i++) {
                ERC20Token token = ERC20Token(attr.tokens[i]);
                uint256 amount = attr.tokenAmounts[i];
                _safeTransfer(token, _attribution, amount);
            }
        }
    }
    
    function _payPendingAttributions(address _attribOwner, Attribution memory attr) internal {
        uint pendingAttributions = pendingAttributionCnt[_attribOwner];
        
        if (pendingAttributions == 0) return;
        if (attr.maxThreshold < attributionCnt[_attribOwner] + pendingAttributions) return;
        
        uint totalETHToPay = pendingAttributions * attr.ethAmount;

        attributionCnt[_attribOwner] += pendingAttributions;
        pendingAttributionCnt[_attribOwner] = 0;
        
        if (totalETHToPay != 0){
            (bool success, ) = _attribOwner.call.value(totalETHToPay)("");
            require(success, "ETH Transfer failed");
        }

        for (uint256 i = 0; i < attr.tokens.length; i++) {
            ERC20Token token = ERC20Token(attr.tokens[i]);
            uint256 amount = attr.tokenAmounts[i] * pendingAttributions;
            _safeTransfer(token, _attribOwner, amount);
        }
    }

    /// @notice Get rewards for specific referrer
    /// @param _account The address to obtain the attribution config
    /// @param _isPromo Indicates if the configuration for a promo should be returned or not
    /// @return ethAmount Amount of ETH in wei
    /// @return tokenLen Number of tokens configured as part of the reward
    /// @return maxThreshold If isPromo == true: Number of promo bonuses still available for that address else: Max number of attributions to pay before requiring approval
    /// @return attribCount Number of referrals
    function getReferralReward(address _account, bool _isPromo) public view returns (uint ethAmount, uint tokenLen, uint maxThreshold, uint attribCount) {
        Attribution memory attr;
        if(_isPromo){
            attr = promoAttributionSettings[_account];
        } else {
            attr = defaultAttributionSettings[_account];
            if (!attr.enabled) {
                attr = defaultAttributionSettings[address(0)];
            }
        }
        
        ethAmount = attr.ethAmount;
        maxThreshold = attr.maxThreshold;
        attribCount = attributionCnt[_account];
        tokenLen = attr.tokens.length;
    }

    /// @notice Get token rewards for specific address
    /// @param _account The address to obtain the attribution's token config
    /// @param _isPromo Indicates if the configuration for a promo should be returned or not
    /// @param _idx Index of token array in the attribution used to obtain the token config
    /// @return token ERC20 contract address
    /// @return tokenAmount Amount of token configured in the attribution
    function getReferralRewardTokens(address _account, bool _isPromo, uint _idx) public view returns (address token, uint tokenAmount) {
        Attribution memory attr;
        if(_isPromo){
            attr = promoAttributionSettings[_account];
        } else {
            attr = defaultAttributionSettings[_account];
            if (!attr.enabled) {
                attr = defaultAttributionSettings[address(0)];
            }
        }
        
        token = attr.tokens[_idx];
        tokenAmount = attr.tokenAmounts[_idx];
    }
    
    fallback() external payable  {
     // ...
    }
    
    // Admin ------------------------------------------------------------------------------------------------

    /// @notice Allows the Owner to allow or prohibit Signer from calling distributePack().
    /// @dev setPause must be called before Signer can call distributePack()
    function setPause(bool _pause) external onlyOwner {
        pause = _pause;
    }

    /// @notice Allows the owner to clear the purchase history. Recipients will be able to receive a starter pack again
    function clearPurchases() external onlyOwner {
        ineligibleVersion++;
    }

    /// @notice Set a starter pack configuration
    /// @dev The Owner can change the default starterpack contents
    /// @param _newPack starter pack configuration
    function changeStarterPack(Pack memory _newPack) public onlyOwner {
        require(_newPack.tokens.length == _newPack.tokenAmounts.length, "Mismatch with Tokens & Amounts");

        for (uint256 i = 0; i < _newPack.tokens.length; i++) {
            require(_newPack.tokenAmounts[i] > 0, "Amounts must be non-zero");
        }

        defaultPack = _newPack;
        sntToken = ERC20Token(defaultPack.stickerMarket.snt());
    }

    /// @notice Set a promo configuration
    /// @dev The Owner can change the promo pack contents
    /// @param _newPromoPack Promo pack configuration
    /// @param _numberOfPacks Max number of promo packs to be given before using the default config
    function changePromoPack(Pack memory _newPromoPack, uint _numberOfPacks) public onlyOwner {
        require(_newPromoPack.tokens.length == _newPromoPack.tokenAmounts.length, "Mismatch with Tokens & Amounts");

        for (uint256 i = 0; i < _newPromoPack.tokens.length; i++) {
            require(_newPromoPack.tokenAmounts[i] > 0, "Amounts must be non-zero");
        }

        promoPack = _newPromoPack;
        promoAvailable = _numberOfPacks;
    }

    /// @notice Safety function allowing the owner to immediately pause starterpack distribution and withdraw all balances in the the contract
    function withdraw(address[] calldata _tokens) external onlyOwner {
        pause = true;
        withdrawTokens(_tokens, owner);
    }

    /// @notice Changes the Signer of the contract
    /// @param _newSigner The new Signer of the contract
    function changeSigner(address _newSigner) public onlyOwner {
        require(_newSigner != address(0), "zero_address not allowed");
        signer = _newSigner;
    }

    /// @notice Changes the owner of the contract
    /// @param _newOwner The new owner of the contract
    function changeOwner(address payable _newOwner) external onlyOwner {
        require(_newOwner != address(0), "zero_address not allowed");
        owner = _newOwner;
    }
    
    /// @notice Set default/custom payout and threshold for referrals
    /// @param _isPromo indicates if this attribution config is a promo or default config
    /// @param _ethAmount Payout for referrals
    /// @param _thresholds Max number of referrals allowed beforee requiring approval
    /// @param _assignedTo Use a valid address here to set custom settings. To set the default payout and threshold, use address(0);
    function setPayoutAndThreshold(
        bool _isPromo,
        uint256 _ethAmount,
        address[] calldata _tokens,
        uint256[] calldata _tokenAmounts,
        uint256[] calldata _thresholds,
        address[] calldata _assignedTo
    ) external onlyOwner {
        require(_thresholds.length == _assignedTo.length, "Array length mismatch");
        require(_tokens.length == _tokenAmounts.length, "Array length mismatch");
        
        for (uint256 i = 0; i < _thresholds.length; i++) {
            bool enabled = _assignedTo[i] != address(0);
            
            Attribution memory attr = Attribution({
                enabled: enabled,
                ethAmount: _ethAmount,
                maxThreshold: _thresholds[i],
                tokens: _tokens,
                tokenAmounts: _tokenAmounts
            });
            
            if(_isPromo){
                promoAttributionSettings[_assignedTo[i]] = attr;
            } else {
                if(enabled){
                    _payPendingAttributions(_assignedTo[i], attr); 
                }
                defaultAttributionSettings[_assignedTo[i]] = attr;
            }
        }
    }
    
    /// @notice Remove attribution configuration for addresses
    /// @param _assignedTo Array of addresses with an attribution configured
    /// @param _isPromo Indicates if the configuration to delete is the promo or default
    function removePayoutAndThreshold(address[] calldata _assignedTo, bool _isPromo) external onlyOwner {
        if (_isPromo) {
            for (uint256 i = 0; i < _assignedTo.length; i++) {
                delete promoAttributionSettings[_assignedTo[i]];
            }
        } else {
            for (uint256 i = 0; i < _assignedTo.length; i++) {
                delete defaultAttributionSettings[_assignedTo[i]];
            }
        }
    }

    /// @param _signer allows the contract deployer(owner) to define the signer on construction
    constructor(address _signer) public {
        require(_signer != address(0), "zero_address not allowed");
        owner = msg.sender;
        signer = _signer;
    }
}
/// @title Starterpack Distributor
/// @notice Attempts to deliver 1 and only 1 starterpack containing ETH, ERC20 Tokens and NFT Stickerpacks to an eligible recipient
/// @dev The contract assumes Signer has verified an In-App Purchase Receipt, only 1 pack per address is allowed, unless the owner calls clearPurchases()
function getPromoPack() external view returns(address stickerMarket, uint256 ethAmount, address[] memory tokens, uint[] memory tokenAmounts, uint[] memory stickerPackIds, uint256 available) {
    stickerMarket = address(promoPack.stickerMarket);
    ethAmount = promoPack.ethAmount;
    tokens = promoPack.tokens;
    tokenAmounts = promoPack.tokenAmounts;
    stickerPackIds = promoPack.stickerPackIds;
    available = promoAvailable;
}

/// @notice Get the promo pack configuration
/// @return stickerMarket address Stickermarket contract address
/// @return ethAmount uint256 ETH amount in wei that will be sent to a recipient
/// @return tokens address[] List of tokens that will be sent to a recipient
/// @return tokenAmounts uint[] Amount of tokens that will be sent to a recipient
/// @return stickerPackIds uint[] List of sticker packs to send to a recipient
/// @return available uint number of promo packs available