// 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 "../ccip/IRouterClient.sol";
import "./integration/ICWReserveVerifier.sol";

/**
 * @title CWMultiTokenBridgeL1
 * @notice Chain 138 side escrow bridge for non-prefunded c* -> cW* routing.
 * @dev Locks canonical c* tokens on send and releases them on return messages.
 */
contract CWMultiTokenBridgeL1 {
    using SafeERC20 for IERC20;

    IRouterClient public sendRouter;
    address public receiveRouter;
    address public feeToken;
    address public admin;
    address public reserveVerifier;

    struct DestinationConfig {
        address receiverBridge;
        bool enabled;
    }

    mapping(address => mapping(uint64 => DestinationConfig)) public destinations;
    mapping(bytes32 => bool) public processed;
    mapping(address => bool) public supportedCanonicalToken;
    mapping(address => bool) public paused;
    mapping(address => uint256) public lockedBalance;
    mapping(address => uint256) public totalOutstanding;
    mapping(address => mapping(uint64 => uint256)) public outstandingMinted;
    mapping(address => mapping(uint64 => uint256)) public maxOutstanding;

    event Locked(address indexed canonicalToken, address indexed user, uint256 amount);
    event Released(address indexed canonicalToken, address indexed recipient, uint256 amount);
    event MessageSent(
        bytes32 indexed messageId,
        address indexed canonicalToken,
        uint64 indexed destinationChainSelector,
        address recipient,
        uint256 amount
    );
    event DestinationConfigured(address indexed canonicalToken, uint64 indexed chainSelector, address receiverBridge, bool enabled);
    event SupportedCanonicalTokenConfigured(address indexed canonicalToken, bool enabled);
    event TokenPauseUpdated(address indexed canonicalToken, bool paused);
    event MaxOutstandingUpdated(address indexed canonicalToken, uint64 indexed chainSelector, uint256 maxOutstandingAmount);
    event OutstandingUpdated(
        address indexed canonicalToken,
        uint64 indexed chainSelector,
        uint256 chainOutstanding,
        uint256 totalOutstandingAmount,
        uint256 lockedBalanceAmount
    );
    event SendRouterUpdated(address indexed newRouter);
    event ReceiveRouterUpdated(address indexed newRouter);
    event FeeTokenUpdated(address indexed newFeeToken);
    event AdminChanged(address indexed newAdmin);
    event ReserveVerifierUpdated(address indexed newVerifier);

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

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

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

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

    function configureSupportedCanonicalToken(address canonicalToken, bool enabled) external onlyAdmin {
        require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
        if (!enabled) {
            require(lockedBalance[canonicalToken] == 0, "CWMultiTokenBridgeL1: token still locked");
            require(totalOutstanding[canonicalToken] == 0, "CWMultiTokenBridgeL1: token still outstanding");
        }
        supportedCanonicalToken[canonicalToken] = enabled;
        emit SupportedCanonicalTokenConfigured(canonicalToken, enabled);
    }

    function setTokenPaused(address canonicalToken, bool isPaused) external onlyAdmin {
        require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
        paused[canonicalToken] = isPaused;
        emit TokenPauseUpdated(canonicalToken, isPaused);
    }

    function setMaxOutstanding(address canonicalToken, uint64 chainSelector, uint256 amount) external onlyAdmin {
        require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
        maxOutstanding[canonicalToken][chainSelector] = amount;
        emit MaxOutstandingUpdated(canonicalToken, chainSelector, amount);
    }

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

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

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

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

    function setReserveVerifier(address newVerifier) external onlyAdmin {
        reserveVerifier = newVerifier;
        emit ReserveVerifierUpdated(newVerifier);
    }

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

    function availableToMint(
        address canonicalToken,
        uint64 destinationChainSelector
    ) public view returns (uint256) {
        return _availableToMint(
            canonicalToken,
            destinationChainSelector,
            lockedBalance[canonicalToken],
            totalOutstanding[canonicalToken],
            outstandingMinted[canonicalToken][destinationChainSelector]
        );
    }

    function lockAndSend(
        address canonicalToken,
        uint64 destinationChainSelector,
        address recipient,
        uint256 amount
    ) external payable returns (bytes32 messageId) {
        require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
        require(recipient != address(0), "CWMultiTokenBridgeL1: zero recipient");
        require(amount > 0, "CWMultiTokenBridgeL1: zero amount");
        require(supportedCanonicalToken[canonicalToken], "CWMultiTokenBridgeL1: unsupported token");
        require(!paused[canonicalToken], "CWMultiTokenBridgeL1: token paused");

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

        IERC20(canonicalToken).safeTransferFrom(msg.sender, address(this), amount);
        uint256 nextLockedBalance = lockedBalance[canonicalToken] + amount;
        uint256 currentTotalOutstanding = totalOutstanding[canonicalToken];
        uint256 currentChainOutstanding = outstandingMinted[canonicalToken][destinationChainSelector];
        require(
            amount <= _availableToMint(
                canonicalToken,
                destinationChainSelector,
                nextLockedBalance,
                currentTotalOutstanding,
                currentChainOutstanding
            ),
            "CWMultiTokenBridgeL1: exceeds escrow capacity"
        );

        lockedBalance[canonicalToken] = nextLockedBalance;
        totalOutstanding[canonicalToken] = currentTotalOutstanding + amount;
        outstandingMinted[canonicalToken][destinationChainSelector] = currentChainOutstanding + amount;

        if (reserveVerifier != address(0) && !ICWReserveVerifier(reserveVerifier).verifyLock(canonicalToken, destinationChainSelector, amount)) {
            revert("CWMultiTokenBridgeL1: reserve verification failed");
        }

        emit Locked(canonicalToken, msg.sender, amount);
        emit OutstandingUpdated(
            canonicalToken,
            destinationChainSelector,
            outstandingMinted[canonicalToken][destinationChainSelector],
            totalOutstanding[canonicalToken],
            lockedBalance[canonicalToken]
        );

        IRouterClient.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, destinationChainSelector, recipient, amount);
        _refundNativeExcess(fee);
        return messageId;
    }

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

        (address canonicalToken, address recipient, uint256 amount) =
            abi.decode(message.data, (address, address, uint256));
        require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
        require(recipient != address(0), "CWMultiTokenBridgeL1: zero recipient");
        require(amount > 0, "CWMultiTokenBridgeL1: zero amount");
        require(supportedCanonicalToken[canonicalToken], "CWMultiTokenBridgeL1: unsupported token");
        require(!paused[canonicalToken], "CWMultiTokenBridgeL1: token paused");

        DestinationConfig memory peer = destinations[canonicalToken][message.sourceChainSelector];
        require(peer.enabled, "CWMultiTokenBridgeL1: peer disabled");
        require(_decodeSender(message.sender) == peer.receiverBridge, "CWMultiTokenBridgeL1: peer mismatch");
        require(outstandingMinted[canonicalToken][message.sourceChainSelector] >= amount, "CWMultiTokenBridgeL1: outstanding underflow");
        require(totalOutstanding[canonicalToken] >= amount, "CWMultiTokenBridgeL1: total outstanding underflow");
        require(lockedBalance[canonicalToken] >= amount, "CWMultiTokenBridgeL1: locked underflow");

        processed[message.messageId] = true;
        outstandingMinted[canonicalToken][message.sourceChainSelector] -= amount;
        totalOutstanding[canonicalToken] -= amount;
        lockedBalance[canonicalToken] -= amount;

        IERC20(canonicalToken).safeTransfer(recipient, amount);
        emit Released(canonicalToken, recipient, amount);
        emit OutstandingUpdated(
            canonicalToken,
            message.sourceChainSelector,
            outstandingMinted[canonicalToken][message.sourceChainSelector],
            totalOutstanding[canonicalToken],
            lockedBalance[canonicalToken]
        );
    }

    function withdrawToken(address token, address to, uint256 amount) external onlyAdmin {
        if (supportedCanonicalToken[token] || lockedBalance[token] > 0) {
            uint256 currentBalance = IERC20(token).balanceOf(address(this));
            require(currentBalance >= lockedBalance[token], "CWMultiTokenBridgeL1: escrow invariant broken");
            require(amount <= currentBalance - lockedBalance[token], "CWMultiTokenBridgeL1: amount locked");
        }
        IERC20(token).safeTransfer(to, amount);
    }

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

    receive() external payable {}

    function _buildMessage(
        address receiverBridge,
        address canonicalToken,
        address recipient,
        uint256 amount
    ) internal view returns (IRouterClient.EVM2AnyMessage memory message) {
        message = IRouterClient.EVM2AnyMessage({
            receiver: abi.encode(receiverBridge),
            data: abi.encode(canonicalToken, recipient, amount),
            tokenAmounts: new IRouterClient.TokenAmount[](0),
            feeToken: feeToken,
            extraArgs: ""
        });
    }

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

        if (feeToken == address(0)) {
            require(msg.value >= fee, "CWMultiTokenBridgeL1: 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, "CWMultiTokenBridgeL1: refund failed");
    }

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

    function _availableToMint(
        address canonicalToken,
        uint64 destinationChainSelector,
        uint256 lockedBalanceAmount,
        uint256 totalOutstandingAmount,
        uint256 chainOutstanding
    ) internal view returns (uint256) {
        if (!supportedCanonicalToken[canonicalToken]) {
            return 0;
        }

        uint256 globalAvailable = lockedBalanceAmount > totalOutstandingAmount
            ? lockedBalanceAmount - totalOutstandingAmount
            : 0;

        uint256 chainCap = maxOutstanding[canonicalToken][destinationChainSelector];
        if (chainCap == 0) {
            return globalAvailable;
        }

        if (chainOutstanding >= chainCap) {
            return 0;
        }

        uint256 chainAvailable = chainCap - chainOutstanding;
        return globalAvailable < chainAvailable ? globalAvailable : chainAvailable;
    }
}
