// M5 options — the option dictionary of D04 4.6, with defaults and validation.

///|
/// The deepest a parsed value may nest, and the ceiling on
/// `Options::max_depth`.
///
/// Every walk over a value is iterative -- `copy_json`, `json_equal`,
/// `Schema::evaluate` and the renderer all carry an explicit work list -- so
/// the depth a caller may configure is bounded by the heap, not by the native
/// stack. That is what lets this number be a *sanity bound* rather than a
/// stack bound: it exists so that a typo cannot ask for an allocation the
/// machine cannot make, not to stop a caller going deeper than the engine can.
///
/// The history matters, because the ceiling used to be 512 and the reason was
/// different. `copy_json`, `json_equal` and `walk_schema` were recursive, and
/// the wasm backend's stack overflows somewhere between 896 and 1024 frames;
/// capping `max_depth` at 512 was the only way to keep "a refusal, never a
/// crash" true, and it had the side effect that a caller could tighten the
/// limit but never loosen it. Making the walks iterative removed the reason,
/// so the ceiling is now one million: far above any document, and still a
/// number rather than infinity.
pub const MAX_VALUE_DEPTH : Int32 = 1048576

///|
/// The deepest a *declaration* may nest (JSON Schema or the library's skeleton
/// language), and the ceiling on `Options::max_depth` when reading one.
///
/// Declarations are still parsed recursively by the two schema parsers, so this
/// one really is a stack bound and is deliberately much lower than
/// `MAX_VALUE_DEPTH`. A declaration 256 levels deep is already far past what
/// any real tool schema needs; a value 512 levels deep is a document.
pub const MAX_SCHEMA_DEPTH : Int32 = 256

///|
/// Repair mode. `Strict` turns the first repair record into `E010`.
pub(all) enum Mode {
  Lenient
  Strict
}

///|
/// Explicit `Eq` implementation for `Mode`.
pub impl Eq for Mode with fn equal(self : Mode, other : Mode) -> Bool {
  (self is Lenient && other is Lenient) || (self is Strict && other is Strict)
}

///|
/// Explicit `Eq` extension for `Mode`.
pub extend Mode with Eq::{not_equal, equal}

///|
/// Policy for duplicate object keys (BR-19).
pub(all) enum DuplicateKeys {
  KeepLast
  KeepFirst
  AsError
}

///|
/// Explicit `Eq` implementation for `DuplicateKeys`.
pub impl Eq for DuplicateKeys with fn equal(
  self : DuplicateKeys,
  other : DuplicateKeys,
) -> Bool {
  (self is KeepLast && other is KeepLast) ||
  (self is KeepFirst && other is KeepFirst) ||
  (self is AsError && other is AsError)
}

///|
/// Explicit `Eq` extension for `DuplicateKeys`.
pub extend DuplicateKeys with Eq::{not_equal, equal}

///|
/// How an unterminated number appears in a snapshot (FR-11).
pub(all) enum IncompleteNumber {
  Drop
  TrimToValid
}

///|
/// Explicit `Eq` implementation for `IncompleteNumber`.
pub impl Eq for IncompleteNumber with fn equal(
  self : IncompleteNumber,
  other : IncompleteNumber,
) -> Bool {
  (self is Drop && other is Drop) ||
  (self is TrimToValid && other is TrimToValid)
}

///|
/// Explicit `Eq` extension for `IncompleteNumber`.
pub extend IncompleteNumber with Eq::{not_equal, equal}

///|
/// How a dangling key (key with colon but no value) appears in a snapshot.
pub(all) enum PendingKey {
  OmitKey
  NullKey
}

///|
/// Explicit `Eq` implementation for `PendingKey`.
pub impl Eq for PendingKey with fn equal(self : PendingKey, other : PendingKey) -> Bool {
  (self is OmitKey && other is OmitKey) || (self is NullKey && other is NullKey)
}

///|
/// Explicit `Eq` extension for `PendingKey`.
pub extend PendingKey with Eq::{not_equal, equal}

///|
/// How `NaN`, `Infinity` and `-Infinity` are converted (BR-12).
pub(all) enum NonFinite {
  AsNull
  AsError
  AsString
}

