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

import "./BondManager.sol";
import "./libraries/MerkleProofVerifier.sol";
import "./libraries/FraudProofTypes.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
 * @title ChallengeManager
 * @notice Manages fraud proof challenges for trustless bridge claims
 * @dev Permissionless challenging mechanism with automated slashing on successful challenges
 */
contract ChallengeManager is ReentrancyGuard {
    BondManager public immutable bondManager;
    uint256 public immutable challengeWindow; // Challenge window duration in seconds

    enum FraudProofType {
        NonExistentDeposit,  // Deposit doesn't exist on source chain
        IncorrectAmount,     // Amount mismatch
        IncorrectRecipient,  // Recipient mismatch
        DoubleSpend          // Deposit already claimed elsewhere
    }

    struct Challenge {
        address challenger;
        uint256 depositId;
        FraudProofType proofType;
        bytes proof;
        uint256 timestamp;
        bool resolved;
    }

    struct Claim {
        uint256 depositId;        // Slot 0
        address asset;            // Slot 1 (20 bytes) + 12 bytes padding
        address recipient;        // Slot 2 (20 bytes) + 12 bytes padding
        uint256 amount;           // Slot 3
        uint256 challengeWindowEnd; // Slot 4
        bool finalized;           // Slot 5 (1 byte) + 31 bytes padding
        bool challenged;          // Slot 6 (1 byte) + 31 bytes padding
        // Note: Could pack finalized and challenged in same slot, but keeping separate for clarity
    }

    mapping(uint256 => Claim) public claims; // depositId => Claim
    mapping(uint256 => Challenge) public challenges; // depositId => Challenge

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

    event ClaimChallenged(
        uint256 indexed depositId,
        address indexed challenger,
        FraudProofType proofType
    );

    event FraudProven(
        uint256 indexed depositId,
        address indexed challenger,
        FraudProofType proofType,
        uint256 slashedAmount
    );

    event ChallengeRejected(
        uint256 indexed depositId,
        address indexed challenger
    );

    event ClaimFinalized(
        uint256 indexed depositId
    );

    error ZeroDepositId();
    error ClaimNotFound();
    error ClaimAlreadyFinalized();
    error ClaimAlreadyChallenged();
    error ChallengeWindowExpired();
    error ChallengeWindowNotExpired();
    error InvalidFraudProof();
    error ChallengeNotFound();
    error ChallengeAlreadyResolved();

    /**
     * @notice Constructor
     * @param _bondManager Address of BondManager contract
     * @param _challengeWindow Challenge window duration in seconds
     */
    constructor(address _bondManager, uint256 _challengeWindow) {
        require(_bondManager != address(0), "ChallengeManager: zero bond manager");
        require(_challengeWindow > 0, "ChallengeManager: zero challenge window");
        bondManager = BondManager(payable(_bondManager));
        challengeWindow = _challengeWindow;
    }

    /**
     * @notice Register a claim (called by InboxETH)
     * @param depositId Deposit ID from source chain
     * @param asset Asset address (address(0) for native ETH)
     * @param amount Deposit amount
     * @param recipient Recipient address
     */
    function registerClaim(
        uint256 depositId,
        address asset,
        uint256 amount,
        address recipient
    ) external {
        if (depositId == 0) revert ZeroDepositId();

        // Only allow one claim per deposit ID
        require(claims[depositId].depositId == 0, "ChallengeManager: claim already registered");

        uint256 challengeWindowEnd = block.timestamp + challengeWindow;

        claims[depositId] = Claim({
            depositId: depositId,
            asset: asset,
            amount: amount,
            recipient: recipient,
            challengeWindowEnd: challengeWindowEnd,
            finalized: false,
            challenged: false
        });

        emit ClaimSubmitted(depositId, asset, amount, recipient, challengeWindowEnd);
    }

    /**
     * @notice Challenge a claim with fraud proof
     * @param depositId Deposit ID of the claim to challenge
     * @param proofType Type of fraud proof
     * @param proof Fraud proof data (format depends on proofType)
     */
    function challengeClaim(
        uint256 depositId,
        FraudProofType proofType,
        bytes calldata proof
    ) external nonReentrant {
        if (depositId == 0) revert ZeroDepositId();

        Claim storage claim = claims[depositId];
        if (claim.depositId == 0) revert ClaimNotFound();
        if (claim.finalized) revert ClaimAlreadyFinalized();
        if (claim.challenged) revert ClaimAlreadyChallenged();
        if (block.timestamp > claim.challengeWindowEnd) revert ChallengeWindowExpired();

        // Verify fraud proof (pass storage reference to save gas)
        if (!_verifyFraudProof(depositId, claim, proofType, proof)) {
            revert InvalidFraudProof();
        }

        // Mark claim as challenged
        claim.challenged = true;

        // Store challenge
        challenges[depositId] = Challenge({
            challenger: msg.sender,
            depositId: depositId,
            proofType: proofType,
            proof: proof,
            timestamp: block.timestamp,
            resolved: false
        });

        emit ClaimChallenged(depositId, msg.sender, proofType);

        // Automatically slash bond and mark challenge as resolved
        (uint256 challengerReward, ) = bondManager.slashBond(depositId, msg.sender);
        
        challenges[depositId].resolved = true;

        emit FraudProven(depositId, msg.sender, proofType, challengerReward * 2); // Total slashed amount
    }

    /**
     * @notice Finalize a claim after challenge window expires without challenge
     * @param depositId Deposit ID to finalize
     */
    function finalizeClaim(uint256 depositId) external {
        if (depositId == 0) revert ZeroDepositId();

        Claim storage claim = claims[depositId];
        if (claim.depositId == 0) revert ClaimNotFound();
        if (claim.finalized) revert ClaimAlreadyFinalized();
        if (claim.challenged) revert ClaimAlreadyChallenged();
        if (block.timestamp <= claim.challengeWindowEnd) revert ChallengeWindowNotExpired();

        claim.finalized = true;

        emit ClaimFinalized(depositId);
    }

    /**
     * @notice Finalize multiple claims in batch (gas optimization)
     * @param depositIds Array of deposit IDs to finalize
     */
    function finalizeClaimsBatch(uint256[] calldata depositIds) external {
        uint256 length = depositIds.length;
        require(length > 0, "ChallengeManager: empty array");
        require(length <= 50, "ChallengeManager: batch too large"); // Prevent gas limit issues

        for (uint256 i = 0; i < length; i++) {
            uint256 depositId = depositIds[i];
            if (depositId == 0) continue; // Skip zero IDs

            Claim storage claim = claims[depositId];
            if (claim.depositId == 0) continue; // Skip non-existent claims
            if (claim.finalized) continue; // Skip already finalized
            if (claim.challenged) continue; // Skip challenged claims
            if (block.timestamp <= claim.challengeWindowEnd) continue; // Skip if window not expired

            claim.finalized = true;
            emit ClaimFinalized(depositId);
        }
    }

    /**
     * @notice Verify fraud proof (internal function)
     * @dev Verifies fraud proofs against source chain state using Merkle proofs
     * @param depositId Deposit ID
     * @param claim Claim data
     * @param proofType Type of fraud proof
     * @param proof Proof data (encoded according to proofType)
     * @return True if fraud proof is valid
     */
    function _verifyFraudProof(
        uint256 depositId,
        Claim storage claim, // Changed to storage to save gas
        FraudProofType proofType,
        bytes calldata proof
    ) internal view returns (bool) {
        if (proof.length == 0) return false;

        if (proofType == FraudProofType.NonExistentDeposit) {
            return _verifyNonExistentDeposit(depositId, claim, proof);
        } else if (proofType == FraudProofType.IncorrectAmount) {
            return _verifyIncorrectAmount(depositId, claim, proof);
        } else if (proofType == FraudProofType.IncorrectRecipient) {
            return _verifyIncorrectRecipient(depositId, claim, proof);
        } else if (proofType == FraudProofType.DoubleSpend) {
            return _verifyDoubleSpend(depositId, claim, proof);
        }

        return false;
    }

    /**
     * @notice Verify non-existent deposit fraud proof
     * @param depositId Deposit ID
     * @param claim Claim data
     * @param proof Encoded NonExistentDepositProof
     * @return True if proof is valid
     */
    function _verifyNonExistentDeposit(
        uint256 depositId,
        Claim storage claim, // Changed to storage to save gas
        bytes calldata proof
    ) internal view returns (bool) {
        FraudProofTypes.NonExistentDepositProof memory fraudProof =
            FraudProofTypes.decodeNonExistentDeposit(proof);

        // Verify state root against block header
        if (!MerkleProofVerifier.verifyStateRoot(fraudProof.blockHeader, fraudProof.stateRoot)) {
            return false;
        }

        // Hash the claimed deposit data
        bytes32 claimedDepositHash = MerkleProofVerifier.hashDepositData(
            depositId,
            claim.asset,
            claim.amount,
            claim.recipient,
            block.timestamp // Note: In production, use actual deposit timestamp from source chain
        );

        // Verify that the claimed deposit hash matches the proof
        if (claimedDepositHash != fraudProof.depositHash) {
            return false;
        }

        // Verify non-existence proof (deposit doesn't exist in Merkle tree)
        return MerkleProofVerifier.verifyDepositNonExistence(
            fraudProof.stateRoot,
            fraudProof.depositHash,
            fraudProof.merkleProof,
            fraudProof.leftSibling,
            fraudProof.rightSibling
        );
    }

    /**
     * @notice Verify incorrect amount fraud proof
     * @param depositId Deposit ID
     * @param claim Claim data
     * @param proof Encoded IncorrectAmountProof
     * @return True if proof is valid
     */
    function _verifyIncorrectAmount(
        uint256 depositId,
        Claim storage claim, // Changed to storage to save gas
        bytes calldata proof
    ) internal view returns (bool) {
        FraudProofTypes.IncorrectAmountProof memory fraudProof =
            FraudProofTypes.decodeIncorrectAmount(proof);

        // Verify state root against block header
        if (!MerkleProofVerifier.verifyStateRoot(fraudProof.blockHeader, fraudProof.stateRoot)) {
            return false;
        }

        // Verify that actual amount differs from claimed amount
        if (fraudProof.actualAmount == claim.amount) {
            return false; // Amounts match, not a fraud
        }

        // Hash the actual deposit data
        bytes32 actualDepositHash = MerkleProofVerifier.hashDepositData(
            depositId,
            claim.asset,
            fraudProof.actualAmount,
            claim.recipient,
            block.timestamp // Note: In production, use actual deposit timestamp
        );

        // Verify Merkle proof for actual deposit
        return MerkleProofVerifier.verifyDepositExistence(
            fraudProof.stateRoot,
            actualDepositHash,
            fraudProof.merkleProof
        );
    }

    /**
     * @notice Verify incorrect recipient fraud proof
     * @param depositId Deposit ID
     * @param claim Claim data
     * @param proof Encoded IncorrectRecipientProof
     * @return True if proof is valid
     */
    function _verifyIncorrectRecipient(
        uint256 depositId,
        Claim storage claim, // Changed to storage to save gas
        bytes calldata proof
    ) internal view returns (bool) {
        FraudProofTypes.IncorrectRecipientProof memory fraudProof =
            FraudProofTypes.decodeIncorrectRecipient(proof);

        // Verify state root against block header
        if (!MerkleProofVerifier.verifyStateRoot(fraudProof.blockHeader, fraudProof.stateRoot)) {
            return false;
        }

        // Verify that actual recipient differs from claimed recipient
        if (fraudProof.actualRecipient == claim.recipient) {
            return false; // Recipients match, not a fraud
        }

        // Hash the actual deposit data
        bytes32 actualDepositHash = MerkleProofVerifier.hashDepositData(
            depositId,
            claim.asset,
            claim.amount,
            fraudProof.actualRecipient,
            block.timestamp // Note: In production, use actual deposit timestamp
        );

        // Verify Merkle proof for actual deposit
        return MerkleProofVerifier.verifyDepositExistence(
            fraudProof.stateRoot,
            actualDepositHash,
            fraudProof.merkleProof
        );
    }

    /**
     * @notice Verify double spend fraud proof
     * @param depositId Deposit ID
     * @param claim Claim data
     * @param proof Encoded DoubleSpendProof
     * @return True if proof is valid
     */
    function _verifyDoubleSpend(
        uint256 depositId,
        Claim storage claim, // Changed to storage to save gas
        bytes calldata proof
    ) internal view returns (bool) {
        FraudProofTypes.DoubleSpendProof memory fraudProof =
            FraudProofTypes.decodeDoubleSpend(proof);

        // Verify that the previous claim ID is different (same deposit claimed twice)
        if (fraudProof.previousClaimId == depositId) {
        // Check if previous claim exists and is finalized (use storage to save gas)
        Claim storage previousClaim = claims[fraudProof.previousClaimId];
        if (previousClaim.depositId == 0 || !previousClaim.finalized) {
            return false; // Previous claim doesn't exist or isn't finalized
        }

        // Verify that the deposit data matches (same deposit, different claim)
        if (
            previousClaim.asset == claim.asset &&
            previousClaim.amount == claim.amount &&
            previousClaim.recipient == claim.recipient
        ) {
            return true; // Double spend detected
        }
        }

        return false;
    }

    /**
     * @notice Check if a claim can be finalized
     * @param depositId Deposit ID to check
     * @return canFinalize_ True if claim can be finalized
     * @return reason Reason if cannot finalize
     */
    function canFinalize(uint256 depositId) external view returns (bool canFinalize_, string memory reason) {
        Claim memory claim = claims[depositId];
        
        if (claim.depositId == 0) {
            return (false, "Claim not found");
        }
        if (claim.finalized) {
            return (false, "Already finalized");
        }
        if (claim.challenged) {
            return (false, "Claim was challenged");
        }
        if (block.timestamp <= claim.challengeWindowEnd) {
            return (false, "Challenge window not expired");
        }

        return (true, "");
    }

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

    /**
     * @notice Get challenge information
     * @param depositId Deposit ID
     * @return Challenge data
     */
    function getChallenge(uint256 depositId) external view returns (Challenge memory) {
        return challenges[depositId];
    }
}
