contract MerkleRedeem is IDistributor, Ownable {
    using FixedPoint for uint256;
    using SafeERC20 for IERC20;

    IERC20 public immutable rewardToken;

    // Recorded weeks
    mapping(uint256 => bytes32) public weekMerkleRoots;
    mapping(uint256 => mapping(address => bool)) public claimed;

    IVault public immutable vault;

    constructor(IVault _vault, IERC20 _rewardToken) {
        vault = _vault;
        rewardToken = _rewardToken;
        _rewardToken.approve(address(_vault), type(uint256).max);
    }

    function _disburse(address recipient, uint256 balance) private {
        if (balance > 0) {
            emit RewardPaid(recipient, address(rewardToken), balance);
            rewardToken.safeTransfer(recipient, balance);
        }
    }

    function _disburseToInternalBalance(address recipient, uint256 balance) private {
        if (balance > 0) {
            IVault.UserBalanceOp[] memory ops = new IVault.UserBalanceOp[](1);

            ops[0] = IVault.UserBalanceOp({
                asset: IAsset(address(rewardToken)),
                amount: balance,
                sender: address(this),
                recipient: payable(recipient),
                kind: IVault.UserBalanceOpKind.DEPOSIT_INTERNAL
            });

            emit RewardPaid(recipient, address(rewardToken), balance);
            vault.manageUserBalance(ops);
        }
    }

    /**
     * @notice Allows a user to claim a particular week's worth of rewards
     */
    function claimWeek(
        address liquidityProvider,
        uint256 week,
        uint256 claimedBalance,
        bytes32[] memory merkleProof
    ) external {
        require(msg.sender == liquidityProvider, "user must claim own balance");
        require(!claimed[week][liquidityProvider], "cannot claim twice");
        require(verifyClaim(liquidityProvider, week, claimedBalance, merkleProof), "Incorrect merkle proof");

        claimed[week][liquidityProvider] = true;
        _disburse(liquidityProvider, claimedBalance);
    }

    struct Claim {
        uint256 week;
        uint256 balance;
        bytes32[] merkleProof;
    }

    function _processClaims(address liquidityProvider, Claim[] memory claims) internal returns (uint256 totalBalance) {
        Claim memory claim;
        for (uint256 i = 0; i < claims.length; i++) {
            claim = claims[i];

            require(!claimed[claim.week][liquidityProvider], "cannot claim twice");
            require(
                verifyClaim(liquidityProvider, claim.week, claim.balance, claim.merkleProof),
                "Incorrect merkle proof"
            );

            totalBalance = totalBalance.add(claim.balance);
            claimed[claim.week][liquidityProvider] = true;
        }
    }

    /**
     * @notice Allows a user to claim multiple weeks of reward
     */
    function claimWeeks(address liquidityProvider, Claim[] memory claims) external {
        require(msg.sender == liquidityProvider, "user must claim own balance");

        uint256 totalBalance = _processClaims(liquidityProvider, claims);
        _disburse(liquidityProvider, totalBalance);
    }

    /**
     * @notice Allows a user to claim multiple weeks of reward to internal balance
     */
    function claimWeeksToInternalBalance(address liquidityProvider, Claim[] memory claims) external {
        require(msg.sender == liquidityProvider, "user must claim own balance");

        uint256 totalBalance = _processClaims(liquidityProvider, claims);

        _disburseToInternalBalance(liquidityProvider, totalBalance);
    }

    /**
     * @notice Allows a user to claim several weeks of rewards to a callback
     */
    function claimWeeksWithCallback(
        address liquidityProvider,
        IDistributorCallback callbackContract,
        bytes calldata callbackData,
        Claim[] memory claims
    ) external {
        require(msg.sender == liquidityProvider, "user must claim own balance");
        uint256 totalBalance = _processClaims(liquidityProvider, claims);

        _disburseToInternalBalance(address(callbackContract), totalBalance);

        callbackContract.distributorCallback(callbackData);
    }

    function claimStatus(
        address liquidityProvider,
        uint256 begin,
        uint256 end
    ) external view returns (bool[] memory) {
        require(begin <= end, "weeks must be specified in ascending order");
        uint256 size = 1 + end - begin;
        bool[] memory arr = new bool[](size);
        for (uint256 i = 0; i < size; i++) {
            arr[i] = claimed[begin + i][liquidityProvider];
        }
        return arr;
    }

    function merkleRoots(uint256 begin, uint256 end) external view returns (bytes32[] memory) {
        require(begin <= end, "weeks must be specified in ascending order");
        uint256 size = 1 + end - begin;
        bytes32[] memory arr = new bytes32[](size);
        for (uint256 i = 0; i < size; i++) {
            arr[i] = weekMerkleRoots[begin + i];
        }
        return arr;
    }

    function verifyClaim(
        address liquidityProvider,
        uint256 week,
        uint256 claimedBalance,
        bytes32[] memory merkleProof
    ) public view returns (bool) {
        bytes32 leaf = keccak256(abi.encodePacked(liquidityProvider, claimedBalance));
        return MerkleProof.verify(merkleProof, weekMerkleRoots[week], leaf);
    }

    /**
     * @notice
     * Allows the owner to add funds to the contract as a merkle tree, These tokens will
     * be withdrawn from the sender
     * These will be pulled from the user
     */
    function seedAllocations(
        uint256 week,
        bytes32 _merkleRoot,
        uint256 amount
    ) external onlyOwner {
        require(weekMerkleRoots[week] == bytes32(0), "cannot rewrite merkle root");
        weekMerkleRoots[week] = _merkleRoot;
        rewardToken.safeTransferFrom(msg.sender, address(this), amount);
        emit RewardAdded(address(rewardToken), amount);
    }
}
function claimWeek(
    address liquidityProvider,
    uint256 week,
    uint256 claimedBalance,
    bytes32[] memory merkleProof
) external {
    require(msg.sender == liquidityProvider, "user must claim own balance");
    require(!claimed[week][liquidityProvider], "cannot claim twice");
    require(verifyClaim(liquidityProvider, week, claimedBalance, merkleProof), "Incorrect merkle proof");

    claimed[week][liquidityProvider] = true;
    _disburse(liquidityProvider, claimedBalance);
}

/**
 * @notice Allows a user to claim a particular week's worth of rewards
 */