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

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

/**
 * @title AereGovLock
 * @notice A self-contained, non-transferable, CHECKPOINTED ERC20Votes governance
 *         vote source built on a CUSTODY-LOCK (veToken-style) model. It is a
 *         drop-in IVotes source for the existing AereGovernor
 *         (0xd16C2551Bf4f3d0DDF961282078917b2e8679999) and its TimelockController
 *         (0xf95738Cf366Dd2ea76fBa1b2736163BDc11D2e55).
 *
 *         WHY THIS EXISTS. The previous vote source, AereStakedGovVotes (gAERE
 *         0xD7575795A055cb093957B86f0607871A2D37805C), was a LAZY PULL-BASED
 *         MIRROR of external staking contracts. It had a fatal class of bug:
 *         voting power could DETACH from the capital that was supposed to back
 *         it. Concretely, AereLockedStaking.earlyExit() returns a user's
 *         principal instantly WITHOUT burning any mirrored gAERE, and sync() is
 *         opt-in, so a single unit of never-truly-locked AERE could be recycled
 *         through many accounts (sybils) to manufacture N-fold voting power.
 *         Worse, quorum was measured off an OPT-IN synced total supply, so the
 *         denominator was meaningless.
 *
 *         THE FIX (this contract). Power CANNOT detach from capital because the
 *         capital lives HERE, in custody, for the whole life of the vote:
 *           - lock() is the ONLY way votes are minted, and it requires native
 *             AERE to be transferred INTO this contract in the same call
 *             (payable). Votes minted == principal deposited, exactly 1:1.
 *           - withdraw() is the ONLY way votes are burned, and it is the ONLY
 *             way principal leaves. It is gated by unlockTime, burns the votes
 *             FIRST, then returns the exact principal. There is NO earlyExit,
 *             NO admin drain, NO detached-claim path.
 *           - Therefore, at ALL times and at EVERY historical snapshot:
 *                 totalSupply() == sum of all live locks == this contract's
 *                 native balance credited through lock().
 *             One unit of capital yields AT MOST one unit of votes. Recycling is
 *             impossible: to give capital to a sybil you must withdraw, which
 *             burns the votes before the capital can move.
 *
 *         NON-TRANSFERABLE, DELEGATABLE. Balances represent a custody claim, not
 *         a spendable asset, so transfer / transferFrom / approve REVERT. Vote
 *         DELEGATION works normally via ERC20Votes.delegate() (it moves voting
 *         units, never the underlying custody claim), so holders can delegate to
 *         a representative without ever detaching capital.
 *
 *         CLOCK. Block-number clock (OpenZeppelin default), matching AereGovernor,
 *         so getPastVotes / getPastTotalSupply are snapshot-fixed at a proposal's
 *         past block and cannot be retroactively entered. Lock expiry uses wall
 *         clock (block.timestamp); vote accounting uses block number. This split
 *         is the standard veToken pattern.
 *
 *         MEANINGFUL QUORUM. Because totalSupply() == total locked capital,
 *         GovernorVotesQuorumFraction computes quorum as a real fraction of real
 *         locked capital, not of an opt-in mirror.
 */
