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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "../compliance/LegallyCompliantBase.sol";
import "./IRWAToken.sol";
import "./RWATokenInterfaces.sol";
import "./RWAEIP712.sol";
import "./libraries/RWATaxonomy.sol";
import "./IGruMonetaryPolicyGate.sol";

/**
 * @title RWAToken
 * @notice ERC-20 representing an M00 GRU liquidity index (LiXAU, LiPMG, LiBMG*).
 * @dev Not CompliantFiatToken / c* eMoney. Supply and indexValue are governed off-chain policy;
 *      on-chain indexValue is an attested level (6 decimals) for integrations and oracles.
 */
contract RWAToken is ERC20, Pausable, Ownable, LegallyCompliantBase, IRWAToken {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant INDEX_PUBLISHER_ROLE = keccak256("INDEX_PUBLISHER_ROLE");
    string public constant EIP712_VERSION = "1";

    uint8 public constant INDEX_VALUE_DECIMALS = 6;

    uint8 private immutable _decimalsStorage;
    bytes32 private immutable _methodologyDocumentHash;
    string private _indexTicker;
    Classification private _classification;
    uint256 private _indexValue;
    uint256 private _indexUpdatedAt;
    address private _policyGate;

    event PolicyGateUpdated(address indexed previousGate, address indexed newGate);

    constructor(
        string memory name_,
        string memory symbol_,
        uint8 decimals_,
        string memory indexTicker_,
        string memory assetClass_,
        string memory assetGroup_,
        string memory instrumentType_,
        string memory underlyingAsset_,
        string memory gruLayer_,
        address initialOwner,
        address complianceAdmin,
        address indexPublisher,
        uint256 initialIndexValue_,
        uint256 initialSupply_,
        bytes32 methodologyDocumentHash_
    ) ERC20(name_, symbol_) Ownable(initialOwner) LegallyCompliantBase(complianceAdmin) {
        require(decimals_ <= 18, "RWAToken: decimals");
        _decimalsStorage = decimals_;
        require(methodologyDocumentHash_ != bytes32(0), "RWAToken: methodology hash");
        _methodologyDocumentHash = methodologyDocumentHash_;
        bytes32 tickerHash = RWATaxonomy.validateProduct(indexTicker_, assetClass_, gruLayer_);

        _indexTicker = indexTicker_;
        _classification = Classification({
            indexTicker: tickerHash,
            assetClass: RWATaxonomy.hashString(assetClass_),
            assetGroup: RWATaxonomy.hashString(assetGroup_),
            instrumentType: RWATaxonomy.hashString(instrumentType_),
            underlyingAsset: RWATaxonomy.hashString(underlyingAsset_),
            gruLayer: RWATaxonomy.hashString(gruLayer_)
        });

        _grantRole(MINTER_ROLE, initialOwner);
        _grantRole(INDEX_PUBLISHER_ROLE, indexPublisher);

        _setIndexValue(initialIndexValue_);

        if (initialSupply_ > 0) {
            _mint(msg.sender, initialSupply_);
        }
    }

    function decimals() public view override(ERC20) returns (uint8) {
        return _decimalsStorage;
    }

    function indexTicker() external view override returns (string memory) {
        return _indexTicker;
    }

    function classification() external view override returns (Classification memory) {
        return _classification;
    }

    function indexValue() external view override returns (uint256) {
        return _indexValue;
    }

    function indexValueDecimals() external pure override returns (uint8) {
        return INDEX_VALUE_DECIMALS;
    }

    function indexUpdatedAt() external view override returns (uint256) {
        return _indexUpdatedAt;
    }

    function methodologyDocumentHash() external view override returns (bytes32) {
        return _methodologyDocumentHash;
    }

    function isRwaIndex() external pure override returns (bool) {
        return true;
    }

    function isEmoney() external pure override returns (bool) {
        return false;
    }

    function eip20Version() external pure returns (string memory) {
        return "1.0.0";
    }

    /// @notice EIP-712 domain for off-chain `IndexValueAttestation` signatures (publisher role).
    function eip712DomainSeparator() external view returns (bytes32) {
        return _eip712Domain();
    }

    function hashIndexValueAttestation(
        string calldata ticker,
        uint256 newValue,
        uint256 nonce,
        uint256 deadline
    ) external view returns (bytes32) {
        return RWAEIP712.hashIndexAttestation(_eip712Domain(), ticker, newValue, nonce, deadline);
    }

    function _eip712Domain() internal view returns (bytes32) {
        return RWAEIP712.domainSeparator(name(), EIP712_VERSION, block.chainid, address(this));
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(AccessControl)
        returns (bool)
    {
        return interfaceId == type(IRWAToken).interfaceId
            || interfaceId == type(IRWAToken165).interfaceId
            || interfaceId == type(IERC20).interfaceId
            || interfaceId == type(IERC20Metadata).interfaceId
            || super.supportsInterface(interfaceId);
    }

    function policyGate() external view returns (address) {
        return _policyGate;
    }

    function setPolicyGate(address gate) external onlyOwner {
        emit PolicyGateUpdated(_policyGate, gate);
        _policyGate = gate;
    }

    function updateIndexValue(uint256 newValue) external override onlyRole(INDEX_PUBLISHER_ROLE) {
        uint256 old = _indexValue;
        _setIndexValue(newValue);
        emit IndexValueUpdated(old, newValue, msg.sender);
    }

    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
        if (_policyGate != address(0)) {
            require(IGruMonetaryPolicyGate(_policyGate).canMint(address(this), amount), "RWAToken: policy gate");
        }
        _mint(to, amount);
    }

    function burn(uint256 amount) external {
        _burn(msg.sender, amount);
    }

    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    function _setIndexValue(uint256 newValue) internal {
        _indexValue = newValue;
        _indexUpdatedAt = block.timestamp;
    }

    function _update(address from, address to, uint256 amount) internal override whenNotPaused {
        super._update(from, to, amount);
        if (from != address(0) && to != address(0)) {
            bytes32 legalRefHash = _generateLegalReferenceHash(
                from,
                to,
                amount,
                abi.encodePacked(symbol(), " RWA Index Transfer")
            );
            emit ValueTransferDeclared(from, to, amount, legalRefHash);
        }
    }
}
