//! `transition_adr` — move an ADR through its lifecycle states.
//!
//! Valid transitions:
//! - `Proposed` → `Accepted` (requires ondaod evaluation)
//! - `Proposed` → `Deprecated`
//! - `Accepted` → `Superseded`
//!
//! Phase 1 records the transition as an EntityAssert on the ADR
//! entity. The full ADR file rewrite is deferred to the byte-stable
//! ADR renderer.

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 = "transition_adr",
    description = "Move an ADR from Proposed to Accepted/Deprecated, or from Accepted to \
                   Superseded. Accept requires ondaod.",
    validation_sla = "Synchronous",
    precondition = "ondaod",
    pre_validators = "adr_transition_allowed",
    effects = "Update:ADR:status",
    render_paths = "",
    witness_shape = "adr_transition",
    actor_policy = "developer,admin"
)]
fn transition_adr(inputs: &serde_json::Value, ctx: &mut OpContext) -> Result<OpOutput, OpError> {
    let id = inputs
        .get("id")
        .and_then(|v| v.as_str())
        .ok_or_else(|| OpError::body("transition_adr", "missing 'id'"))?
        .to_owned();
    let new_status = inputs
        .get("new_status")
        .and_then(|v| v.as_str())
        .ok_or_else(|| OpError::body("transition_adr", "missing 'new_status'"))?
        .to_owned();

    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("status"), Value::Str(SmolStr::new(&new_status)))],
                entity: EntityId::new(&id),
                valid_from: None,
            },
            timestamp,
        },
        &signing_key,
    );
    ctx.commit_layer.apply(&op);

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

/// Reject transitions that are not in the allowed set, and reject
/// Accept transitions without ondaod engagement.
#[onto_validator(
    id = "adr_transition_allowed",
    description = "Reject unsupported ADR transitions; require ondaod when transitioning to \
                   Accepted.",
    category = "Contextual",
    slice_query = "ondaod"
)]
fn adr_transition_allowed(slice: &Slice, _ctx: &ValidationCtx) -> Verdict {
    let Some(inputs) = slice.get("inputs") else {
        return Verdict::Unknown;
    };
    let new_status = inputs
        .get("new_status")
        .and_then(|v| v.as_str())
        .unwrap_or("");
    let allowed = matches!(new_status, "Accepted" | "Deprecated" | "Superseded");
    if !allowed {
        return Verdict::reject(
            format!("unsupported ADR transition target '{new_status}'"),
            "adr_transition_allowed",
        );
    }
    if new_status == "Accepted" {
        let Some(ondaod) = slice.get("ondaod") else {
            return Verdict::reject(
                "Accept transition requires ondaod evaluation",
                "adr_transition_allowed",
            );
        };
        let has_engagement = ondaod
            .get("engaged_tensions")
            .and_then(|v| v.as_array())
            .map(|a| !a.is_empty())
            .unwrap_or(false)
            || ondaod
                .get("synthesis_state")
                .and_then(|v| v.as_str())
                .map(|s| !s.is_empty())
                .unwrap_or(false);
        if !has_engagement {
            return Verdict::reject(
                "Accept transition requires engaged_tensions OR synthesis_state rationale",
                "adr_transition_allowed",
            );
        }
    }
    Verdict::Accept
}
