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

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";

/**
 * @dev Minimal view interfaces into the CANONICAL live staking contracts on
 *      chain 2800. No change to either staking contract is required; both
 *      expose the state read here as public getters.
 *
 *      AereStakingV2   0x1D95eF6D17aeAB732dF914Ba2d018c270BC155FC (delegated stake)
 *      AereLockedStaking 0x21108c28A849b05aE6b7a3a5bc435C9Bc897E7Ad (fixed-term locks)
 */
interface IAereStakingV2Votes {
    function validatorCount() external view returns (uint256);
    function validatorList(uint256 index) external view returns (address);
    function delegations(address validator, address delegator)
        external
        view
        returns (uint256 amount, uint256 stakedAt, uint256 lastClaimTime, uint256 rewardAccrued);
    function validators(address who)
        external
        view
        returns (
            bool active,
            bool exists,
            uint256 selfStake,
            uint256 totalDelegated,
            uint256 commissionBps,
            uint256 commissionAccrued
        );
}

interface IAereLockedStakingVotes {
    function lockCount(address user) external view returns (uint256);
    function getLock(address user, uint256 lockId)
        external
        view
        returns (uint256 amount, uint64 startedAt, uint64 unlockAt, uint8 tier, bool withdrawn);
}

/**
 * @title AereStakedGovVotes (gAERE)
 * @notice A THIN, NON-CUSTODIAL, CHECKPOINTED voting-power adapter over AERE's
 *         real staking. It is the ERC20Votes source the AereGovernor reads.
 *
 *         WHY THIS EXISTS. The live staking contracts (AereStakingV2,
 *         AereLockedStaking) hold real delegated / locked AERE but do NOT expose
 *         ERC20Votes and are NOT checkpointed, so a raw current-balance read is
 *         flash-stake vote-borrowable within a single block. OpenZeppelin's
 *         Governor requires an IVotes source with historical snapshots
 *         (getPastVotes / getPastTotalSupply) so that a proposal's weight is
 *         fixed at a past block that an attacker cannot retroactively enter.
 *         This adapter provides exactly that: a checkpointed mirror of the
 *         correct staking source.
 *
 *         HOW IT WORKS.
 *           - It CUSTODIES NOTHING. It never receives, holds, or moves AERE. The
 *             staked AERE stays in the staking contracts at all times.
 *           - gAERE balance is a pure MIRROR of an account's live staked power:
 *             sum of its AereStakingV2 delegations across validators, plus its
 *             own validator self-stake if it is a validator, plus its
 *             non-withdrawn AereLockedStaking locks.
 *           - Anyone may call sync(account) to reconcile that account's gAERE
 *             balance up or down to its current staked power. Each sync writes an
 *             ERC20Votes checkpoint, which is what makes governance weight
 *             snapshot-safe.
 *           - gAERE is NON-TRANSFERABLE (balances are derived, not spendable).
 *             Vote DELEGATION still works via the standard ERC20Votes delegate().
 *
 *         HONEST SCOPE (chain 2800, today).
 *           AereStakingV2 currently reports validatorCount = 0 and totalStaked = 0
 *           and AereLockedStaking holds no live locks, so stakePowerOf() returns 0
 *           for every account and gAERE total supply is 0. Network-wide voting
 *           power is therefore 0 until real validators / delegations / locks
 *           exist and holders call sync() + delegate(). This adapter does not
 *           manufacture power; it faithfully reflects that none is staked yet.
 *
 *         Clock: block number (OpenZeppelin default). getPastVotes at a proposal
 *         snapshot block is what defeats flash-stake vote borrowing.
 */
contract AereStakedGovVotes is ERC20Votes {
    IAereStakingV2Votes public immutable staking; // AereStakingV2
    IAereLockedStakingVotes public immutable locked; // AereLockedStaking

    /// @notice Bounded scans so sync() can never be gas-bricked by an unbounded
    ///         validator / lock set. If a real account ever exceeds these, its
    ///         mirrored power is capped at the scanned window (documented, safe).
    uint256 public constant MAX_VALIDATOR_SCAN = 512;
    uint256 public constant MAX_LOCK_SCAN = 512;

    event Synced(address indexed account, uint256 oldPower, uint256 newPower);

    constructor(address stakingV2, address lockedStaking)
        ERC20("AERE Staked Governance Votes", "gAERE")
        ERC20Permit("AERE Staked Governance Votes")
    {
        require(stakingV2 != address(0) && lockedStaking != address(0), "gAERE: zero staking");
        staking = IAereStakingV2Votes(stakingV2);
        locked = IAereLockedStakingVotes(lockedStaking);
    }

    /**
     * @notice Live staked voting power of `account`, read from the canonical
     *         staking contracts. Delegations + validator self-stake + live locks.
     */
    function stakePowerOf(address account) public view returns (uint256 power) {
        uint256 vc = staking.validatorCount();
        if (vc > MAX_VALIDATOR_SCAN) vc = MAX_VALIDATOR_SCAN;
        for (uint256 i = 0; i < vc; i++) {
            address v = staking.validatorList(i);
            (uint256 amt, , , ) = staking.delegations(v, account);
            power += amt;
        }

        // If `account` is itself a validator, its live (post-slash) self-stake
        // is a slashable bond and counts toward its governance weight.
        (, bool exists, uint256 selfStake, , , ) = staking.validators(account);
        if (exists) power += selfStake;

        uint256 lc = locked.lockCount(account);
        if (lc > MAX_LOCK_SCAN) lc = MAX_LOCK_SCAN;
        for (uint256 j = 0; j < lc; j++) {
            (uint256 lamt, , , , bool withdrawn) = locked.getLock(account, j);
            if (!withdrawn) power += lamt;
        }
    }

    /**
     * @notice Permissionlessly reconcile `account`'s gAERE balance to its live
     *         staked power, writing a fresh checkpoint. Callable by anyone (so
     *         the mirror can be kept honest, e.g. burning power after an unbond).
     *         The account must still self-delegate (or delegate) for the mirrored
     *         balance to become countable voting power.
     */
    function sync(address account) public {
        uint256 target = stakePowerOf(account);
        uint256 cur = balanceOf(account);
        if (target > cur) {
            _mint(account, target - cur);
        } else if (cur > target) {
            _burn(account, cur - target);
        }
        emit Synced(account, cur, target);
    }

    /// @notice Convenience: sync the caller.
    function syncSelf() external {
        sync(msg.sender);
    }

    /**
     * @dev Non-transferable mirror: allow only mint (from == 0) and burn
     *      (to == 0). Regular transfers revert. Vote delegation is unaffected
     *      (it moves voting units, not token balances).
     */
    function _beforeTokenTransfer(address from, address to, uint256 amount) internal override {
        require(from == address(0) || to == address(0), "gAERE: non-transferable mirror");
        super._beforeTokenTransfer(from, to, amount);
    }
}
