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

import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

/// @title AereSessionKeyValidator - scoped session keys for AereModularAccount
/// @notice Singleton validator module (ERC-7579 module type 1). Per-account state
///         is keyed by msg.sender: ALL mutating functions take effect for the
///         calling account, so registration is owner-only by construction (the
///         account's execute()/installModule() paths are gated to the EntryPoint,
///         self-calls and the root owner).
///
///         A session is (key, validAfter, validUntil, maxValuePerOp) plus an
///         allowlist of (target, selector) pairs. Selector 0x00000000 is the
///         sentinel for plain native transfers (empty calldata).
///
///         validateUserOp returns the CANONICAL ERC-4337 validationData packing:
///           authorizer (160 bits) | validUntil (48) << 160 | validAfter (48) << 208
///         with authorizer = 0 on signature+scope success, 1 on failure.
///         AereModularAccount unpacks it and enforces the time window itself
///         because AereEntryPointV2 reverts on any non-zero account return.
contract AereSessionKeyValidator {
    struct Session {
        bool enabled;
        uint48 validAfter;
        uint48 validUntil;
        uint256 maxValuePerOp;
    }

    uint256 internal constant SIG_VALIDATION_FAILED = 1;
    bytes4 internal constant EIP1271_MAGIC = 0x1626ba7e;
    bytes4 internal constant EIP1271_FAIL = 0xffffffff;

    /// @notice Sentinel selector matching an empty-calldata (native transfer) call.
    bytes4 public constant SELECTOR_NATIVE_TRANSFER = 0x00000000;

    /// @notice Selectors of AereModularAccount execution functions a session key
    ///         is allowed to enter through.
    bytes4 public constant EXECUTE_SELECTOR = bytes4(keccak256("execute(address,uint256,bytes)"));
    bytes4 public constant EXECUTE_BATCH_SELECTOR = bytes4(keccak256("executeBatch((address,uint256,bytes)[])"));

    /// @dev Mirror of AereModularAccount.Call for callData decoding.
    struct Call {
        address target;
        uint256 value;
        bytes data;
    }

    /// @notice account => sessionKey => session parameters.
    mapping(address => mapping(address => Session)) public sessions;

    /// @notice account => sessionKey => target => selector => allowed.
    mapping(address => mapping(address => mapping(address => mapping(bytes4 => bool)))) public allowedCall;

    event SessionEnabled(address indexed account, address indexed key, uint48 validAfter, uint48 validUntil, uint256 maxValuePerOp);
    event SessionRevoked(address indexed account, address indexed key);
    event PermissionAdded(address indexed account, address indexed key, address target, bytes4 selector);
    event PermissionRemoved(address indexed account, address indexed key, address target, bytes4 selector);

    error InvalidSessionKey();
    error InvalidWindow();
    error LengthMismatch();
    error SessionNotEnabled();

    // ---- ERC-7579-style module base -------------------------------------------

    function isModuleType(uint256 moduleTypeId) external pure returns (bool) {
        return moduleTypeId == 1;
    }

    /// @notice Optional initial session at install time. initData layout:
    ///         abi.encode(address key, uint48 validAfter, uint48 validUntil,
    ///                    uint256 maxValuePerOp, address[] targets, bytes4[] selectors)
    ///         Pass empty bytes to install with no session.
    function onInstall(bytes calldata data) external {
        if (data.length == 0) return;
        (
            address key,
            uint48 validAfter,
            uint48 validUntil,
            uint256 maxValuePerOp,
            address[] memory targets,
            bytes4[] memory selectors
        ) = abi.decode(data, (address, uint48, uint48, uint256, address[], bytes4[]));
        _enableSession(msg.sender, key, validAfter, validUntil, maxValuePerOp, targets, selectors);
    }

    /// @notice Per-account session state is NOT bulk-cleared on uninstall (nested
    ///         mappings are not enumerable). The account removes routing on
    ///         uninstall, which makes stale state unreachable; revoke sessions
    ///         explicitly before a planned reinstall.
    function onUninstall(bytes calldata) external {}

    // ---- Owner-only registration (msg.sender = the smart account) ------------

    function enableSession(
        address key,
        uint48 validAfter,
        uint48 validUntil,
        uint256 maxValuePerOp,
        address[] calldata targets,
        bytes4[] calldata selectors
    ) external {
        address[] memory t = targets;
        bytes4[] memory s = selectors;
        _enableSession(msg.sender, key, validAfter, validUntil, maxValuePerOp, t, s);
    }

    function revokeSession(address key) external {
        if (!sessions[msg.sender][key].enabled) revert SessionNotEnabled();
        sessions[msg.sender][key].enabled = false;
        emit SessionRevoked(msg.sender, key);
    }

    function addPermission(address key, address target, bytes4 selector) external {
        if (!sessions[msg.sender][key].enabled) revert SessionNotEnabled();
        allowedCall[msg.sender][key][target][selector] = true;
        emit PermissionAdded(msg.sender, key, target, selector);
    }

    function removePermission(address key, address target, bytes4 selector) external {
        allowedCall[msg.sender][key][target][selector] = false;
        emit PermissionRemoved(msg.sender, key, target, selector);
    }

    function _enableSession(
        address account,
        address key,
        uint48 validAfter,
        uint48 validUntil,
        uint256 maxValuePerOp,
        address[] memory targets,
        bytes4[] memory selectors
    ) internal {
        if (key == address(0)) revert InvalidSessionKey();
        if (validUntil != 0 && validUntil <= validAfter) revert InvalidWindow();
        if (targets.length != selectors.length) revert LengthMismatch();
        sessions[account][key] = Session({
            enabled: true,
            validAfter: validAfter,
            validUntil: validUntil,
            maxValuePerOp: maxValuePerOp
        });
        for (uint256 i = 0; i < targets.length; i++) {
            allowedCall[account][key][targets[i]][selectors[i]] = true;
            emit PermissionAdded(account, key, targets[i], selectors[i]);
        }
        emit SessionEnabled(account, key, validAfter, validUntil, maxValuePerOp);
    }

    // ---- Validation (called by the account, msg.sender = account) -------------

    /// @param userOpHash       hash the session key signed (EIP-191 envelope).
    /// @param accountCallData  the userOp.callData; must be execute()/executeBatch()
    ///                         on the account, fully inside the session allowlist.
    /// @param signature        65-byte ECDSA signature by the session key.
    function validateUserOp(
        bytes32 userOpHash,
        bytes calldata accountCallData,
        bytes calldata signature
    ) external view returns (uint256 validationData) {
        address account = msg.sender;
        (address key, ECDSA.RecoverError err) =
            ECDSA.tryRecover(ECDSA.toEthSignedMessageHash(userOpHash), signature);
        if (err != ECDSA.RecoverError.NoError || key == address(0)) return SIG_VALIDATION_FAILED;

        Session memory s = sessions[account][key];
        if (!s.enabled) return SIG_VALIDATION_FAILED;
        if (!_checkCallData(account, key, s.maxValuePerOp, accountCallData)) {
            return SIG_VALIDATION_FAILED;
        }

        // Canonical 4337 packing; the account enforces the time window.
        return (uint256(s.validUntil) << 160) | (uint256(s.validAfter) << 208);
    }

    function _checkCallData(
        address account,
        address key,
        uint256 maxValuePerOp,
        bytes calldata accountCallData
    ) internal view returns (bool) {
        if (accountCallData.length < 4) return false;
        bytes4 outer = bytes4(accountCallData[0:4]);

        if (outer == EXECUTE_SELECTOR) {
            (address target, uint256 value, bytes memory data) =
                abi.decode(accountCallData[4:], (address, uint256, bytes));
            return _checkSingle(account, key, maxValuePerOp, target, value, data);
        }

        if (outer == EXECUTE_BATCH_SELECTOR) {
            Call[] memory calls = abi.decode(accountCallData[4:], (Call[]));
            if (calls.length == 0) return false;
            for (uint256 i = 0; i < calls.length; i++) {
                if (!_checkSingle(account, key, maxValuePerOp, calls[i].target, calls[i].value, calls[i].data)) {
                    return false;
                }
            }
            return true;
        }

        // Session keys may only enter through execute/executeBatch.
        return false;
    }

    function _checkSingle(
        address account,
        address key,
        uint256 maxValuePerOp,
        address target,
        uint256 value,
        bytes memory data
    ) internal view returns (bool) {
        if (value > maxValuePerOp) return false;
        bytes4 sel;
        if (data.length == 0) {
            sel = SELECTOR_NATIVE_TRANSFER;
        } else if (data.length >= 4) {
            assembly {
                sel := mload(add(data, 32))
            }
        } else {
            return false; // 1-3 byte calldata is never in scope
        }
        return allowedCall[account][key][target][sel];
    }

    // ---- EIP-1271 pass-through (msg.sender = account) --------------------------

    /// @notice Validates an off-chain signature by a currently-active session key.
    ///         Scoping does not apply (no call is being made); only key validity
    ///         and the time window are enforced.
    function isValidSignatureWithSender(address, bytes32 hash, bytes calldata signature)
        external
        view
        returns (bytes4)
    {
        address account = msg.sender;
        (address key, ECDSA.RecoverError err) =
            ECDSA.tryRecover(ECDSA.toEthSignedMessageHash(hash), signature);
        if (err != ECDSA.RecoverError.NoError || key == address(0)) return EIP1271_FAIL;
        Session memory s = sessions[account][key];
        if (!s.enabled) return EIP1271_FAIL;
        if (s.validAfter != 0 && block.timestamp < s.validAfter) return EIP1271_FAIL;
        if (s.validUntil != 0 && block.timestamp > s.validUntil) return EIP1271_FAIL;
        return EIP1271_MAGIC;
    }
}
