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

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

/**
 * @title Optimized CCIP Router
 * @notice Optimized version with message batching and fee caching
 * @dev Performance optimizations for CCIP message handling
 */
contract CCIPRouterOptimized is IRouterClient {
    using SafeERC20 for IERC20;

    address public admin;
    uint256 public baseFee = 1 ether;
    uint256 public dataFeePerByte = 1000;
    uint256 public tokenFeePerToken = 1 ether;

    mapping(uint64 => address[]) public supportedTokens;
    
    // Fee caching
    mapping(bytes32 => uint256) public cachedFees;
    uint256 public cacheExpiry = 1 hours;
    mapping(bytes32 => uint256) public cacheTimestamp;

    // Message batching
    struct BatchedMessage {
        bytes32[] messageIds;
        uint64 destinationChainSelector;
        uint256 totalFee;
        uint256 timestamp;
    }
    
    mapping(uint256 => BatchedMessage) public batches;
    uint256 public batchId;
    uint256 public batchWindow = 5 minutes;
    uint256 public maxBatchSize = 100;

    event RouterAdminChanged(address indexed oldAdmin, address indexed newAdmin);
    event BaseFeeUpdated(uint256 oldFee, uint256 newFee);
    event MessageBatched(uint256 indexed batchId, uint256 messageCount);
    event FeeCached(bytes32 indexed cacheKey, uint256 fee);

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

    constructor() {
        admin = msg.sender;
    }

    /**
     * @inheritdoc IRouterClient
     */
    function ccipSend(
        uint64 destinationChainSelector,
        EVM2AnyMessage memory message
    ) external payable returns (bytes32 messageId, uint256 fees) {
        fees = getFee(destinationChainSelector, message);
        
        // Handle fee payment
        if (fees > 0) {
            if (message.feeToken == address(0)) {
                // Native token (ETH) fees
                require(msg.value >= fees, "CCIPRouterOptimized: insufficient native token fee");
            } else {
                // ERC20 token fees
                IERC20(message.feeToken).safeTransferFrom(msg.sender, address(this), fees);
            }
        }

        messageId = keccak256(abi.encodePacked(block.timestamp, block.number, message.data));

        emit MessageSent(
            messageId,
            destinationChainSelector,
            msg.sender,
            message.receiver,
            message.data,
            message.tokenAmounts,
            message.feeToken,
            message.extraArgs
        );
        return (messageId, fees);
    }

    /**
     * @inheritdoc IRouterClient
     */
    function getFee(
        uint64 destinationChainSelector,
        EVM2AnyMessage memory message
    ) public view override returns (uint256 fee) {
        // Check cache
        bytes32 cacheKey = keccak256(abi.encode(destinationChainSelector, message.receiver, message.data.length));
        if (cacheTimestamp[cacheKey] != 0 && block.timestamp < cacheTimestamp[cacheKey] + cacheExpiry) {
            return cachedFees[cacheKey];
        }
        
        // Calculate fee
        fee = baseFee;
        fee += uint256(message.data.length) * dataFeePerByte;

        for (uint256 i = 0; i < message.tokenAmounts.length; i++) {
            fee += message.tokenAmounts[i].amount * tokenFeePerToken;
        }
        
        return fee;
    }

    /**
     * @notice Batch multiple messages
     */
    function batchSend(
        uint64 destinationChainSelector,
        EVM2AnyMessage[] memory messages
    ) external payable returns (uint256 batchId_, bytes32[] memory messageIds) {
        require(messages.length <= maxBatchSize, "CCIPRouterOptimized: batch too large");
        
        batchId_ = batchId++;
        messageIds = new bytes32[](messages.length);
        uint256 totalFee = 0;
        
        for (uint256 i = 0; i < messages.length; i++) {
            uint256 fee = getFee(destinationChainSelector, messages[i]);
            totalFee += fee;
            
            bytes32 messageId = keccak256(abi.encodePacked(block.timestamp, block.number, i, messages[i].data));
            messageIds[i] = messageId;
        }
        
        require(msg.value >= totalFee, "CCIPRouterOptimized: insufficient fee");
        
        batches[batchId_] = BatchedMessage({
            messageIds: messageIds,
            destinationChainSelector: destinationChainSelector,
            totalFee: totalFee,
            timestamp: block.timestamp
        });
        
        emit MessageBatched(batchId_, messages.length);
        return (batchId_, messageIds);
    }

    /**
     * @notice Cache fee calculation
     */
    function cacheFee(
        uint64 destinationChainSelector,
        bytes memory receiver,
        uint256 dataLength
    ) external returns (uint256 fee) {
        bytes32 cacheKey = keccak256(abi.encode(destinationChainSelector, receiver, dataLength));
        
        // Calculate fee
        fee = baseFee + (dataLength * dataFeePerByte);
        
        // Cache it
        cachedFees[cacheKey] = fee;
        cacheTimestamp[cacheKey] = block.timestamp;
        
        emit FeeCached(cacheKey, fee);
        return fee;
    }

    /**
     * @inheritdoc IRouterClient
     */
    function getSupportedTokens(
        uint64 destinationChainSelector
    ) external view override returns (address[] memory) {
        return supportedTokens[destinationChainSelector];
    }

    /**
     * @notice Update base fee
     */
    function updateBaseFee(uint256 newFee) external onlyAdmin {
        require(newFee > 0, "CCIPRouterOptimized: fee must be greater than 0");
        emit BaseFeeUpdated(baseFee, newFee);
        baseFee = newFee;
    }

    /**
     * @notice Update cache expiry
     */
    function setCacheExpiry(uint256 newExpiry) external onlyAdmin {
        cacheExpiry = newExpiry;
    }

    /**
     * @notice Update batch window
     */
    function setBatchWindow(uint256 newWindow) external onlyAdmin {
        batchWindow = newWindow;
    }

    /**
     * @notice Update max batch size
     */
    function setMaxBatchSize(uint256 newSize) external onlyAdmin {
        require(newSize > 0, "CCIPRouterOptimized: size must be greater than 0");
        maxBatchSize = newSize;
    }

    /**
     * @notice Change admin
     */
    function changeAdmin(address newAdmin) external onlyAdmin {
        require(newAdmin != address(0), "CCIPRouterOptimized: zero address");
        emit RouterAdminChanged(admin, newAdmin);
        admin = newAdmin;
    }
}

