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

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

/**
 * @title AereReferralRegistry — AERE Season 1 "Altitude" referrals
 * @notice Referral codes bound to accounts. A referrer is credited AP ONLY
 *         once their invitee reaches ACTIVATION, defined as ALL of:
 *
 *           1. a passkey smart account was created for the invitee
 *              (checked via a Foundation-settable `passkeyFlag` oracle over the
 *               live passkey-account source), AND
 *           2. the invitee completed First Flight check-in
 *              (AereQuestCheckIn.hasCheckedIn), AND
 *           3. the invitee has been credited for >= MIN_DISTINCT_QUESTS
 *              distinct on-chain quests (AereQuestAttestor.distinctQuestCount).
 *
 *         This "activation, not signup" rule is the core anti-farm defense:
 *         a referrer earns nothing for a wallet that just binds a code, only
 *         for one that actually onboards and uses the chain.
 *
 *   REWARD SHAPE (all Foundation-tunable):
 *     - DIMINISHING RETURNS: per-referral reward halves every `halvingEvery`
 *       activated referrals, floored at `minReward`.
 *     - CAPPED ONGOING SHARE: cumulative referral AP per referrer is capped at
 *       cap(tier); rewards clamp to the remaining headroom.
 *     - PER-ACCOUNT CAP RAISED BY TIER: tier = activatedReferrals / tierSize,
 *       and cap(tier) = tierCapBase + tier * tierCapStep, so a proven referrer
 *       unlocks more headroom over time instead of an unlimited faucet.
 *
 *   This contract is authorized as an attestor on the ledger by the Foundation
 *   and credits referrers directly via LEDGER.credit(...). It holds no funds.
 *   Owner = Foundation multisig.
 *
 *   DISCLAIMER: AP is not a token, has no guaranteed value, and confers no
 *   right to returns. Rewards depend on the network growing and are not
 *   promised. There is no listing.
 */
