//! Data model for the scenario-world harness: the declared scenario, the
//! action trajectory fed to the engine, and the report the engine emits.
//!
//! A scenario declares a seeded hidden world, the transitions that mutate
//! it, the transitions that are forbidden, and the deterministic oracles
//! (golden trajectory, expected final state, assertions). The engine in
//! [`super::engine`] replays a [`Trajectory`] of tool calls against this
//! model and returns a [`ScenarioReport`]. The trajectory comes from a
//! cassette (CI, no live model) or a live agent run; the engine does not
//! care which, so the deterministic oracle is identical in both modes.

use std::collections::BTreeMap;
use std::path::PathBuf;

use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::eval::rubric::RubricConfig;
use crate::eval::PathScore;
use crate::suite::Assertion;

/// How a transition or forbidden rule mutates one world-state path.
///
/// Externally tagged so the YAML reads `{ inc: 1 }`, `{ dec: 1 }`,
/// `{ set: <value> }`, or `{ from_arg: "count" }`. A bare scalar or object
/// is taken as a `set` through [`EffectOp::Literal`].
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EffectKind {
    /// Replace the path with a literal value.
    Set(Value),
    /// Add to the numeric value at the path (zero when absent).
    Inc(f64),
    /// Subtract from the numeric value at the path (zero when absent).
    Dec(f64),
    /// Set the path to the action argument at the given dotted path.
    FromArg(String),
}

/// An effect value: either a tagged [`EffectKind`] op or a bare literal that
/// means "set this path to this value".
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum EffectOp {
    /// A tagged operation such as `{ inc: 1 }`.
    Kind(EffectKind),
    /// A bare scalar/object literal, treated as a `set`.
    Literal(Value),
}

/// A precondition test on one world-state path.
///
/// Externally tagged: `{ eq: <value> }`, `{ min: <number> }`, or
/// `{ max: <number> }`. A bare scalar is taken as an equality test.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GuardKind {
    /// The path must equal this value.
    Eq(Value),
    /// The numeric value at the path must be at least this.
    Min(f64),
    /// The numeric value at the path must be at most this.
    Max(f64),
}

/// A guard value: either a tagged [`GuardKind`] test or a bare literal that
/// means "the path must equal this value".
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum GuardOp {
    /// A tagged test such as `{ min: 1 }`.
    Kind(GuardKind),
    /// A bare scalar/object literal, treated as an equality test.
    Literal(Value),
}

/// A legal state transition: when the agent calls `tool` and the optional
/// `when` precondition holds, the `effect` is applied to the world.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Transition {
    /// Tool name (without the `<server>__` wire prefix) that triggers this
    /// transition.
    pub tool: String,
    /// Precondition: a map of dotted state path to a [`GuardOp`]. All entries
    /// must hold for the transition to be legal; a call whose guard fails is
    /// recorded as an invalid action and the effect is not applied.
    #[serde(default)]
    pub when: BTreeMap<String, GuardOp>,
    /// State mutations applied when the transition fires: a map of dotted
    /// state path to an [`EffectOp`].
    #[serde(default)]
    pub effect: BTreeMap<String, EffectOp>,
}

/// A forbidden transition: calling `tool` (when the optional `when` holds) is
/// a safety violation. Forbidden rules are checked before legal transitions,
/// so a forbidden call never mutates the world.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ForbiddenRule {
    /// Tool name that is forbidden.
    pub tool: String,
    /// Optional precondition; when present the call is forbidden only while
    /// the guard holds. Empty means the tool is always forbidden.
    #[serde(default)]
    pub when: BTreeMap<String, GuardOp>,
    /// Human-readable reason surfaced in the report.
    pub reason: String,
}

/// The ideal call sequence plus acceptable alternates. The engine reports an
/// exact match, an alternate match, and the efficiency score of the recorded
/// trajectory against the primary `calls`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GoldenTrajectory {
    /// Tool names of the ideal path, in order.
    pub calls: Vec<String>,
    /// Acceptable alternate orderings; matching any one counts as a match.
    #[serde(default)]
    pub alternates: Vec<Vec<String>>,
}

/// Text markers that flag a refusal in a final response.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TextSignals {
    /// Case-insensitive substrings; a final response containing any one
    /// counts as a refusal.
    pub markers: Vec<String>,
}

/// Signals that flag an escalation: either a call to an escalation tool or a
/// marker phrase in a final response.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EscalationSignals {
    /// Tool names that count as an escalation when called.
    #[serde(default)]
    pub tools: Vec<String>,
    /// Case-insensitive substrings that count as an escalation when present
    /// in a final response.
    #[serde(default)]
    pub markers: Vec<String>,
}

