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

/**
 * @title AereAttestationGateway — unified registry for regulator attestations
 * @notice The compliance landscape splits the same primitive — "this entity
 *         has been assessed by an authorised body" — into incompatible silos:
 *
 *           - MiCA Title II/V: CASP licence + best-execution conformity
 *           - EU AI Act Annex III: high-risk AI conformity
 *           - SEC 1099-DA: broker reporting compliance
 *           - FCA CP24/x: UK consumer-duty conformity
 *           - FINMA / MAS: equivalents in CH / SG
 *
 *         AereAttestationGateway gives those bodies (and the protocols
 *         that need to RELY on their judgement) a single on-chain shape.
 *
 *         MODEL:
 *
 *           1. The Foundation publishes a SCHEMA when a new attestation
 *              shape is needed. Each schema has an id (e.g. keccak256
 *              ("mica-casp-v1") ), an immutable hash of the off-chain
 *              human-readable spec, and a semver pin. Schemas are
 *              append-only — fixing typos requires a new schemaId.
 *
 *           2. Per-schema, the Foundation registers AUTHORISED ATTESTORS
 *              — addresses that this schema accepts as valid issuers.
 *              The Foundation is one such attestor for some schemas; for
 *              others (MiCA-Art-78 conformity, say) the only valid
 *              attestor is the customer's own NCA-registered auditor.
 *
 *           3. An attestor issues an attestation. The payload itself
 *              lives off-chain (ipfs:// or https://, the full PDF / JSON
 *              spec); the chain stores ONLY the hash + validity window
 *              + the schema/subject/attestor triple. No PII.
 *
 *           4. Revocation: the issuing attestor can revoke at any time.
 *              Reads return `isValidNow(...) == false` immediately. A
 *              revoked attestation cannot be un-revoked (the attestor
 *              would issue a fresh one). The previous attestation's
 *              revocation is preserved in event history forever.
 *
 *           5. INHERITANCE (optional): an attestation can reference a
 *              parent attestation id. The validity check chases the
 *              chain: if any ancestor is revoked or expired, the child
 *              fails. Useful for "this provider is allowed to issue
 *              best-execution receipts BECAUSE Foundation has attested
 *              their MiCA licence." Cycle protection via depth bound.
 *
 *         WHY NOT EAS?
 *           Ethereum Attestation Service does generic on-chain
 *           attestations beautifully. This contract is intentionally
 *           narrower: it bakes in inheritance + per-schema attestor
 *           gating + a Foundation-published schema registry. Consumers
 *           that need richer features can adopt EAS in parallel.
 *
 *         GAS:
 *           attest() ~70-80k cold-path (1 SSTORE for the main record,
 *           1 for the per-(subject,schema) head pointer).
 *
 *         IMMUTABILITY:
 *           - FOUNDATION immutable.
 *           - Schemas append-only; cannot be edited or removed.
 *           - Attestor authorisation flags can be toggled OFF but never
 *             retroactively invalidate past attestations the attestor
 *             issued while authorised.
 *           - MAX_INHERITANCE_DEPTH constant.
 */