contract AereReferralRegistry is Ownable, ReentrancyGuard {

    bytes32 public constant REFERRAL_QUEST_ID = keccak256("season1-referral");

    IAerePointsLedger   public immutable LEDGER;
    IAereQuestCheckIn   public immutable CHECKIN;
    IAereQuestAttestor  public immutable ATTESTOR;

    /// @notice Settable "has a passkey account" flag oracle for activation.
    IAccountFlag public passkeyFlag;

    /* -------------------------------- config -------------------------------- */

    uint256 public minDistinctQuests = 2;
    uint256 public baseReward   = 50;   // AP for a referral in the first bracket
    uint256 public halvingEvery = 10;   // halve per-referral reward every N referrals
    uint256 public minReward    = 5;    // floor
    uint256 public tierSize     = 10;   // referrals per tier step
    uint256 public tierCapBase  = 250;  // cap at tier 0
    uint256 public tierCapStep  = 250;  // extra cap per tier

    /* --------------------------------- state -------------------------------- */

    mapping(bytes32 => address) public codeOwner;      // code => referrer
    mapping(address => bytes32) public accountCode;    // referrer => code
    mapping(address => address) public referredBy;     // invitee => referrer
    mapping(address => bool)    public referralCredited; // invitee => paid out
    mapping(address => uint256) public activatedReferrals;   // referrer => count
    mapping(address => uint256) public referralPointsEarned; // referrer => AP

    /* --------------------------------- events ------------------------------- */

    event PasskeyFlagSet(address indexed oracle);
    event ReferralParamsSet(
        uint256 minDistinctQuests,
        uint256 baseReward,
        uint256 halvingEvery,
        uint256 minReward,
        uint256 tierSize,
        uint256 tierCapBase,
        uint256 tierCapStep
    );
    event CodeRegistered(address indexed referrer, bytes32 indexed code);
    event ReferrerBound(address indexed invitee, address indexed referrer, bytes32 indexed code);
    event ReferralCredited(
        address indexed referrer,
        address indexed invitee,
        uint256 reward,
        uint256 activatedReferralCount
    );

    /* --------------------------------- errors ------------------------------- */

    error ZeroCode();
    error CodeTaken();
    error AlreadyHasCode();
    error UnknownCode();
    error SelfReferral();
    error AlreadyBound();
    error NoReferrer();
    error AlreadyCredited();
    error NotActivated();
    error BadParams();

    constructor(address ledger, address checkIn, address attestor) {
        require(ledger != address(0) && checkIn != address(0) && attestor != address(0), "zero-ref");
        LEDGER = IAerePointsLedger(ledger);
        CHECKIN = IAereQuestCheckIn(checkIn);
        ATTESTOR = IAereQuestAttestor(attestor);
    }

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

    function setPasskeyFlag(address oracle) external onlyOwner {
        passkeyFlag = IAccountFlag(oracle);
        emit PasskeyFlagSet(oracle);
    }

    function setReferralParams(
        uint256 _minDistinctQuests,
        uint256 _baseReward,
        uint256 _halvingEvery,
        uint256 _minReward,
        uint256 _tierSize,
        uint256 _tierCapBase,
        uint256 _tierCapStep
    ) external onlyOwner {
        if (_halvingEvery == 0 || _tierSize == 0) revert BadParams();
        if (_minReward > _baseReward) revert BadParams();
        minDistinctQuests = _minDistinctQuests;
        baseReward   = _baseReward;
        halvingEvery = _halvingEvery;
        minReward    = _minReward;
        tierSize     = _tierSize;
        tierCapBase  = _tierCapBase;
        tierCapStep  = _tierCapStep;
        emit ReferralParamsSet(
            _minDistinctQuests, _baseReward, _halvingEvery, _minReward,
            _tierSize, _tierCapBase, _tierCapStep
        );
    }

    /* ------------------------------- codes ---------------------------------- */

    /// @notice Claim a unique referral code for the caller. One code per account.
    /// @dev CODES ARE NON-SEMANTIC AND FIRST-COME. The contract attaches no meaning
    ///      to the bytes32: any caller may claim any currently-unused value, and the
    ///      claim is therefore front-runnable. A "branded" string (for example a
    ///      company or handle) is NOT reserved for anyone and can be squatted by the
    ///      first sender to register it. This is acceptable for this version because
    ///      a code carries no monetary value: AP is non-transferable, has no listing,
    ///      and a squatted code only routes activation credit to whoever holds it, it
    ///      cannot take funds or another account's AP. Branded-code reservation is
    ///      intentionally OUT OF SCOPE for this version. If branded codes ever matter,
    ///      a future version should add an owner-gated reservation list or derive the
    ///      code deterministically from msg.sender instead of accepting arbitrary bytes.
    function registerCode(bytes32 code) external {
        if (code == bytes32(0)) revert ZeroCode();
        if (codeOwner[code] != address(0)) revert CodeTaken();
        if (accountCode[msg.sender] != bytes32(0)) revert AlreadyHasCode();
        codeOwner[code] = msg.sender;
        accountCode[msg.sender] = code;
        emit CodeRegistered(msg.sender, code);
    }

    /// @notice Bind the caller (invitee) to the referrer who owns `code`.
    ///         One-shot and immutable; cannot self-refer.
    function bindReferrer(bytes32 code) external {
        if (referredBy[msg.sender] != address(0)) revert AlreadyBound();
        address referrer = codeOwner[code];
        if (referrer == address(0)) revert UnknownCode();
        if (referrer == msg.sender) revert SelfReferral();
        referredBy[msg.sender] = referrer;
        emit ReferrerBound(msg.sender, referrer, code);
    }

    /* ----------------------------- activation ------------------------------- */

    /// @notice True iff `invitee` satisfies all three activation gates.
    function isActivated(address invitee) public view returns (bool) {
        if (address(passkeyFlag) == address(0)) return false;
        if (!passkeyFlag.isFlagged(invitee)) return false;
        if (!CHECKIN.hasCheckedIn(invitee)) return false;
        if (ATTESTOR.distinctQuestCount(invitee) < minDistinctQuests) return false;
        return true;
    }

    /// @notice Credit the referrer of `invitee` once the invitee has activated.
    ///         Permissionless: anyone may trigger (referrer or a relayer).
    function claimReferral(address invitee) external nonReentrant {
        address referrer = referredBy[invitee];
        if (referrer == address(0)) revert NoReferrer();
        if (referralCredited[invitee]) revert AlreadyCredited();
        if (!isActivated(invitee)) revert NotActivated();

        uint256 n = activatedReferrals[referrer];
        uint256 reward = _diminishingReward(n);
        uint256 cap = _capForCount(n);
        uint256 already = referralPointsEarned[referrer];
        uint256 remaining = cap > already ? cap - already : 0;
        uint256 actual = reward < remaining ? reward : remaining;

        // Effects first (reentrancy-safe; also guarded).
        referralCredited[invitee] = true;
        activatedReferrals[referrer] = n + 1;
        if (actual > 0) {
            referralPointsEarned[referrer] = already + actual;
        }

        // Interaction: credit the ledger only when there is headroom.
        if (actual > 0) {
            LEDGER.credit(referrer, actual, REFERRAL_QUEST_ID);
        }

        emit ReferralCredited(referrer, invitee, actual, n + 1);
    }

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

    function _diminishingReward(uint256 n) internal view returns (uint256) {
        uint256 shifts = n / halvingEvery;
        if (shifts > 255) shifts = 255;
        uint256 r = baseReward >> shifts;
        return r > minReward ? r : minReward;
    }

    function _capForCount(uint256 n) internal view returns (uint256) {
        uint256 tier = n / tierSize;
        return tierCapBase + tier * tierCapStep;
    }

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

    function nextReferralReward(address referrer) external view returns (uint256) {
        uint256 n = activatedReferrals[referrer];
        uint256 reward = _diminishingReward(n);
        uint256 cap = _capForCount(n);
        uint256 already = referralPointsEarned[referrer];
        uint256 remaining = cap > already ? cap - already : 0;
        return reward < remaining ? reward : remaining;
    }

    function referralStats(address referrer)
        external
        view
        returns (uint256 activated, uint256 pointsEarned, uint256 currentCap)
    {
        activated = activatedReferrals[referrer];
        pointsEarned = referralPointsEarned[referrer];
        currentCap = _capForCount(activated);
    }
}