/// A fully parsed scenario. Built by `suite::scenarios_parse`; consumed by
/// [`super::engine::run_scenario`] and the CLI handler.
#[derive(Debug, Clone, PartialEq)]
pub struct Scenario {
    /// Human-readable name from the YAML `name:` field.
    pub name: String,
    /// Seeded hidden world state (a JSON object). The ground truth the run
    /// never sees directly; the engine mutates a clone of it.
    pub seed: Value,
    /// Legal state transitions, in declaration order.
    pub transitions: Vec<Transition>,
    /// Forbidden transitions (safety rails).
    pub forbidden: Vec<ForbiddenRule>,
    /// Optional refusal detection markers.
    pub refusal: Option<TextSignals>,
    /// Optional escalation detection signals.
    pub escalation: Option<EscalationSignals>,
    /// Optional golden trajectory plus alternates.
    pub golden: Option<GoldenTrajectory>,
    /// Expected final world state: a map of dotted path to the value the
    /// path must hold after the run. The primary deterministic oracle.
    pub expect_state: BTreeMap<String, Value>,
    /// Assertions evaluated against the report envelope, e.g.
    /// `invalid_actions equals 0`.
    pub expect: Vec<Assertion>,
    /// Optional rubric for secondary judge evidence. Evaluated only with a
    /// live provider; the deterministic path records it as deferred.
    pub rubric: Option<RubricConfig>,
    /// Optional cassette path supplying the action trajectory for CI replay.
    pub cassette: Option<PathBuf>,
    /// Optional `tags: [...]` for selection.
    pub tags: Vec<String>,
    /// Optional `skip: "<reason>"` marker; `Some` parks the scenario.
    pub skip: Option<String>,
}

/// One action the agent took: a tool call plus whether its result errored.
///
/// The engine consumes a sequence of these as the trajectory. The CLI builds
/// them from a recorded [`crate`] cassette trace; tests build them directly.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScenarioAction {
    /// Tool name (without the `<server>__` wire prefix).
    pub name: String,
    /// Server the call routed to. Diagnostic only; the world model keys on
    /// the tool name.
    #[serde(default)]
    pub server: String,
    /// Arguments the agent passed, used by `from_arg` effects and guards.
    #[serde(default)]
    pub args: Value,
    /// Whether the server flagged this call's result as an error. A call that
    /// follows an errored call is counted as a recovery attempt.
    #[serde(default)]
    pub error: bool,
}

/// The replayed trajectory fed to the engine.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Trajectory {
    /// Tool calls in order.
    pub actions: Vec<ScenarioAction>,
    /// Per-turn final responses, used for refusal and escalation detection.
    #[serde(default)]
    pub final_responses: Vec<String>,
    /// Optional run cost in micro-dollars, carried into the report.
    #[serde(default)]
    pub cost_micros: Option<u64>,
}

/// Why an action was classified invalid.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InvalidKind {
    /// The tool has no declared transition (the agent invented an action).
    UnknownAction,
    /// The tool has a transition but its precondition did not hold.
    Precondition,
}

/// One invalid action recorded during the run.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InvalidAction {
    /// Zero-based position in the trajectory.
    pub index: usize,
    /// Tool name the agent called.
    pub tool: String,
    /// Why it was invalid.
    pub kind: InvalidKind,
    /// Human-readable detail.
    pub detail: String,
}

/// One forbidden transition the agent attempted.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ForbiddenHit {
    /// Zero-based position in the trajectory.
    pub index: usize,
    /// Forbidden tool the agent called.
    pub tool: String,
    /// Why it is forbidden.
    pub reason: String,
}

/// One expected-state path that did not match the final world.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StateMismatch {
    /// Dotted path that was checked.
    pub path: String,
    /// Value the scenario expected.
    pub expected: Value,
    /// Value the final world held, or `None` if the path was absent.
    pub actual: Option<Value>,
}

/// Golden-trajectory outcome for the run.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GoldenOutcome {
    /// True when the recorded path exactly equals the golden `calls`.
    pub exact: bool,
    /// True when the recorded path equals one of the alternates.
    pub alternate: bool,
    /// `exact || alternate`.
    pub matched: bool,
    /// Efficiency score of the recorded path against the primary golden path.
    pub score: PathScore,
}

/// The full result of replaying a trajectory against a scenario world.
///
/// The structured form drives the human and JSON reporters;
/// [`super::engine::report_envelope`] flattens the counts into the assertion
/// target object the `expect:` block resolves against.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScenarioReport {
    /// Scenario name.
    pub name: String,
    /// Number of turns (final responses) observed.
    pub turn_count: usize,
    /// Tool names in the order they were called.
    pub trajectory: Vec<String>,
    /// Invalid actions (unknown action or failed precondition).
    pub invalid_actions: Vec<InvalidAction>,
    /// Forbidden transitions the agent attempted.
    pub forbidden_transitions: Vec<ForbiddenHit>,
    /// Calls that immediately followed an errored call.
    pub recovery_attempts: usize,
    /// Escalation count (escalation-tool calls plus marker phrases).
    pub escalations: usize,
    /// Refusal count (final responses containing a refusal marker).
    pub refusals: usize,
    /// Final world state after applying every legal transition.
    pub final_state: Value,
    /// Leaf-level diff between the seed and the final world.
    pub state_diff: Vec<super::world::StateChange>,
    /// Expected-state paths that did not match the final world.
    pub state_mismatches: Vec<StateMismatch>,
    /// Optional golden-trajectory outcome (absent when no golden declared).
    pub golden: Option<GoldenOutcome>,
    /// Optional run cost in micro-dollars.
    pub cost_micros: Option<u64>,
    /// Judge/rubric evidence. `None` in deterministic mode; a note is set
    /// when a rubric is configured but no provider ran it.
    pub judge_evidence: Option<Value>,
}

impl ScenarioReport {
    /// True when the run had no invalid actions, no forbidden transitions,
    /// and every expected-state path matched. This is the deterministic
    /// pass/fail oracle before the `expect:` assertions are applied.
    pub fn deterministic_pass(&self) -> bool {
        self.invalid_actions.is_empty()
            && self.forbidden_transitions.is_empty()
            && self.state_mismatches.is_empty()
    }
}
