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

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./IDBIS_EIP712Helper.sol";
import "./DBIS_RootRegistry.sol";
import "./DBIS_SignerRegistry.sol";
import "./StablecoinReferenceRegistry.sol";

interface IBlocklist {
    function isBlocked(address account) external view returns (bool);
}

struct SwapAuth {
    bytes32 messageId;
    bytes32 lpaId;
    bytes32 venue;
    address tokenIn;
    address tokenOut;
    uint256 amountIn;
    uint256 minAmountOut;
    uint256 deadline;
    bytes32 quoteHash;
    address quoteIssuer;
    uint256 chainId;
    address verifyingContract;
}

contract DBIS_ConversionRouter is AccessControl, Pausable, ReentrancyGuard {
    bytes32 public constant ROUTER_ADMIN_ROLE = keccak256("ROUTER_ADMIN");
    uint256 public constant CHAIN_ID = 138;

    bytes32 public constant SIGNER_REGISTRY_KEY = keccak256("SignerRegistry");
    bytes32 public constant STABLECOIN_REGISTRY_KEY = keccak256("StablecoinReferenceRegistry");

    bytes32 private constant EIP712_DOMAIN_TYPEHASH = keccak256(
        "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
    );
    bytes32 private constant SWAPAUTH_TYPEHASH = keccak256(
        "SwapAuth(bytes32 messageId,bytes32 lpaId,bytes32 venue,address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,uint256 deadline,bytes32 quoteHash,address quoteIssuer,uint256 chainId,address verifyingContract)"
    );

    DBIS_RootRegistry public rootRegistry;
    address public eip712Helper;
    mapping(bytes32 => bool) public usedSwapMessageIds;
    mapping(bytes32 => bool) public venueAllowlist;
    mapping(address => bool) public quoteIssuerAllowlist;
    address public blocklistContract;

    event ConversionExecuted(
        bytes32 indexed messageId,
        bytes32 indexed quoteHash,
        bytes32 venue,
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 amountOut,
        address quoteIssuer
    );

    constructor(address admin, address _rootRegistry, address _eip712Helper) {
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        _grantRole(ROUTER_ADMIN_ROLE, admin);
        rootRegistry = DBIS_RootRegistry(_rootRegistry);
        eip712Helper = _eip712Helper;
    }

    function addVenue(bytes32 venue) external onlyRole(ROUTER_ADMIN_ROLE) {
        venueAllowlist[venue] = true;
    }

    function removeVenue(bytes32 venue) external onlyRole(ROUTER_ADMIN_ROLE) {
        venueAllowlist[venue] = false;
    }

    function addQuoteIssuer(address issuer) external onlyRole(ROUTER_ADMIN_ROLE) {
        quoteIssuerAllowlist[issuer] = true;
    }

    function removeQuoteIssuer(address issuer) external onlyRole(ROUTER_ADMIN_ROLE) {
        quoteIssuerAllowlist[issuer] = false;
    }

    function setBlocklist(address _blocklist) external onlyRole(ROUTER_ADMIN_ROLE) {
        blocklistContract = _blocklist;
    }

    function pause() external onlyRole(ROUTER_ADMIN_ROLE) {
        _pause();
    }

    function unpause() external onlyRole(ROUTER_ADMIN_ROLE) {
        _unpause();
    }

    function _domainSeparator() private view returns (bytes32) {
        return keccak256(
            abi.encode(
                EIP712_DOMAIN_TYPEHASH,
                keccak256("DBISConversionRouter"),
                keccak256("1"),
                CHAIN_ID,
                address(this)
            )
        );
    }

    function getSwapAuthDigest(SwapAuth calldata auth) external view returns (bytes32) {
        return IDBIS_EIP712Helper(eip712Helper).getSwapAuthDigest(
            _domainSeparator(),
            SWAPAUTH_TYPEHASH,
            auth.messageId,
            auth.lpaId,
            auth.venue,
            auth.tokenIn,
            auth.tokenOut,
            auth.amountIn,
            auth.minAmountOut,
            auth.deadline,
            auth.quoteHash,
            auth.quoteIssuer,
            auth.chainId,
            auth.verifyingContract
        );
    }

    function submitSwapAuth(SwapAuth calldata auth, bytes[] calldata signatures, uint256 amountOut) external nonReentrant whenNotPaused {
        require(auth.chainId == CHAIN_ID, "DBIS: wrong chain");
        require(auth.verifyingContract == address(this), "DBIS: wrong contract");
        require(block.timestamp <= auth.deadline, "DBIS: expired");
        require(!usedSwapMessageIds[auth.messageId], "DBIS: replay");
        require(venueAllowlist[auth.venue], "DBIS: venue not allowed");
        require(quoteIssuerAllowlist[auth.quoteIssuer], "DBIS: quote issuer not allowed");
        require(amountOut >= auth.minAmountOut, "DBIS: slippage");
        _requireStablecoinActive(auth.tokenOut);
        _requireNotBlocked();
        bytes32 digest = IDBIS_EIP712Helper(eip712Helper).getSwapAuthDigest(
            _domainSeparator(),
            SWAPAUTH_TYPEHASH,
            auth.messageId,
            auth.lpaId,
            auth.venue,
            auth.tokenIn,
            auth.tokenOut,
            auth.amountIn,
            auth.minAmountOut,
            auth.deadline,
            auth.quoteHash,
            auth.quoteIssuer,
            auth.chainId,
            auth.verifyingContract
        );
        address[] memory signers = IDBIS_EIP712Helper(eip712Helper).recoverSigners(digest, signatures);
        require(!DBIS_SignerRegistry(rootRegistry.getComponent(SIGNER_REGISTRY_KEY)).hasDuplicateSigners(signers), "DBIS: duplicate signer");
        require(DBIS_SignerRegistry(rootRegistry.getComponent(SIGNER_REGISTRY_KEY)).areSignersActiveAtBlock(signers, block.number), "DBIS: signer not active");
        (bool ok, ) = DBIS_SignerRegistry(rootRegistry.getComponent(SIGNER_REGISTRY_KEY)).validateSignersForSwap(signers, auth.amountIn);
        require(ok, "DBIS: quorum not met");
        usedSwapMessageIds[auth.messageId] = true;
        emit ConversionExecuted(
            auth.messageId,
            auth.quoteHash,
            auth.venue,
            auth.tokenIn,
            auth.tokenOut,
            auth.amountIn,
            amountOut,
            auth.quoteIssuer
        );
    }

    function _requireStablecoinActive(address tokenOut) private view {
        StablecoinReferenceRegistry stableReg = StablecoinReferenceRegistry(rootRegistry.getComponent(STABLECOIN_REGISTRY_KEY));
        if (address(stableReg) != address(0)) require(stableReg.isActive(tokenOut), "DBIS: tokenOut not active");
    }

    function _requireNotBlocked() private view {
        if (blocklistContract != address(0)) require(!IBlocklist(blocklistContract).isBlocked(msg.sender), "DBIS: blocked");
    }

}
