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

/**
 * @title MultisigGovernanceV3
 * @notice OpenZeppelin-style multisig governance for critical owner functions.
 * @dev Security audit recommendation: replaces single-owner control with
 *      M-of-N multisig approval for critical protocol operations.
 *
 * @custom:security-enhancement From Professional Security Audit 2026
 * @custom:purpose Reduce centralization risk by requiring multiple signatures
 * @custom:audit-fix V3 applies all HIGH/MEDIUM/LOW audit findings plus enhancements.
 *
 * Design:
 * - Configurable M-of-N threshold (minimum 2).
 * - Any signer can propose a transaction.
 * - Each signer can confirm or revoke their confirmation.
 * - Transaction executes only when the confirmation threshold is met.
 * - Transactions expire after a configurable window (default 7 days).
 * - Signers can be added/removed via the multisig itself (max 20 signers).
 * - Removing a signer purges their ghost confirmations from all pending txs.
 */
contract MultisigGovernanceV3 {

    // ============ Errors ============

    error NotSigner();
    error ZeroAddress();
    error InvalidThreshold();
    error DuplicateSigner();
    error SignerNotFound();
    error TransactionNotFound();
    error TransactionAlreadyExecuted();
    error TransactionExpired();
    error AlreadyConfirmed();
    error NotConfirmed();
    error ThresholdNotMet();
    error ExecutionFailed();
    error CannotRemoveLastSigner();
    error ThresholdExceedsSigners();
    // audit-v3: MEDIUM-2: max-signers cap enforcement
    error MaxSignersReached();
    // audit-v3: LOW-2: custom error replaces require string in onlySelf
    error OnlySelf();

    // ============ Events ============

    event TransactionProposed(
        uint256 indexed txId,
        address indexed proposer,
        address indexed target,
        bytes data,
        uint256 value,
        uint256 expiresAt
    );

    event TransactionConfirmed(
        uint256 indexed txId,
        address indexed signer
    );

    event ConfirmationRevoked(
        uint256 indexed txId,
        address indexed signer
    );

    event TransactionExecuted(
        uint256 indexed txId,
        address indexed executor
    );

    event SignerAdded(address indexed signer);
    event SignerRemoved(address indexed signer);
    event ThresholdChanged(uint256 oldThreshold, uint256 newThreshold);

    // audit-v3: LOW-3: emitted in receive() when ETH arrives.
    event ETHReceived(address indexed sender, uint256 amount);

    // ============ Structs ============

    struct Transaction {
        address target;
        uint256 value;
        bytes data;
        bool executed;
        uint256 confirmations;
        uint256 proposedAt;
        uint256 expiresAt;
    }

    // ============ Constants ============

    /// @notice Default transaction expiry window (7 days).
    uint256 public constant TRANSACTION_EXPIRY = 7 days;

    // audit-v3: MEDIUM-1: threshold can never drop below 2.
    uint256 public constant MINIMUM_THRESHOLD = 2;

    // audit-v3: MEDIUM-2: maximum number of signers.
    uint256 public constant MAX_SIGNERS = 20;

    // ============ State Variables ============

    /// @notice List of signers.
    address[] public signers;

    /// @notice Mapping for O(1) signer lookup.
    mapping(address => bool) public isSigner;

    /// @notice Number of confirmations required to execute a transaction.
    uint256 public threshold;

    /// @notice All proposed transactions.
    mapping(uint256 => Transaction) public transactions;

    /// @notice Confirmation status: txId => signer => confirmed.
    mapping(uint256 => mapping(address => bool)) public confirmations;

    /// @notice Total number of proposed transactions.
    uint256 public transactionCount;

    // audit-v3: HIGH-1: tracks IDs of all unexecuted, non-expired pending transactions.
    ///      Added to in proposeTransaction; removed in executeTransaction and when expired
    ///      during removeSigner iteration.
    uint256[] public pendingTxIds;

    // audit-v3: HIGH-1: index of each txId in pendingTxIds for O(1) removal.
    mapping(uint256 => uint256) internal _pendingIndex;

    // ============ Modifiers ============

    modifier onlySigner() {
        if (!isSigner[msg.sender]) revert NotSigner();
        _;
    }

    // audit-v3: LOW-2: replaced require string with custom error OnlySelf.
    modifier onlySelf() {
        if (msg.sender != address(this)) revert OnlySelf();
        _;
    }

    modifier txExists(uint256 txId) {
        if (txId >= transactionCount) revert TransactionNotFound();
        _;
    }

    modifier notExecuted(uint256 txId) {
        if (transactions[txId].executed) revert TransactionAlreadyExecuted();
        _;
    }

    modifier notExpired(uint256 txId) {
        if (block.timestamp > transactions[txId].expiresAt) revert TransactionExpired();
        _;
    }

    // ============ Constructor ============

    /// @notice Deploy the multisig governance contract.
    /// @param _signers   Initial list of signers.
    /// @param _threshold Number of confirmations required (M in M-of-N).
    // audit-v3: MEDIUM-1: constructor enforces MINIMUM_THRESHOLD = 2.
    // audit-v3: MEDIUM-2: constructor enforces MAX_SIGNERS.
    constructor(address[] memory _signers, uint256 _threshold) {
        if (_signers.length == 0) revert InvalidThreshold();
        // MEDIUM-1: threshold must be at least MINIMUM_THRESHOLD.
        if (_threshold < MINIMUM_THRESHOLD || _threshold > _signers.length) {
            revert InvalidThreshold();
        }
        // MEDIUM-2: initial signer set cannot exceed MAX_SIGNERS.
        if (_signers.length > MAX_SIGNERS) revert MaxSignersReached();

        for (uint256 i = 0; i < _signers.length;) {
            address signer = _signers[i];
            if (signer == address(0)) revert ZeroAddress();
            if (isSigner[signer]) revert DuplicateSigner();

            isSigner[signer] = true;
            signers.push(signer);

            emit SignerAdded(signer);
            unchecked { ++i; }
        }

        threshold = _threshold;
    }

    // ============ Transaction Lifecycle ============

    /// @notice Propose a new transaction.
    /// @param target The target contract address.
    /// @param value  ETH value to send.
    /// @param data   The calldata to execute.
    /// @return txId  The ID of the proposed transaction.
    // audit-v3: HIGH-1: adds txId to pendingTxIds for efficient signer removal sweep.
    function proposeTransaction(
        address target,
        uint256 value,
        bytes calldata data
    ) external onlySigner returns (uint256 txId) {
        if (target == address(0)) revert ZeroAddress();

        txId = transactionCount++;

        transactions[txId] = Transaction({
            target: target,
            value: value,
            data: data,
            executed: false,
            confirmations: 0,
            proposedAt: block.timestamp,
            expiresAt: block.timestamp + TRANSACTION_EXPIRY
        });

        // HIGH-1: register in pendingTxIds before auto-confirming.
        _addToPending(txId);

        emit TransactionProposed(
            txId, msg.sender, target, data, value,
            block.timestamp + TRANSACTION_EXPIRY
        );

        // Auto-confirm for the proposer.
        _confirm(txId);
    }

    /// @notice Confirm a pending transaction.
    /// @param txId The transaction ID to confirm.
    function confirmTransaction(uint256 txId)
        external
        onlySigner
        txExists(txId)
        notExecuted(txId)
        notExpired(txId)
    {
        _confirm(txId);
    }

    /// @notice Revoke a previous confirmation.
    /// @param txId The transaction ID to revoke confirmation for.
    // audit-v3: LOW-1: notExpired added to revokeConfirmation.
    function revokeConfirmation(uint256 txId)
        external
        onlySigner
        txExists(txId)
        notExecuted(txId)
        notExpired(txId)
    {
        if (!confirmations[txId][msg.sender]) revert NotConfirmed();

        confirmations[txId][msg.sender] = false;
        transactions[txId].confirmations--;

        emit ConfirmationRevoked(txId, msg.sender);
    }

    /// @notice Execute a transaction that has met the confirmation threshold.
    /// @param txId The transaction ID to execute.
    // audit-v3: HIGH-1: removes txId from pendingTxIds on execution.
    function executeTransaction(uint256 txId)
        external
        onlySigner
        txExists(txId)
        notExecuted(txId)
        notExpired(txId)
    {
        Transaction storage txn = transactions[txId];
        if (txn.confirmations < threshold) revert ThresholdNotMet();

        txn.executed = true;

        // HIGH-1: remove from pending tracking.
        _removeFromPending(txId);

        (bool success, ) = txn.target.call{value: txn.value}(txn.data);
        if (!success) revert ExecutionFailed();

        emit TransactionExecuted(txId, msg.sender);
    }

    // ============ Signer Management (via multisig only) ============

    /// @notice Add a new signer.  Can only be called by the multisig itself.
    /// @param signer The address to add as a signer.
    // audit-v3: MEDIUM-2: enforces MAX_SIGNERS cap.
    function addSigner(address signer) external onlySelf {
        if (signer == address(0)) revert ZeroAddress();
        if (isSigner[signer]) revert DuplicateSigner();
        // MEDIUM-2: cap at MAX_SIGNERS.
        if (signers.length >= MAX_SIGNERS) revert MaxSignersReached();

        isSigner[signer] = true;
        signers.push(signer);

        emit SignerAdded(signer);
    }

    /// @notice Remove a signer.  Can only be called by the multisig itself.
    /// @param signer The address to remove.
    // audit-v3: HIGH-1: iterates pendingTxIds and decrements confirmation count
    ///      for any pending tx the removed signer had confirmed, preventing ghost confirmations.
    function removeSigner(address signer) external onlySelf {
        if (!isSigner[signer]) revert SignerNotFound();
        if (signers.length <= 1) revert CannotRemoveLastSigner();
        if (signers.length - 1 < threshold) revert ThresholdExceedsSigners();

        isSigner[signer] = false;

        // Swap-and-pop from signers array.
        uint256 length = signers.length;
        for (uint256 i = 0; i < length;) {
            if (signers[i] == signer) {
                signers[i] = signers[length - 1];
                signers.pop();
                break;
            }
            unchecked { ++i; }
        }

        // HIGH-1: purge ghost confirmations from all pending transactions.
        // Iterate pendingTxIds in reverse so we can safely remove stale entries as we go.
        uint256 pendingLen = pendingTxIds.length;
        uint256 i = pendingLen;
        while (i > 0) {
            unchecked { --i; }
            uint256 txId = pendingTxIds[i];
            Transaction storage txn = transactions[txId];

            // Clean up expired entries opportunistically.
            if (block.timestamp > txn.expiresAt) {
                _removeFromPendingAt(i);
                continue;
            }

            // If the removed signer had confirmed this tx, revoke their confirmation.
            if (confirmations[txId][signer]) {
                confirmations[txId][signer] = false;
                txn.confirmations--;
                // Note: we do not emit ConfirmationRevoked here as the signer is no longer
                // a participant — callers should observe SignerRemoved to infer the change.
            }
        }

        emit SignerRemoved(signer);
    }

    /// @notice Change the confirmation threshold.  Can only be called by the multisig itself.
    /// @param newThreshold The new threshold value.
    // audit-v3: MEDIUM-1: enforces MINIMUM_THRESHOLD = 2 in changeThreshold.
    function changeThreshold(uint256 newThreshold) external onlySelf {
        // MEDIUM-1: threshold must remain at or above MINIMUM_THRESHOLD.
        if (newThreshold < MINIMUM_THRESHOLD || newThreshold > signers.length) {
            revert InvalidThreshold();
        }

        uint256 oldThreshold = threshold;
        threshold = newThreshold;

        emit ThresholdChanged(oldThreshold, newThreshold);
    }

    // ============ View Functions ============

    /// @notice Get the list of all signers.
    /// @return Array of signer addresses.
    function getSigners() external view returns (address[] memory) {
        return signers;
    }

    /// @notice Get the number of signers.
    /// @return Number of signers.
    function getSignerCount() external view returns (uint256) {
        return signers.length;
    }

    /// @notice Get transaction details.
    /// @param txId The transaction ID.
    /// @return target           The target address.
    /// @return value            The ETH value.
    /// @return data             The calldata.
    /// @return executed         Whether the transaction has been executed.
    /// @return numConfirmations The current number of confirmations.
    /// @return expiresAt        The expiry timestamp.
    /// @return proposedAt       The proposal timestamp.
    // audit-v3: Enhancement: proposedAt added to return tuple.
    function getTransaction(uint256 txId) external view returns (
        address target,
        uint256 value,
        bytes memory data,
        bool executed,
        uint256 numConfirmations,
        uint256 expiresAt,
        uint256 proposedAt
    ) {
        Transaction storage txn = transactions[txId];
        return (
            txn.target,
            txn.value,
            txn.data,
            txn.executed,
            txn.confirmations,
            txn.expiresAt,
            txn.proposedAt
        );
    }

    /// @notice Check if a transaction is ready to execute.
    /// @param txId The transaction ID.
    /// @return ready True if the transaction can be executed.
    function isTransactionReady(uint256 txId) external view returns (bool) {
        if (txId >= transactionCount) return false;
        Transaction storage txn = transactions[txId];
        return !txn.executed &&
               txn.confirmations >= threshold &&
               block.timestamp <= txn.expiresAt;
    }

    /// @notice Get the list of all pending (unexecuted, tracked) transaction IDs.
    /// @return Array of pending transaction IDs.
    function getPendingTxIds() external view returns (uint256[] memory) {
        return pendingTxIds;
    }

    // ============ Internal Functions ============

    function _confirm(uint256 txId) internal {
        if (confirmations[txId][msg.sender]) revert AlreadyConfirmed();

        confirmations[txId][msg.sender] = true;
        transactions[txId].confirmations++;

        emit TransactionConfirmed(txId, msg.sender);
    }

    /// @dev Add a txId to the pendingTxIds tracking array and record its index.
    // audit-v3: HIGH-1: pendingTxIds management — add.
    function _addToPending(uint256 txId) internal {
        _pendingIndex[txId] = pendingTxIds.length;
        pendingTxIds.push(txId);
    }

    /// @dev Remove a txId from pendingTxIds by its stored index (swap-and-pop).
    // audit-v3: HIGH-1: pendingTxIds management — remove by txId.
    function _removeFromPending(uint256 txId) internal {
        uint256 idx = _pendingIndex[txId];
        _removeFromPendingAt(idx);
    }

    /// @dev Remove the entry at position `idx` in pendingTxIds (swap-and-pop).
    // audit-v3: HIGH-1: pendingTxIds management — remove by index.
    function _removeFromPendingAt(uint256 idx) internal {
        uint256 lastIdx = pendingTxIds.length - 1;
        if (idx != lastIdx) {
            uint256 movedTxId = pendingTxIds[lastIdx];
            pendingTxIds[idx] = movedTxId;
            _pendingIndex[movedTxId] = idx;
        }
        pendingTxIds.pop();
        // Clear the index of the removed entry (set to 0; harmless since 0 is also a valid
        // index, but the entry will no longer be in the array).
        // The txId itself is already removed from the array so _pendingIndex won't be
        // re-consulted for it unless it's re-proposed (which assigns a new txId anyway).
    }

    // ============ Receive ETH ============

    // audit-v3: LOW-3: ETHReceived event emitted when ETH arrives.
    receive() external payable {
        emit ETHReceived(msg.sender, msg.value);
    }
}