contract AereGovLock is ERC20Votes, ReentrancyGuard {
    /// @notice One custody lock per account. `amount` is the native AERE held in
    ///         custody for this account; `unlockTime` is the earliest wall-clock
    ///         time at which it (and the votes it backs) may be withdrawn.
    struct Lock {
        uint256 amount;
        uint256 unlockTime;
    }

    /// @notice account => its single custody lock position.
    mapping(address => Lock) private _locks;

    /// @notice Minimum lock duration enforced on every lock / extension, in
    ///         seconds. Set once at deployment. May be 0 (no minimum).
    uint256 public immutable minLockDuration;

    /// @notice Sanity cap on how far in the future a lock may be set, to prevent
    ///         fat-finger effectively-permanent locks and timestamp overflow.
    uint256 public constant MAX_LOCK_DURATION = 1460 days; // ~4 years

    event Locked(address indexed account, uint256 addedAmount, uint256 newAmount, uint256 unlockTime);
    event Withdrawn(address indexed account, uint256 amount);

    /**
     * @param _minLockDuration Minimum seconds a deposit must stay locked. Pass 0
     *        to disable the minimum. Must be <= MAX_LOCK_DURATION.
     */
    constructor(uint256 _minLockDuration)
        ERC20("AERE Governance Lock", "govAERE")
        ERC20Permit("AERE Governance Lock")
    {
        require(_minLockDuration <= MAX_LOCK_DURATION, "govAERE: min > max");
        minLockDuration = _minLockDuration;
    }

    // ---------------------------------------------------------------------
    // Custody lock: capital IN -> votes minted 1:1.
    // ---------------------------------------------------------------------

    /**
     * @notice Lock native AERE into custody and mint an equal amount of
     *         non-transferable, checkpointed votes. This is the ONLY mint path.
     *
     *         `amount` MUST equal msg.value; it is an explicit argument purely so
     *         the deposited quantity is unambiguous in calldata and events. The
     *         real transfer of value is msg.value (native AERE) into this
     *         contract, so the votes are always fully backed by custody here.
     *
     *         If the caller already has a lock, this ADDS to it and the position
     *         adopts the new (later or equal) unlock time. The unlock time can
     *         only move FORWARD, never be shortened by a subsequent lock, so
     *         capital already committed cannot be released early by re-locking.
     *
     * @param amount     Quantity of native AERE to lock. Must equal msg.value and be > 0.
     * @param unlockTime Wall-clock timestamp when withdrawal becomes allowed.
     */
    function lock(uint256 amount, uint256 unlockTime) external payable nonReentrant {
        require(amount > 0, "govAERE: zero amount");
        require(msg.value == amount, "govAERE: value != amount");

        Lock storage position = _locks[msg.sender];

        // Enforce the minimum duration from now, and never allow shortening an
        // existing commitment.
        uint256 minUnlock = block.timestamp + minLockDuration;
        require(unlockTime >= minUnlock, "govAERE: below min duration");
        require(unlockTime <= block.timestamp + MAX_LOCK_DURATION, "govAERE: above max duration");
        require(unlockTime >= position.unlockTime, "govAERE: cannot shorten lock");

        position.amount += amount;
        position.unlockTime = unlockTime;

        // Mint votes 1:1 with the deposited capital. _mint writes an ERC20Votes
        // checkpoint, making the new weight snapshot-safe.
        _mint(msg.sender, amount);

        emit Locked(msg.sender, amount, position.amount, unlockTime);
    }

    /**
     * @notice Extend an existing lock's unlock time WITHOUT adding capital. Votes
     *         are unchanged (they are already 1:1 with the locked capital). Only
     *         allowed to move the unlock time forward.
     */
    function extendLock(uint256 newUnlockTime) external {
        Lock storage position = _locks[msg.sender];
        require(position.amount > 0, "govAERE: no lock");
        require(newUnlockTime > position.unlockTime, "govAERE: not later");
        require(newUnlockTime >= block.timestamp + minLockDuration, "govAERE: below min duration");
        require(newUnlockTime <= block.timestamp + MAX_LOCK_DURATION, "govAERE: above max duration");
        position.unlockTime = newUnlockTime;
        emit Locked(msg.sender, 0, position.amount, newUnlockTime);
    }

    // ---------------------------------------------------------------------
    // Custody release: votes burned FIRST -> capital OUT. The only exit.
    // ---------------------------------------------------------------------

    /**
     * @notice Withdraw the full locked principal AFTER unlockTime, burning the
     *         backing votes in the same call. This is the ONLY burn path and the
     *         ONLY way capital leaves the contract. Votes are burned BEFORE the
     *         native transfer (checks-effects-interactions), so at no instant can
     *         an account hold votes whose capital has already left.
     */
    function withdraw() external nonReentrant {
        Lock storage position = _locks[msg.sender];
        uint256 amount = position.amount;
        require(amount > 0, "govAERE: no lock");
        require(block.timestamp >= position.unlockTime, "govAERE: still locked");

        // EFFECTS: clear the position and burn the votes before moving value.
        position.amount = 0;
        position.unlockTime = 0;
        _burn(msg.sender, amount);

        // INTERACTION: return the exact principal.
        (bool ok, ) = payable(msg.sender).call{value: amount}("");
        require(ok, "govAERE: native transfer failed");

        emit Withdrawn(msg.sender, amount);
    }

    // ---------------------------------------------------------------------
    // Views.
    // ---------------------------------------------------------------------

    /// @notice The custody lock (amount, unlockTime) for `account`.
    function locks(address account) external view returns (uint256 amount, uint256 unlockTime) {
        Lock storage position = _locks[account];
        return (position.amount, position.unlockTime);
    }

    /// @notice Principal currently held in custody for `account` (== its votes balance).
    function lockedBalanceOf(address account) external view returns (uint256) {
        return _locks[account].amount;
    }

    /// @notice Unlock timestamp for `account`'s lock (0 if none).
    function unlockTimeOf(address account) external view returns (uint256) {
        return _locks[account].unlockTime;
    }

    /**
     * @notice Total native AERE held in custody through lock(). Under normal use
     *         this equals totalSupply(). It is exposed so anyone can verify the
     *         backing invariant on-chain. (A forced native send via selfdestruct
     *         could push the raw contract balance above this figure; such surplus
     *         is never counted as votes and is never withdrawable, so it cannot
     *         inflate voting power.)
     */
    function totalLocked() external view returns (uint256) {
        return totalSupply();
    }

    // ---------------------------------------------------------------------
    // Non-transferability. Balances are custody claims, not spendable tokens.
    // Mint (from == 0) and burn (to == 0) are allowed; everything else reverts.
    // Vote delegation is unaffected (it moves voting units, not balances).
    // ---------------------------------------------------------------------

    function _beforeTokenTransfer(address from, address to, uint256 amount) internal override {
        require(from == address(0) || to == address(0), "govAERE: non-transferable");
        super._beforeTokenTransfer(from, to, amount);
    }

    function transfer(address, uint256) public pure override returns (bool) {
        revert("govAERE: non-transferable");
    }

    function transferFrom(address, address, uint256) public pure override returns (bool) {
        revert("govAERE: non-transferable");
    }

    function approve(address, uint256) public pure override returns (bool) {
        revert("govAERE: non-transferable");
    }

    function increaseAllowance(address, uint256) public pure override returns (bool) {
        revert("govAERE: non-transferable");
    }

    function decreaseAllowance(address, uint256) public pure override returns (bool) {
        revert("govAERE: non-transferable");
    }
}
