//! [`GraphOp`] — the sync unit and the API's entire write vocabulary
//! (Design Invariants 4 & 5). Every mutation is an op; ops are appended to the
//! oplog and the graph tables are derived state applied in the same
//! transaction.
//!
//! Ops carry their own UUIDv7 ids so that replaying an oplog on another device
//! reproduces the graph byte-for-byte. Mint ids with [`crate::Grit::new_id`].

use serde::{Deserialize, Serialize};
use serde_json::Value as Json;
use uuid::Uuid;

use crate::clock::TimestampMs;
use crate::hlc::Hlc;

/// A single append-only write. See module docs.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum GraphOp {
    /// Create an entity node.
    AddNode {
        /// UUIDv7 identity, minted by the caller ([`crate::Grit::new_id`]).
        id: Uuid,
        /// Entity kind, free-form (e.g. `"person"`, `"concept"`).
        kind: String,
        /// Display name; FTS-indexed.
        name: String,
        /// One-paragraph summary; FTS-indexed.
        #[serde(default)]
        summary: String,
        /// Arbitrary JSON attributes.
        #[serde(default = "empty_object")]
        attrs: Json,
        /// Namespace; filtered in every query.
        #[serde(default)]
        group_id: String,
    },

    /// Create a fact edge between two nodes ("fat edge": the fact sentence
    /// lives on the edge).
    AddEdge {
        /// UUIDv7 identity.
        id: Uuid,
        /// Source node id.
        src: Uuid,
        /// Destination node id.
        dst: Uuid,
        /// Relation label (e.g. `"WORKS_AT"`).
        rel: String,
        /// Natural-language fact sentence; FTS-indexed.
        #[serde(default)]
        fact: String,
        /// Arbitrary JSON attributes.
        #[serde(default = "empty_object")]
        attrs: Json,
        /// Namespace.
        #[serde(default)]
        group_id: String,
        /// Event time the fact became true; `None` = unknown / always.
        #[serde(default)]
        valid_at: Option<TimestampMs>,
    },

    /// Record raw provenance (a chat turn, a document chunk, a sensor-event
    /// summary) and link it to the nodes/edges it mentions.
    AddEpisode {
        /// UUIDv7 identity.
        id: Uuid,
        /// Where this came from (e.g. `"chat"`, `"doc:notes.md"`).
        source: String,
        /// Raw text; FTS-indexed.
        content: String,
        /// Event time of the episode.
        occurred_at: TimestampMs,
        /// Namespace.
        #[serde(default)]
        group_id: String,
        /// Node/edge ids this episode is provenance for.
        #[serde(default)]
        mentions: Vec<Uuid>,
    },

    /// Close an edge's event-time interval: the fact stopped being true at
    /// `invalid_at`. Never deletes; concurrent invalidations converge to the
    /// minimum (earliest) time.
    InvalidateEdge {
        /// Edge to invalidate.
        edge_id: Uuid,
        /// Event time the fact stopped holding.
        invalid_at: TimestampMs,
    },

    /// Fold node `from` into node `into`. Layer 2 decides *whether* (an LLM
    /// judgment); grit only executes. Re-points edges and mentions, marks
    /// `from` expired with a `merged_into` audit pointer.
    MergeNodes {
        /// The node that disappears.
        from: Uuid,
        /// The node that absorbs it.
        into: Uuid,
    },

    /// Explicit, audited right-to-forget. The ONLY destructive op: hard-deletes
    /// and permanently tombstones exactly the listed ids (a tombstoned id can
    /// never be re-added, which is what makes purge commute with concurrent
    /// adds). List incident edge ids explicitly — their fact sentences carry
    /// the content being forgotten; [`crate::Grit::node_history`] enumerates
    /// them. The purge op itself stays in the oplog as the audit record.
    Purge {
        /// Node/edge/episode ids to erase.
        ids: Vec<Uuid>,
    },
}

fn empty_object() -> Json {
    Json::Object(serde_json::Map::new())
}

/// One row of the oplog: an op plus its global identity and merge metadata.
/// This is the unit a future sync mechanism ships between devices.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OplogEntry {
    /// UUIDv7 identity of the op itself (dedup key across devices).
    pub id: Uuid,
    /// Hybrid logical clock at issue time; total merge order.
    pub hlc: Hlc,
    /// Device that issued the op.
    pub device_id: String,
    /// The op.
    pub op: GraphOp,
}
