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

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

/**
 * @title AereLockedStaking
 * @notice Liquidity-lock staking with fixed terms (whitepaper §3.3).
 *
 * Users lock native AERE for a fixed term and earn fixed APY paid in AERE
 * on maturity. Early exit forfeits all reward and returns principal only.
 *
 * Tiers (matched to stake-v2.html):
 *   0 — 30 days  · 10% APY
 *   1 — 90 days  · 15% APY
 *   2 — 180 days · 22% APY
 *   3 — 365 days · 30% APY
 *
 * Reward pool is seeded by the Foundation; the contract refuses to pay
 * a maturity if its balance cannot cover principal + reward (so other
 * stakers' principal can never be drained to pay rewards).
 */
contract AereLockedStaking is Ownable, ReentrancyGuard {
    // tier => APY in basis points (10000 = 100%)
    uint256[4] public APY_BPS = [1000, 1500, 2200, 3000];
    // tier => lock duration in seconds
    uint64[4] private DURATIONS = [30 days, 90 days, 180 days, 365 days];

    uint256 public constant MIN_STAKE = 1 ether;          // 1 AERE
    uint256 public constant MAX_STAKE = 100_000 ether;    // sanity cap per lock

    struct Lock {
        uint256 amount;
        uint64 startedAt;
        uint64 unlockAt;
        uint8 tier;
        bool withdrawn;
    }

    mapping(address => Lock[]) private _locks;

    // Total principal currently locked (excludes rewards owed). Used to
    // determine how much of `address(this).balance` is the reward reserve.
    uint256 public totalPrincipal;

    event Staked(address indexed user, uint256 indexed lockId, uint256 amount, uint8 tier, uint64 unlockAt);
    event Withdrawn(address indexed user, uint256 indexed lockId, uint256 principal, uint256 reward);
    event EarlyExited(address indexed user, uint256 indexed lockId, uint256 principal);
    event RewardReserveDeposited(address indexed from, uint256 amount);

    /**
     * @notice Lock `msg.value` for the tier's term and earn its APY on maturity.
     */
    function stake(uint8 tier) external payable nonReentrant returns (uint256 lockId) {
        require(tier < 4, "AereLockedStaking: bad tier");
        require(msg.value >= MIN_STAKE, "AereLockedStaking: below min");
        require(msg.value <= MAX_STAKE, "AereLockedStaking: above max");

        uint64 nowTs = uint64(block.timestamp);
        uint64 unlockAt = nowTs + DURATIONS[tier];

        lockId = _locks[msg.sender].length;
        _locks[msg.sender].push(Lock({
            amount: msg.value,
            startedAt: nowTs,
            unlockAt: unlockAt,
            tier: tier,
            withdrawn: false
        }));

        totalPrincipal += msg.value;
        emit Staked(msg.sender, lockId, msg.value, tier, unlockAt);
    }

    /**
     * @notice Reward owed if this lock matures untouched. Returns 0 if the
     *         lock has already been withdrawn or early-exited.
     *         reward = principal * APY_BPS * duration / 365 days / 10000
     */
    function rewardOf(address user, uint256 lockId) public view returns (uint256) {
        if (lockId >= _locks[user].length) return 0;
        Lock memory l = _locks[user][lockId];
        if (l.withdrawn) return 0;
        return (l.amount * APY_BPS[l.tier] * DURATIONS[l.tier]) / (365 days * 10000);
    }

    /**
     * @notice Claim a matured lock. Pays principal + full reward.
     */
    function withdraw(uint256 lockId) external nonReentrant {
        Lock storage l = _locks[msg.sender][lockId];
        require(!l.withdrawn, "AereLockedStaking: already withdrawn");
        require(block.timestamp >= l.unlockAt, "AereLockedStaking: still locked");

        l.withdrawn = true;
        totalPrincipal -= l.amount;

        uint256 reward = (l.amount * APY_BPS[l.tier] * DURATIONS[l.tier]) / (365 days * 10000);
        uint256 total = l.amount + reward;

        // Pay only if the reward reserve covers it (the principal we hold
        // for other locks must never be touched).
        require(address(this).balance >= total + totalPrincipal, "AereLockedStaking: reward reserve insufficient");

        (bool ok, ) = msg.sender.call{value: total}("");
        require(ok, "AereLockedStaking: transfer failed");
        emit Withdrawn(msg.sender, lockId, l.amount, reward);
    }

    /**
     * @notice Forfeit the reward and recover principal before maturity.
     */
    function earlyExit(uint256 lockId) external nonReentrant {
        Lock storage l = _locks[msg.sender][lockId];
        require(!l.withdrawn, "AereLockedStaking: already withdrawn");

        l.withdrawn = true;
        totalPrincipal -= l.amount;

        (bool ok, ) = msg.sender.call{value: l.amount}("");
        require(ok, "AereLockedStaking: transfer failed");
        emit EarlyExited(msg.sender, lockId, l.amount);
    }

    // ── Views ────────────────────────────────────────────────────────────

    function lockCount(address user) external view returns (uint256) {
        return _locks[user].length;
    }

    function getLock(address user, uint256 lockId) external view returns (Lock memory) {
        return _locks[user][lockId];
    }

    function tierDuration(uint8 tier) external view returns (uint64) {
        require(tier < 4, "AereLockedStaking: bad tier");
        return DURATIONS[tier];
    }

    /// @notice AERE held above outstanding principal — the actual reward reserve.
    function rewardReserve() external view returns (uint256) {
        uint256 bal = address(this).balance;
        return bal > totalPrincipal ? bal - totalPrincipal : 0;
    }

    // ── Reward reserve seeding ───────────────────────────────────────────

    /// @notice Foundation tops up the reward reserve. Anyone can donate.
    receive() external payable {
        emit RewardReserveDeposited(msg.sender, msg.value);
    }
}
