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

/// @title  Vaultfire Founders Edition
/// @notice Public 5-edition companion to the Genesis 1/1. Anyone can mint at
///         a fixed price; supply hard-capped at 5; proceeds withdrawable by
///         the owner; EIP-2981 5% secondary royalties to the owner.
/// @dev    Minimal ERC-721 + ERC-2981. No external libraries.
contract VaultfireFoundersEdition {
    string  public constant name   = "Vaultfire Founders Edition";
    string  public constant symbol = "VFFE";

    address public immutable owner;
    string  private _baseURI;
    uint256 public constant MAX_SUPPLY = 5;
    uint256 public constant PRICE = 0.1 ether;
    uint96  public constant ROYALTY_BPS = 500; // 5%
    uint256 public totalSupply;

    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);
    event Minted(address indexed to, uint256 indexed tokenId);
    event Withdraw(address indexed to, uint256 amount);

    error SoldOut();
    error WrongPrice();
    error NotOwner();
    error ZeroAddress();
    error NotApproved();
    error NotTokenOwner();
    error NonexistentToken();
    error WithdrawFailed();

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    /// @param owner_   The address that becomes the contract owner and receives
    ///                 all mint proceeds + royalties. Required because the
    ///                 contract is deployed via the Arachnid CREATE2 factory,
    ///                 so msg.sender is the factory — not the user.
    /// @param baseURI_ Base URL for tokenURI (e.g. https://theloopbreaker.com/nft/founders/).
    constructor(address owner_, string memory baseURI_) {
        if (owner_ == address(0)) revert ZeroAddress();
        owner = owner_;
        _baseURI = baseURI_;
    }

    // ---------- Mint ----------

    function mint() external payable returns (uint256 tokenId) {
        if (totalSupply >= MAX_SUPPLY) revert SoldOut();
        if (msg.value != PRICE) revert WrongPrice();
        unchecked { tokenId = ++totalSupply; }
        _ownerOf[tokenId] = msg.sender;
        unchecked { _balanceOf[msg.sender] += 1; }
        emit Transfer(address(0), msg.sender, tokenId);
        emit Minted(msg.sender, tokenId);
        // Auto-forward proceeds to the owner so funds land in ghostkey.base.eth
        // immediately on every mint — no manual withdraw needed.
        (bool ok, ) = owner.call{value: msg.value}("");
        if (!ok) revert WithdrawFailed();
    }

    function remaining() external view returns (uint256) {
        return MAX_SUPPLY - totalSupply;
    }

    // ---------- Withdraw ----------

    function withdraw() external onlyOwner {
        uint256 bal = address(this).balance;
        (bool ok, ) = owner.call{value: bal}("");
        if (!ok) revert WithdrawFailed();
        emit Withdraw(owner, bal);
    }

    // ---------- Metadata ----------

    function setBaseURI(string calldata baseURI_) external onlyOwner {
        _baseURI = baseURI_;
    }

    function tokenURI(uint256 tokenId) external view returns (string memory) {
        if (_ownerOf[tokenId] == address(0)) revert NonexistentToken();
        return string(abi.encodePacked(_baseURI, _toString(tokenId), ".json"));
    }

    function contractURI() external view returns (string memory) {
        return string(abi.encodePacked(_baseURI, "contract.json"));
    }

    // ---------- ERC-721 ----------

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

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

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

    function getApproved(uint256 tokenId) external view returns (address) {
        if (_ownerOf[tokenId] == address(0)) revert NonexistentToken();
        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 {
        _transfer(from, to, tokenId);
    }

    function safeTransferFrom(address from, address to, uint256 tokenId) external {
        _transfer(from, to, tokenId);
        _checkOnERC721Received(from, to, tokenId, "");
    }

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

    function _transfer(address from, address to, uint256 tokenId) internal {
        if (to == address(0)) revert ZeroAddress();
        address o = _ownerOf[tokenId];
        if (o == address(0)) revert NonexistentToken();
        if (o != from) revert NotTokenOwner();
        if (msg.sender != o
            && _approved[tokenId] != msg.sender
            && !_operatorApprovals[o][msg.sender]) revert NotApproved();
        delete _approved[tokenId];
        unchecked {
            _balanceOf[from] -= 1;
            _balanceOf[to]   += 1;
        }
        _ownerOf[tokenId] = to;
        emit Transfer(from, to, tokenId);
    }

    function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) internal {
        if (to.code.length == 0) return;
        try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 ret) {
            if (ret != IERC721Receiver.onERC721Received.selector) revert NotApproved();
        } catch {
            revert NotApproved();
        }
    }

    // ---------- ERC-2981 royalties ----------

    function royaltyInfo(uint256 /*tokenId*/, uint256 salePrice)
        external view returns (address receiver, uint256 royaltyAmount)
    {
        receiver = owner;
        royaltyAmount = (salePrice * ROYALTY_BPS) / 10000;
    }

    // ---------- ERC-165 ----------

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

    // ---------- utils ----------

    function _toString(uint256 v) internal pure returns (string memory) {
        if (v == 0) return "0";
        uint256 n = v;
        uint256 len;
        while (n != 0) { len++; n /= 10; }
        bytes memory b = new bytes(len);
        while (v != 0) { len--; b[len] = bytes1(uint8(48 + v % 10)); v /= 10; }
        return string(b);
    }
}

interface IERC721Receiver {
    function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4);
}
