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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/**
 * @title AereStakingV2
 * @notice Bug-fix redeploy of AereStaking. Delegated stake pool for AERE Network
 *         validators: token holders delegate native AERE to validators and earn a
 *         configurable APY (default 8%, matching the whitepaper / genesis rewardRate).
 *         Validators post a self-stake bond and take a commission on delegator rewards.
 *
 *         ─────────────────────────── WHY V2 ───────────────────────────
 *         V1 (0xAbDb01d9A4f41792129b2654Fb6DDB9689360DEc, chain 2800) shipped with
 *         five confirmed bugs. V1 currently has 0 validators / 0 staked / 0 balance,
 *         so a clean V2 with corrected logic is safe (no state to migrate).
 *
 *         F2 (HIGH — reward drain on top-up).
 *           V1.delegate() only set lastClaimBlock on the FIRST delegation. Topping up
 *           an existing delegation left lastClaimBlock untouched, so the freshly-added
 *           stake retroactively earned a full period of rewards on stake that did not
 *           exist yet (~10x inflation for a 9x top-up).
 *           FIX: every amount-changing path SETTLES pending rewards into an accrued
 *           bucket and resets the accrual anchor (lastClaimTime) BEFORE changing the
 *           balance, so a top-up earns 0 retroactive reward on the new stake.
 *
 *         F3 (HIGH — locked self-stake).
 *           V1 had NO path returning a validator's registerValidator() self-stake.
 *           The only withdraw primitives (unbond/claimUnbonded) acted on DELEGATIONS,
 *           and a validator never self-delegated, so the bond was locked forever.
 *           FIX: deregisterValidator() queues the remaining self-stake into the SAME
 *           unbonding queue used by delegators; claimUnbonded() releases it after
 *           UNBONDING_PERIOD. Works on an already-deactivated (slashed-out) validator
 *           too, so residual post-slash self-stake is always recoverable. totalStaked
 *           is decremented by exactly the live (post-slash) self-stake, so slashed
 *           value is accounted (it stays in the pool) and never double-withdrawn.
 *
 *         F4 (MED — unbond brick on re-registration).
 *           V1.registerValidator() overwrote the whole Validator struct, zeroing
 *           totalDelegated while delegations still existed. A slashed-then-deactivated
 *           validator that re-registered bricked every delegator's unbond (totalDelegated
 *           underflow, panic 0x11), and pushed a DUPLICATE entry into validatorList.
 *           FIX: re-registration PRESERVES totalDelegated, commissionAccrued and all
 *           delegations; self-stake is ADDED (never reset); the validator is pushed to
 *           validatorList exactly once (exists flag).
 *
 *         F5 (MED — APY ~5x underpay).
 *           V1.pendingRewards() divided by a hardcoded 315,360,000 blocks/year, which
 *           assumes 0.1s blocks. The chain runs ~0.5s blocks (measured 0.5055 s/block),
 *           so realized APY was ~1.6% instead of the intended 8% (exactly 5x low), and
 *           any future block-time change would shift it again.
 *           FIX: accrual is TIMESTAMP-based (reward = amount * rewardRateBps *
 *           secondsElapsed / (10000 * 365 days)), immune to block time. The rate is an
 *           owner-settable parameter, rewardRateBps, default 800 (8% APY) so the
 *           Foundation can tune tokenomics without a redeploy.
 *
 *         LOW (commission silently kept).
 *           V1 deducted validator commission from each delegator's reward but never
 *           credited it anywhere — it was silently dropped in the pool.
 *           FIX: commission is credited to the validator's commissionAccrued balance on
 *           every settle and withdrawn via claimCommission().
 *
 *         Everything else is intentionally identical to V1: delegate, unbond,
 *         claimUnbonded, owner-only slash, MIN_VALIDATOR_SELF_STAKE, MIN_DELEGATION,
 *         UNBONDING_PERIOD, SLASH_RATE_BPS.
 *
 *         SCOPE / NON-GOALS (unchanged from V1, documented honestly):
 *           - A validator's OWN self-stake earns no staking reward; only delegations
 *             accrue (self-stake is a slashable bond). Same as V1.
 *           - Rewards + commission are paid from this contract's native AERE balance,
 *             which the Foundation must seed until protocol-level minting is wired to
 *             consensus. Same as V1 (see claimRewards).
 *           - slash() leaves the slashed AERE in the pool (totalStaked reduced, balance
 *             unchanged) as surplus backing rewards. There is deliberately NO admin
 *             withdrawal of principal. Same as V1.
 */