///|
/// Explicit `Eq` implementation for `NonFinite`.
pub impl Eq for NonFinite with fn equal(self : NonFinite, other : NonFinite) -> Bool {
  (self is AsNull && other is AsNull) ||
  (self is AsError && other is AsError) ||
  (self is AsString && other is AsString)
}

///|
/// Explicit `Eq` extension for `NonFinite`.
pub extend NonFinite with Eq::{not_equal, equal}

///|
/// Policy for content after the first root value (FR-15).
pub(all) enum MultiRoot {
  KeepFirst
  AsArray
  AsError
}

///|
/// Explicit `Eq` implementation for `MultiRoot`.
pub impl Eq for MultiRoot with fn equal(self : MultiRoot, other : MultiRoot) -> Bool {
  (self is KeepFirst && other is KeepFirst) ||
  (self is AsArray && other is AsArray) ||
  (self is AsError && other is AsError)
}

///|
/// Explicit `Eq` extension for `MultiRoot`.
pub extend MultiRoot with Eq::{not_equal, equal}

///|
/// Per-rule switches. Disabling a rule makes that single code raise `E010`
/// instead of being recorded (D09 9.5).
pub(all) struct RuleSet {
  priv flags : Array[Bool]
}

///|
/// All rules enabled.
///
/// The size is derived from `RepairCode::count()` rather than written as a
/// literal. It used to be the literal `22`, which was wrong the moment it was
/// written: `RepairCode::number()` is 1-based, so a 22-slot array covers
/// `R001` … `R021` and leaves `R022 NormalizeLiteral` — the code added in the
/// same release — outside the table. `is_enabled` then fell into its
/// out-of-range branch, which returned `true`, so `R022` looked enabled while
/// `disable(R022)` was a silent no-op: a caller could not switch the rule off,
/// and nothing said so. Sizing the table from the enum removes the class of
/// bug rather than the instance.
pub fn RuleSet::all() -> RuleSet {
  let flags : Array[Bool] = []
  // Slot 0 is unused: `number()` is 1-based so that `R001` reads as 1.
  for _i = 0; _i <= RepairCode::count(); _i = _i + 1 {
    flags.push(true)
  }
  { flags, }
}

///|
/// A copy of this rule set with one rule disabled.
///
/// Disabling a rule that is outside the table is refused loudly rather than
/// ignored: a silent no-op here means the caller believes a repair is
/// switched off when it is not, which is a fail-open configuration bug. The
/// table is sized from the enum, so the only way to reach this branch is a
/// programming error, and it should be reported as one.
pub fn RuleSet::disable(self : RuleSet, code : RepairCode) -> RuleSet {
  let flags : Array[Bool] = []
  for i = 0; i < self.flags.length(); i = i + 1 {
    flags.push(self.flags[i])
  }
  let n = code.number()
  if n < 0 || n >= flags.length() {
    abort(
      "RuleSet::disable: " +
      code.code() +
      " has no slot in the rule table (" +
      flags.length().to_string() +
      " slots)",
    )
  }
  flags[n] = false
  { flags, }
}

///|
/// A copy of this rule set with one rule enabled. Out of range aborts, for the
/// same reason `disable` does.
pub fn RuleSet::enable(self : RuleSet, code : RepairCode) -> RuleSet {
  let flags : Array[Bool] = []
  for i = 0; i < self.flags.length(); i = i + 1 {
    flags.push(self.flags[i])
  }
  let n = code.number()
  if n < 0 || n >= flags.length() {
    abort(
      "RuleSet::enable: " +
      code.code() +
      " has no slot in the rule table (" +
      flags.length().to_string() +
      " slots)",
    )
  }
  flags[n] = true
  { flags, }
}

///|
/// Whether a repair code may be reported without raising `E010`.
///
/// A code with no slot is NOT enabled: the answer must fail closed. The
/// previous `true` meant a rule the table did not know about ran anyway, and
/// a rule that runs is a rule the caller cannot refuse.
pub fn RuleSet::is_enabled(self : RuleSet, code : RepairCode) -> Bool {
  let n = code.number()
  if n >= 0 && n < self.flags.length() {
    self.flags[n]
  } else {
    false
  }
}

