// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.24;

import {EveryChannelWitnessRegistry} from "./EveryChannelWitnessRegistry.sol";

/// @title EveryChannelObservationLedger
/// @notice Observation consensus ledger for reality-derived every.channel epochs.
/// Witnesses attest to the same derived observation hash, and the first candidate to hit quorum
/// finalizes the `(stream, epoch)` slot.
contract EveryChannelObservationLedger {
    struct ObservationHeader {
        bytes32 streamHash;
        bytes32 epochHash;
        bytes32 parentObservationHash;
        bytes32 dataRoot;
        bytes32 locatorHash;
        uint64 observedUnixMs;
        uint64 sequence;
    }

    struct Candidate {
        bytes32 slot;
        address proposer;
        uint64 observedUnixMs;
        uint64 sequence;
        uint32 attestations;
        bool finalized;
    }

    EveryChannelWitnessRegistry public immutable witnessRegistry;
    uint256 public immutable quorum;

    mapping(bytes32 => ObservationHeader) private observationHeaders;
    mapping(bytes32 => Candidate) public candidates;
    mapping(bytes32 => bytes32) public finalizedObservationBySlot;
    mapping(bytes32 => mapping(address => bool)) public hasAttested;

    event ObservationProposed(
        bytes32 indexed slot,
        bytes32 indexed observationHash,
        address indexed proposer,
        bytes32 streamHash,
        bytes32 epochHash,
        bytes32 parentObservationHash,
        bytes32 dataRoot,
        bytes32 locatorHash,
        uint64 observedUnixMs,
        uint64 sequence
    );

    event ObservationAttested(
        bytes32 indexed slot,
        bytes32 indexed observationHash,
        address indexed witness,
        uint32 attestations
    );

    event ObservationFinalized(
        bytes32 indexed slot,
        bytes32 indexed observationHash,
        uint32 attestations
    );

    error NotWitness(address caller);
    error InvalidRegistry(address registry);
    error InvalidQuorum(uint256 quorum);
    error UnknownObservation(bytes32 observationHash);
    error ObservationAlreadyExists(bytes32 observationHash);
    error ObservationSlotAlreadyFinalized(bytes32 slot, bytes32 observationHash);
    error DuplicateAttestation(bytes32 observationHash, address witness);

    constructor(address registry, uint256 quorumThreshold) {
        if (registry == address(0)) revert InvalidRegistry(registry);
        if (quorumThreshold == 0) revert InvalidQuorum(quorumThreshold);
        witnessRegistry = EveryChannelWitnessRegistry(registry);
        quorum = quorumThreshold;
    }

    modifier onlyWitness() {
        if (!witnessRegistry.isWitness(msg.sender)) revert NotWitness(msg.sender);
        _;
    }

    function observationSlot(
        bytes32 streamHash,
        bytes32 epochHash
    ) public pure returns (bytes32) {
        return keccak256(abi.encode(streamHash, epochHash));
    }

    function hashObservationHeader(
        ObservationHeader memory header
    ) public pure returns (bytes32) {
        return keccak256(
            abi.encode(
                header.streamHash,
                header.epochHash,
                header.parentObservationHash,
                header.dataRoot,
                header.locatorHash,
                header.observedUnixMs,
                header.sequence
            )
        );
    }

    function getObservationHeader(
        bytes32 observationHash
    ) external view returns (ObservationHeader memory) {
        if (candidates[observationHash].slot == bytes32(0)) {
            revert UnknownObservation(observationHash);
        }
        return observationHeaders[observationHash];
    }

    function proposeObservation(
        ObservationHeader calldata header
    ) external returns (bytes32 observationHash) {
        observationHash = hashObservationHeader(header);
        bytes32 slot = observationSlot(header.streamHash, header.epochHash);
        bytes32 finalized = finalizedObservationBySlot[slot];
        if (finalized != bytes32(0) && finalized != observationHash) {
            revert ObservationSlotAlreadyFinalized(slot, finalized);
        }
        if (candidates[observationHash].slot != bytes32(0)) {
            revert ObservationAlreadyExists(observationHash);
        }

        observationHeaders[observationHash] = header;
        candidates[observationHash] = Candidate({
            slot: slot,
            proposer: msg.sender,
            observedUnixMs: header.observedUnixMs,
            sequence: header.sequence,
            attestations: 0,
            finalized: false
        });

        emit ObservationProposed(
            slot,
            observationHash,
            msg.sender,
            header.streamHash,
            header.epochHash,
            header.parentObservationHash,
            header.dataRoot,
            header.locatorHash,
            header.observedUnixMs,
            header.sequence
        );

        if (witnessRegistry.isWitness(msg.sender)) {
            _attest(observationHash, msg.sender);
        }
    }

    function attestObservation(bytes32 observationHash) external onlyWitness {
        _attest(observationHash, msg.sender);
    }

    function _attest(bytes32 observationHash, address witness) internal {
        Candidate storage candidate = candidates[observationHash];
        if (candidate.slot == bytes32(0)) revert UnknownObservation(observationHash);
        if (hasAttested[observationHash][witness]) {
            revert DuplicateAttestation(observationHash, witness);
        }

        bytes32 finalized = finalizedObservationBySlot[candidate.slot];
        if (finalized != bytes32(0) && finalized != observationHash) {
            revert ObservationSlotAlreadyFinalized(candidate.slot, finalized);
        }

        hasAttested[observationHash][witness] = true;
        candidate.attestations += 1;

        emit ObservationAttested(
            candidate.slot,
            observationHash,
            witness,
            candidate.attestations
        );

        if (!candidate.finalized && candidate.attestations >= quorum) {
            candidate.finalized = true;
            finalizedObservationBySlot[candidate.slot] = observationHash;
            emit ObservationFinalized(
                candidate.slot,
                observationHash,
                candidate.attestations
            );
        }
    }
}