contract AereAttestationGateway {

    /* ================================ config ================================ */

    address public immutable FOUNDATION;
    uint256 public constant MAX_INHERITANCE_DEPTH = 8;

    /* ================================= types ================================= */

    struct Schema {
        bytes32 specHash;      // SHA256 of the off-chain human-readable spec
        string  name;          // e.g. "mica-casp-v1"
        string  semver;        // e.g. "1.0.0"
        string  specUri;       // ipfs:// or https:// — full spec doc
        uint64  publishedAt;
        bool    exists;
    }

    struct Attestation {
        bytes32 schemaId;
        bytes32 subject;       // keccak256 of the subject identifier
        address attestor;
        bytes32 parentId;      // 0x0 if no parent
        bytes32 payloadHash;   // SHA256 of the off-chain payload
        string  payloadUri;
        uint64  validFrom;
        uint64  validUntil;    // 0 = no expiry
        bool    revoked;
        bool    exists;
    }

    /* ================================ storage ================================ */

    /// @notice schemaId → Schema metadata. Append-only.
    mapping(bytes32 => Schema) public schemas;

    /// @notice schemaId → attestor → authorised flag. Toggleable.
    mapping(bytes32 => mapping(address => bool)) public isAuthorisedAttestor;

    /// @notice attestor → next nonce (used to derive attestationId).
    mapping(address => uint256) public attestorNonce;

    /// @notice attestationId → Attestation. Append-only except for `revoked`.
    mapping(bytes32 => Attestation) internal _attestations;

    /// @notice (subject, schemaId) → most-recent attestationId for that pair.
    ///         Convenience for consumers that just want "is this subject
    ///         currently attested under this schema?"
    /// AUDIT FIX (HIGH #24): only updated when prior is invalid OR new
    /// passes _isValid at write-time. Prevents griefing where attestor B
    /// posts an already-expired record that wipes attestor A's valid one.
    mapping(bytes32 => mapping(bytes32 => bytes32)) public latestFor;
    /// @notice (subject, schemaId, attestor) → that attestor's most-recent
    ///         attestation id for that subject. Consumers reading per-attestor
    ///         data go through this map.
    mapping(bytes32 => mapping(bytes32 => mapping(address => bytes32))) public latestForBy;

    /* ================================ events ================================ */

    event SchemaPublished(bytes32 indexed schemaId, bytes32 specHash, string name, string semver, string specUri);
    event AttestorAuthorisation(bytes32 indexed schemaId, address indexed attestor, bool authorised);
    event Attested(
        bytes32 indexed attestationId,
        bytes32 indexed schemaId,
        bytes32 indexed subject,
        address attestor,
        bytes32 parentId,
        bytes32 payloadHash,
        uint64  validFrom,
        uint64  validUntil,
        string  payloadUri
    );
    event Revoked(bytes32 indexed attestationId, address indexed attestor);

    /* ================================ errors ================================ */

    error NotFoundation();
    error SchemaExists();
    error SchemaUnknown();
    error EmptySpec();
    error NotAuthorisedAttestor();
    error AttestationExists();
    error AttestationUnknown();
    error AlreadyRevoked();
    error NotIssuer();
    error BadValidityWindow();
    error InheritanceDepthExceeded();
    error ParentInvalid();
    error ParentSubjectMismatch();

    /* =============================== modifiers ============================== */

    modifier onlyFoundation() {
        if (msg.sender != FOUNDATION) revert NotFoundation();
        _;
    }

    /* ============================== constructor ============================= */

    constructor(address foundation) {
        require(foundation != address(0), "zero-foundation");
        FOUNDATION = foundation;
    }

    /* ============================== schema ops ============================== */

    function publishSchema(
        bytes32 schemaId,
        bytes32 specHash,
        string calldata name,
        string calldata semver,
        string calldata specUri
    ) external onlyFoundation {
        if (specHash == bytes32(0)) revert EmptySpec();
        if (schemas[schemaId].exists) revert SchemaExists();
        schemas[schemaId] = Schema({
            specHash: specHash,
            name: name,
            semver: semver,
            specUri: specUri,
            publishedAt: uint64(block.timestamp),
            exists: true
        });
        emit SchemaPublished(schemaId, specHash, name, semver, specUri);
    }

    function setAttestor(bytes32 schemaId, address attestor, bool authorised) external onlyFoundation {
        if (!schemas[schemaId].exists) revert SchemaUnknown();
        isAuthorisedAttestor[schemaId][attestor] = authorised;
        emit AttestorAuthorisation(schemaId, attestor, authorised);
    }

    /* ============================== attest path ============================= */

    /**
     * @notice Issue an attestation. msg.sender MUST be authorised for the
     *         schema. The attestationId is keccak256(attestor, nonce++).
     * @return attestationId The deterministic id.
     */
    function attest(
        bytes32 schemaId,
        bytes32 subject,
        bytes32 parentId,
        bytes32 payloadHash,
        uint64 validFrom,
        uint64 validUntil,
        string calldata payloadUri
    ) external returns (bytes32 attestationId) {
        if (!schemas[schemaId].exists) revert SchemaUnknown();
        if (!isAuthorisedAttestor[schemaId][msg.sender]) revert NotAuthorisedAttestor();
        if (validUntil != 0 && validUntil < validFrom) revert BadValidityWindow();

        // If a parent is supplied, sanity-check inheritance depth + existence
        // BEFORE writing the new record. The chase is shallow (≤ MAX_DEPTH).
        if (parentId != bytes32(0)) {
            Attestation storage parent = _attestations[parentId];
            if (!parent.exists) revert ParentInvalid();
            // AUDIT FIX (HIGH #23): parent must attest the SAME subject. Without
            // this, low-bar attestors could chain inheritance to high-bar parents
            // of unrelated subjects, fabricating transitive trust.
            if (parent.subject != subject) revert ParentSubjectMismatch();
            _checkInheritanceDepth(parentId);
        }

        uint256 nonce = attestorNonce[msg.sender]++;
        attestationId = keccak256(abi.encode(msg.sender, nonce));
        if (_attestations[attestationId].exists) revert AttestationExists();

        _attestations[attestationId] = Attestation({
            schemaId: schemaId,
            subject: subject,
            attestor: msg.sender,
            parentId: parentId,
            payloadHash: payloadHash,
            payloadUri: payloadUri,
            validFrom: validFrom,
            validUntil: validUntil,
            revoked: false,
            exists: true
        });

        // Per-attestor latest pointer always updates (consumer chooses
        // which attestor to read).
        latestForBy[subject][schemaId][msg.sender] = attestationId;
        // AUDIT FIX (HIGH #24): only overwrite the schema-wide latestFor
        // when the prior is invalid (revoked/expired/missing) OR the new
        // attestation is itself currently valid.
        bytes32 prior = latestFor[subject][schemaId];
        bool priorValid = prior != bytes32(0) && _isValid(prior, block.timestamp, 0);
        bool newValidNow = (validFrom <= block.timestamp) && (validUntil == 0 || validUntil >= block.timestamp);
        if (!priorValid || newValidNow) {
            latestFor[subject][schemaId] = attestationId;
        }

        emit Attested(attestationId, schemaId, subject, msg.sender, parentId, payloadHash, validFrom, validUntil, payloadUri);
    }

    function revoke(bytes32 attestationId) external {
        Attestation storage a = _attestations[attestationId];
        if (!a.exists) revert AttestationUnknown();
        if (a.attestor != msg.sender) revert NotIssuer();
        if (a.revoked) revert AlreadyRevoked();
        a.revoked = true;
        emit Revoked(attestationId, msg.sender);
    }

    /* ================================ views ================================= */

    /// @notice Full attestation record (struct memory copy).
    function attestationOf(bytes32 attestationId) external view returns (Attestation memory) {
        return _attestations[attestationId];
    }

    /// @notice The "is this attestation good RIGHT NOW" check.
    ///         Returns false if:
    ///           - the attestation doesn't exist
    ///           - it's revoked
    ///           - block.timestamp is outside [validFrom, validUntil]
    ///           - any ancestor (within MAX depth) fails the same check
    function isValidNow(bytes32 attestationId) external view returns (bool) {
        return _isValid(attestationId, block.timestamp, 0);
    }

    function _isValid(bytes32 id, uint256 nowTs, uint256 depth) internal view returns (bool) {
        if (depth >= MAX_INHERITANCE_DEPTH) return false;
        Attestation storage a = _attestations[id];
        if (!a.exists || a.revoked) return false;
        if (nowTs < a.validFrom) return false;
        if (a.validUntil != 0 && nowTs > a.validUntil) return false;
        if (a.parentId == bytes32(0)) return true;
        return _isValid(a.parentId, nowTs, depth + 1);
    }

    /// @notice Convenience: subject is currently attested under schema by
    ///         the LATEST issued attestation for that (subject, schema).
    function subjectCurrentlyAttested(bytes32 subject, bytes32 schemaId) external view returns (bool) {
        bytes32 id = latestFor[subject][schemaId];
        if (id == bytes32(0)) return false;
        return _isValid(id, block.timestamp, 0);
    }

    /// @dev Validates that walking up from parent doesn't exceed MAX_DEPTH.
    function _checkInheritanceDepth(bytes32 parentId) internal view {
        uint256 depth = 1;
        bytes32 cur = parentId;
        while (cur != bytes32(0)) {
            if (depth >= MAX_INHERITANCE_DEPTH) revert InheritanceDepthExceeded();
            Attestation storage a = _attestations[cur];
            if (!a.exists) revert ParentInvalid();
            cur = a.parentId;
            unchecked { depth++; }
        }
    }
}
