//! `manage_backlog_item` — add or update one backlog entry.
//!
//! Mutates `ctx.backlog` (a JSON object with an `items` array) and
//! rewrites `reflection/backlog.ncl` on disk. Each mutation appears as
//! one EntityAssert in the commit layer so the witness binds to the
//! resulting state root.

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;

/// Witness payload kind emitted by this op.
pub const WITNESS_SHAPE: &str = "backlog_item";

/// Add or update one backlog entry.
///
/// Inputs:
/// - `action` (String, required): `"add"` or `"update"`.
/// - `id` (String, required for update).
/// - `title` (String, required for add).
/// - `kind` (String, optional): `"Bug" | "Feature" | "Refactor" | …`.
/// - `priority` (String, optional): `"High" | "Medium" | "Low"`.
/// - `status` (String, optional): `"Open" | "InProgress" | "Closed"`.
#[onto_operation(
    id = "manage_backlog_item",
    description = "Add or update one backlog entry in reflection/backlog.ncl. Add asserts a fresh \
                   entry; update mutates an existing one in place.",
    validation_sla = "Synchronous",
    precondition = "backlog_open_items",
    pre_validators = "backlog_item_well_formed",
    effects = "Insert:BacklogItem:id,title,kind,priority,status",
    render_paths = "",
    witness_shape = "backlog_item",
    actor_policy = "developer,admin"
)]
fn manage_backlog_item(
    inputs: &serde_json::Value,
    ctx: &mut OpContext,
) -> Result<OpOutput, OpError> {
    let action = inputs
        .get("action")
        .and_then(|v| v.as_str())
        .ok_or_else(|| OpError::body("manage_backlog_item", "missing 'action' input"))?
        .to_owned();

    let mut attrs: Vec<(AttrId, Value)> = Vec::new();
    let mut mutated_attrs: Vec<String> = Vec::new();
    let mut effect_kind = "Update";

    let id = {
        let backlog = ctx
            .backlog
            .get_or_insert_with(|| serde_json::json!({ "items": [] }));
        let items = backlog
            .get_mut("items")
            .and_then(|v| v.as_array_mut())
            .ok_or_else(|| {
                OpError::body("manage_backlog_item", "ctx.backlog.items is not an array")
            })?;

        let id = inputs
            .get("id")
            .and_then(|v| v.as_str())
            .map(str::to_owned)
            .unwrap_or_else(|| format!("bl-{:04}", items.len() + 1));

        let existing_idx = items.iter().position(|item| {
            item.get("id")
                .and_then(|v| v.as_str())
                .map(|s| s == id)
                .unwrap_or(false)
        });
        let target = match (action.as_str(), existing_idx) {
            ("add", None) => {
                effect_kind = "Insert";
                let mut fresh = serde_json::Map::new();
                fresh.insert("id".to_owned(), serde_json::Value::String(id.clone()));
                items.push(serde_json::Value::Object(fresh));
                items
                    .last_mut()
                    .and_then(|v| v.as_object_mut())
                    .expect("just pushed")
            }
            ("add", Some(_)) => {
                return Err(OpError::body(
                    "manage_backlog_item",
                    format!("action='add' but id '{id}' already exists"),
                ));
            }
            ("update", Some(idx)) => items[idx].as_object_mut().ok_or_else(|| {
                OpError::body(
                    "manage_backlog_item",
                    "existing backlog item is not an object",
                )
            })?,
            ("update", None) => {
                return Err(OpError::body(
                    "manage_backlog_item",
                    format!("action='update' but id '{id}' not present"),
                ));
            }
            (other, _) => {
                return Err(OpError::body(
                    "manage_backlog_item",
                    format!("unknown action '{other}' (expected 'add' | 'update')"),
                ));
            }
        };

        for key in ["title", "kind", "priority", "status"] {
            if let Some(v) = inputs.get(key).and_then(|x| x.as_str()) {
                target.insert(key.to_owned(), serde_json::Value::String(v.to_owned()));
                attrs.push((AttrId::new(key), Value::Str(SmolStr::new(v))));
                mutated_attrs.push(key.to_owned());
            }
        }
        if attrs.is_empty() {
            return Err(OpError::body(
                "manage_backlog_item",
                "no mutable attrs provided (title/kind/priority/status all empty)",
            ));
        }
        id
    };

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

    if let Some(root) = ctx.project_root.as_ref() {
        let backlog_path =
            ontoref_ontology::layout::resolve_section(root, "reflection/backlog.ncl");
        if let Some(parent) = backlog_path.parent() {
            std::fs::create_dir_all(parent).map_err(|e| OpError::Render {
                path: parent.display().to_string(),
                reason: format!("creating parent dir: {e}"),
            })?;
        }
        let backlog_snapshot = ctx
            .backlog
            .as_ref()
            .expect("ctx.backlog was initialised above");
        let body = render_backlog_stub(backlog_snapshot);
        std::fs::write(&backlog_path, body).map_err(|e| OpError::Render {
            path: backlog_path.display().to_string(),
            reason: format!("writing backlog.ncl: {e}"),
        })?;
    }

    let witness_payload =
        canonical::encode(&op).map_err(|e| OpError::body("manage_backlog_item", e.to_string()))?;

    Ok(OpOutput {
        effects: vec![EffectRecord {
            kind: effect_kind.to_owned(),
            entity: id,
            attrs: mutated_attrs,
            dimensions: vec![],
        }],
        witness_payload,
    })
}

fn render_backlog_stub(backlog: &serde_json::Value) -> String {
    let mut out = String::new();
    out.push_str("# Generated by `manage_backlog_item` op (O4). Re-run to update.\n\n");
    out.push_str(&serde_json::to_string_pretty(backlog).unwrap_or_else(|_| "{}".to_owned()));
    out.push('\n');
    out
}

/// Reject malformed backlog requests at the validator boundary.
#[onto_validator(
    id = "backlog_item_well_formed",
    description = "Reject manage_backlog_item when action is missing/invalid or required fields \
                   are absent.",
    category = "Contextual",
    slice_query = "backlog_open_items"
)]
fn backlog_item_well_formed(slice: &Slice, _ctx: &ValidationCtx) -> Verdict {
    let Some(inputs) = slice.get("inputs") else {
        return Verdict::Unknown;
    };
    let action = inputs.get("action").and_then(|v| v.as_str()).unwrap_or("");
    match action {
        "add" => {
            let has_title = inputs
                .get("title")
                .and_then(|v| v.as_str())
                .map(|s| !s.is_empty())
                .unwrap_or(false);
            if has_title {
                Verdict::Accept
            } else {
                Verdict::reject(
                    "action='add' requires a non-empty 'title' input",
                    "backlog_item_well_formed",
                )
            }
        }
        "update" => {
            let has_id = inputs
                .get("id")
                .and_then(|v| v.as_str())
                .map(|s| !s.is_empty())
                .unwrap_or(false);
            if has_id {
                Verdict::Accept
            } else {
                Verdict::reject(
                    "action='update' requires an 'id' input",
                    "backlog_item_well_formed",
                )
            }
        }
        "" => Verdict::reject(
            "missing 'action' input (expected 'add' | 'update')",
            "backlog_item_well_formed",
        ),
        other => Verdict::reject(
            format!("unknown action '{other}' (expected 'add' | 'update')"),
            "backlog_item_well_formed",
        ),
    }
}
