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

/// @title StreetCredV3
/// @notice Versioned, explainable reputation. Every score carries evidence
///         provenance and a schema version. Sybil-flagged addresses cannot
///         boost their own score.
contract StreetCredV3 {
    uint256 public constant MAX_RECOMPUTE_LOOKBACK = 128;

    // ---------------------------------------------------------------------
    // Types
    // ---------------------------------------------------------------------

    struct Signal {
        uint32 schemaVersion;
        uint32 weightBps; // 0..10000 contribution to score
        bytes32 evidenceCommitment; // merkle root or content hash
        uint8 classification; // 0=self, 1=econ-backed, 2=verified, 3=disputed, 4=expired, 5=legacy, 6=cross-chain
        address attester;
        uint64 observedAt;
        uint64 expiresAt;
        bool negative;
    }

    struct Score {
        uint32 schemaVersion;
        uint32 scoreBps;
        uint32 evidenceCount;
        bytes32 evidenceRoot;
        uint64 lastUpdated;
        bool sybilFlagged;
    }

    mapping(bytes32 => Score) public scoreOf; // key = keccak256(chainId, agentIdOrWallet)
    mapping(bytes32 => Signal[]) internal _signals; // ordered signals for auditability
    mapping(address => bool) public authorizedAttester;

    address public owner;

    // ---------------------------------------------------------------------
    // Errors + Events
    // ---------------------------------------------------------------------

    error NotOwner();
    error UnauthorizedAttester();
    error SybilFlagged();
    error ZeroAddress();
    error InvalidWeight();
    error InvalidLookback();
    error AttesterMismatch();
    error InvalidSignal();

    event AttesterSetV3(address indexed attester, bool authorized);
    event SybilFlaggedV3(bytes32 indexed subject, bool flagged);
    event SignalAddedV3(bytes32 indexed subject, uint256 index, uint8 classification, uint32 weightBps, bool negative);
    event ScoreUpdatedV3(bytes32 indexed subject, uint32 scoreBps, uint32 evidenceCount, bytes32 evidenceRoot);

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

    constructor(address _owner) {
        if (_owner == address(0)) revert ZeroAddress();
        owner = _owner;
    }

    function setAttester(address a, bool v) external onlyOwner {
        authorizedAttester[a] = v;
        emit AttesterSetV3(a, v);
    }

    function setSybilFlag(bytes32 subject, bool flag) external onlyOwner {
        scoreOf[subject].sybilFlagged = flag;
        emit SybilFlaggedV3(subject, flag);
    }

    // ---------------------------------------------------------------------
    // Attestation ingress
    // ---------------------------------------------------------------------

    function addSignal(bytes32 subject, Signal calldata s) external {
        if (!authorizedAttester[msg.sender]) revert UnauthorizedAttester();
        if (s.attester != msg.sender) revert AttesterMismatch();
        if (s.weightBps > 10_000) revert InvalidWeight();
        if (
            subject == bytes32(0) || s.evidenceCommitment == bytes32(0) || s.classification > 6
                || s.observedAt > block.timestamp || (s.expiresAt != 0 && s.expiresAt <= s.observedAt)
        ) revert InvalidSignal();
        Score storage sc = scoreOf[subject];
        if (sc.sybilFlagged && !s.negative) revert SybilFlagged();
        _signals[subject].push(s);
        emit SignalAddedV3(subject, _signals[subject].length - 1, s.classification, s.weightBps, s.negative);
    }

    /// @notice Recompute score from the last N signals. Bounded; no unbounded loops.
    ///         Invariant #12. Weighted differently per classification with EXPLICIT weights.
    function recompute(bytes32 subject, uint256 lookback) external {
        if (!authorizedAttester[msg.sender]) revert UnauthorizedAttester();
        if (lookback > MAX_RECOMPUTE_LOOKBACK) revert InvalidLookback();
        Signal[] storage arr = _signals[subject];
        uint256 n = arr.length;
        if (lookback > n) lookback = n;
        uint256 startIdx = n - lookback;

        uint256 positive = 0;
        uint256 negative = 0;
        uint256 denom = 0;
        uint32 included = 0;
        bytes32 evidenceRoot = bytes32(0);
        for (uint256 i = startIdx; i < n; ++i) {
            Signal storage sig = arr[i];
            if (sig.expiresAt != 0 && sig.expiresAt <= block.timestamp) continue;
            uint256 factor = _classificationWeight(sig.classification);
            uint256 contrib = uint256(sig.weightBps) * factor;
            if (sig.negative) {
                negative += contrib;
            } else {
                positive += contrib;
            }
            denom += factor;
            ++included;
            evidenceRoot = keccak256(
                abi.encode(
                    evidenceRoot,
                    i,
                    sig.schemaVersion,
                    sig.weightBps,
                    sig.evidenceCommitment,
                    sig.classification,
                    sig.attester,
                    sig.observedAt,
                    sig.expiresAt,
                    sig.negative
                )
            );
        }
        uint256 net = positive > negative ? positive - negative : 0;
        uint32 score = denom == 0 ? 0 : uint32(net / denom);
        Score storage sc = scoreOf[subject];
        sc.schemaVersion = 1;
        sc.scoreBps = score;
        sc.evidenceCount = included;
        sc.evidenceRoot = evidenceRoot;
        sc.lastUpdated = uint64(block.timestamp);
        emit ScoreUpdatedV3(subject, score, sc.evidenceCount, sc.evidenceRoot);
    }

    function _classificationWeight(uint8 c) private pure returns (uint256) {
        // Self-asserted = 1, econ-backed = 3, independently verified = 5,
        // disputed = 0, expired = 0, legacy = 1, cross-chain = 2.
        if (c == 0) return 1;
        if (c == 1) return 3;
        if (c == 2) return 5;
        if (c == 3) return 0;
        if (c == 4) return 0;
        if (c == 5) return 1;
        if (c == 6) return 2;
        return 0;
    }

    function signalCount(bytes32 subject) external view returns (uint256) {
        return _signals[subject].length;
    }

    function signalAt(bytes32 subject, uint256 i) external view returns (Signal memory) {
        return _signals[subject][i];
    }
}
