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

/// @title  Vaultfire Genesis NFT
/// @notice One-of-one commemorative NFT for the Vaultfire protocol genesis.
/// @dev    Minimal ERC-721 with a single immutable token (#1) minted to the
///         deployer at construction. The tokenURI is fixed at deploy time
///         and points to canonical JSON metadata served from
///         theloopbreaker.com/nft/vaultfire-genesis.json.
contract VaultfireGenesisNFT {
    string  public constant name   = "Vaultfire Genesis";
    string  public constant symbol = "VFGEN";

    address public immutable owner;
    string  private _tokenURI;
    bool    public minted;

    mapping(uint256 => address) private _ownerOf;
    mapping(address => uint256) private _balanceOf;
    mapping(uint256 => address) private _approved;
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    error AlreadyMinted();
    error NotOwner();
    error NotMinted();
    error ZeroAddress();
    error NotAuthorized();
    error WrongFrom();
    error InvalidReceiver();

    /// @param recipient_ The address that receives ownership of the contract
    ///                   AND token #1. Required because the contract is deployed
    ///                   via the Arachnid CREATE2 factory, so msg.sender is the
    ///                   factory — not the user. Must be passed explicitly.
    /// @param tokenURI_  The canonical metadata URL (e.g. https://theloopbreaker.com/nft/vaultfire-genesis.json).
    constructor(address recipient_, string memory tokenURI_) {
        if (recipient_ == address(0)) revert ZeroAddress();
        owner = recipient_;
        _tokenURI = tokenURI_;
        // Auto-mint #1 to recipient on construction
        _ownerOf[1] = recipient_;
        _balanceOf[recipient_] = 1;
        minted = true;
        emit Transfer(address(0), recipient_, 1);
    }

    /// @notice Allow the owner to update the metadata URL once if the host changes.
    /// @dev    One-shot escape hatch; can be hard-disabled by passing the zero string.
    function setTokenURI(string calldata newURI) external {
        if (msg.sender != owner) revert NotOwner();
        _tokenURI = newURI;
    }

    function tokenURI(uint256 tokenId) external view returns (string memory) {
        if (tokenId != 1 || !minted) revert NotMinted();
        return _tokenURI;
    }

    /* ───── ERC-721 minimal surface ───── */

    function balanceOf(address who) external view returns (uint256) {
        if (who == address(0)) revert ZeroAddress();
        return _balanceOf[who];
    }

    function ownerOf(uint256 tokenId) public view returns (address) {
        address o = _ownerOf[tokenId];
        if (o == address(0)) revert NotMinted();
        return o;
    }

    function approve(address to, uint256 tokenId) external {
        address o = ownerOf(tokenId);
        if (msg.sender != o && !_operatorApprovals[o][msg.sender]) revert NotAuthorized();
        _approved[tokenId] = to;
        emit Approval(o, to, tokenId);
    }

    function getApproved(uint256 tokenId) external view returns (address) {
        ownerOf(tokenId); // reverts if not minted
        return _approved[tokenId];
    }

    function setApprovalForAll(address operator, bool approved) external {
        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    function isApprovedForAll(address o, address operator) external view returns (bool) {
        return _operatorApprovals[o][operator];
    }

    function transferFrom(address from, address to, uint256 tokenId) public {
        address o = ownerOf(tokenId);
        if (o != from) revert WrongFrom();
        if (to == address(0)) revert InvalidReceiver();
        if (
            msg.sender != o &&
            _approved[tokenId] != msg.sender &&
            !_operatorApprovals[o][msg.sender]
        ) revert NotAuthorized();
        delete _approved[tokenId];
        _balanceOf[from] -= 1;
        _balanceOf[to]   += 1;
        _ownerOf[tokenId] = to;
        emit Transfer(from, to, tokenId);
    }

    function safeTransferFrom(address from, address to, uint256 tokenId) external {
        transferFrom(from, to, tokenId);
    }

    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata) external {
        transferFrom(from, to, tokenId);
    }

    function totalSupply() external pure returns (uint256) { return 1; }
    function tokenByIndex(uint256 i) external pure returns (uint256) { require(i == 0, "out"); return 1; }
    function tokenOfOwnerByIndex(address o, uint256 i) external view returns (uint256) {
        require(i == 0 && _balanceOf[o] == 1, "out");
        require(_ownerOf[1] == o, "not owner");
        return 1;
    }

    /// @notice ERC-165
    function supportsInterface(bytes4 iid) external pure returns (bool) {
        return
            iid == 0x01ffc9a7 || // ERC-165
            iid == 0x80ac58cd || // ERC-721
            iid == 0x5b5e139f;   // ERC-721 Metadata
    }
}
