// File: contracts/AccessTokenVerifier.sol



// SPDX-License-Identifier: MIT
pragma solidity >=0.8.13;

import { IAccessTokenVerifier, AccessToken, FunctionCall, EIP712Domain } from "./interfaces/IAccessTokenVerifier.sol";
import { KeyInfrastructure } from "./KeyInfrastructure.sol";

contract AccessTokenVerifier is IAccessTokenVerifier, KeyInfrastructure {
    bytes32 private constant EIP712DOMAIN_TYPEHASH =
        keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");

    // solhint-disable max-line-length
    bytes32 private constant FUNCTIONCALL_TYPEHASH =
        keccak256("FunctionCall(bytes4 functionSignature,address target,address caller,bytes parameters)");

    // solhint-disable max-line-length
    bytes32 private constant TOKEN_TYPEHASH =
        keccak256(
            "AccessToken(uint256 expiry,FunctionCall functionCall)FunctionCall(bytes4 functionSignature,address target,address caller,bytes parameters)"
        );

    // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to
    // invalidate the cached domain separator if the chain id changes.
    bytes32 private immutable _cachedDomainSeparator;
    uint256 private immutable _cachedChainId;
    address private immutable _cachedThis;

    constructor(address root) KeyInfrastructure(root) {
        _cachedChainId = block.chainid;
        _cachedDomainSeparator = _buildDomainSeparator();
        _cachedThis = address(this);
    }

    function hash(EIP712Domain memory eip712Domain) internal pure returns (bytes32) {
        return
            keccak256(
                abi.encode(
                    EIP712DOMAIN_TYPEHASH,
                    keccak256(bytes(eip712Domain.name)),
                    keccak256(bytes(eip712Domain.version)),
                    eip712Domain.chainId,
                    eip712Domain.verifyingContract
                )
            );
    }

    function hash(FunctionCall calldata call) internal pure returns (bytes32) {
        return
            keccak256(
                abi.encode(
                    FUNCTIONCALL_TYPEHASH,
                    call.functionSignature,
                    call.target,
                    call.caller,
                    keccak256(call.parameters)
                )
            );
    }

    function hash(AccessToken calldata token) internal pure returns (bytes32) {
        return keccak256(abi.encode(TOKEN_TYPEHASH, token.expiry, hash(token.functionCall)));
    }

    function verifySignerOf(AccessToken calldata token, uint8 v, bytes32 r, bytes32 s) external view returns (address) {
        return _retrieveSignerFromToken(token, v, r, s);
    }

    function verify(AccessToken calldata token, uint8 v, bytes32 r, bytes32 s) public view override returns (bool) {
        address signer = _retrieveSignerFromToken(token, v, r, s);
        return _isActiveIssuer[signer];
    }

    function _domainSeparator() internal view returns (bytes32) {
        if (address(this) == _cachedThis && block.chainid == _cachedChainId) {
            return _cachedDomainSeparator;
        } else {
            return _buildDomainSeparator();
        }
    }

    function _buildDomainSeparator() private view returns (bytes32) {
        return
            hash(
                EIP712Domain({
                    name: "Ethereum Access Token",
                    version: "1",
                    chainId: block.chainid,
                    verifyingContract: address(this)
                })
            );
    }

    function _retrieveSignerFromToken(
        AccessToken calldata token,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal view returns (address) {
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), hash(token)));
        // HE -> Has Expired
        require(token.expiry > block.timestamp, "AccessToken: HE");

        if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
            // ISS -> Invalid Signature S
            revert("AccessToken: ISS");
        }

        if (v != 27 && v != 28) {
            // ISV -> Invalid Signature V
            revert("AccessToken: ISV");
        }

        // If the signature is valid (and not malleable), return the signer address
        address signer = ecrecover(digest, v, r, s);

        if (signer == address(0)) {
            // IS -> Invalid Signature
            revert("AccessToken: IS");
        }

        return signer;
    }
}

// File: contracts/interfaces/IAccessTokenVerifier.sol



// SPDX-License-Identifier: MIT
pragma solidity >=0.8.13;

struct EIP712Domain {
    string name;
    string version;
    uint256 chainId;
    address verifyingContract;
}

struct FunctionCall {
    bytes4 functionSignature;
    address target;
    address caller;
    bytes parameters;
}

struct AccessToken {
    uint256 expiry;
    FunctionCall functionCall;
}

interface IAccessTokenVerifier {
    function verify(AccessToken calldata token, uint8 v, bytes32 r, bytes32 s) external view returns (bool);
}

// File: contracts/KeyInfrastructure.sol



//SPDX-License-Identifier: MIT
pragma solidity >=0.8.13;

contract KeyInfrastructure {
    address internal immutable _root;
    address internal _intermediate;
    address[] internal _activeIssuers;
    mapping(address => bool) internal _isActiveIssuer;

    event IntermediateRotated(address newKey);
    event IssuerActivated(address activatedIssuer);
    event IssuerDeactivated(address deactivatedIssuer);

    modifier onlyRoot() {
        // MBR -> Must be root
        require(msg.sender == _root, "unauthorised: MBR");
        _;
    }

    modifier onlyIntermediate() {
        // MBI -> Must be intermediate
        require(msg.sender == _intermediate, "unauthorised: MBI");
        _;
    }

    constructor(address root) {
        _root = root;
    }

    function rotateIntermediate(address newIntermediate) public onlyRoot {
        _intermediate = newIntermediate;
        emit IntermediateRotated(newIntermediate);
    }

    function activateIssuers(address[] calldata newIssuers) public onlyIntermediate {
        for (uint256 i = 0; i < newIssuers.length; i++) {
            address newKey = newIssuers[i];
            if (!_isActiveIssuer[newKey]) {
                _addToActiveIssuers(newKey);
                emit IssuerActivated(newKey);
            }
        }
    }

    function deactivateIssuers(address[] calldata issuers) public onlyIntermediate {
        for (uint256 i = 0; i < issuers.length; i++) {
            address oldKey = issuers[i];
            if (_isActiveIssuer[oldKey]) {
                _removeFromActiveIssuers(oldKey);
                emit IssuerDeactivated(oldKey);
            }
        }
    }

    function getRootKey() public view returns (address) {
        return _root;
    }

    function getIntermediateKey() public view returns (address) {
        return _intermediate;
    }

    function getActiveIssuers() public view returns (address[] memory) {
        return _activeIssuers;
    }

    function isActiveIssuer(address addr) public view returns (bool) {
        return _isActiveIssuer[addr];
    }

    function _addToActiveIssuers(address addr) internal {
        _isActiveIssuer[addr] = true;
        _activeIssuers.push(addr);
    }

    function _removeFromActiveIssuers(address addr) internal {
        _isActiveIssuer[addr] = false;

        for (uint256 i = 0; i < _activeIssuers.length; i++) {
            address issuer = _activeIssuers[i];
            if (addr == issuer) {
                _activeIssuers[i] = _activeIssuers[_activeIssuers.length - 1];
                _activeIssuers.pop();
                break;
            }
        }
    }
}