// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/**
 * @title AereAgent — machine-account registry for the agentic economy
 * @notice Autonomous workers (Claude, GPT, Gemini, bespoke agents) register
 *         an on-chain identity, fund a prepaid balance, and then settle
 *         per-call payments to API providers via the AERE402 facilitator.
 *
 *         Identity model:
 *           - Each agent has a stable agentId (monotonic uint256).
 *           - Each agent has an OPERATOR address (the human / org running
 *             the agent) and a SIGNING address (the secp256k1 key the
 *             agent uses for EIP-712 payment authorisations). Both are
 *             rotatable by the operator via setSigner / transferOperator.
 *           - Prepaid balance per (agent, settlementToken). Settlement
 *             token is whatever ERC-20 the operator funds (typically
 *             WAERE or USDC.e). Operators top up with `topUp(agentId,
 *             token, amount)` and withdraw idle balance with
 *             `withdraw(agentId, token, amount)`.
 *
 *         Rate limits:
 *           - per-call cap   (uint128, in token units)
 *           - per-hour cap   (uint128, in token units)
 *           - per-day cap    (uint128, in token units)
 *           Enforced at the AERE402Facilitator (which calls debit() here).
 *
 *         Pause:
 *           - Operator can pause the agent instantly. Facilitator checks
 *             paused flag before debiting; reverts on a paused agent.
 *
 * @dev    Only the AERE402_FACILITATOR (immutable, set in constructor)
 *         can call debit(). This makes the facilitator the SOLE
 *         spend-side authority, and lets the operator change keys
 *         without juggling token-level approvals.
 */
