// FR-32 — SafetyPolicy: the safety-relevant half of `Options`, named by the
// situation the caller is in instead of by eleven parameter values.
//
// `Options` describes the protocol (chunking, delta tracking, prose, indent) as
// well as the risk contract. A caller that only wants to say "this value came
// from a model and I am going to execute it" should not have to derive that
// from `max_loss` plus `reject_ambiguous` plus the duplicate-key policy, and a
// reviewer should not have to read the call site to find out what was allowed.
// A policy is that decision, in one value, with a name.
//
// The mapping is deliberately one-way: `SafetyPolicy::to_options` turns a
// policy into library options, and `Options::with_policy` overlays the policy's
// safety knobs on an existing options value so the protocol knobs survive.

///|
/// A named set of safety-relevant settings (FR-32).
///
/// Every field here can also be set on `Options` directly; the policy exists so
/// that the *intent* is part of the value and can be printed in a log line
/// (`SafetyPolicy::name`).
pub(all) struct SafetyPolicy {
  /// Short stable name of the profile, e.g. `execution`.
  profile : String
  /// `Strict` turns the first repair into `E010` whatever it was.
  mode : Mode
  /// Highest exactness the policy accepts (FR-17).
  max_loss : Exactness
  /// Refuse a repair with a second reading (FR-27).
  reject_ambiguous : Bool
  /// Duplicate object key policy (BR-19).
  duplicate_keys : DuplicateKeys
  /// How an unterminated number appears (FR-11).
  incomplete_number : IncompleteNumber
  /// How a dangling key appears.
  pending_key : PendingKey
  /// What happens to content after the first root value (FR-15).
  multi_root : MultiRoot
  /// Nesting limit.
  max_depth : Int32
  /// Input length limit, in code units.
  max_input_len : Int32
  /// Which repair rules may run at all.
  rules : RuleSet
}

///|
/// The three profiles that differ in what they accept, from least to most
/// careful. `permissive` is the documented default; `interactive` refuses a
/// repair that invented content but lets a human judge a second reading;
/// `execution` refuses both.
pub fn SafetyPolicy::permissive() -> SafetyPolicy {
  {
    profile: "permissive",
    mode: Lenient,
    max_loss: Lossy,
    reject_ambiguous: false,
    duplicate_keys: KeepLast,
    incomplete_number: Drop,
    pending_key: OmitKey,
    multi_root: KeepFirst,
    max_depth: 512,
    max_input_len: 67108864,
    rules: RuleSet::all(),
  }
}

///|
/// A human reads the result and can supply the missing judgement: notation
/// repairs pass, a repair that invented, dropped or truncated a value is
/// refused (`max_loss: Cosmetic`), and a second reading is reported through
/// `RepairResult::ambiguous` instead of being refused, because a person is
/// allowed to choose.
pub fn SafetyPolicy::interactive() -> SafetyPolicy {
  { ..SafetyPolicy::permissive(), profile: "interactive", max_loss: Cosmetic, }
}

///|
/// Nothing human sits between the parse and the effect: notation repairs pass,
/// a repair that changed a value raises `E010` and a repair with a second
/// reading raises `E011` (FR-27).
///
/// This is the policy behind `Options::for_execution()`.
pub fn SafetyPolicy::for_execution() -> SafetyPolicy {
  {
    ..SafetyPolicy::interactive(),
    profile: "execution",
    reject_ambiguous: true,
  }
}

///|
/// An agent loop, where a refusal is cheap because the refusal is handed back
/// to the model that produced the input (FR-28).
///
/// In safety terms this is `for_execution()` plus a resource bound: a tool call
/// is a handful of arguments, so nesting is capped at 64 and the input at one
/// million code units, while the general defaults are 512 and 64 MiB. What an
/// agent additionally needs is protocol, not policy, which is why the streaming
/// preset is `Options::for_agent()` — this policy plus `for_llm()`'s delta
/// stream and prose handling.
pub fn SafetyPolicy::for_agent() -> SafetyPolicy {
  {
    ..SafetyPolicy::for_execution(),
    profile: "agent",
    // A tool call is a handful of arguments, not a 64 MiB document. The bound
    // is part of the agent profile because an agent is the one caller that
    // cannot look at the input before deciding to accept it.
    max_depth: 64,
    max_input_len: 1048576,
  }
}

