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

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

/**
 * @title AerePointsLedger — AERE Season 1 "Altitude" non-transferable points
 * @notice A CREDIT-ONLY, NON-TRANSFERABLE ledger of "Altitude Points" (AP).
 *
 *  HARD SAFETY DESIGN — AP is NOT a token and can never become one:
 *    - transfer / transferFrom / approve HARD-REVERT (NotTransferable). AP can
 *      never move between accounts, so it can never trade as a shadow token.
 *    - There is NO burn, NO redeem, NO conversion, NO "claim your tokens"
 *      path anywhere in this contract. AP is a scoreboard, nothing else.
 *    - Only Foundation-authorized attestors may `credit`. There is no mint()
 *      callable by users and no way for AP to leave an account.
 *
 *  HONESTY / DISCLAIMER (must accompany any user-facing surfacing of AP):
 *    Altitude Points are NOT a token, carry NO guaranteed value, and confer
 *    NO right to returns. Any future reward depends on the network growing
 *    and is not promised. There is no listing.
 *
 *  ERC-20 read shape (name/symbol/decimals/balanceOf/totalSupply) plus a
 *  mint-style Transfer(address(0), account, amount) event are emitted purely
 *  so block explorers / the AERE indexer can display balances. The WRITE side
 *  of ERC-20 is deliberately bricked.
 *
 *  Owner = Foundation multisig. Ownable only manages the attestor allowlist;
 *  the owner cannot move, mint to itself for transfer, or reduce balances.
 */
contract AerePointsLedger is Ownable {

    string public constant name = "AERE Altitude Points";
    string public constant symbol = "AP";
    uint8  public constant decimals = 0; // whole points, not a token

    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    /// @notice Foundation-authorized credit sources (the quest attestor and the
    ///         referral registry are added here post-deploy).
    mapping(address => bool) public isAttestor;

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

    /// @dev ERC-20 mint-style event for indexers. address(0) => account only.
    event Transfer(address indexed from, address indexed to, uint256 value);
    /// @dev Rich credit event carrying the quest attribution for the indexer.
    event PointsCredited(address indexed account, uint256 amount, bytes32 indexed questId, uint256 newBalance);
    event AttestorSet(address indexed attestor, bool allowed);

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

    error NotTransferable();
    error NotAttestor();
    error ZeroAccount();
    error ZeroAmount();

    /* ------------------------------- attestors ------------------------------ */

    function setAttestor(address attestor, bool allowed) external onlyOwner {
        if (attestor == address(0)) revert ZeroAccount();
        isAttestor[attestor] = allowed;
        emit AttestorSet(attestor, allowed);
    }

    modifier onlyAttestor() {
        if (!isAttestor[msg.sender]) revert NotAttestor();
        _;
    }

    /* -------------------------------- credit -------------------------------- */

    /// @notice Credit `amount` AP to `account`, attributed to `questId`.
    ///         Credit-only: balances can only ever go up.
    function credit(address account, uint256 amount, bytes32 questId) external onlyAttestor {
        if (account == address(0)) revert ZeroAccount();
        if (amount == 0) revert ZeroAmount();
        balanceOf[account] += amount;
        totalSupply += amount;
        emit Transfer(address(0), account, amount);
        emit PointsCredited(account, amount, questId, balanceOf[account]);
    }

    /* --------------------------- bricked ERC-20 write ----------------------- */

    function transfer(address, uint256) external pure returns (bool) {
        revert NotTransferable();
    }

    function transferFrom(address, address, uint256) external pure returns (bool) {
        revert NotTransferable();
    }

    function approve(address, uint256) external pure returns (bool) {
        revert NotTransferable();
    }

    /// @notice Always zero — AP has no allowance concept.
    function allowance(address, address) external pure returns (uint256) {
        return 0;
    }
}