contract AereAgent is ReentrancyGuard {

    /// @notice The only contract that may call debit().
    address public immutable AERE402_FACILITATOR;

    struct Agent {
        bool registered;
        bool paused;
        address operator;        // human / org running the agent (rotatable)
        address signer;          // secp256k1 key that signs payment auths (rotatable)
        bytes32 passkeyPubX;     // optional WebAuthn passkey for additional auth surface
        bytes32 passkeyPubY;
        uint128 perCallCap;
        uint128 perHourCap;
        uint128 perDayCap;
        string metadataUri;       // ipfs / https with agent description
    }

    /// @notice agentId → Agent
    mapping(uint256 => Agent) public agents;
    uint256 public nextAgentId;

    /// @notice agentId → token → prepaid balance
    mapping(uint256 => mapping(address => uint256)) public balances;

    /// @notice agentId → token → (hour epoch, spent in that hour)
    mapping(uint256 => mapping(address => uint256)) public hourlySpentEpoch;
    mapping(uint256 => mapping(address => uint256)) public hourlySpent;

    /// @notice agentId → token → (day epoch, spent in that day)
    mapping(uint256 => mapping(address => uint256)) public dailySpentEpoch;
    mapping(uint256 => mapping(address => uint256)) public dailySpent;

    /* --------------------------------- events ------------------------------- */

    event AgentRegistered(uint256 indexed agentId, address indexed operator, address indexed signer, string metadataUri);
    event OperatorTransferred(uint256 indexed agentId, address indexed oldOperator, address indexed newOperator);
    event SignerSet(uint256 indexed agentId, address indexed newSigner);
    event PasskeySet(uint256 indexed agentId, bytes32 pubX, bytes32 pubY);
    event RateLimitsSet(uint256 indexed agentId, uint128 perCallCap, uint128 perHourCap, uint128 perDayCap);
    event AgentPausedSet(uint256 indexed agentId, bool paused);
    event MetadataUpdated(uint256 indexed agentId, string metadataUri);
    event TopUp(uint256 indexed agentId, address indexed token, address indexed funder, uint256 amount);
    event Withdrawn(uint256 indexed agentId, address indexed token, address indexed recipient, uint256 amount);
    event Debited(uint256 indexed agentId, address indexed token, address indexed payee, uint256 amount);

    /* --------------------------------- errors ------------------------------- */

    error NotOperator();
    error NotFacilitator();
    error UnknownAgent();
    error AgentPausedErr();
    error InsufficientBalance(uint256 have, uint256 want);
    error PerCallCapExceeded(uint256 want, uint256 cap);
    error PerHourCapExceeded(uint256 wouldBe, uint256 cap);
    error PerDayCapExceeded(uint256 wouldBe, uint256 cap);
    error ZeroAddress();
    error ZeroAmount();
    error TransferFailed();

    /* ------------------------------- modifiers ------------------------------- */

    modifier onlyOperator(uint256 agentId) {
        Agent storage a = agents[agentId];
        if (!a.registered) revert UnknownAgent();
        if (msg.sender != a.operator) revert NotOperator();
        _;
    }

    /* ------------------------------- constructor ---------------------------- */

    constructor(address facilitator) {
        if (facilitator == address(0)) revert ZeroAddress();
        AERE402_FACILITATOR = facilitator;
    }

    /* ------------------------------- registration --------------------------- */

    function registerAgent(
        address signer,
        bytes32 passkeyPubX,
        bytes32 passkeyPubY,
        uint128 perCallCap,
        uint128 perHourCap,
        uint128 perDayCap,
        string calldata metadataUri
    ) external returns (uint256 agentId) {
        if (signer == address(0)) revert ZeroAddress();
        agentId = nextAgentId++;
        agents[agentId] = Agent({
            registered: true,
            paused: false,
            operator: msg.sender,
            signer: signer,
            passkeyPubX: passkeyPubX,
            passkeyPubY: passkeyPubY,
            perCallCap: perCallCap,
            perHourCap: perHourCap,
            perDayCap: perDayCap,
            metadataUri: metadataUri
        });
        emit AgentRegistered(agentId, msg.sender, signer, metadataUri);
        emit RateLimitsSet(agentId, perCallCap, perHourCap, perDayCap);
        if (passkeyPubX != bytes32(0) || passkeyPubY != bytes32(0)) {
            emit PasskeySet(agentId, passkeyPubX, passkeyPubY);
        }
    }

    function transferOperator(uint256 agentId, address newOperator) external onlyOperator(agentId) {
        if (newOperator == address(0)) revert ZeroAddress();
        address old = agents[agentId].operator;
        agents[agentId].operator = newOperator;
        emit OperatorTransferred(agentId, old, newOperator);
    }

    function setSigner(uint256 agentId, address newSigner) external onlyOperator(agentId) {
        if (newSigner == address(0)) revert ZeroAddress();
        agents[agentId].signer = newSigner;
        emit SignerSet(agentId, newSigner);
    }

    function setPasskey(uint256 agentId, bytes32 pubX, bytes32 pubY) external onlyOperator(agentId) {
        agents[agentId].passkeyPubX = pubX;
        agents[agentId].passkeyPubY = pubY;
        emit PasskeySet(agentId, pubX, pubY);
    }

    function setRateLimits(uint256 agentId, uint128 perCallCap, uint128 perHourCap, uint128 perDayCap) external onlyOperator(agentId) {
        agents[agentId].perCallCap = perCallCap;
        agents[agentId].perHourCap = perHourCap;
        agents[agentId].perDayCap = perDayCap;
        emit RateLimitsSet(agentId, perCallCap, perHourCap, perDayCap);
    }

    function setPaused(uint256 agentId, bool paused) external onlyOperator(agentId) {
        agents[agentId].paused = paused;
        emit AgentPausedSet(agentId, paused);
    }

    function updateMetadata(uint256 agentId, string calldata metadataUri) external onlyOperator(agentId) {
        agents[agentId].metadataUri = metadataUri;
        emit MetadataUpdated(agentId, metadataUri);
    }

    /* ------------------------------- balance --------------------------------- */

    function topUp(uint256 agentId, address token, uint256 amount) external nonReentrant {
        if (!agents[agentId].registered) revert UnknownAgent();
        if (amount == 0) revert ZeroAmount();
        bool ok = IERC20(token).transferFrom(msg.sender, address(this), amount);
        if (!ok) revert TransferFailed();
        balances[agentId][token] += amount;
        emit TopUp(agentId, token, msg.sender, amount);
    }

    function withdraw(uint256 agentId, address token, uint256 amount, address recipient) external onlyOperator(agentId) nonReentrant {
        if (recipient == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();
        uint256 bal = balances[agentId][token];
        if (bal < amount) revert InsufficientBalance(bal, amount);
        unchecked { balances[agentId][token] = bal - amount; }
        bool ok = IERC20(token).transfer(recipient, amount);
        if (!ok) revert TransferFailed();
        emit Withdrawn(agentId, token, recipient, amount);
    }

    /* --------------------------------- debit -------------------------------- */

    /// @notice Called by AERE402Facilitator to debit the agent's prepaid
    ///         balance and transfer `amount` of `token` to `payee`.
    ///         Enforces per-call / per-hour / per-day caps + paused.
    function debit(
        uint256 agentId,
        address token,
        address payee,
        uint256 amount
    ) external nonReentrant {
        if (msg.sender != AERE402_FACILITATOR) revert NotFacilitator();
        Agent storage a = agents[agentId];
        if (!a.registered) revert UnknownAgent();
        if (a.paused) revert AgentPausedErr();
        if (amount == 0) revert ZeroAmount();
        if (payee == address(0)) revert ZeroAddress();

        if (a.perCallCap != 0 && amount > a.perCallCap) revert PerCallCapExceeded(amount, a.perCallCap);

        uint256 bal = balances[agentId][token];
        if (bal < amount) revert InsufficientBalance(bal, amount);

        uint256 thisHour = block.timestamp / 1 hours;
        uint256 thisDay  = block.timestamp / 1 days;

        uint256 hourSpent = hourlySpentEpoch[agentId][token] == thisHour ? hourlySpent[agentId][token] : 0;
        uint256 daySpent  = dailySpentEpoch[agentId][token]  == thisDay  ? dailySpent[agentId][token]  : 0;

        uint256 newHourSpent = hourSpent + amount;
        uint256 newDaySpent  = daySpent + amount;

        if (a.perHourCap != 0 && newHourSpent > a.perHourCap) revert PerHourCapExceeded(newHourSpent, a.perHourCap);
        if (a.perDayCap  != 0 && newDaySpent  > a.perDayCap)  revert PerDayCapExceeded(newDaySpent, a.perDayCap);

        unchecked {
            balances[agentId][token] = bal - amount;
            hourlySpentEpoch[agentId][token] = thisHour;
            hourlySpent[agentId][token] = newHourSpent;
            dailySpentEpoch[agentId][token] = thisDay;
            dailySpent[agentId][token] = newDaySpent;
        }

        bool ok = IERC20(token).transfer(payee, amount);
        if (!ok) revert TransferFailed();
        emit Debited(agentId, token, payee, amount);
    }

    /* --------------------------------- views -------------------------------- */

    function signerOf(uint256 agentId) external view returns (address) {
        return agents[agentId].signer;
    }
}
