//! `transition_tier` — tier-2 catalogued surface for ADR-033.
//!
//! Routes through [`crate::tier_transition::execute`], the single canonical
//! Rust function (Hard constraint `transition-tier-single-rust-function`).
//! The CLI surface `ore tier set` and this op handler both call into the
//! same function — different reachability paths, one logic.
//!
//! Inputs:
//!   - `target_tier` (String): one of `"Tier0"`, `"Tier1"`, `"Tier2"`.
//!
//! Current ops state is read from `<project_root>/.ontoref/config.ncl`
//! via a string-level parse. The `OpContext::project_root` field is
//! required — the op fails with `OpError` when absent.
//!
//! Pending migrations are NOT discovered by this handler — the guard
//! treats them as empty here, matching the watcher's advisory behaviour.
//! Strict pending checks remain in the CLI surface (where `tier.nu`
//! shells out to `ore migrate pending` before calling the binary).

use std::path::Path;

use ontoref_derive::onto_operation;
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::tier::{OpsConfig, Phase, Tier};
use crate::tier_transition::{execute, TransitionContext};

fn parse_tier(s: &str) -> Result<Tier, OpError> {
    match s {
        "Tier0" => Ok(Tier::Tier0),
        "Tier1" => Ok(Tier::Tier1),
        "Tier2" => Ok(Tier::Tier2),
        other => Err(OpError::body(
            "transition_tier",
            format!("unknown tier '{other}': expected Tier0 | Tier1 | Tier2"),
        )),
    }
}

/// Read the current ops state from `<project_root>/.ontoref/config.ncl`
/// via string-level inspection. Returns defaults (Tier0/Progressive)
/// when the file is absent or lacks an `ops` block — matching the
/// `#[serde(default)]` semantics of [`OpsConfig`].
fn current_ops_from_disk(project_root: &Path) -> OpsConfig {
    let config_path = project_root.join(".ontoref").join("config.ncl");
    let contents = std::fs::read_to_string(&config_path).unwrap_or_default();
    let tier = if contents.contains("'Tier2") {
        Tier::Tier2
    } else if contents.contains("'Tier1") {
        Tier::Tier1
    } else {
        Tier::Tier0
    };
    let phase = if contents.contains("'Pure") {
        Phase::Pure
    } else {
        Phase::Progressive
    };
    OpsConfig { tier, phase }
}

#[onto_operation(
    id = "transition_tier",
    description = "Mutate the project's `ops.tier` declaration. Enforces ADR-029 / ADR-033 \
                   guards: refuses when migrations are pending, atomically rewrites \
                   .ontoref/config.ncl, emits a tier-transition witness covering the change.",
    validation_sla = "Synchronous",
    precondition = "",
    pre_validators = "",
    effects = "Update:ProjectOps:tier,phase",
    render_paths = ".ontoref/config.ncl",
    witness_shape = "tier_transition",
    actor_policy = "developer,admin"
)]
fn transition_tier(inputs: &serde_json::Value, ctx: &mut OpContext) -> Result<OpOutput, OpError> {
    let target_str = inputs
        .get("target_tier")
        .and_then(|v| v.as_str())
        .ok_or_else(|| OpError::body("transition_tier", "missing 'target_tier'"))?;
    let target = parse_tier(target_str)?;

    let project_root = ctx
        .project_root
        .clone()
        .ok_or_else(|| OpError::body("transition_tier", "OpContext.project_root is required"))?;

    let current_ops = current_ops_from_disk(&project_root);
    let from_tier = current_ops.tier;

    // Pending migrations: empty here (advisory). The CLI surface enforces
    // strict pending checks before dispatch.
    let transition_ctx = TransitionContext {
        project_root: project_root.clone(),
        current_ops: current_ops.clone(),
        pending_migrations: Vec::new(),
        // The ops surface already carries the actor's signing key — forward
        // it so a Tier2 descent can emit its mandatory witness (ADR-036).
        #[cfg(feature = "substrate")]
        descent_signing_key: Some(ctx.signing_key.clone()),
    };

    let outcome = execute(target, transition_ctx)
        .map_err(|e| OpError::body("transition_tier", format!("tier transition failed: {e}")))?;

    let entity_id = "project_ops:state".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("tier"),
                        Value::Str(SmolStr::new(format!("{:?}", outcome.to_tier))),
                    ),
                    (
                        AttrId::new("phase"),
                        Value::Str(SmolStr::new(format!("{:?}", outcome.phase))),
                    ),
                    (
                        AttrId::new("from_tier"),
                        Value::Str(SmolStr::new(format!("{from_tier:?}"))),
                    ),
                ],
                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("transition_tier", e.to_string()))?;

    Ok(OpOutput {
        effects: vec![EffectRecord {
            kind: "Update".to_owned(),
            entity: entity_id,
            attrs: vec!["tier".into(), "phase".into()],
            dimensions: vec![],
        }],
        witness_payload,
    })
}
