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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/contracts/interfaces/IAny2EVMMessageReceiver.sol";

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

/**
 * @title CWMultiTokenBridgeL2
 * @notice Destination/public-chain bridge for minting and burning cW* assets without prefunding.
 * @dev Supports multiple canonical -> mirrored token pairs behind one bridge address.
 *      Outbound CCIP uses Chainlink Client.EVM2AnyMessage encoding (required by public routers).
 */
contract CWMultiTokenBridgeL2 is IAny2EVMMessageReceiver {
    using SafeERC20 for IERC20;

    IRouterClient public sendRouter;
    address public receiveRouter;
    address public feeToken;
    address public admin;
    uint256 public ccipGasLimit;

    struct DestinationConfig {
        address receiverBridge;
        bool enabled;
    }

    mapping(uint64 => DestinationConfig) public destinations;
    mapping(address => address) public canonicalToMirrored;
    mapping(address => address) public mirroredToCanonical;
    mapping(bytes32 => bool) public processed;
    mapping(address => bool) public tokenPairFrozen;
    mapping(uint64 => bool) public destinationFrozen;
    mapping(address => bool) public paused;
    mapping(address => uint256) public mintedTotal;
    mapping(address => uint256) public burnedTotal;

    event TokenPairConfigured(address indexed canonicalToken, address indexed mirroredToken);
    event DestinationConfigured(uint64 indexed chainSelector, address receiverBridge, bool enabled);
    event Minted(address indexed canonicalToken, address indexed mirroredToken, address indexed recipient, uint256 amount);
    event Burned(address indexed canonicalToken, address indexed mirroredToken, address indexed user, uint256 amount);
    event MessageSent(
        bytes32 indexed messageId,
        address indexed canonicalToken,
        address indexed mirroredToken,
        uint64 destinationChainSelector,
        address recipient,
        uint256 amount
    );
    event TokenPairFrozen(address indexed canonicalToken, address indexed mirroredToken);
    event DestinationFrozen(uint64 indexed chainSelector, address receiverBridge);
    event MirroredTokenPauseUpdated(address indexed mirroredToken, bool paused);
    event SupplyAccountingUpdated(
        address indexed canonicalToken,
        address indexed mirroredToken,
        uint256 mintedTotalAmount,
        uint256 burnedTotalAmount,
        uint256 liveSupply
    );
    event SendRouterUpdated(address indexed newRouter);
    event ReceiveRouterUpdated(address indexed newRouter);
    event FeeTokenUpdated(address indexed newFeeToken);
    event CcipGasLimitUpdated(uint256 newGasLimit);
    event AdminChanged(address indexed newAdmin);

    modifier onlyAdmin() {
        require(msg.sender == admin, "CWMultiTokenBridgeL2: only admin");
        _;
    }

    modifier onlyReceiveRouter() {
        require(msg.sender == receiveRouter, "CWMultiTokenBridgeL2: only receive router");
        _;
    }

    constructor(address _sendRouter, address _receiveRouter, address _feeToken) {
        require(_sendRouter != address(0), "CWMultiTokenBridgeL2: zero send router");
        require(_receiveRouter != address(0), "CWMultiTokenBridgeL2: zero receive router");
        sendRouter = IRouterClient(_sendRouter);
        receiveRouter = _receiveRouter;
        feeToken = _feeToken;
        admin = msg.sender;
        ccipGasLimit = 300_000;
    }

    function configureTokenPair(address canonicalToken, address mirroredToken) external onlyAdmin {
        require(canonicalToken != address(0), "CWMultiTokenBridgeL2: zero canonical");
        require(mirroredToken != address(0), "CWMultiTokenBridgeL2: zero mirrored");
        require(!tokenPairFrozen[canonicalToken], "CWMultiTokenBridgeL2: token pair frozen");

        address previousCanonical = mirroredToCanonical[mirroredToken];
        if (previousCanonical != address(0) && previousCanonical != canonicalToken) {
            require(!tokenPairFrozen[previousCanonical], "CWMultiTokenBridgeL2: token pair frozen");
            delete canonicalToMirrored[previousCanonical];
        }

        canonicalToMirrored[canonicalToken] = mirroredToken;
        mirroredToCanonical[mirroredToken] = canonicalToken;
        emit TokenPairConfigured(canonicalToken, mirroredToken);
    }

    function configureDestination(uint64 chainSelector, address receiverBridge, bool enabled) external onlyAdmin {
        require(receiverBridge != address(0), "CWMultiTokenBridgeL2: zero bridge");
        require(!destinationFrozen[chainSelector], "CWMultiTokenBridgeL2: destination frozen");
        destinations[chainSelector] = DestinationConfig({
            receiverBridge: receiverBridge,
            enabled: enabled
        });
        emit DestinationConfigured(chainSelector, receiverBridge, enabled);
    }

    function freezeTokenPair(address canonicalToken) external onlyAdmin {
        address mirroredToken = canonicalToMirrored[canonicalToken];
        require(mirroredToken != address(0), "CWMultiTokenBridgeL2: token not configured");
        tokenPairFrozen[canonicalToken] = true;
        emit TokenPairFrozen(canonicalToken, mirroredToken);
    }

    function freezeDestination(uint64 chainSelector) external onlyAdmin {
        address receiverBridge = destinations[chainSelector].receiverBridge;
        require(receiverBridge != address(0), "CWMultiTokenBridgeL2: destination not configured");
        destinationFrozen[chainSelector] = true;
        emit DestinationFrozen(chainSelector, receiverBridge);
    }

    function setTokenPaused(address mirroredToken, bool isPaused) external onlyAdmin {
        require(mirroredToCanonical[mirroredToken] != address(0), "CWMultiTokenBridgeL2: token not configured");
        paused[mirroredToken] = isPaused;
        emit MirroredTokenPauseUpdated(mirroredToken, isPaused);
    }

    function setSendRouter(address newRouter) external onlyAdmin {
        require(newRouter != address(0), "CWMultiTokenBridgeL2: zero router");
        sendRouter = IRouterClient(newRouter);
        emit SendRouterUpdated(newRouter);
    }

    function setReceiveRouter(address newRouter) external onlyAdmin {
        require(newRouter != address(0), "CWMultiTokenBridgeL2: zero router");
        receiveRouter = newRouter;
        emit ReceiveRouterUpdated(newRouter);
    }

    function setFeeToken(address newFeeToken) external onlyAdmin {
        feeToken = newFeeToken;
        emit FeeTokenUpdated(newFeeToken);
    }

    function setCcipGasLimit(uint256 newGasLimit) external onlyAdmin {
        require(newGasLimit > 0, "CWMultiTokenBridgeL2: zero gas limit");
        ccipGasLimit = newGasLimit;
        emit CcipGasLimitUpdated(newGasLimit);
    }

    function changeAdmin(address newAdmin) external onlyAdmin {
        require(newAdmin != address(0), "CWMultiTokenBridgeL2: zero admin");
        admin = newAdmin;
        emit AdminChanged(newAdmin);
    }

    function calculateFee(
        address mirroredToken,
        uint64 destinationChainSelector,
        address recipient,
        uint256 amount
    ) external view returns (uint256) {
        address canonicalToken = mirroredToCanonical[mirroredToken];
        require(canonicalToken != address(0), "CWMultiTokenBridgeL2: token not configured");
        DestinationConfig memory dest = destinations[destinationChainSelector];
        require(dest.enabled, "CWMultiTokenBridgeL2: destination disabled");
        return sendRouter.getFee(
            destinationChainSelector,
            _buildMessage(dest.receiverBridge, canonicalToken, recipient, amount)
        );
    }

    function circulatingSupply(address mirroredToken) external view returns (uint256) {
        uint256 totalMinted = mintedTotal[mirroredToken];
        uint256 totalBurned = burnedTotal[mirroredToken];
        return totalMinted > totalBurned ? totalMinted - totalBurned : 0;
    }

    function ccipReceive(Client.Any2EVMMessage calldata message) external onlyReceiveRouter {
        require(!processed[message.messageId], "CWMultiTokenBridgeL2: replayed");

        (address canonicalToken, address recipient, uint256 amount) =
            abi.decode(message.data, (address, address, uint256));
        require(recipient != address(0), "CWMultiTokenBridgeL2: zero recipient");
        require(amount > 0, "CWMultiTokenBridgeL2: zero amount");

        address mirroredToken = canonicalToMirrored[canonicalToken];
        require(mirroredToken != address(0), "CWMultiTokenBridgeL2: token not configured");
        require(!paused[mirroredToken], "CWMultiTokenBridgeL2: token paused");

        DestinationConfig memory peer = destinations[message.sourceChainSelector];
        require(peer.enabled, "CWMultiTokenBridgeL2: peer disabled");
        require(_decodeSender(message.sender) == peer.receiverBridge, "CWMultiTokenBridgeL2: peer mismatch");

        processed[message.messageId] = true;
        ICWMintBurnToken(mirroredToken).mint(recipient, amount);
        mintedTotal[mirroredToken] += amount;
        emit Minted(canonicalToken, mirroredToken, recipient, amount);
        emit SupplyAccountingUpdated(
            canonicalToken,
            mirroredToken,
            mintedTotal[mirroredToken],
            burnedTotal[mirroredToken],
            IERC20(mirroredToken).totalSupply()
        );
    }

    function burnAndSend(
        address mirroredToken,
        uint64 destinationChainSelector,
        address recipient,
        uint256 amount
    ) external payable returns (bytes32 messageId) {
        require(recipient != address(0), "CWMultiTokenBridgeL2: zero recipient");
        require(amount > 0, "CWMultiTokenBridgeL2: zero amount");

        address canonicalToken = mirroredToCanonical[mirroredToken];
        require(canonicalToken != address(0), "CWMultiTokenBridgeL2: token not configured");
        require(!paused[mirroredToken], "CWMultiTokenBridgeL2: token paused");

        DestinationConfig memory dest = destinations[destinationChainSelector];
        require(dest.enabled, "CWMultiTokenBridgeL2: destination disabled");

        ICWMintBurnToken(mirroredToken).burnFrom(msg.sender, amount);
        burnedTotal[mirroredToken] += amount;
        emit Burned(canonicalToken, mirroredToken, msg.sender, amount);
        emit SupplyAccountingUpdated(
            canonicalToken,
            mirroredToken,
            mintedTotal[mirroredToken],
            burnedTotal[mirroredToken],
            IERC20(mirroredToken).totalSupply()
        );

        Client.EVM2AnyMessage memory message =
            _buildMessage(dest.receiverBridge, canonicalToken, recipient, amount);
        uint256 fee = sendRouter.getFee(destinationChainSelector, message);

        _collectAndApproveFee(fee);

        messageId = feeToken == address(0)
            ? sendRouter.ccipSend{value: fee}(destinationChainSelector, message)
            : sendRouter.ccipSend(destinationChainSelector, message);

        emit MessageSent(messageId, canonicalToken, mirroredToken, destinationChainSelector, recipient, amount);
        _refundNativeExcess(fee);
        return messageId;
    }

    function withdrawToken(address token, address to, uint256 amount) external onlyAdmin {
        IERC20(token).safeTransfer(to, amount);
    }

    function withdrawNative(address payable to, uint256 amount) external onlyAdmin {
        (bool ok, ) = to.call{value: amount}("");
        require(ok, "CWMultiTokenBridgeL2: native transfer failed");
    }

    receive() external payable {}

    function _buildMessage(
        address receiverBridge,
        address canonicalToken,
        address recipient,
        uint256 amount
    ) internal view returns (Client.EVM2AnyMessage memory message) {
        message = Client.EVM2AnyMessage({
            receiver: abi.encode(receiverBridge),
            data: abi.encode(canonicalToken, recipient, amount),
            tokenAmounts: new Client.EVMTokenAmount[](0),
            feeToken: feeToken,
            extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: ccipGasLimit}))
        });
    }

    function _collectAndApproveFee(uint256 fee) internal {
        if (fee == 0) {
            return;
        }

        if (feeToken == address(0)) {
            require(msg.value >= fee, "CWMultiTokenBridgeL2: insufficient native fee");
            return;
        }

        IERC20 feeErc20 = IERC20(feeToken);
        feeErc20.safeTransferFrom(msg.sender, address(this), fee);
        feeErc20.forceApprove(address(sendRouter), fee);
    }

    function _refundNativeExcess(uint256 feeUsed) internal {
        if (feeToken != address(0) || msg.value <= feeUsed) {
            return;
        }

        uint256 refund = msg.value - feeUsed;
        (bool ok, ) = payable(msg.sender).call{value: refund}("");
        require(ok, "CWMultiTokenBridgeL2: refund failed");
    }

    function _decodeSender(bytes memory senderData) internal pure returns (address senderAddress) {
        if (senderData.length == 0) {
            return address(0);
        }
        senderAddress = abi.decode(senderData, (address));
    }
}
