// File: @openzeppelin/contracts/token/ERC20/IERC20.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `from` to `to` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

// File: @uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol



pragma solidity >=0.5.0;

interface IUniswapV2Factory {
    event PairCreated(address indexed token0, address indexed token1, address pair, uint);

    function feeTo() external view returns (address);
    function feeToSetter() external view returns (address);

    function getPair(address tokenA, address tokenB) external view returns (address pair);
    function allPairs(uint) external view returns (address pair);
    function allPairsLength() external view returns (uint);

    function createPair(address tokenA, address tokenB) external returns (address pair);

    function setFeeTo(address) external;
    function setFeeToSetter(address) external;
}

// File: @uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol



pragma solidity >=0.5.0;

interface IUniswapV2Pair {
    event Approval(address indexed owner, address indexed spender, uint value);
    event Transfer(address indexed from, address indexed to, uint value);

    function name() external pure returns (string memory);
    function symbol() external pure returns (string memory);
    function decimals() external pure returns (uint8);
    function totalSupply() external view returns (uint);
    function balanceOf(address owner) external view returns (uint);
    function allowance(address owner, address spender) external view returns (uint);

    function approve(address spender, uint value) external returns (bool);
    function transfer(address to, uint value) external returns (bool);
    function transferFrom(address from, address to, uint value) external returns (bool);

    function DOMAIN_SEPARATOR() external view returns (bytes32);
    function PERMIT_TYPEHASH() external pure returns (bytes32);
    function nonces(address owner) external view returns (uint);

    function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;

    event Mint(address indexed sender, uint amount0, uint amount1);
    event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
    event Swap(
        address indexed sender,
        uint amount0In,
        uint amount1In,
        uint amount0Out,
        uint amount1Out,
        address indexed to
    );
    event Sync(uint112 reserve0, uint112 reserve1);

    function MINIMUM_LIQUIDITY() external pure returns (uint);
    function factory() external view returns (address);
    function token0() external view returns (address);
    function token1() external view returns (address);
    function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
    function price0CumulativeLast() external view returns (uint);
    function price1CumulativeLast() external view returns (uint);
    function kLast() external view returns (uint);

    function mint(address to) external returns (uint liquidity);
    function burn(address to) external returns (uint amount0, uint amount1);
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
    function skim(address to) external;
    function sync() external;

    function initialize(address, address) external;
}

// File: contracts/Interface/IOracle.sol



// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

interface IOracle {
    function update() external;

    function consult(
        address token,
        uint amountIn
    ) external view returns (uint amountOut);

    function timePassed() external returns (bool);
}

// File: contracts/ZKittyBotSubscribe.sol



// SPDX-License-Identifier: Unlicense
import "./Interface/IOracle.sol";
pragma solidity ^0.8.0;

