// M5 options โ€” the option dictionary of D04 4.6, with defaults and validation.

///|
/// 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.
pub fn RuleSet::all() -> RuleSet {
  let flags : Array[Bool] = []
  for _i = 0; _i < 22; _i = _i + 1 {
    flags.push(true)
  }
  { flags, }
}

///|
/// A copy of this rule set with one rule disabled.
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() {
    flags[n] = false
  }
  { flags, }
}

///|
/// A copy of this rule set with one rule enabled.
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() {
    flags[n] = true
  }
  { flags, }
}

///|
/// Whether a repair code may be reported without raising `E010`.
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 {
    true
  }
}

///|
/// 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
  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,
    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,
  }
}

///|
/// 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 MendError {
  if self.max_depth <= 0 {
    raise InvalidOption("max_depth must be a positive integer")
  }
  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.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",
      )
    }
  }
}