///|
/// Whether two rule sets enable exactly the same rules (FR-30). Used to decide
/// whether an options value still matches a named safety profile.
pub fn RuleSet::same_as(self : RuleSet, other : RuleSet) -> Bool {
  if self.flags.length() != other.flags.length() {
    return false
  }
  for i = 0; i < self.flags.length(); i = i + 1 {
    if self.flags[i] != other.flags[i] {
      return false
    }
  }
  true
}

///|
/// All options. Defaults are the ones documented in D04 4.6.
pub(all) struct Options {
  mode : Mode
  max_depth : Int32
  max_input_len : Int32
  max_token_len : Int32
  /// How much skipped prelude is kept so that `finish` can retry it as JSON.
  /// `skip_prose` only recognises `{` or `[`, so a scalar root looks exactly
  /// like prose while it is being read; keeping the text is what lets the
  /// retry recover it. Beyond this bound the skipped text is dropped and the
  /// document is refused as before, which keeps the memory cost of a
  /// pathological input bounded.
  max_prose_buffer : Int32
  duplicate_keys : DuplicateKeys
  incomplete_number : IncompleteNumber
  pending_key : PendingKey
  non_finite : NonFinite
  strip_fence : Bool
  collect_repairs : Bool
  want_text : Bool
  skip_prose : Bool
  quote_lookahead : Bool
  multi_root : MultiRoot
  rules : RuleSet
  indent : Int32
  max_repairs : Int32
  /// Highest exactness a result may have (FR-17). `Lossy` accepts anything and
  /// therefore never changes behaviour.
  max_loss : Exactness
  /// Record incremental deltas so that `PartialParser::drain_deltas` can return
  /// them (FR-18). Off by default: tracking costs work and memory.
  collect_deltas : Bool
  /// Refuse a repair that admits a second reading (FR-27): the first ambiguous
  /// record raises `E011` instead of being recorded. Off by default.
  reject_ambiguous : Bool
}

///|
/// Documented defaults: lenient, depth 512, 64 Mi code units, duplicate keys
/// keep the last value, snapshots drop unfinished numbers, all rules on.
pub fn Options::default() -> Options {
  {
    mode: Lenient,
    max_depth: 512,
    max_input_len: 67108864,
    max_token_len: 16777216,
    max_prose_buffer: 65536,
    duplicate_keys: KeepLast,
    incomplete_number: Drop,
    pending_key: OmitKey,
    non_finite: AsNull,
    strip_fence: true,
    collect_repairs: true,
    want_text: true,
    skip_prose: false,
    quote_lookahead: false,
    multi_root: KeepFirst,
    rules: RuleSet::all(),
    indent: 0,
    max_repairs: 10000,
    max_loss: Lossy,
    collect_deltas: false,
    reject_ambiguous: false,
  }
}

// ---------------------------------------------------------------------------
// Presets (FR-19). `Options` has eighteen fields; most callers only need one
// of three profiles.
// ---------------------------------------------------------------------------

///|
/// The documented defaults: lenient, every rule on, no exactness gate, no
/// delta tracking. Equivalent to `Options::default()`.
pub fn Options::lenient() -> Options {
  Options::default()
}

///|
/// Reject any input that needs a repair: the first recorded repair becomes
/// `E010`. Equivalent to `{ ..Options::default(), mode: Strict }`.
pub fn Options::strict() -> Options {
  { ..Options::default(), mode: Strict, }
}

///|
/// Profile for consuming a large language model's tool-call arguments
/// (FR-18, FR-19): the documented defaults plus `collect_deltas`, so that
/// `PartialParser::drain_deltas` is available. `duplicate_keys` stays
/// `KeepLast` and `incomplete_number` stays `Drop`, which are the two settings
/// a delta stream can express.
pub fn Options::for_llm() -> Options {
  { ..Options::default(), collect_deltas: true, }
}

///|
/// Profile for a caller that will execute the parsed value (FR-17, FR-27):
/// notation repairs are accepted, but a repair that admits a second reading
/// raises `E011` and anything that invented, dropped or truncated a value
/// raises `E010`. Distinct from `strict()`, which rejects even a normalised
/// quote, and from `lenient()`, which accepts both.
pub fn Options::for_execution() -> Options {
  { ..Options::default(), max_loss: Cosmetic, reject_ambiguous: true, }
}