import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract ZKittyBotSubscribe {
    /*
     * @title Represents the shares of each recipient.
     */
    struct Share {
        uint256 recipientOne;
        uint256 recipientTwo;
    }

    /*
     * @title Details of each subscription tier.
     */

    struct Tier {
        uint256 subscriptionFee;
        uint256 walletLimit;
        string name;
        bool active;
    }

    /*
     * @title Details of a user's subscription.
     */

    struct Subscription {
        uint256 tier;
        uint256 expiry;
    }

    uint256 public constant PRECISION = 100_00;

    /*
     * @title Represents the status of a proposal.
     */

    enum ProposalStatus {
        Executed,
        Canceled,
        Pending
    }

    /*
     * @title Details of a proposal.
     */

    struct Proposal {
        address[] targets;
        uint256[] values;
        bytes[] calldatas;
        bytes32 descriptionHash;
        ProposalStatus status;
    }

    /*
     * @title Ensure the function is called by one of the recipients.
     */

    modifier onlyRecipients() {
        require(
            msg.sender == recipientOne || msg.sender == recipientTwo,
            "Not authorized"
        );
        _;
    }

    uint256 public zkittyTokenPriceInEth;
    uint256 public settingsChangeNonce;
    uint256 public minTokensForDiscount;
    uint256 public discount;

    mapping(uint256 => Proposal) public proposals;
    mapping(uint256 => mapping(address => bool)) public changeConfirmations;
    Share public revSharePercent;
    address public recipientOne;
    address public recipientTwo;
    address public zkittyToken;
    address public UNISWAP_FACTORY_ADDRESS;
    address public WETH_ADDRESS;
    address public oracle;
    uint256 public currentTierCount;
    mapping(uint256 => Tier) public tiers;
    mapping(address => Subscription) public subscriptions;

    /*
     * @title Contract Constructor
     * @param _weth Address of WETH token.
     * @param _recipientOne Address of the first recipient.
     * @param _recipientTwo Address of the second recipient.
     * @param _revSharePercentOne Share percentage of the first recipient.
     * @param _revSharePercentTwo Share percentage of the second recipient.
     * @param _tierFees List of tier fees.
     * @param _names Names of tiers.
     * @param _tierWalletLimits List of tier wallet limits.
     * @dev Initialize the contract.
     */

    constructor(
        address _weth,
        address _recipientOne,
        address _recipientTwo,
        uint256 _revSharePercentOne,
        uint256 _revSharePercentTwo,
        uint256[] memory _tierFees,
        uint256[] memory _tierWalletLimits,
        string[] memory _names
    ) {
        WETH_ADDRESS = _weth;
        recipientOne = _recipientOne;
        recipientTwo = _recipientTwo;
        revSharePercent = Share(_revSharePercentOne, _revSharePercentTwo);

        for (uint256 i = 0; i < _tierFees.length; i++) {
            addTier(_tierFees[i], _tierWalletLimits[i], _names[i], true);
        }
    }

    /*
     * @title Set Discount Rate & Minimum Amount for Discount
     * @param newDiscount New discount rate.
     * @param newMinForDiscount New minimum amount.
     * @dev Sets the discount rate for users holding a certain minimum amount of tokens.
     */

    function setDiscount(
        uint256 _newDiscount,
        uint256 _newMinForDiscount
    ) external onlyRecipients {
        require(_newDiscount <= 10000, "Discount exceeds 100%");
        discount = _newDiscount;
        minTokensForDiscount = _newMinForDiscount;
        emit MinForDiscountUpdated(_newMinForDiscount);
        emit DiscountUpdated(_newDiscount);
    }

    function updateZKittyTokenPrice() public {
        IOracle(oracle).update();

        zkittyTokenPriceInEth = IOracle(oracle).consult(WETH_ADDRESS, 1 ether);
    }

    /*
     * @title Subscribe to a tier.
     * @param tierId ID of the tier to subscribe to.
     * @dev Allows a user to subscribe to a specific tier.
     */

    function subscribe(
        uint256 tierId,
        bool usingToken,
        address onBehalf,
        string calldata referredBY
    ) external payable {
        Tier memory tier = tiers[tierId];
        require(tier.active, "Tier is inactive");

        uint256 applicableSubscriptionFee = tier.subscriptionFee;

        if (zkittyToken != address(0)) {
            if (IOracle(oracle).timePassed()) {
                updateZKittyTokenPrice();
            }
            if (
                IERC20(zkittyToken).balanceOf(msg.sender) >=
                minTokensForDiscount
            ) {
                applicableSubscriptionFee =
                    (applicableSubscriptionFee * (PRECISION - discount)) /
                    PRECISION;
            }
        }

        if (usingToken && UNISWAP_FACTORY_ADDRESS != address(0)) {
            uint256 tokenAmountRequired = (zkittyTokenPriceInEth *
                applicableSubscriptionFee) / 1 ether;
            uint256 recipientOneShare = (tokenAmountRequired *
                revSharePercent.recipientOne) / PRECISION;
            uint256 recipientTwoShare = tokenAmountRequired - recipientOneShare;

            require(
                msg.value == 0 &&
                    IERC20(zkittyToken).transferFrom(
                        msg.sender,
                        recipientOne,
                        recipientOneShare
                    ) &&
                    IERC20(zkittyToken).transferFrom(
                        msg.sender,
                        recipientTwo,
                        recipientTwoShare
                    ),
                "Incorrect subscription fee in ZKittyToken or failed transfer"
            );
        } else {
            require(
                applicableSubscriptionFee > 0 &&
                    msg.value == applicableSubscriptionFee,
                "Incorrect subscription fee in ETH"
            );

            uint256 recipientOneShare = (msg.value *
                revSharePercent.recipientOne) / PRECISION;
            uint256 recipientTwoShare = msg.value - recipientOneShare;

            payable(recipientOne).transfer(recipientOneShare);
            payable(recipientTwo).transfer(recipientTwoShare);
        }
        if (onBehalf != address(0)) {
            subscriptions[onBehalf] = Subscription(
                tierId,
                block.timestamp + 30 days
            );
            emit UserSubscribed(msg.sender, tierId, usingToken, referredBY);
        } else {
            subscriptions[msg.sender] = Subscription(
                tierId,
                block.timestamp + 30 days
            );
            emit UserSubscribed(msg.sender, tierId, usingToken, referredBY);
        }
    }

    /**
     * @dev Sets the address for the UNISWAP_FACTORY_ADDRESS.
     * @param _uniswapFactoryAddress Address of the Uniswap factory.
     */
    function setUniswapFactoryAddress(
        address _uniswapFactoryAddress
    ) external onlyRecipients {
        require(_uniswapFactoryAddress != address(0), "Zero address provided");
        UNISWAP_FACTORY_ADDRESS = _uniswapFactoryAddress;
        emit UniswapFactoryAddressUpdated(_uniswapFactoryAddress);
    }

    /**
     * @dev Sets the address for the zkittyToken.
     * @param _zkittyToken Address of the ZKitty token.
     */
    function setZKittyToken(
        address _zkittyToken,
        address _Oracle
    ) external onlyRecipients {
        zkittyToken = _zkittyToken;
        oracle = _Oracle;
        emit ZKittyTokenAddressUpdated(_zkittyToken);
    }

    /**
     * @dev Allows one of the recipients to change their address.
     * @param newAddress The new address to set for the calling recipient.
     *
     * Emits either a RecipientOneChanged or RecipientTwoChanged event based on the sender.
     */
    function changeRecipient(address newAddress) public {
        if (msg.sender == recipientOne) {
            recipientOne = newAddress;
            emit RecipientOneChanged(newAddress);
        } else if (msg.sender == recipientTwo) {
            recipientTwo = newAddress;
            emit RecipientTwoChanged(newAddress);
        } else {
            revert("Not authorized");
        }
    }

    /**
     * @dev Adds a new subscription tier.
     * @param subscriptionFee The fee for the subscription tier.
     * @param walletLimit The maximum wallet limit for this tier.
     * @param name The name of the tier.
     * @param isActive Flag to indicate if the tier is active or not.
     */
    function addTier(
        uint256 subscriptionFee,
        uint256 walletLimit,
        string memory name,
        bool isActive
    ) public onlyRecipients {
        tiers[currentTierCount] = Tier({
            subscriptionFee: subscriptionFee,
            walletLimit: walletLimit,
            name: name,
            active: isActive
        });

        emit TierAdded(
            currentTierCount,
            subscriptionFee,
            walletLimit,
            isActive
        );

        currentTierCount++;
    }

    /**
     * @dev Deactivates an existing subscription tier.
     * @param tierId The ID of the tier to be deactivated.
     */

    function deactivateTier(uint256 tierId) public onlyRecipients {
        tiers[tierId].active = false;
        emit TierDeactivated(tierId);
    }

    /*
     *  @title Set Revenue Share Percentages
     *  @param Share percentage for recipient one
     *  @param Share percentage for recipient two
     *  @dev Sets the revenue share percentages for recipients
     */
    function setRevSharePercent(
        uint256 _revSharePercentOne,
        uint256 _revSharePercentTwo
    ) public {
        require(msg.sender == address(this), "Not authorized");
        require(
            _revSharePercentOne + _revSharePercentTwo == PRECISION,
            "The sum must be equal to 100%"
        );

        revSharePercent = Share(_revSharePercentOne, _revSharePercentTwo);

        emit RevSharePercentUpdated(_revSharePercentOne, _revSharePercentTwo);
    }

    /*
     * @title Create a new proposal for changing contract settings.
     * @param targets Addresses of the contracts/targets involved in the proposal.
     * @param values ETH values for the proposed actions.
     * @param calldatas Calldata for the proposed actions.
     * @param descriptionHash IPFS hash of the proposal description.
     * @dev Allows recipients to create a proposal for changes.
     */

    function propose(
        address[] memory targets,
        uint256[] memory values,
        bytes[] memory calldatas,
        bytes32 descriptionHash
    ) public onlyRecipients returns (uint256) {
        settingsChangeNonce++;

        for (uint256 i = 0; i < targets.length; i++) {
            require(targets[i] != address(0), "Zero address target");
        }

        proposals[settingsChangeNonce] = Proposal({
            targets: targets,
            values: values,
            calldatas: calldatas,
            descriptionHash: descriptionHash,
            status: ProposalStatus.Pending
        });

        emit ProposalCreated(settingsChangeNonce, descriptionHash);

        return settingsChangeNonce;
    }

    /*
     * @title Confirm a proposal.
     * @param proposalId ID of the proposal to be confirmed.
     * @dev Allows a recipient to confirm a proposal.
     */
    function confirmProposal(uint256 proposalId) public onlyRecipients {
        require(
            !changeConfirmations[proposalId][msg.sender],
            "Already confirmed"
        );
        require(
            proposals[proposalId].status == ProposalStatus.Pending,
            "Proposal not pending"
        );

        changeConfirmations[proposalId][msg.sender] = true;

        emit ProposalConfirmed(proposalId, msg.sender);
    }

    /*
     * @title Cancel a proposal.
     * @param proposalId ID of the proposal to be canceled.
     * @dev Allows a recipient to cancel a pending proposal.
     */
    function cancelProposal(uint256 proposalId) public onlyRecipients {
        require(
            proposals[proposalId].status == ProposalStatus.Pending,
            "Proposal not pending"
        );

        proposals[proposalId].status = ProposalStatus.Canceled;

        emit ProposalCanceled(proposalId);
    }

    /*
     * @title Execute a proposal.
     * @param proposalId ID of the proposal to be executed.
     * @dev Allows a recipient to execute a proposal if it's approved by both.
     */
    function executeProposal(uint256 proposalId) public onlyRecipients {
        Proposal storage proposal = proposals[proposalId];
        require(
            proposal.status == ProposalStatus.Pending,
            "Proposal not pending"
        );
        require(
            changeConfirmations[proposalId][recipientOne] &&
                changeConfirmations[proposalId][recipientTwo],
            "Both recipients must confirm"
        );

        for (uint i = 0; i < proposal.targets.length; i++) {
            (bool success, ) = proposal.targets[i].call{
                value: proposal.values[i]
            }(proposal.calldatas[i]);
            require(success, "Transaction execution failed");
        }

        proposal.status = ProposalStatus.Executed;

        emit ProposalExecuted(proposalId);
    }

    // Events
    event MinForDiscountUpdated(uint256 newMinForDiscount);
    event DiscountUpdated(uint256 newDiscount);
    event ZKittyTokenPriceUpdated(uint256 newPrice);
    event UserSubscribed(
        address indexed subscriber,
        uint256 tierId,
        bool usingToken,
        string referredBy
    );
    event RecipientOneChanged(address indexed newRecipient);
    event RecipientTwoChanged(address indexed newRecipient);
    event TierAdded(
        uint256 tierId,
        uint256 subscriptionFee,
        uint256 walletLimit,
        bool isActive
    );
    event TierDeactivated(uint256 tierId);
    event RevSharePercentUpdated(
        uint256 recipientOnePercent,
        uint256 recipientTwoPercent
    );
    event ProposalCreated(uint256 proposalId, bytes32 description);
    event ProposalConfirmed(uint256 proposalId, address indexed confirmer);
    event ProposalCanceled(uint256 proposalId);
    event ProposalExecuted(uint256 proposalId);
    event UniswapFactoryAddressUpdated(
        address indexed newUniswapFactoryAddress
    );
    event ZKittyTokenAddressUpdated(address indexed newZKittyToken);
}