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

import "./BondManager.sol";
import "./ChallengeManager.sol";
import "./LiquidityPoolETH.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
 * @title InboxETH
 * @notice Receives and processes claims from relayers for trustless bridge deposits
 * @dev Permissionless claim submission requiring bonds and challenge mechanism
 */
contract InboxETH is ReentrancyGuard {
    BondManager public immutable bondManager;
    ChallengeManager public immutable challengeManager;
    LiquidityPoolETH public immutable liquidityPool;

    // Rate limiting
    uint256 public constant MIN_DEPOSIT = 0.001 ether; // Minimum deposit to prevent dust
    uint256 public constant COOLDOWN_PERIOD = 60 seconds; // Cooldown between claims per relayer
    mapping(address => uint256) public lastClaimTime; // relayer => last claim timestamp
    mapping(address => uint256) public claimsPerHour; // relayer => claims in current hour
    mapping(address => uint256) public hourStart; // relayer => current hour start timestamp
    uint256 public constant MAX_CLAIMS_PER_HOUR = 100; // Max claims per hour per relayer

    // Relayer fees (optional, can be enabled via governance)
    uint256 public relayerFeeBps = 0; // Basis points (0 = disabled, 100 = 1%)
    mapping(uint256 => RelayerFee) public relayerFees; // depositId => RelayerFee

    struct RelayerFee {
        address relayer;
        uint256 amount;
        bool claimed;
    }

    struct ClaimData {
        uint256 depositId;
        address asset;
        uint256 amount;
        address recipient;
        address relayer;
        uint256 timestamp;
        bool exists;
    }

    mapping(uint256 => ClaimData) public claims; // depositId => ClaimData

    event RelayerFeeSet(uint256 newFeeBps);
    event RelayerFeeClaimed(uint256 indexed depositId, address indexed relayer, uint256 amount);

    event ClaimSubmitted(
        uint256 indexed depositId,
        address indexed relayer,
        address asset,
        uint256 amount,
        address indexed recipient,
        uint256 bondAmount,
        uint256 challengeWindowEnd
    );

    error ZeroDepositId();
    error ZeroAsset();
    error ZeroAmount();
    error ZeroRecipient();
    error ClaimAlreadyExists();
    error InsufficientBond();
    error DepositTooSmall();
    error CooldownActive();
    error RateLimitExceeded();
    error RelayerFeeNotEnabled();

    /**
     * @notice Constructor
     * @param _bondManager Address of BondManager contract
     * @param _challengeManager Address of ChallengeManager contract
     * @param _liquidityPool Address of LiquidityPoolETH contract
     */
    constructor(
        address _bondManager,
        address _challengeManager,
        address _liquidityPool
    ) {
        require(_bondManager != address(0), "InboxETH: zero bond manager");
        require(_challengeManager != address(0), "InboxETH: zero challenge manager");
        require(_liquidityPool != address(0), "InboxETH: zero liquidity pool");
        
        bondManager = BondManager(payable(_bondManager));
        challengeManager = ChallengeManager(payable(_challengeManager));
        liquidityPool = LiquidityPoolETH(payable(_liquidityPool));
    }

    /**
     * @notice Submit a claim for a deposit from source chain
     * @param depositId Deposit ID from source chain (ChainID 138)
     * @param asset Asset address (address(0) for native ETH)
     * @param amount Deposit amount
     * @param recipient Recipient address on Ethereum
     * @return bondAmount Amount of bond posted
     */
    function submitClaim(
        uint256 depositId,
        address asset,
        uint256 amount,
        address recipient,
        bytes calldata
    ) external payable nonReentrant returns (uint256 bondAmount) {
        if (depositId == 0) revert ZeroDepositId();
        if (asset == address(0) && amount == 0) revert ZeroAmount();
        if (recipient == address(0)) revert ZeroRecipient();

        // Rate limiting checks
        if (amount < MIN_DEPOSIT) revert DepositTooSmall();
        
        // Cooldown check
        if (block.timestamp < lastClaimTime[msg.sender] + COOLDOWN_PERIOD) {
            revert CooldownActive();
        }

        // Hourly rate limit check
        uint256 currentHour = block.timestamp / 3600;
        if (hourStart[msg.sender] != currentHour) {
            hourStart[msg.sender] = currentHour;
            claimsPerHour[msg.sender] = 0;
        }
        if (claimsPerHour[msg.sender] >= MAX_CLAIMS_PER_HOUR) {
            revert RateLimitExceeded();
        }

        // Check if claim already exists
        if (claims[depositId].exists) revert ClaimAlreadyExists();

        // Calculate required bond
        uint256 requiredBond = bondManager.getRequiredBond(amount);
        
        // Calculate relayer fee if enabled
        uint256 relayerFee = 0;
        uint256 bridgeAmount = amount;
        if (relayerFeeBps > 0) {
            relayerFee = (amount * relayerFeeBps) / 10000;
            bridgeAmount = amount - relayerFee;
            
            // Store relayer fee
            relayerFees[depositId] = RelayerFee({
                relayer: msg.sender,
                amount: relayerFee,
                claimed: false
            });
        }

        if (msg.value < requiredBond) revert InsufficientBond();

        // Post bond (pass relayer address explicitly)
        bondAmount = bondManager.postBond{value: requiredBond}(depositId, bridgeAmount, msg.sender);
        
        // Update rate limiting
        lastClaimTime[msg.sender] = block.timestamp;
        claimsPerHour[msg.sender]++;

        // Register claim with ChallengeManager (use bridgeAmount after fee)
        challengeManager.registerClaim(depositId, asset, bridgeAmount, recipient);

        // Determine asset type for liquidity pool
        LiquidityPoolETH.AssetType assetType = asset == address(0) 
            ? LiquidityPoolETH.AssetType.ETH 
            : LiquidityPoolETH.AssetType.WETH;

        // Add pending claim to liquidity pool (use bridgeAmount after fee deduction)
        liquidityPool.addPendingClaim(bridgeAmount, assetType);

        // Store claim data (use bridgeAmount for amount)
        claims[depositId] = ClaimData({
            depositId: depositId,
            asset: asset,
            amount: bridgeAmount, // Store bridge amount (after fee)
            recipient: recipient,
            relayer: msg.sender,
            timestamp: block.timestamp,
            exists: true
        });

        // Get challenge window end time
        (uint256 challengeWindowEnd, ) = _getChallengeWindowEnd(depositId);

        emit ClaimSubmitted(
            depositId,
            msg.sender,
            asset,
            bridgeAmount, // Emit bridge amount (after fee)
            recipient,
            bondAmount,
            challengeWindowEnd
        );

        return bondAmount;
    }

    /**
     * @notice Submit multiple claims in batch (gas optimization)
     * @param depositIds Array of deposit IDs
     * @param assets Array of asset addresses
     * @param amounts Array of deposit amounts
     * @param recipients Array of recipient addresses
     * @param proofs Array of proof data
     * @return totalBondAmount Total bond amount posted
     */
    function submitClaimsBatch(
        uint256[] calldata depositIds,
        address[] calldata assets,
        uint256[] calldata amounts,
        address[] calldata recipients,
        bytes[] calldata proofs
    ) external payable nonReentrant returns (uint256 totalBondAmount) {
        uint256 length = depositIds.length;
        require(length > 0, "InboxETH: empty array");
        require(length <= 20, "InboxETH: batch too large"); // Prevent gas limit issues
        require(length == assets.length && length == amounts.length && 
                length == recipients.length && length == proofs.length, 
                "InboxETH: length mismatch");

        // Calculate total required bond
        uint256 totalRequiredBond = 0;
        for (uint256 i = 0; i < length; i++) {
            if (depositIds[i] == 0) revert ZeroDepositId();
            if (assets[i] == address(0) && amounts[i] == 0) revert ZeroAmount();
            if (recipients[i] == address(0)) revert ZeroRecipient();
            if (claims[depositIds[i]].exists) revert ClaimAlreadyExists();
            
            totalRequiredBond += bondManager.getRequiredBond(amounts[i]);
        }

        if (msg.value < totalRequiredBond) revert InsufficientBond();

        // Process each claim
        for (uint256 i = 0; i < length; i++) {
            // Rate limiting checks (simplified for batch - check first item)
            if (i == 0) {
                if (amounts[i] < MIN_DEPOSIT) revert DepositTooSmall();
                if (block.timestamp < lastClaimTime[msg.sender] + COOLDOWN_PERIOD) {
                    revert CooldownActive();
                }
                uint256 currentHour = block.timestamp / 3600;
                if (hourStart[msg.sender] != currentHour) {
                    hourStart[msg.sender] = currentHour;
                    claimsPerHour[msg.sender] = 0;
                }
            }
            if (claimsPerHour[msg.sender] + i >= MAX_CLAIMS_PER_HOUR) {
                revert RateLimitExceeded();
            }

            // Calculate relayer fee if enabled
            uint256 relayerFee = 0;
            uint256 bridgeAmount = amounts[i];
            if (relayerFeeBps > 0) {
                relayerFee = (amounts[i] * relayerFeeBps) / 10000;
                bridgeAmount = amounts[i] - relayerFee;
                relayerFees[depositIds[i]] = RelayerFee({
                    relayer: msg.sender,
                    amount: relayerFee,
                    claimed: false
                });
            }

            uint256 requiredBond = bondManager.getRequiredBond(bridgeAmount);
            
            // Post bond
            uint256 bondAmount = bondManager.postBond{value: requiredBond}(
                depositIds[i],
                bridgeAmount,
                msg.sender
            );
            totalBondAmount += bondAmount;

            // Register claim (use bridgeAmount)
            challengeManager.registerClaim(depositIds[i], assets[i], bridgeAmount, recipients[i]);

            // Determine asset type
            LiquidityPoolETH.AssetType assetType = assets[i] == address(0) 
                ? LiquidityPoolETH.AssetType.ETH 
                : LiquidityPoolETH.AssetType.WETH;

            // Add pending claim (use bridgeAmount)
            liquidityPool.addPendingClaim(bridgeAmount, assetType);

            // Store claim data (use bridgeAmount)
            claims[depositIds[i]] = ClaimData({
                depositId: depositIds[i],
                asset: assets[i],
                amount: bridgeAmount,
                recipient: recipients[i],
                relayer: msg.sender,
                timestamp: block.timestamp,
                exists: true
            });

            // Get challenge window end time
            (uint256 challengeWindowEnd, ) = _getChallengeWindowEnd(depositIds[i]);

            emit ClaimSubmitted(
                depositIds[i],
                msg.sender,
                assets[i],
                bridgeAmount,
                recipients[i],
                bondAmount,
                challengeWindowEnd
            );
        }

        // Update rate limiting
        lastClaimTime[msg.sender] = block.timestamp;
        claimsPerHour[msg.sender] += length;

        // Refund excess bond if any
        if (msg.value > totalBondAmount) {
            (bool success, ) = payable(msg.sender).call{value: msg.value - totalBondAmount}("");
            require(success, "InboxETH: refund failed");
        }

        return totalBondAmount;
    }

    /**
     * @notice Get claim status
     * @param depositId Deposit ID
     * @return exists True if claim exists
     * @return finalized True if claim is finalized
     * @return challenged True if claim was challenged
     * @return challengeWindowEnd Timestamp when challenge window ends
     */
    function getClaimStatus(
        uint256 depositId
    ) external view returns (
        bool exists,
        bool finalized,
        bool challenged,
        uint256 challengeWindowEnd
    ) {
        if (!claims[depositId].exists) {
            return (false, false, false, 0);
        }

        ChallengeManager.Claim memory claim = challengeManager.getClaim(depositId);
        (challengeWindowEnd, ) = _getChallengeWindowEnd(depositId);

        return (
            true,
            claim.finalized,
            claim.challenged,
            challengeWindowEnd
        );
    }

    /**
     * @notice Get claim data
     * @param depositId Deposit ID
     * @return Claim data
     */
    function getClaim(uint256 depositId) external view returns (ClaimData memory) {
        return claims[depositId];
    }

    /**
     * @notice Internal function to get challenge window end time
     * @param depositId Deposit ID
     * @return challengeWindowEnd Timestamp
     * @return exists True if claim exists
     */
    function _getChallengeWindowEnd(
        uint256 depositId
    ) internal view returns (uint256 challengeWindowEnd, bool exists) {
        ChallengeManager.Claim memory claim = challengeManager.getClaim(depositId);
        if (claim.depositId == 0) {
            return (0, false);
        }
        return (claim.challengeWindowEnd, true);
    }

    /**
     * @notice Set relayer fee (only callable by owner/multisig in future upgrade)
     * @param _relayerFeeBps New relayer fee in basis points (0 = disabled)
     */
    function setRelayerFee(uint256 _relayerFeeBps) external {
        // Note: In production, add access control (owner/multisig)
        // For now, this is a placeholder for future governance
        require(_relayerFeeBps <= 1000, "InboxETH: fee too high"); // Max 10%
        relayerFeeBps = _relayerFeeBps;
        emit RelayerFeeSet(_relayerFeeBps);
    }

    /**
     * @notice Claim relayer fee for a finalized deposit
     * @param depositId Deposit ID to claim fee for
     */
    function claimRelayerFee(uint256 depositId) external nonReentrant {
        if (relayerFeeBps == 0) revert RelayerFeeNotEnabled();
        
        RelayerFee storage fee = relayerFees[depositId];
        if (fee.relayer == address(0)) revert("InboxETH: no fee for deposit");
        if (fee.claimed) revert("InboxETH: fee already claimed");
        if (fee.relayer != msg.sender) revert("InboxETH: not fee recipient");

        // Verify claim is finalized
        ChallengeManager.Claim memory claim = challengeManager.getClaim(depositId);
        if (!claim.finalized) revert("InboxETH: claim not finalized");

        fee.claimed = true;
        
        // Transfer fee to relayer
        (bool success, ) = payable(msg.sender).call{value: fee.amount}("");
        require(success, "InboxETH: fee transfer failed");

        emit RelayerFeeClaimed(depositId, msg.sender, fee.amount);
    }

    /**
     * @notice Get relayer fee for a deposit
     * @param depositId Deposit ID
     * @return fee Relayer fee information
     */
    function getRelayerFee(uint256 depositId) external view returns (RelayerFee memory) {
        return relayerFees[depositId];
    }
}
