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

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

interface ICWMintBurnMetadata is IERC20Metadata {
    function mint(address to, uint256 amount) external;
    function burnFrom(address from, uint256 amount) external;
}

/**
 * @title USDWPublicWrapVault
 * @notice Locks native public-chain USDW and mints the shared cWUSDW mirror 1:1 after decimals normalization.
 * @dev The same cWUSDW supply can be minted by both this vault and the GRU bridge. This contract deliberately
 *      does not expose an admin path to withdraw the underlying USDW reserve because that requires a fuller
 *      liability model across wrap-originated and bridge-originated cWUSDW supply.
 */
contract USDWPublicWrapVault is AccessControl, Pausable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    bytes32 public constant RESERVE_OPERATOR_ROLE = keccak256("RESERVE_OPERATOR_ROLE");
    bytes32 public constant EMERGENCY_ADMIN_ROLE = keccak256("EMERGENCY_ADMIN_ROLE");

    IERC20 public immutable nativeUsdw;
    ICWMintBurnMetadata public immutable wrappedUsdw;
    uint8 public immutable nativeDecimals;
    uint8 public immutable wrappedDecimals;

    event LiquiditySeeded(address indexed operator, uint256 nativeAmount, uint256 wrappedEquivalent);
    event Wrapped(
        address indexed caller,
        address indexed recipient,
        uint256 nativeAmount,
        uint256 wrappedAmount
    );
    event Unwrapped(
        address indexed caller,
        address indexed recipient,
        uint256 wrappedAmount,
        uint256 nativeAmount
    );
    event NonUnderlyingTokenRecovered(address indexed token, address indexed recipient, uint256 amount);

    error InsufficientUnderlyingLiquidity(uint256 requested, uint256 available);
    error NonCanonicalAmount(uint256 amount);
    error UnderlyingTokenProtected();
    error UnsupportedDecimals(uint8 nativeDecimals, uint8 wrappedDecimals);
    error ZeroAddress();
    error ZeroAmount();
    error ZeroRecipient();

    constructor(address admin, address nativeUsdw_, address wrappedUsdw_) {
        if (admin == address(0) || nativeUsdw_ == address(0) || wrappedUsdw_ == address(0)) revert ZeroAddress();

        nativeUsdw = IERC20(nativeUsdw_);
        wrappedUsdw = ICWMintBurnMetadata(wrappedUsdw_);
        nativeDecimals = IERC20Metadata(nativeUsdw_).decimals();
        wrappedDecimals = IERC20Metadata(wrappedUsdw_).decimals();

        uint8 diff = nativeDecimals > wrappedDecimals
            ? nativeDecimals - wrappedDecimals
            : wrappedDecimals - nativeDecimals;
        if (diff > 77) revert UnsupportedDecimals(nativeDecimals, wrappedDecimals);

        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        _grantRole(RESERVE_OPERATOR_ROLE, admin);
        _grantRole(EMERGENCY_ADMIN_ROLE, admin);
    }

    function seedLiquidity(uint256 nativeAmount) external onlyRole(RESERVE_OPERATOR_ROLE) nonReentrant {
        if (nativeAmount == 0) revert ZeroAmount();

        nativeUsdw.safeTransferFrom(msg.sender, address(this), nativeAmount);
        emit LiquiditySeeded(msg.sender, nativeAmount, _toWrappedFloor(nativeAmount));
    }

    function wrap(uint256 nativeAmount, address recipient) external whenNotPaused nonReentrant returns (uint256 wrappedAmount) {
        if (nativeAmount == 0) revert ZeroAmount();
        if (recipient == address(0)) revert ZeroRecipient();

        wrappedAmount = _toWrappedExact(nativeAmount);
        nativeUsdw.safeTransferFrom(msg.sender, address(this), nativeAmount);
        wrappedUsdw.mint(recipient, wrappedAmount);

        emit Wrapped(msg.sender, recipient, nativeAmount, wrappedAmount);
    }

    function unwrap(uint256 wrappedAmount, address recipient) external whenNotPaused nonReentrant returns (uint256 nativeAmount) {
        if (wrappedAmount == 0) revert ZeroAmount();
        if (recipient == address(0)) revert ZeroRecipient();

        nativeAmount = _toNativeExact(wrappedAmount);
        uint256 available = nativeUsdw.balanceOf(address(this));
        if (available < nativeAmount) revert InsufficientUnderlyingLiquidity(nativeAmount, available);

        wrappedUsdw.burnFrom(msg.sender, wrappedAmount);
        nativeUsdw.safeTransfer(recipient, nativeAmount);

        emit Unwrapped(msg.sender, recipient, wrappedAmount, nativeAmount);
    }

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

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

    function recoverNonUnderlyingToken(
        address token,
        address recipient,
        uint256 amount
    ) external onlyRole(EMERGENCY_ADMIN_ROLE) nonReentrant {
        if (token == address(nativeUsdw)) revert UnderlyingTokenProtected();
        if (recipient == address(0)) revert ZeroRecipient();
        IERC20(token).safeTransfer(recipient, amount);
        emit NonUnderlyingTokenRecovered(token, recipient, amount);
    }

    function availableUnderlyingLiquidity() external view returns (uint256) {
        return nativeUsdw.balanceOf(address(this));
    }

    function availableWrappedLiquidity() external view returns (uint256) {
        return _toWrappedFloor(nativeUsdw.balanceOf(address(this)));
    }

    function wrappedSupply() external view returns (uint256) {
        return wrappedUsdw.totalSupply();
    }

    function liquidityCoverageBps() external view returns (uint256) {
        uint256 supply = wrappedUsdw.totalSupply();
        if (supply == 0) {
            return 0;
        }
        return (_toWrappedFloor(nativeUsdw.balanceOf(address(this))) * 10_000) / supply;
    }

    function previewWrap(uint256 nativeAmount) external view returns (uint256) {
        if (nativeAmount == 0) revert ZeroAmount();
        return _toWrappedExact(nativeAmount);
    }

    function previewUnwrap(uint256 wrappedAmount) external view returns (uint256) {
        if (wrappedAmount == 0) revert ZeroAmount();
        return _toNativeExact(wrappedAmount);
    }

    function _toWrappedExact(uint256 nativeAmount) internal view returns (uint256) {
        if (nativeDecimals == wrappedDecimals) {
            return nativeAmount;
        }
        if (nativeDecimals > wrappedDecimals) {
            uint256 divisor = 10 ** (nativeDecimals - wrappedDecimals);
            if (nativeAmount % divisor != 0) revert NonCanonicalAmount(nativeAmount);
            return nativeAmount / divisor;
        }
        return nativeAmount * (10 ** (wrappedDecimals - nativeDecimals));
    }

    function _toNativeExact(uint256 wrappedAmount) internal view returns (uint256) {
        if (nativeDecimals == wrappedDecimals) {
            return wrappedAmount;
        }
        if (wrappedDecimals > nativeDecimals) {
            uint256 divisor = 10 ** (wrappedDecimals - nativeDecimals);
            if (wrappedAmount % divisor != 0) revert NonCanonicalAmount(wrappedAmount);
            return wrappedAmount / divisor;
        }
        return wrappedAmount * (10 ** (nativeDecimals - wrappedDecimals));
    }

    function _toWrappedFloor(uint256 nativeAmount) internal view returns (uint256) {
        if (nativeDecimals == wrappedDecimals) {
            return nativeAmount;
        }
        if (nativeDecimals > wrappedDecimals) {
            return nativeAmount / (10 ** (nativeDecimals - wrappedDecimals));
        }
        return nativeAmount * (10 ** (wrappedDecimals - nativeDecimals));
    }
}