contract AereStakingV2 is Ownable, ReentrancyGuard {
    uint256 public constant MIN_VALIDATOR_SELF_STAKE = 500 ether;       // 500 AERE
    uint256 public constant MIN_DELEGATION = 1 ether;                    // 1 AERE
    uint256 public constant UNBONDING_PERIOD = 7 days;
    uint256 public constant SLASH_RATE_BPS = 500;                        // 5%
    uint256 public constant MAX_COMMISSION_BPS = 5000;                   // 50% cap on commission
    uint256 public constant BPS_DENOM = 10000;
    uint256 public constant YEAR = 365 days;                             // 31,536,000 s (timestamp accrual base)
    uint256 public constant MAX_REWARD_RATE_BPS = 10000;                 // 100% APY guard on the settable rate

    /// @notice Owner-settable annual reward rate in basis points. Default 800 = 8% APY
    ///         (matches whitepaper / genesis). Foundation can tune via setRewardRateBps.
    uint256 public rewardRateBps = 800;

    struct Validator {
        bool active;
        bool exists;                 // ever registered (validatorList dedup + re-register detection)
        uint256 selfStake;
        uint256 totalDelegated;
        uint256 commissionBps;       // out of 10000
        uint256 commissionAccrued;   // validator's claimable commission (LOW fix)
    }

    struct Delegation {
        uint256 amount;
        uint256 stakedAt;
        uint256 lastClaimTime;       // timestamp accrual anchor (F5: was lastClaimBlock)
        uint256 rewardAccrued;       // settled NET rewards owed to the delegator (F2 fix)
    }

    struct UnbondRequest {
        uint256 amount;
        uint256 readyAt;
    }

    mapping(address => Validator) public validators;
    mapping(address => mapping(address => Delegation)) public delegations; // validator => delegator => stake
    mapping(address => UnbondRequest[]) public unbondQueue;

    address[] public validatorList;
    uint256 public totalStaked;

    event ValidatorRegistered(address indexed validator, uint256 selfStake, uint256 commissionBps);
    event ValidatorDeactivated(address indexed validator);
    event ValidatorDeregistered(address indexed validator, uint256 selfStakeUnbonded, uint256 readyAt);
    event Delegated(address indexed validator, address indexed delegator, uint256 amount);
    event UnbondInitiated(address indexed validator, address indexed delegator, uint256 amount, uint256 readyAt);
    event UnbondClaimed(address indexed delegator, uint256 amount);
    event RewardsClaimed(address indexed delegator, uint256 amount);
    event CommissionClaimed(address indexed validator, uint256 amount);
    event ValidatorSlashed(address indexed validator, uint256 amount, string reason);
    event RewardRateUpdated(uint256 oldRateBps, uint256 newRateBps);

    constructor(address initialOwner) {
        require(initialOwner != address(0), "AereStakingV2: zero owner");
        _transferOwnership(initialOwner);
    }

    /* ----------------------------- reward math ------------------------------ */

    /// @notice Gross reward for `amount` staked over `secondsElapsed`, at the current
    ///         rewardRateBps. Timestamp-based → immune to block-time changes (F5).
    function _grossReward(uint256 amount, uint256 secondsElapsed) internal view returns (uint256) {
        if (amount == 0 || secondsElapsed == 0) return 0;
        return (amount * rewardRateBps * secondsElapsed) / (BPS_DENOM * YEAR);
    }

    /// @notice Settle a delegation's accrued rewards up to now, splitting off validator
    ///         commission, and advance the accrual anchor. MUST be called before any
    ///         change to d.amount so freshly-added/removed stake cannot earn rewards for
    ///         a window in which it was not staked (F2). Credits commission to the
    ///         validator (LOW fix).
    function _settle(address validator, address delegator) internal {
        Delegation storage d = delegations[validator][delegator];
        if (d.amount > 0) {
            uint256 secs = block.timestamp - d.lastClaimTime;
            uint256 gross = _grossReward(d.amount, secs);
            if (gross > 0) {
                uint256 commission = (gross * validators[validator].commissionBps) / BPS_DENOM;
                d.rewardAccrued += (gross - commission);
                validators[validator].commissionAccrued += commission;
            }
        }
        d.lastClaimTime = block.timestamp;
    }

    /* ---------------------------- validator ops ----------------------------- */

    /**
     * @notice Register as a validator, or re-activate a previously-deactivated one.
     *         Sender deposits self-stake (msg.value, >= MIN_VALIDATOR_SELF_STAKE).
     * @param commissionBps Validator's cut of delegator rewards (e.g. 1000 = 10%).
     *
     * F4 FIX: on re-registration the existing totalDelegated, commissionAccrued and all
     * delegations are PRESERVED (never zeroed); self-stake is ADDED; the address is
     * appended to validatorList only once.
     */
    function registerValidator(uint256 commissionBps) external payable nonReentrant {
        require(msg.value >= MIN_VALIDATOR_SELF_STAKE, "AereStakingV2: insufficient self-stake");
        require(commissionBps <= MAX_COMMISSION_BPS, "AereStakingV2: max commission 50%");
        Validator storage v = validators[msg.sender];
        require(!v.active, "AereStakingV2: already active");

        if (!v.exists) {
            v.exists = true;
            validatorList.push(msg.sender);     // F4: exactly once, no duplicates
        }
        // F4: do NOT touch totalDelegated / commissionAccrued / delegations.
        v.active = true;
        v.selfStake += msg.value;               // add to any residual (e.g. post-slash) self-stake
        v.commissionBps = commissionBps;
        totalStaked += msg.value;
        emit ValidatorRegistered(msg.sender, v.selfStake, commissionBps);
    }

    /**
     * @notice Exit as a validator: queue the entire remaining self-stake into the same
     *         unbonding path used by delegators, deactivate, and stop earning. Recover
     *         the funds with claimUnbonded() after UNBONDING_PERIOD (F3).
     *
     *         Callable even if already deactivated (slashed below the minimum), so a
     *         slashed-out validator can still recover its residual self-stake. Existing
     *         delegations are untouched; delegators unbond independently.
     */
    function deregisterValidator() external nonReentrant {
        Validator storage v = validators[msg.sender];
        require(v.exists, "AereStakingV2: not a validator");
        uint256 stake = v.selfStake;
        require(stake > 0, "AereStakingV2: no self-stake");

        // effects: remove the live (post-slash) self-stake from the pool accounting.
        v.selfStake = 0;
        if (v.active) {
            v.active = false;
            emit ValidatorDeactivated(msg.sender);
        }
        totalStaked -= stake;

        uint256 readyAt = block.timestamp + UNBONDING_PERIOD;
        unbondQueue[msg.sender].push(UnbondRequest({ amount: stake, readyAt: readyAt }));
        emit ValidatorDeregistered(msg.sender, stake, readyAt);
    }

    /* ---------------------------- delegation ops ---------------------------- */

    /// @notice Delegate AERE to a validator. msg.value is the delegation amount.
    ///         F2 FIX: settles pending rewards and advances the accrual anchor BEFORE
    ///         increasing the balance, so a top-up earns no retroactive reward.
    function delegate(address validator) external payable nonReentrant {
        require(msg.value >= MIN_DELEGATION, "AereStakingV2: below minimum delegation");
        require(validators[validator].active, "AereStakingV2: validator not active");

        Delegation storage d = delegations[validator][msg.sender];
        if (d.amount == 0) {
            d.stakedAt = block.timestamp;
            d.lastClaimTime = block.timestamp;
        } else {
            _settle(validator, msg.sender);      // F2: settle before top-up
        }
        d.amount += msg.value;
        validators[validator].totalDelegated += msg.value;
        totalStaked += msg.value;
        emit Delegated(validator, msg.sender, msg.value);
    }

    /// @notice Initiate unbonding of a delegation. Funds release after UNBONDING_PERIOD.
    ///         Settles first so the delegator keeps rewards earned on the unbonded
    ///         portion up to now (and cannot lose them).
    function unbond(address validator, uint256 amount) external nonReentrant {
        Delegation storage d = delegations[validator][msg.sender];
        require(d.amount >= amount, "AereStakingV2: insufficient delegation");

        _settle(validator, msg.sender);
        d.amount -= amount;
        validators[validator].totalDelegated -= amount;   // F4: totalDelegated preserved on re-register, so no underflow
        totalStaked -= amount;

        uint256 readyAt = block.timestamp + UNBONDING_PERIOD;
        unbondQueue[msg.sender].push(UnbondRequest({ amount: amount, readyAt: readyAt }));
        emit UnbondInitiated(validator, msg.sender, amount, readyAt);
    }

    /// @notice Claim all matured unbonding requests (delegation exits and validator
    ///         self-stake exits share this queue).
    function claimUnbonded() external nonReentrant {
        uint256 total = 0;
        UnbondRequest[] storage queue = unbondQueue[msg.sender];
        uint256 i = 0;
        while (i < queue.length) {
            if (queue[i].readyAt <= block.timestamp) {
                total += queue[i].amount;
                queue[i] = queue[queue.length - 1];
                queue.pop();
            } else {
                i++;
            }
        }
        require(total > 0, "AereStakingV2: nothing to claim");
        (bool ok, ) = msg.sender.call{value: total}("");
        require(ok, "AereStakingV2: transfer failed");
        emit UnbondClaimed(msg.sender, total);
    }

    /* -------------------------------- rewards ------------------------------- */

    /// @notice Pending NET rewards for a delegator (after validator commission),
    ///         timestamp-based at the current rewardRateBps (F5). Drop-in signature.
    function pendingRewards(address validator, address delegator) public view returns (uint256) {
        Delegation memory d = delegations[validator][delegator];
        uint256 pending = d.rewardAccrued;
        if (d.amount > 0) {
            uint256 secs = block.timestamp - d.lastClaimTime;
            uint256 gross = _grossReward(d.amount, secs);
            uint256 commission = (gross * validators[validator].commissionBps) / BPS_DENOM;
            pending += (gross - commission);
        }
        return pending;
    }

    /// @notice Claim accrued delegator rewards. Paid from the pool's native balance.
    function claimRewards(address validator) external nonReentrant {
        _settle(validator, msg.sender);
        Delegation storage d = delegations[validator][msg.sender];
        uint256 reward = d.rewardAccrued;
        require(reward > 0, "AereStakingV2: nothing to claim");
        d.rewardAccrued = 0;
        // Rewards come from the protocol-level mining supply once wired to consensus;
        // until then the pool must be funded by the Foundation.
        require(address(this).balance >= reward, "AereStakingV2: pool insufficient (needs foundation top-up)");
        (bool ok, ) = msg.sender.call{value: reward}("");
        require(ok, "AereStakingV2: transfer failed");
        emit RewardsClaimed(msg.sender, reward);
    }

    /// @notice Validator claims commission credited from delegator rewards (LOW fix).
    ///         Commission materializes when a delegation is settled (on the delegator's
    ///         delegate top-up / unbond / claimRewards).
    function claimCommission() external nonReentrant {
        Validator storage v = validators[msg.sender];
        uint256 amount = v.commissionAccrued;
        require(amount > 0, "AereStakingV2: nothing to claim");
        v.commissionAccrued = 0;
        require(address(this).balance >= amount, "AereStakingV2: pool insufficient (needs foundation top-up)");
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "AereStakingV2: transfer failed");
        emit CommissionClaimed(msg.sender, amount);
    }

    /* -------------------------------- admin --------------------------------- */

    /// @notice Owner tunes the annual reward rate (F5). Default 800 = 8% APY.
    ///         Applies from the next settle onward for every delegation.
    function setRewardRateBps(uint256 newRateBps) external onlyOwner {
        require(newRateBps > 0 && newRateBps <= MAX_REWARD_RATE_BPS, "AereStakingV2: rate out of range");
        uint256 old = rewardRateBps;
        rewardRateBps = newRateBps;
        emit RewardRateUpdated(old, newRateBps);
    }

    /// @notice Slash a validator's self-stake (owner-only). Identical to V1: removes
    ///         SLASH_RATE_BPS of the live self-stake from the pool accounting and
    ///         deactivates the validator if it falls below the minimum. Slashed AERE
    ///         stays in the pool as surplus (no admin withdrawal of principal).
    function slash(address validator, string calldata reason) external onlyOwner {
        Validator storage v = validators[validator];
        require(v.active, "AereStakingV2: validator not active");
        uint256 slashAmount = (v.selfStake * SLASH_RATE_BPS) / BPS_DENOM;
        v.selfStake -= slashAmount;
        totalStaked -= slashAmount;
        if (v.selfStake < MIN_VALIDATOR_SELF_STAKE) {
            v.active = false;
            emit ValidatorDeactivated(validator);
        }
        emit ValidatorSlashed(validator, slashAmount, reason);
    }

    /* --------------------------------- views -------------------------------- */

    function validatorCount() external view returns (uint256) {
        return validatorList.length;
    }

    function unbondQueueLength(address account) external view returns (uint256) {
        return unbondQueue[account].length;
    }

    /// @notice Allow the Foundation to seed the reward pool.
    receive() external payable {}
}