///|
/// Profile for an agent loop that streams a tool call and hands a refusal back
/// to the model (FR-28, FR-30): the execution policy's safety knobs plus the
/// protocol an agent channel needs — the delta stream
/// (`collect_deltas: true`, from `for_llm()`) so a consumer can follow the
/// value as it is written, and `skip_prose: true` because a model that answers
/// in a chat channel often prefixes the arguments with a sentence and the
/// prefix is not part of the arguments.
///
/// The safety knobs are the agent profile's; what a *streaming* agent adds is
/// protocol, so this is `SafetyPolicy::for_agent()` with the delta stream and
/// prose handling turned on. There is exactly one definition of the agent
/// profile, and `SafetyPolicy::of_options` reads it back.
pub fn Options::for_agent() -> Options {
  {
    ..SafetyPolicy::for_agent().to_options(),
    collect_deltas: true,
    skip_prose: true,
  }
}

///|
/// The options a named profile denotes when applied to `self`, protocol
/// included. This is what a caller that selects a profile *by name* — the
/// MCP tools, `--policy`, `serve` — must use, so that the profile it selects
/// and the library preset of the same name agree.
pub fn Options::for_profile(self : Options, name : String) -> Options? {
  match name {
    "permissive" => Some(self.with_profile(SafetyPolicy::permissive()))
    "interactive" => Some(self.with_profile(SafetyPolicy::interactive()))
    "execution" => Some(self.with_profile(SafetyPolicy::for_execution()))
    "agent" => Some(self.with_profile(SafetyPolicy::for_agent()))
    "strict" => Some(self.with_profile(SafetyPolicy::strict()))
    _ => None
  }
}

///|
/// Profile for a caller whose only output is the repaired text — a pipe, a log
/// line, a file rewrite (FR-30). The value is what is wanted and nothing else
/// is: no repair records are kept (they cost an allocation per repair and an
/// array that grows with the input) and the text is compact.
///
/// This is a preset, not a separate engine mode: the tree is still built
/// internally, because the duplicate-key policy, `+` concatenation and the
/// `fill_null` replay all read the value that was already accepted (see
/// `docs/RULES.md` §2.4 for why the value cannot simply be dropped).
pub fn Options::for_text() -> Options {
  { ..Options::default(), collect_repairs: false, indent: 0, want_text: true, }
}

///|
/// Validate option values, raising `E050` for anything out of range.
pub fn Options::validate(self : Options) -> Unit raise InterJSONError {
  if self.max_depth <= 0 {
    raise InvalidOption("max_depth must be a positive integer")
  }
  // The ceiling is not a policy choice: three recursive walks (`copy_json`,
  // `json_equal`, `walk_schema`) and the engine's own frame stack are bounded
  // by it, and a caller that asks for a deeper limit would get a stack
  // overflow where it asked for an `E020`. Refusing the option turns a crash
  // into a documented `E050`.
  if self.max_depth > MAX_VALUE_DEPTH {
    raise InvalidOption(
      "max_depth must not exceed " +
      MAX_VALUE_DEPTH.to_string() +
      "; deeper values are refused rather than parsed",
    )
  }
  if self.max_input_len <= 0 {
    raise InvalidOption("max_input_len must be a positive integer")
  }
  if self.max_token_len <= 0 {
    raise InvalidOption("max_token_len must be a positive integer")
  }
  if self.max_prose_buffer <= 0 {
    raise InvalidOption("max_prose_buffer must be a positive integer")
  }
  if self.indent < 0 {
    raise InvalidOption("indent must not be negative")
  }
  if self.max_repairs <= 0 {
    raise InvalidOption("max_repairs must be a positive integer")
  }
  if self.collect_deltas {
    // A delta stream cannot take back a container it already announced, so a
    // duplicate key whose new value is a container cannot be ignored.
    if self.duplicate_keys is KeepFirst {
      raise InvalidOption(
        "collect_deltas requires duplicate_keys KeepLast or AsError: KeepFirst drops a value whose container was already announced",
      )
    }
    if self.incomplete_number is TrimToValid {
      raise InvalidOption(
        "collect_deltas requires incomplete_number Drop: an unfinished number cannot be streamed as a delta",
      )
    }
  }
}