//! Workflow input reference resolution (v1.2 Workflows) — pure, DB-free.
//!
//! A step's declared `input` is a JSON value whose string leaves may carry
//! `{{ … }}` references, resolved at run time against the run's accumulated
//! context: the run `input` and each prior step's `output`.
//!
//! Reference grammar:
//!   `{{ input }}` · `{{ input.a.b }}`
//!   `{{ steps.<name>.output }}` · `{{ steps.<name>.output.a.b }}`
//!
//! A string that is *exactly* a single `{{ … }}` reference is replaced by the
//! referenced JSON value (type-preserving). A reference embedded in a larger
//! string interpolates as text. A missing reference is a hard error (a workflow
//! referencing a non-existent output is a definition bug we surface, not hide).
//!
//! The same reference walker backs the `when` condition evaluator
//! (`workflow_expr`).

use std::collections::{BTreeMap, BTreeSet};

use serde_json::{Map, Value};
use thiserror::Error;

/// The run context a template/condition resolves against.
#[derive(Debug, Default, Clone)]
pub struct RunContext {
    /// The run's top-level input.
    pub input: Value,
    /// Prior step outputs, keyed by step name.
    pub steps: BTreeMap<String, Value>,
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum TemplateError {
    #[error("unresolved reference `{0}` (no such input/step output)")]
    MissingRef(String),
    #[error("malformed reference `{0}`")]
    MalformedRef(String),
    #[error("reference `{0}` targets undeclared step `{1}`")]
    UnknownStep(String, String),
}

impl RunContext {
    /// Resolve a dotted reference path (`input.a.b` / `steps.name.output.a.b`)
    /// to the JSON value it points at, or `None` if absent.
    #[must_use]
    pub fn resolve_ref(&self, path: &str) -> Option<&Value> {
        let parts: Vec<&str> = path.split('.').collect();
        let (root_val, rest): (&Value, &[&str]) = match parts.first()?.trim() {
            "input" => (&self.input, &parts[1..]),
            "steps" => {
                // steps.<name>.output[.deep...]
                let name = parts.get(1)?;
                if parts.get(2).map(|s| s.trim()) != Some("output") {
                    return None;
                }
                (self.steps.get(*name)?, &parts[3..])
            }
            _ => return None,
        };
        let mut cur = root_val;
        for seg in rest {
            let seg = seg.trim();
            cur = match cur {
                Value::Object(m) => m.get(seg)?,
                Value::Array(a) => a.get(seg.parse::<usize>().ok()?)?,
                _ => return None,
            };
        }
        Some(cur)
    }
}

/// Resolve every `{{ … }}` reference in `value` against `ctx`.
///
/// # Errors
/// [`TemplateError::MissingRef`] if a reference does not resolve;
/// [`TemplateError::MalformedRef`] on a syntactically-bad reference.
pub fn resolve(value: &Value, ctx: &RunContext) -> Result<Value, TemplateError> {
    match value {
        Value::String(s) => resolve_string(s, ctx),
        Value::Array(a) => Ok(Value::Array(
            a.iter()
                .map(|v| resolve(v, ctx))
                .collect::<Result<_, _>>()?,
        )),
        Value::Object(m) => {
            let mut out = Map::with_capacity(m.len());
            for (k, v) in m {
                out.insert(k.clone(), resolve(v, ctx)?);
            }
            Ok(Value::Object(out))
        }
        other => Ok(other.clone()),
    }
}

fn resolve_string(s: &str, ctx: &RunContext) -> Result<Value, TemplateError> {
    // Exact-single-reference: preserve the referenced value's JSON type.
    if let Some(inner) = exact_single_ref(s) {
        return ctx
            .resolve_ref(inner)
            .cloned()
            .ok_or_else(|| TemplateError::MissingRef(inner.to_string()));
    }
    // Otherwise interpolate references into the surrounding text.
    let mut out = String::with_capacity(s.len());
    let mut rest = s;
    while let Some(open) = rest.find("{{") {
        out.push_str(&rest[..open]);
        let after = &rest[open + 2..];
        let close = after
            .find("}}")
            .ok_or_else(|| TemplateError::MalformedRef(s.to_string()))?;
        let refpath = after[..close].trim();
        let val = ctx
            .resolve_ref(refpath)
            .ok_or_else(|| TemplateError::MissingRef(refpath.to_string()))?;
        out.push_str(&render_scalar(val));
        rest = &after[close + 2..];
    }
    out.push_str(rest);
    Ok(Value::String(out))
}

/// If `s` is exactly one `{{ ref }}` (ignoring surrounding whitespace and with
/// no other braces), return the trimmed inner reference.
fn exact_single_ref(s: &str) -> Option<&str> {
    let t = s.trim();
    let inner = t.strip_prefix("{{")?.strip_suffix("}}")?;
    if inner.contains("{{") || inner.contains("}}") {
        return None;
    }
    Some(inner.trim())
}

/// Render a JSON scalar for string interpolation (strings raw, others via JSON).
fn render_scalar(v: &Value) -> String {
    match v {
        Value::String(s) => s.clone(),
        Value::Null => String::new(),
        other => other.to_string(),
    }
}

/// Apply-time validation: every reference in `value` must be syntactically valid
/// and target either `input` or a declared step's `output`. Step outputs don't
/// exist yet at apply time, so this checks *shape*, not resolvability.
///
/// # Errors
/// [`TemplateError`] on a malformed reference or one naming an undeclared step.
pub fn validate(value: &Value, step_names: &BTreeSet<String>) -> Result<(), TemplateError> {
    for r in collect_refs(value)? {
        let parts: Vec<&str> = r.split('.').map(str::trim).collect();
        match parts.first().copied() {
            Some("input") => {}
            Some("steps") => {
                let name = parts
                    .get(1)
                    .filter(|s| !s.is_empty())
                    .ok_or_else(|| TemplateError::MalformedRef(r.clone()))?;
                if parts.get(2).copied() != Some("output") {
                    return Err(TemplateError::MalformedRef(r.clone()));
                }
                if !step_names.contains(*name) {
                    return Err(TemplateError::UnknownStep(r.clone(), (*name).to_string()));
                }
            }
            _ => return Err(TemplateError::MalformedRef(r.clone())),
        }
    }
    Ok(())
}

/// Collect every `{{ … }}` reference path found anywhere in `value`.
fn collect_refs(value: &Value) -> Result<Vec<String>, TemplateError> {
    let mut acc = Vec::new();
    collect_into(value, &mut acc)?;
    Ok(acc)
}

fn collect_into(value: &Value, acc: &mut Vec<String>) -> Result<(), TemplateError> {
    match value {
        Value::String(s) => {
            let mut rest = s.as_str();
            while let Some(open) = rest.find("{{") {
                let after = &rest[open + 2..];
                let close = after
                    .find("}}")
                    .ok_or_else(|| TemplateError::MalformedRef(s.clone()))?;
                acc.push(after[..close].trim().to_string());
                rest = &after[close + 2..];
            }
        }
        Value::Array(a) => {
            for v in a {
                collect_into(v, acc)?;
            }
        }
        Value::Object(m) => {
            for v in m.values() {
                collect_into(v, acc)?;
            }
        }
        _ => {}
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    fn ctx() -> RunContext {
        let mut steps = BTreeMap::new();
        steps.insert("validate".to_string(), json!({ "ok": true, "score": 7 }));
        steps.insert("fetch".to_string(), json!([10, 20, 30]));
        RunContext {
            input: json!({ "order": { "id": "abc", "total": 42 } }),
            steps,
        }
    }

    #[test]
    fn exact_single_ref_preserves_type() {
        let c = ctx();
        assert_eq!(
            resolve(&json!("{{ input.order.total }}"), &c).unwrap(),
            json!(42)
        );
        assert_eq!(
            resolve(&json!("{{ steps.validate.output.ok }}"), &c).unwrap(),
            json!(true)
        );
        // whole-object ref
        assert_eq!(
            resolve(&json!("{{ steps.validate.output }}"), &c).unwrap(),
            json!({ "ok": true, "score": 7 })
        );
    }

    #[test]
    fn array_index_and_nested_object() {
        let c = ctx();
        assert_eq!(
            resolve(&json!("{{ steps.fetch.output.1 }}"), &c).unwrap(),
            json!(20)
        );
        assert_eq!(
            resolve(&json!({ "id": "{{ input.order.id }}" }), &c).unwrap(),
            json!({ "id": "abc" })
        );
    }

    #[test]
    fn interpolation_renders_text() {
        let c = ctx();
        assert_eq!(
            resolve(
                &json!("order {{ input.order.id }} scored {{ steps.validate.output.score }}"),
                &c
            )
            .unwrap(),
            json!("order abc scored 7")
        );
    }

    #[test]
    fn missing_ref_is_error() {
        let c = ctx();
        assert_eq!(
            resolve(&json!("{{ steps.nope.output }}"), &c).unwrap_err(),
            TemplateError::MissingRef("steps.nope.output".to_string())
        );
        assert!(matches!(
            resolve(&json!("{{ input.order.missing }}"), &c).unwrap_err(),
            TemplateError::MissingRef(_)
        ));
    }

    #[test]
    fn validate_shape_against_declared_steps() {
        let steps: BTreeSet<String> = ["validate".to_string()].into_iter().collect();
        // ok: input + declared step
        assert!(validate(
            &json!({ "a": "{{ input.x }}", "b": "{{ steps.validate.output.ok }}" }),
            &steps
        )
        .is_ok());
        // unknown step
        assert!(matches!(
            validate(&json!("{{ steps.ghost.output }}"), &steps).unwrap_err(),
            TemplateError::UnknownStep(_, _)
        ));
        // malformed root
        assert!(matches!(
            validate(&json!("{{ bogus.x }}"), &steps).unwrap_err(),
            TemplateError::MalformedRef(_)
        ));
        // steps without .output
        assert!(matches!(
            validate(&json!("{{ steps.validate.result }}"), &steps).unwrap_err(),
            TemplateError::MalformedRef(_)
        ));
    }

    #[test]
    fn non_template_values_pass_through() {
        let c = ctx();
        assert_eq!(resolve(&json!(123), &c).unwrap(), json!(123));
        assert_eq!(resolve(&json!(null), &c).unwrap(), json!(null));
        assert_eq!(resolve(&json!("plain"), &c).unwrap(), json!("plain"));
    }
}
