//! `update_ontology_edge` — add an edge to the core ontology DAG.
//!
//! Pre-validator rejects cycles. Phase 1 cycle check is conservative —
//! a direct loop (from == to) and any 1-step reverse (from → to where
//! to → from already exists) trigger reject.

use ontoref_derive::{onto_operation, onto_validator};
use ontoref_types::canonical;
use ontoref_types::operation::{OpBody, OpPayload};
use ontoref_types::{AttrId, EntityId, Operation, Value};
use smol_str::SmolStr;

use crate::context::OpContext;
use crate::error::OpError;
use crate::output::{EffectRecord, OpOutput};
use crate::validation::traits::{Slice, ValidationCtx};
use crate::validation::verdict::Verdict;

#[onto_operation(
    id = "update_ontology_edge",
    description = "Insert one edge into core.ncl edges array. Pre-validator rejects direct \
                   self-loops and 1-step cycles.",
    validation_sla = "Synchronous",
    precondition = "ontology_edge_dag_check",
    pre_validators = "ontology_edge_no_cycle",
    effects = "Insert:OntologyEdge:from,to,kind",
    render_paths = "",
    witness_shape = "edge_upsert",
    actor_policy = "developer,admin"
)]
fn update_ontology_edge(
    inputs: &serde_json::Value,
    ctx: &mut OpContext,
) -> Result<OpOutput, OpError> {
    let from = inputs
        .get("from")
        .and_then(|v| v.as_str())
        .ok_or_else(|| OpError::body("update_ontology_edge", "missing 'from'"))?
        .to_owned();
    let to = inputs
        .get("to")
        .and_then(|v| v.as_str())
        .ok_or_else(|| OpError::body("update_ontology_edge", "missing 'to'"))?
        .to_owned();
    let kind = inputs
        .get("kind")
        .and_then(|v| v.as_str())
        .ok_or_else(|| OpError::body("update_ontology_edge", "missing 'kind'"))?
        .to_owned();

    let entity_id = format!("edge:{from}:{to}:{kind}");
    let signing_key = ctx.signing_key.clone();
    let actor = ontoref_types::PublicKey::from_signing_key(&signing_key);
    let timestamp = ctx.next_hlc();
    let op = Operation::sign(
        OpBody {
            actor,
            parents: vec![],
            payload: OpPayload::EntityAssert {
                attrs: vec![
                    (AttrId::new("from"), Value::Str(SmolStr::new(&from))),
                    (AttrId::new("to"), Value::Str(SmolStr::new(&to))),
                    (AttrId::new("kind"), Value::Str(SmolStr::new(&kind))),
                ],
                entity: EntityId::new(&entity_id),
                valid_from: None,
            },
            timestamp,
        },
        &signing_key,
    );
    ctx.commit_layer.apply(&op);

    let witness_payload =
        canonical::encode(&op).map_err(|e| OpError::body("update_ontology_edge", e.to_string()))?;
    Ok(OpOutput {
        effects: vec![EffectRecord {
            kind: "Insert".to_owned(),
            entity: entity_id,
            attrs: vec!["from".into(), "to".into(), "kind".into()],
            dimensions: vec![],
        }],
        witness_payload,
    })
}

/// Phase 1 DAG check: reject self-loops and 1-step reverse cycles.
/// Slice shape (from `runtime::load_ontology_edge_dag_check`):
/// `{ existing_reverse: <Edge or null>, inputs }`.
#[onto_validator(
    id = "ontology_edge_no_cycle",
    description = "Reject ontology edges that create a direct self-loop or a 1-step reverse cycle.",
    category = "Contextual",
    slice_query = "ontology_edge_dag_check"
)]
fn ontology_edge_no_cycle(slice: &Slice, _ctx: &ValidationCtx) -> Verdict {
    let Some(inputs) = slice.get("inputs") else {
        return Verdict::Unknown;
    };
    let from = inputs.get("from").and_then(|v| v.as_str()).unwrap_or("");
    let to = inputs.get("to").and_then(|v| v.as_str()).unwrap_or("");
    if from.is_empty() || to.is_empty() {
        return Verdict::reject(
            "edge must declare both 'from' and 'to'",
            "ontology_edge_no_cycle",
        );
    }
    if from == to {
        return Verdict::reject("self-loop rejected (from == to)", "ontology_edge_no_cycle");
    }
    let reverse_exists = slice
        .get("existing_reverse")
        .map(|v| !v.is_null())
        .unwrap_or(false);
    if reverse_exists {
        return Verdict::reject(
            format!("1-step cycle: reverse edge {to}→{from} already exists"),
            "ontology_edge_no_cycle",
        );
    }
    Verdict::Accept
}
