// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
 * @title TreasuryVault
 * @notice Asset custody for the export path; only approved modules can move funds; only approved tokens can leave.
 * @dev Enforces per-tx cap, daily cap, and hourly rate limit. See docs/treasury/EXECUTOR_ALLOWLIST_MATRIX.md.
 */
contract TreasuryVault is AccessControl, ReentrancyGuard {
    using SafeERC20 for IERC20;

    bytes32 public constant MODULE_ROLE = keccak256("MODULE_ROLE");
    bytes32 public constant CAP_MANAGER_ROLE = keccak256("CAP_MANAGER_ROLE");

    uint256 public maxPerTx;
    uint256 public dailyCap;
    uint256 public rateLimitPerHour;

    uint256 public dailyUsed;
    uint256 public dayStartTimestamp;
    uint256 public hourlyCount;
    uint256 public hourStartTimestamp;

    mapping(address => bool) public allowedTokens;

    event TransferRequested(address indexed token, address indexed toModule, uint256 amount);
    event ModuleSet(address indexed module, bool approved);
    event TokenSet(address indexed token, bool approved);
    event CapsUpdated(uint256 maxPerTx, uint256 dailyCap, uint256 rateLimitPerHour);

    error NotModule();
    error TokenNotApproved();
    error ExceedsPerTxCap();
    error ExceedsDailyCap();
    error ExceedsRateLimit();
    error ZeroAddress();
    error ZeroAmount();

    constructor(address admin) {
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        _grantRole(CAP_MANAGER_ROLE, admin);
        dayStartTimestamp = block.timestamp;
        hourStartTimestamp = block.timestamp;
    }

    /**
     * @notice Request transfer of tokens to an approved module.
     * @param token Approved token address (canonical 138 list only).
     * @param amount Amount to transfer.
     * @param toModule Recipient module; must be an approved module.
     */
    function requestTransfer(address token, uint256 amount, address toModule)
        external
        nonReentrant
        onlyRole(MODULE_ROLE)
    {
        if (token == address(0) || toModule == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();
        if (!hasRole(MODULE_ROLE, toModule)) revert NotModule();
        if (!allowedTokens[token]) revert TokenNotApproved();
        if (maxPerTx != 0 && amount > maxPerTx) revert ExceedsPerTxCap();

        _updateDailyHourly(amount);
        if (dailyCap != 0 && dailyUsed + amount > dailyCap) revert ExceedsDailyCap();
        if (rateLimitPerHour != 0 && hourlyCount > rateLimitPerHour) revert ExceedsRateLimit();

        dailyUsed += amount;
        IERC20(token).safeTransfer(toModule, amount);
        emit TransferRequested(token, toModule, amount);
    }

    function _updateDailyHourly(uint256) internal {
        uint256 now_ = block.timestamp;
        if (now_ - dayStartTimestamp >= 1 days) {
            dayStartTimestamp = now_;
            dailyUsed = 0;
        }
        if (now_ - hourStartTimestamp >= 1 hours) {
            hourStartTimestamp = now_;
            hourlyCount = 0;
        }
        hourlyCount++;
    }

    function setModule(address module, bool approved) external onlyRole(DEFAULT_ADMIN_ROLE) {
        if (module == address(0)) revert ZeroAddress();
        if (approved) {
            _grantRole(MODULE_ROLE, module);
        } else {
            _revokeRole(MODULE_ROLE, module);
        }
        emit ModuleSet(module, approved);
    }

    function setToken(address token, bool approved) external onlyRole(DEFAULT_ADMIN_ROLE) {
        if (token == address(0)) revert ZeroAddress();
        allowedTokens[token] = approved;
        emit TokenSet(token, approved);
    }

    function setCaps(uint256 _maxPerTx, uint256 _dailyCap, uint256 _rateLimitPerHour)
        external
        onlyRole(CAP_MANAGER_ROLE)
    {
        maxPerTx = _maxPerTx;
        dailyCap = _dailyCap;
        rateLimitPerHour = _rateLimitPerHour;
        emit CapsUpdated(_maxPerTx, _dailyCap, _rateLimitPerHour);
    }

    function deposit(address token, uint256 amount) external nonReentrant {
        if (token == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();
        if (!allowedTokens[token]) revert TokenNotApproved();
        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
    }
}
