// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

/// @title ERC8004ValidationRegistryV3Safe
/// @notice Validator staking + slashing with slashed-stake released to a
///         governance-owned treasury. Fixes R3 HIGH-R3-07 (locked slashed stake)
///         and provides pull-payment withdrawal of unslashed stake at any time.
contract ERC8004ValidationRegistryV3Safe {
    struct Validator {
        address stakeAsset;
        uint256 stakedAmount;
        uint256 lockedUntil;
        uint256 slashedAmount;
        bool active;
    }

    mapping(address => Validator) public validators;
    /// @notice Slashed stake credited for treasury-initiated withdrawal.
    /// @dev asset => treasury account => amount; address(0) denotes native currency.
    mapping(address => mapping(address => uint256)) public withdrawableSlashedStake;

    address public owner;
    address public treasury;
    uint256 public minStake;

    error NotOwner();
    error ZeroAddress();
    error AlreadyRegistered();
    error NotRegistered();
    error StakeTooLow();
    error StillLocked();
    error SlashExceedsStake();
    error TransferFailed();
    error NothingToWithdraw();

    event ValidatorRegisteredV3(address indexed validator, uint256 amount);
    event ValidatorSlashedV3(address indexed validator, uint256 amount, address indexed treasury, bytes32 reason);
    event ValidatorWithdrawnV3(address indexed validator, uint256 amount);
    event TreasurySetV3(address indexed previous, address indexed current);
    event MinStakeSetV3(uint256 previous, uint256 current);
    event SlashedStakeWithdrawnV3(address indexed treasury, address indexed asset, uint256 amount);

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    constructor(address _owner, address _treasury, uint256 _minStake) {
        if (_owner == address(0) || _treasury == address(0)) revert ZeroAddress();
        owner = _owner;
        treasury = _treasury;
        minStake = _minStake;
    }

    function setTreasury(address t) external onlyOwner {
        if (t == address(0)) revert ZeroAddress();
        emit TreasurySetV3(treasury, t);
        treasury = t;
    }

    function setMinStake(uint256 s) external onlyOwner {
        emit MinStakeSetV3(minStake, s);
        minStake = s;
    }

    function registerNative(uint256 lockUntil) external payable {
        if (validators[msg.sender].active) revert AlreadyRegistered();
        if (msg.value < minStake) revert StakeTooLow();
        validators[msg.sender] = Validator({
            stakeAsset: address(0), stakedAmount: msg.value, lockedUntil: lockUntil, slashedAmount: 0, active: true
        });
        emit ValidatorRegisteredV3(msg.sender, msg.value);
    }

    /// @notice Slash. Slashed amount is credited to the current treasury for
    ///         treasury-initiated withdrawal, so it is never locked forever.
    function slash(address validator, uint256 amount, bytes32 reason) external onlyOwner {
        Validator storage v = validators[validator];
        if (!v.active) revert NotRegistered();
        if (amount > v.stakedAmount) revert SlashExceedsStake();
        v.stakedAmount -= amount;
        v.slashedAmount += amount;
        withdrawableSlashedStake[v.stakeAsset][treasury] += amount;
        emit ValidatorSlashedV3(validator, amount, treasury, reason);
    }

    /// @notice Callable any time after lock expires. Pull-style.
    function withdraw() external {
        Validator storage v = validators[msg.sender];
        if (!v.active) revert NotRegistered();
        if (block.timestamp < v.lockedUntil) revert StillLocked();
        uint256 amt = v.stakedAmount;
        v.stakedAmount = 0;
        v.active = false;
        emit ValidatorWithdrawnV3(msg.sender, amt);
        _transferToCaller(v.stakeAsset, amt);
    }

    /// @notice Withdraw slashed stake credited to the calling treasury account.
    function withdrawSlashedStake(address asset) external {
        uint256 amount = withdrawableSlashedStake[asset][msg.sender];
        if (amount == 0) revert NothingToWithdraw();
        withdrawableSlashedStake[asset][msg.sender] = 0;
        emit SlashedStakeWithdrawnV3(msg.sender, asset, amount);
        _transferToCaller(asset, amount);
    }

    function _transferToCaller(address asset, uint256 amount) private {
        if (asset == address(0)) {
            (bool ok,) = payable(msg.sender).call{ value: amount }("");
            if (!ok) revert TransferFailed();
        } else {
            (bool ok, bytes memory ret) =
                asset.call(abi.encodeWithSignature("transfer(address,uint256)", msg.sender, amount));
            if (!ok || (ret.length != 0 && !abi.decode(ret, (bool)))) revert TransferFailed();
        }
    }
}
