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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./AereMessenger.sol";

/**
 * @title AereSpokePool
 * @notice Across-protocol-compatible spoke pool for AERE Network.
 *         Implements the intent-based bridging model:
 *
 *           1. User deposits inputToken on this chain via `deposit(...)`.
 *              Emits a `V3FundsDeposited` event with intent parameters.
 *           2. Solver observes the event, sees a profitable spread, and calls
 *              `fillRelay(...)` on the destination chain's SpokePool, transferring
 *              outputToken to the user's recipient address.
 *           3. After fill, the destination SpokePool emits `FilledV3Relay` and
 *              sends a settlement message via AereMessenger to the origin
 *              SpokePool. The origin SpokePool then releases the locked
 *              inputToken to the solver's repayment address.
 *
 *           If fillDeadline expires without a fill, user calls `claimRefund(...)`
 *           to recover their input tokens.
 *
 *   This is the canonical Across v3 flow. By using AereMessenger for the
 *   settlement message layer, AERE's SpokePool is fully Hyperlane-compatible
 *   and ready for the public Across solver network once registered.
 */
contract AereSpokePool is Ownable, ReentrancyGuard, IMessageRecipient {
    using SafeERC20 for IERC20;

    AereMessenger public immutable messenger;
    uint32 public immutable localDomain;

    /// Grace period, added on top of fillDeadline, before a depositor may claim
    /// a refund.
    ///
    /// A solver may legitimately fill on the destination at any t < fillDeadline
    /// (fillRelay irreversibly delivers outputToken to the recipient), while the
    /// authenticated settlement that credits the solver only reaches this origin
    /// chain after a cross-chain relay delay L (destination finality + validator
    /// signing + relayer submission). If a refund were gated on fillDeadline
    /// alone, a depositor could front-run a still-in-flight settlement in the
    /// window [fillDeadline, fillDeadline + L]: pull the locked input back, after
    /// which handle() reverts on the `refunded` flag and the solver who already
    /// delivered the output is never repaid.
    ///
    /// REFUND_BUFFER must generously exceed the worst-case L for the messenger's
    /// validator set (including finality, signer polling, relayer congestion and
    /// outage-driven retries). 3 hours is deliberately conservative: it is orders
    /// of magnitude above the seconds-to-minutes of a healthy relay yet still
    /// bounds how long a genuinely unfilled deposit's principal is held before the
    /// depositor can recover it. A settlement for a fill dispatched at (or just
    /// before) the deadline is therefore delivered, setting `settled`, before any
    /// refund becomes possible.
    ///
    /// NOTE: a timer-based refund cannot fully adjudicate the refund-vs-fill
    /// outcome; if the relay were delayed beyond REFUND_BUFFER a refund could
    /// still front-run it. Fully closing that would require an authenticated
    /// "not filled" attestation over the same messenger path (as Across does).
    /// While the messenger only carries fill-settlements, this buffer is the
    /// pragmatic mitigation.
    uint256 public constant REFUND_BUFFER = 3 hours;

    /// Auto-incrementing deposit counter — used as the unique deposit ID.
    uint32 public depositCounter;

    /// Each enrolled remote SpokePool we can talk to.
    mapping(uint32 => bytes32) public remoteSpokePool;

    /// Deposit record: locked input tokens by depositId.
    /// The requested output leg (outputToken / outputAmount / recipient) is
    /// stored in full so the origin can verify, at settlement time, that the
    /// output a solver actually delivered on the destination satisfies the
    /// order before any locked principal is released.
    struct Deposit {
        address depositor;
        address inputToken;
        uint256 inputAmount;
        address outputToken;
        uint256 outputAmount;
        address recipient;
        uint32 destinationChainId;
        uint32 fillDeadline;
        bool settled;
        bool refunded;
    }
    mapping(uint32 => Deposit) public deposits;

    /// Filled relays — replay protection on the destination side.
    mapping(bytes32 => bool) public filled;

    /// Pending refunds for solvers awaiting settlement on the origin chain.
    /// solver => inputToken => amount
    mapping(address => mapping(address => uint256)) public pendingSettlement;

    bool public paused;

    event V3FundsDeposited(
        uint32 indexed depositId,
        address indexed depositor,
        address inputToken,
        uint256 inputAmount,
        uint32 destinationChainId,
        address outputToken,
        uint256 outputAmount,
        address recipient,
        uint32 fillDeadline
    );
    event FilledV3Relay(
        bytes32 indexed relayHash,
        address indexed solver,
        uint32 originChainId,
        uint32 depositId,
        address recipient,
        uint256 outputAmount
    );
    event SettlementClaimed(address indexed solver, address indexed token, uint256 amount);
    event Refunded(uint32 indexed depositId, address indexed depositor, uint256 amount);
    event RemoteSpokePoolEnrolled(uint32 indexed domain, bytes32 spokePool);
    event Paused(bool paused);

    constructor(AereMessenger _messenger) {
        messenger = _messenger;
        localDomain = _messenger.localDomain();
    }

    modifier whenNotPaused() {
        require(!paused, "SpokePool: paused");
        _;
    }

    // ───────────────────── Admin ─────────────────────

    function enrollRemoteSpokePool(uint32 destChainId, bytes32 spokePool) external onlyOwner {
        remoteSpokePool[destChainId] = spokePool;
        emit RemoteSpokePoolEnrolled(destChainId, spokePool);
    }

    function setPaused(bool _paused) external onlyOwner {
        paused = _paused;
        emit Paused(_paused);
    }

    // ───────────────────── Deposit (origin-chain user flow) ─────────────────────

    /**
     * @notice User deposits inputToken on this chain, committing to receive
     *         outputAmount of outputToken on destinationChainId before fillDeadline.
     */
    function deposit(
        address inputToken,
        uint256 inputAmount,
        address outputToken,
        uint256 outputAmount,
        uint32 destinationChainId,
        address recipient,
        uint32 fillDeadline
    ) external nonReentrant whenNotPaused returns (uint32 depositId) {
        require(remoteSpokePool[destinationChainId] != bytes32(0), "SpokePool: dest not enrolled");
        require(fillDeadline > block.timestamp, "SpokePool: deadline passed");
        require(inputAmount > 0 && outputAmount > 0, "SpokePool: zero amount");

        IERC20(inputToken).safeTransferFrom(msg.sender, address(this), inputAmount);

        depositId = ++depositCounter;
        deposits[depositId] = Deposit({
            depositor: msg.sender,
            inputToken: inputToken,
            inputAmount: inputAmount,
            outputToken: outputToken,
            outputAmount: outputAmount,
            recipient: recipient,
            destinationChainId: destinationChainId,
            fillDeadline: fillDeadline,
            settled: false,
            refunded: false
        });

        emit V3FundsDeposited(
            depositId,
            msg.sender,
            inputToken,
            inputAmount,
            destinationChainId,
            outputToken,
            outputAmount,
            recipient,
            fillDeadline
        );
    }

    /// User reclaims input tokens if fill deadline has passed without delivery.
    ///
    /// The refund is gated on fillDeadline + REFUND_BUFFER, not fillDeadline
    /// alone, so an authenticated settlement for a fill dispatched at/just-before
    /// the deadline always lands (setting `settled`) before a refund is possible.
    /// This closes the theft-of-principal race where a depositor front-runs an
    /// in-flight settlement to pull their input back while the solver has already
    /// delivered the output on the destination. See REFUND_BUFFER above.
    ///
    /// A settlement that somehow arrives after a refund still cannot double-pay:
    /// handle() rejects it on the `refunded` flag. A refund after a genuine
    /// expiry (no fill) still works once the buffer has elapsed.
    function claimRefund(uint32 depositId) external nonReentrant {
        Deposit storage d = deposits[depositId];
        require(d.depositor == msg.sender, "SpokePool: not depositor");
        require(!d.refunded && !d.settled, "SpokePool: already resolved");
        require(block.timestamp > uint256(d.fillDeadline) + REFUND_BUFFER, "SpokePool: refund buffer not elapsed");
        d.refunded = true;
        IERC20(d.inputToken).safeTransfer(msg.sender, d.inputAmount);
        emit Refunded(depositId, msg.sender, d.inputAmount);
    }

    // ───────────────────── Fill (destination-chain solver flow) ─────────────────────

    /// Relay struct sent from origin chain in the V3FundsDeposited event.
    struct RelayData {
        uint32 originChainId;
        uint32 depositId;
        address depositor;
        address inputToken;
        uint256 inputAmount;
        address outputToken;
        uint256 outputAmount;
        address recipient;
        uint32 fillDeadline;
        address solverRepaymentAddress;
    }

    /**
     * @notice Solver delivers outputToken to recipient on this chain.
     *         Solver MUST hold the outputToken in advance (it's their working capital).
     */
    function fillRelay(RelayData calldata r) external nonReentrant whenNotPaused {
        require(r.fillDeadline > block.timestamp, "SpokePool: fill window closed");
        // A fill must actually deliver something. A zero-output "fill" delivers
        // nothing and must never be able to unlock the origin principal.
        require(r.outputAmount > 0, "SpokePool: zero output");
        bytes32 relayHash = _relayHash(r);
        require(!filled[relayHash], "SpokePool: already filled");
        filled[relayHash] = true;

        // Solver delivers output tokens directly to recipient
        IERC20(r.outputToken).safeTransferFrom(msg.sender, r.recipient, r.outputAmount);

        // Solver accrues a claim on the origin-chain SpokePool's locked input.
        // The settlement message carries a commitment to the OUTPUT that was
        // actually delivered here (token / amount / recipient) so the origin can
        // verify it satisfies the deposit order before releasing any principal.
        bytes memory body = abi.encode(
            r.depositId,
            r.inputToken,
            r.inputAmount,
            r.outputToken,
            r.outputAmount,
            r.recipient,
            msg.sender, // solver who filled
            r.solverRepaymentAddress
        );
        messenger.dispatch(r.originChainId, remoteSpokePool[r.originChainId], body);

        emit FilledV3Relay(relayHash, msg.sender, r.originChainId, r.depositId, r.recipient, r.outputAmount);
    }

    function _relayHash(RelayData calldata r) internal pure returns (bytes32) {
        return keccak256(abi.encode(
            r.originChainId, r.depositId, r.depositor, r.inputToken, r.inputAmount,
            r.outputToken, r.outputAmount, r.recipient, r.fillDeadline, r.solverRepaymentAddress
        ));
    }

    // ───────────────────── Settlement (origin-chain inbound) ─────────────────────

    /// Called by AereMessenger when a destination chain confirms a fill.
    function handle(uint32 origin, bytes32 sender, bytes calldata body) external payable override {
        require(msg.sender == address(messenger), "SpokePool: caller not messenger");
        require(sender == remoteSpokePool[origin], "SpokePool: unknown remote spoke");

        (
            uint32 depositId,
            address inputToken,
            uint256 inputAmount,
            address outputToken,
            uint256 outputAmount,
            address recipient,
            address solver,
            address solverRepayment
        ) = abi.decode(body, (uint32, address, uint256, address, uint256, address, address, address));

        Deposit storage d = deposits[depositId];
        require(!d.settled, "SpokePool: settled");
        require(!d.refunded, "SpokePool: refunded");
        require(d.inputToken == inputToken && d.inputAmount == inputAmount, "SpokePool: settle mismatch");

        // Bind the release of principal to the OUTPUT that was actually
        // delivered on the destination. Repay only if the fill satisfied the
        // depositor's order: the correct output token, delivered to the intended
        // recipient, in at least the requested amount. This closes the theft
        // where a fill that delivered nothing (or the wrong output, or to the
        // wrong recipient) could still unlock and steal the locked principal.
        require(outputToken == d.outputToken, "SpokePool: output token mismatch");
        require(recipient == d.recipient, "SpokePool: recipient mismatch");
        require(outputAmount >= d.outputAmount, "SpokePool: output below requested");

        d.settled = true;

        // Use repayment address from the message — solver may want to receive on a different address
        address payoutTo = solverRepayment != address(0) ? solverRepayment : solver;
        pendingSettlement[payoutTo][inputToken] += inputAmount;
    }

    /// Solver withdraws accumulated settlements.
    function claimSettlement(address token) external nonReentrant {
        uint256 amount = pendingSettlement[msg.sender][token];
        require(amount > 0, "SpokePool: nothing to claim");
        pendingSettlement[msg.sender][token] = 0;
        IERC20(token).safeTransfer(msg.sender, amount);
        emit SettlementClaimed(msg.sender, token, amount);
    }
}
