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

import {IGenericStateChannelManager} from "./IGenericStateChannelManager.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

/**
 * @title GenericStateChannelManager
 * @notice State channels with committed stateHash: open, fund, close with (stateHash, balanceA, balanceB, nonce). stateHash attests to arbitrary off-chain state (e.g. game, attestation).
 * @dev Same lifecycle as PaymentChannelManager; close/submit/challenge include stateHash so settlement commits to agreed state.
 */
contract GenericStateChannelManager is IGenericStateChannelManager, ReentrancyGuard {
    address public admin;
    bool public paused;

    uint256 public challengeWindowSeconds;

    uint256 private _nextChannelId;
    mapping(uint256 => Channel) private _channels;
    uint256[] private _channelIds;

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

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

    constructor(address _admin, uint256 _challengeWindowSeconds) {
        require(_admin != address(0), "zero admin");
        require(_challengeWindowSeconds > 0, "zero challenge window");
        admin = _admin;
        challengeWindowSeconds = _challengeWindowSeconds;
    }

    function openChannel(address participantB) external payable whenNotPaused returns (uint256 channelId) {
        require(participantB != address(0), "zero participant");
        require(participantB != msg.sender, "self channel");
        require(msg.value > 0, "zero deposit");

        channelId = _nextChannelId++;
        _channels[channelId] = Channel({
            participantA: msg.sender,
            participantB: participantB,
            depositA: msg.value,
            depositB: 0,
            status: ChannelStatus.Open,
            disputeNonce: 0,
            disputeStateHash: bytes32(0),
            disputeBalanceA: 0,
            disputeBalanceB: 0,
            disputeDeadline: 0
        });
        _channelIds.push(channelId);
        emit ChannelOpened(channelId, msg.sender, participantB, msg.value, 0);
        return channelId;
    }

    function fundChannel(uint256 channelId) external payable whenNotPaused {
        Channel storage ch = _channels[channelId];
        require(ch.status == ChannelStatus.Open, "not open");
        require(ch.depositB == 0, "already funded");
        require(msg.sender == ch.participantB, "not participant B");
        require(msg.value > 0, "zero deposit");
        ch.depositB = msg.value;
        emit ChannelOpened(channelId, ch.participantA, ch.participantB, ch.depositA, ch.depositB);
    }

    function closeChannelCooperative(
        uint256 channelId,
        bytes32 stateHash,
        uint256 nonce,
        uint256 balanceA,
        uint256 balanceB,
        uint8 vA,
        bytes32 rA,
        bytes32 sA,
        uint8 vB,
        bytes32 rB,
        bytes32 sB
    ) external nonReentrant {
        Channel storage ch = _channels[channelId];
        require(ch.status == ChannelStatus.Open, "not open");
        uint256 total = ch.depositA + ch.depositB;
        require(balanceA + balanceB == total, "balance sum");
        _verifySignatures(channelId, stateHash, ch.participantA, ch.participantB, nonce, balanceA, balanceB, vA, rA, sA, vB, rB, sB);
        ch.status = ChannelStatus.Closed;
        _transfer(ch.participantA, balanceA);
        _transfer(ch.participantB, balanceB);
        emit ChannelClosed(channelId, stateHash, balanceA, balanceB, true);
    }

    function submitClose(
        uint256 channelId,
        bytes32 stateHash,
        uint256 nonce,
        uint256 balanceA,
        uint256 balanceB,
        uint8 vA,
        bytes32 rA,
        bytes32 sA,
        uint8 vB,
        bytes32 rB,
        bytes32 sB
    ) external {
        Channel storage ch = _channels[channelId];
        require(ch.status == ChannelStatus.Open || ch.status == ChannelStatus.Dispute, "wrong status");
        uint256 total = ch.depositA + ch.depositB;
        require(balanceA + balanceB == total, "balance sum");
        _verifySignatures(channelId, stateHash, ch.participantA, ch.participantB, nonce, balanceA, balanceB, vA, rA, sA, vB, rB, sB);
        if (ch.status == ChannelStatus.Open) {
            ch.status = ChannelStatus.Dispute;
        } else {
            require(nonce > ch.disputeNonce, "older state");
        }
        ch.disputeNonce = nonce;
        ch.disputeStateHash = stateHash;
        ch.disputeBalanceA = balanceA;
        ch.disputeBalanceB = balanceB;
        ch.disputeDeadline = block.timestamp + challengeWindowSeconds;
        emit ChallengeSubmitted(channelId, nonce, stateHash, balanceA, balanceB, ch.disputeDeadline);
    }

    function challengeClose(
        uint256 channelId,
        bytes32 stateHash,
        uint256 nonce,
        uint256 balanceA,
        uint256 balanceB,
        uint8 vA,
        bytes32 rA,
        bytes32 sA,
        uint8 vB,
        bytes32 rB,
        bytes32 sB
    ) external {
        Channel storage ch = _channels[channelId];
        require(ch.status == ChannelStatus.Dispute, "not in dispute");
        uint256 total = ch.depositA + ch.depositB;
        require(balanceA + balanceB == total, "balance sum");
        require(nonce > ch.disputeNonce, "not newer");
        _verifySignatures(channelId, stateHash, ch.participantA, ch.participantB, nonce, balanceA, balanceB, vA, rA, sA, vB, rB, sB);
        ch.disputeNonce = nonce;
        ch.disputeStateHash = stateHash;
        ch.disputeBalanceA = balanceA;
        ch.disputeBalanceB = balanceB;
        ch.disputeDeadline = block.timestamp + challengeWindowSeconds;
        emit ChallengeSubmitted(channelId, nonce, stateHash, balanceA, balanceB, ch.disputeDeadline);
    }

    function finalizeClose(uint256 channelId) external nonReentrant {
        Channel storage ch = _channels[channelId];
        require(ch.status == ChannelStatus.Dispute, "not in dispute");
        require(block.timestamp >= ch.disputeDeadline, "window open");
        ch.status = ChannelStatus.Closed;
        _transfer(ch.participantA, ch.disputeBalanceA);
        _transfer(ch.participantB, ch.disputeBalanceB);
        emit ChannelClosed(channelId, ch.disputeStateHash, ch.disputeBalanceA, ch.disputeBalanceB, false);
    }

    function getChannel(uint256 channelId) external view returns (Channel memory) {
        return _channels[channelId];
    }

    function getChannelCount() external view returns (uint256) {
        return _channelIds.length;
    }

    function getChannelId(address participantA, address participantB) external view returns (uint256) {
        for (uint256 i = 0; i < _channelIds.length; i++) {
            Channel storage ch = _channels[_channelIds[i]];
            if (
                (ch.participantA == participantA && ch.participantB == participantB) ||
                (ch.participantA == participantB && ch.participantB == participantA)
            ) {
                if (ch.status == ChannelStatus.Open || ch.status == ChannelStatus.Dispute) {
                    return _channelIds[i];
                }
            }
        }
        return 0;
    }

    function getChannelIdByIndex(uint256 index) external view returns (uint256) {
        require(index < _channelIds.length, "out of bounds");
        return _channelIds[index];
    }

    function setAdmin(address newAdmin) external onlyAdmin {
        require(newAdmin != address(0), "zero admin");
        admin = newAdmin;
        emit AdminChanged(newAdmin);
    }

    function setChallengeWindow(uint256 newWindow) external onlyAdmin {
        require(newWindow > 0, "zero window");
        uint256 old = challengeWindowSeconds;
        challengeWindowSeconds = newWindow;
        emit ChallengeWindowUpdated(old, newWindow);
    }

    function pause() external onlyAdmin {
        paused = true;
        emit Paused();
    }

    function unpause() external onlyAdmin {
        paused = false;
        emit Unpaused();
    }

    function _verifySignatures(
        uint256 channelId,
        bytes32 stateHash,
        address participantA,
        address participantB,
        uint256 nonce,
        uint256 balanceA,
        uint256 balanceB,
        uint8 vA,
        bytes32 rA,
        bytes32 sA,
        uint8 vB,
        bytes32 rB,
        bytes32 sB
    ) internal pure {
        bytes32 stateHashInner = keccak256(abi.encodePacked(channelId, stateHash, nonce, balanceA, balanceB));
        bytes32 ethSigned = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", stateHashInner));
        address signerA = ECDSA.recover(ethSigned, vA, rA, sA);
        address signerB = ECDSA.recover(ethSigned, vB, rB, sB);
        require(signerA == participantA && signerB == participantB, "invalid sigs");
    }

    function _transfer(address to, uint256 amount) internal {
        if (amount > 0) {
            (bool ok,) = payable(to).call{value: amount}("");
            require(ok, "transfer failed");
        }
    }

    receive() external payable {}
}