///|
/// Refuse every repair, including a normalised quote: `Mode::Strict` turns the
/// first recorded repair into `E010`, whatever its severity. This is the policy
/// behind `Options::strict()`.
///
/// `max_loss` is left where the permissive profile puts it, because under
/// `Strict` it is never consulted: the refusal happens first. Changing it would
/// make the profile look stricter than it is while changing nothing.
pub fn SafetyPolicy::strict() -> SafetyPolicy {
  { ..SafetyPolicy::permissive(), profile: "strict", mode: Strict, }
}

///|
/// The profile name, e.g. `execution`.
pub fn SafetyPolicy::name(self : SafetyPolicy) -> String {
  self.profile
}

///|
/// The policy as a fresh `Options` value: the documented protocol defaults with
/// this policy's safety knobs.
pub fn SafetyPolicy::to_options(self : SafetyPolicy) -> Options {
  {
    ..Options::default(),
    mode: self.mode,
    max_loss: self.max_loss,
    reject_ambiguous: self.reject_ambiguous,
    duplicate_keys: self.duplicate_keys,
    incomplete_number: self.incomplete_number,
    pending_key: self.pending_key,
    multi_root: self.multi_root,
    max_depth: self.max_depth,
    max_input_len: self.max_input_len,
    rules: self.rules,
  }
}

///|
/// The same policy applied to `self`, keeping every protocol field (deltas,
/// prose, indent, repair log, limits the caller set by hand) and replacing only
/// the safety knobs.
pub fn Options::with_policy(self : Options, policy : SafetyPolicy) -> Options {
  {
    ..self,
    mode: policy.mode,
    max_loss: policy.max_loss,
    reject_ambiguous: policy.reject_ambiguous,
    duplicate_keys: policy.duplicate_keys,
    incomplete_number: policy.incomplete_number,
    pending_key: policy.pending_key,
    multi_root: policy.multi_root,
    max_depth: policy.max_depth,
    max_input_len: policy.max_input_len,
    rules: policy.rules,
  }
}

///|
/// The policy a set of options corresponds to, or `None` when the options are
/// not one of the five documented profiles.
///
/// Two options values match the same profile when every safety knob is equal;
/// protocol knobs are ignored, because that is what makes them protocol. The
/// comparison exists so that `--policy` reporting cannot drift: the CLI prints
/// the profile the options it built actually are, not the word the user typed.
pub fn SafetyPolicy::of_options(options : Options) -> String? {
  let candidates : Array[SafetyPolicy] = [
    SafetyPolicy::permissive(),
    SafetyPolicy::interactive(),
    SafetyPolicy::for_execution(),
    SafetyPolicy::for_agent(),
    SafetyPolicy::strict(),
  ]
  for p in candidates {
    if p.matches(options) {
      return Some(p.profile)
    }
  }
  None
}

///|
/// Whether `options` has exactly this policy's safety knobs.
fn SafetyPolicy::matches(self : SafetyPolicy, options : Options) -> Bool {
  options.mode == self.mode &&
  options.max_loss == self.max_loss &&
  options.reject_ambiguous == self.reject_ambiguous &&
  options.duplicate_keys == self.duplicate_keys &&
  options.incomplete_number == self.incomplete_number &&
  options.pending_key == self.pending_key &&
  options.multi_root == self.multi_root &&
  options.max_depth == self.max_depth &&
  options.max_input_len == self.max_input_len &&
  options.rules.same_as(self.rules)
}