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

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

/**
 * @title AerePaymentChannels
 * @notice Bidirectional payment channels (L2-style state channels) for AERE Network.
 * @dev Two parties open a channel by depositing AERE. Off-chain they exchange signed
 *      states (a, b, nonce) representing the current split of the channel balance.
 *      Either party closes by submitting the latest co-signed state on-chain. A
 *      challenge window allows the counterparty to override with a higher-nonce state.
 *      After the challenge window expires, funds are settled.
 *
 *      Whitepaper alignment: "L2 layers — state channels for instant micropayments"
 */
contract AerePaymentChannels is ReentrancyGuard {
    using ECDSA for bytes32;

    enum Status { None, Open, Closing, Closed }

    struct Channel {
        address partyA;
        address partyB;
        uint256 balanceA;
        uint256 balanceB;
        uint256 nonce;
        uint64  challengeDeadline; // 0 unless Closing
        Status  status;
    }

    uint64 public constant CHALLENGE_PERIOD = 1 days;
    uint256 public nextChannelId = 1;
    mapping(uint256 => Channel) public channels;

    event ChannelOpened(uint256 indexed id, address indexed partyA, address indexed partyB, uint256 deposit);
    event ChannelFunded(uint256 indexed id, address indexed party, uint256 amount);
    event ChannelClosing(uint256 indexed id, uint256 nonce, uint256 balanceA, uint256 balanceB, uint64 deadline);
    event ChannelChallenged(uint256 indexed id, uint256 nonce, uint256 balanceA, uint256 balanceB);
    event ChannelClosed(uint256 indexed id, uint256 finalA, uint256 finalB);

    function open(address counterparty) external payable returns (uint256 id) {
        require(counterparty != address(0) && counterparty != msg.sender, "bad cp");
        require(msg.value > 0, "deposit");
        id = nextChannelId++;
        channels[id] = Channel({
            partyA: msg.sender,
            partyB: counterparty,
            balanceA: msg.value,
            balanceB: 0,
            nonce: 0,
            challengeDeadline: 0,
            status: Status.Open
        });
        emit ChannelOpened(id, msg.sender, counterparty, msg.value);
    }

    function fund(uint256 id) external payable {
        Channel storage c = channels[id];
        require(c.status == Status.Open, "not open");
        require(msg.value > 0, "amount");
        if (msg.sender == c.partyA) c.balanceA += msg.value;
        else if (msg.sender == c.partyB) c.balanceB += msg.value;
        else revert("not party");
        emit ChannelFunded(id, msg.sender, msg.value);
    }

    /**
     * @notice Initiate cooperative or unilateral close with a co-signed state.
     * @param id        channel id
     * @param balanceA  proposed final balance for partyA
     * @param balanceB  proposed final balance for partyB
     * @param nonce     state nonce (higher overrides lower during challenge)
     * @param sigA      partyA signature over (this, id, balanceA, balanceB, nonce)
     * @param sigB      partyB signature over the same digest
     */
    function closeWithState(
        uint256 id,
        uint256 balanceA,
        uint256 balanceB,
        uint256 nonce,
        bytes calldata sigA,
        bytes calldata sigB
    ) external nonReentrant {
        Channel storage c = channels[id];
        require(c.status == Status.Open || c.status == Status.Closing, "bad status");
        require(balanceA + balanceB <= c.balanceA + c.balanceB, "overspend");
        if (c.status == Status.Closing) require(nonce > c.nonce, "stale nonce");

        bytes32 digest = _stateDigest(id, balanceA, balanceB, nonce);
        require(digest.recover(sigA) == c.partyA, "sigA");
        require(digest.recover(sigB) == c.partyB, "sigB");

        c.balanceA = balanceA;
        c.balanceB = balanceB;
        c.nonce = nonce;

        if (c.status == Status.Open) {
            c.status = Status.Closing;
            c.challengeDeadline = uint64(block.timestamp) + CHALLENGE_PERIOD;
            emit ChannelClosing(id, nonce, balanceA, balanceB, c.challengeDeadline);
        } else {
            emit ChannelChallenged(id, nonce, balanceA, balanceB);
        }
    }

    function settle(uint256 id) external nonReentrant {
        Channel storage c = channels[id];
        require(c.status == Status.Closing, "not closing");
        require(block.timestamp >= c.challengeDeadline, "challenge");
        c.status = Status.Closed;
        uint256 a = c.balanceA;
        uint256 b = c.balanceB;
        c.balanceA = 0;
        c.balanceB = 0;
        if (a > 0) { (bool ok, ) = c.partyA.call{value: a}(""); require(ok, "A"); }
        if (b > 0) { (bool ok, ) = c.partyB.call{value: b}(""); require(ok, "B"); }
        emit ChannelClosed(id, a, b);
    }

    function _stateDigest(uint256 id, uint256 balanceA, uint256 balanceB, uint256 nonce) internal view returns (bytes32) {
        return keccak256(
            abi.encodePacked("AereChannel", address(this), block.chainid, id, balanceA, balanceB, nonce)
        ).toEthSignedMessageHash();
    }
}
