// M4 diag — positions, repair codes, repair records and fatal errors.
//
// Two signal families are kept strictly apart (ADR-006):
//   * repair records (R codes) are informational and travel with the result;
//   * fatal errors (E codes) abort processing and are raised as `MendError`.

///|
/// Severity of a repair record.
///
/// `Info` means the value is unchanged (only the notation was normalised);
/// `Warning` means content may have been invented, dropped or altered.
pub(all) enum Severity {
  Info
  Warning
}

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

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

///|
/// Repair codes. The string form (`R001` … `R022`) is stable: a code never
/// changes meaning once released, and codes are only ever appended.
pub(all) enum RepairCode {
  StripFence
  StripComment
  NormalizeQuote
  QuoteBareWord
  RemoveExtraComma
  InsertComma
  InsertColon
  FillNull
  CloseContainer
  CloseString
  FixNumber
  ConvertLiteral
  EscapeControl
  FixEscape
  MismatchedCloser
  DropUnexpected
  DropExtraCloser
  MergeDuplicateKey
  ConcatStrings
  DropEllipsis
  SkipProse
  /// A literal was only re-cased (`True` -> `true`). Nothing was invented, so
  /// this is `Info`; a literal that had to be *completed* stays
  /// `ConvertLiteral`, which is `Warning`.
  NormalizeLiteral
}

///|
/// Explicit `Eq` implementation for `RepairCode`.
pub impl Eq for RepairCode with fn equal(self : RepairCode, other : RepairCode) -> Bool {
  self.number() == other.number()
}

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

///|
/// Stable numeric index of a repair code (1 … 21), used by rule switches.
pub fn RepairCode::number(self : RepairCode) -> Int32 {
  match self {
    StripFence => 1
    StripComment => 2
    NormalizeQuote => 3
    QuoteBareWord => 4
    RemoveExtraComma => 5
    InsertComma => 6
    InsertColon => 7
    FillNull => 8
    CloseContainer => 9
    CloseString => 10
    FixNumber => 11
    ConvertLiteral => 12
    EscapeControl => 13
    FixEscape => 14
    MismatchedCloser => 15
    DropUnexpected => 16
    DropExtraCloser => 17
    MergeDuplicateKey => 18
    ConcatStrings => 19
    DropEllipsis => 20
    SkipProse => 21
    NormalizeLiteral => 22
  }
}

///|
/// Stable string form of a repair code, e.g. `R005`.
pub fn RepairCode::code(self : RepairCode) -> String {
  let n = self.number()
  if n < 10 {
    "R00" + n.to_string()
  } else {
    "R0" + n.to_string()
  }
}

///|
/// Human readable name of a repair code, e.g. `RemoveExtraComma`.
pub fn RepairCode::name(self : RepairCode) -> String {
  match self {
    StripFence => "StripFence"
    StripComment => "StripComment"
    NormalizeQuote => "NormalizeQuote"
    QuoteBareWord => "QuoteBareWord"
    RemoveExtraComma => "RemoveExtraComma"
    InsertComma => "InsertComma"
    InsertColon => "InsertColon"
    FillNull => "FillNull"
    CloseContainer => "CloseContainer"
    CloseString => "CloseString"
    FixNumber => "FixNumber"
    ConvertLiteral => "ConvertLiteral"
    EscapeControl => "EscapeControl"
    FixEscape => "FixEscape"
    MismatchedCloser => "MismatchedCloser"
    DropUnexpected => "DropUnexpected"
    DropExtraCloser => "DropExtraCloser"
    MergeDuplicateKey => "MergeDuplicateKey"
    ConcatStrings => "ConcatStrings"
    DropEllipsis => "DropEllipsis"
    SkipProse => "SkipProse"
    NormalizeLiteral => "NormalizeLiteral"
  }
}

///|
/// Where a repaired token sits in the grammar (BR-07a).
pub(all) enum Slot {
  /// An object member name.
  KeySlot
  /// Any value position.
  ValueSlot
}

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

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

///|
/// Default severity of a repair code. `QuoteBareWord` is context dependent and
/// is recorded as `Warning` when it quotes a value (see `BR-07a`).
pub fn RepairCode::severity(self : RepairCode) -> Severity {
  match self {
    StripFence
    | StripComment
    | NormalizeQuote
    | QuoteBareWord
    | RemoveExtraComma
    | InsertComma
    | InsertColon
    | EscapeControl
    | ConcatStrings
    | MismatchedCloser
    | DropExtraCloser
    | SkipProse
    | NormalizeLiteral => Info
    _ => Warning
  }
}

///|
/// Severity of a code in a given slot. Only `QuoteBareWord` differs: quoting a
/// member name is a notation fix, quoting a value invents a string (BR-07a).
pub fn RepairCode::severity_in(self : RepairCode, slot : Slot) -> Severity {
  match (self, slot) {
    (QuoteBareWord, ValueSlot) => Warning
    _ => self.severity()
  }
}

///|
/// Whether a repair admits a second, equally self-consistent reading (FR-27).
///
/// `Unambiguous` means the JSON grammar leaves no other reading of the input.
/// Every other variant names the concrete alternative reading, which a consumer
/// that is about to *execute* the value must refuse: `Options::reject_ambiguous`
/// turns a non-`Unambiguous` repair into `E011`.
///
/// The marker is orthogonal to `Severity`: `["a" "b"]` is `Info` (nothing was
/// invented, both characters were already there) yet structurally ambiguous.
pub(all) enum Ambiguity {
  Unambiguous
  /// R004 in value position: the bare word may be a string, a reference to a
  /// value of the host language, or a misspelled literal.
  BareWord
  /// R006 between two string literals: `["a" "b"]` reads equally as two
  /// elements and as the single concatenated string `"ab"`.
  AdjacentStrings
  /// R019: `"a" + "b"` reads equally as one concatenated string and as two
  /// values separated by an operator that JSON does not have.
  ConcatOperator
  /// R016 after a complete root value: what follows may be a second document,
  /// a further element of a list, or prose.
  TrailingContent
}

///|
/// Explicit `Eq` implementation for `Ambiguity`.
pub impl Eq for Ambiguity with fn equal(self : Ambiguity, other : Ambiguity) -> Bool {
  self.name() == other.name()
}

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

///|
/// Stable lowercase name of an ambiguity, e.g. `adjacent_strings`.
pub fn Ambiguity::name(self : Ambiguity) -> String {
  match self {
    Unambiguous => "unambiguous"
    BareWord => "bare_word"
    AdjacentStrings => "adjacent_strings"
    ConcatOperator => "concat_operator"
    TrailingContent => "trailing_content"
  }
}

///|
/// Whether the two readings of an ambiguity can be told apart from the input
/// alone. Always false today: that is what makes it an ambiguity.
pub fn Ambiguity::is_ambiguous(self : Ambiguity) -> Bool {
  !(self is Unambiguous)
}

///|
/// Fixed English message of an ambiguity, for `E011` and `RetryHint`.
pub fn Ambiguity::message(self : Ambiguity) -> String {
  match self {
    Unambiguous => ""
    BareWord =>
      "bare word in value position may be a string or a host-language value"
    AdjacentStrings =>
      "two adjacent strings may be two elements or one concatenated string"
    ConcatOperator => "a plus sign between strings may concatenate or separate"
    TrailingContent =>
      "text after the first root value may be a second document or prose"
  }
}

///|
/// Fixed English message template of a repair code. Messages never embed user
/// input, so they cannot be used to inject content into logs (D09 9.7).
pub fn RepairCode::message(self : RepairCode) -> String {
  match self {
    StripFence => "code fence removed"
    StripComment => "comment removed"
    NormalizeQuote => "quote normalized to double quote"
    QuoteBareWord => "bare word quoted"
    RemoveExtraComma => "extra comma removed"
    InsertComma => "missing comma inserted"
    InsertColon => "missing colon inserted"
    FillNull => "null filled for missing value"
    CloseContainer => "unclosed container closed"
    CloseString => "unclosed string closed"
    FixNumber => "number normalized or trimmed"
    ConvertLiteral => "literal converted or completed"
    EscapeControl => "control character escaped"
    FixEscape => "invalid escape repaired"
    MismatchedCloser => "mismatched closer replaced"
    DropUnexpected => "unexpected content dropped"
    DropExtraCloser => "extra closer dropped"
    MergeDuplicateKey => "duplicate key merged"
    ConcatStrings => "string concatenation merged"
    DropEllipsis => "ellipsis placeholder dropped"
    SkipProse => "leading prose skipped"
    NormalizeLiteral => "literal re-cased"
  }
}

///|
/// How much of a result was invented rather than read from the input (FR-17).
///
/// The three levels are ordered: `Exact` is the safest, `Lossy` the riskiest.
/// `Options::max_loss` compares against this order.
///
/// * `Exact` — no repair was recorded, so the value is exactly what the input
///   said (a valid document, possibly with trailing whitespace).
/// * `Cosmetic` — only notation-level repairs (`Info` severity) were recorded:
///   quotes normalised, a comment or a code fence removed, a missing comma or
///   colon inserted, a control character escaped. No value was guessed.
/// * `Lossy` — at least one `Warning` severity repair was recorded: a value was
///   invented, dropped, truncated or replaced.
pub(all) enum Exactness {
  Exact
  Cosmetic
  Lossy
}

///|
/// Explicit `Eq` implementation for `Exactness`.
pub impl Eq for Exactness with fn equal(self : Exactness, other : Exactness) -> Bool {
  (self is Exact && other is Exact) ||
  (self is Cosmetic && other is Cosmetic) ||
  (self is Lossy && other is Lossy)
}

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

///|
/// Risk rank of an exactness level: `Exact` 0, `Cosmetic` 1, `Lossy` 2.
pub fn Exactness::rank(self : Exactness) -> Int32 {
  match self {
    Exact => 0
    Cosmetic => 1
    Lossy => 2
  }
}

///|
/// Whether `self` is at most as risky as `limit`.
pub fn Exactness::within(self : Exactness, limit : Exactness) -> Bool {
  self.rank() <= limit.rank()
}

///|
/// Stable lowercase name, e.g. `exact`.
pub fn Exactness::name(self : Exactness) -> String {
  match self {
    Exact => "exact"
    Cosmetic => "cosmetic"
    Lossy => "lossy"
  }
}

///|
/// Aggregate the exactness of a result from its repair records (FR-17).
///
/// The level is the highest risk among the records: no records is `Exact`, any
/// `Warning` is `Lossy`, otherwise `Cosmetic`.
pub fn exactness_of(repairs : Array[Repair]) -> Exactness {
  if repairs.length() == 0 {
    return Exact
  }
  for r in repairs {
    if r.severity is Warning {
      return Lossy
    }
  }
  Cosmetic
}

///|
/// A position inside the input. `offset` counts UTF-16 code units from 0,
/// `line` and `column` are 1-based (BR-20).
pub(all) struct Position {
  offset : Int32
  line : Int32
  column : Int32
}

///|
/// Explicit `Eq` implementation for `Position`.
pub impl Eq for Position with fn equal(self : Position, other : Position) -> Bool {
  self.offset == other.offset &&
  self.line == other.line &&
  self.column == other.column
}

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

///|
/// One repair record: what was changed, where, how serious it is, and whether
/// the change admits a second reading.
pub(all) struct Repair {
  code : RepairCode
  position : Position
  severity : Severity
  message : String
  /// `Unambiguous` unless the site could be read more than one way (FR-27).
  ambiguity : Ambiguity
}

///|
/// Whether any record in `repairs` is ambiguous (FR-27).
pub fn has_ambiguity(repairs : Array[Repair]) -> Bool {
  for r in repairs {
    if r.ambiguity.is_ambiguous() {
      return true
    }
  }
  false
}

///|
/// Fatal error. Every variant carries the data needed to diagnose the input
/// without re-running the parser.
pub(all) suberror MendError {
  EmptyInput
  NoJsonFound(Int32)
  StrictViolation(RepairCode, Position, String)
  /// An unambiguous reading was required but the repair admits a second one
  /// (E011, FR-27). Raised before the `max_loss` gate, because the ambiguity is
  /// the more specific diagnosis.
  AmbiguousRepair(RepairCode, Position, Ambiguity)
  DepthLimit(Int32, Int32)
  SizeLimit(Int32, Int32)
  TokenLimit(Int32, Int32)
  InvalidState(String)
  InvalidUtf8(Int32)
  InvalidOption(String)
  /// A provider event payload could not be interpreted (E060). Raised by the
  /// `jsonmend/llm` adapters; the payload is a fixed explanation.
  InvalidEvent(String)
  /// A skeleton declaration could not be read, or a value does not satisfy it
  /// (E070, FR-34). The payload is a fixed explanation.
  SchemaError(String)
}

///|
/// Stable error code string, e.g. `E010`.
pub fn MendError::code(self : MendError) -> String {
  match self {
    EmptyInput => "E001"
    NoJsonFound(_offset) => "E002"
    StrictViolation(_code, _pos, _msg) => "E010"
    AmbiguousRepair(_code, _pos, _msg) => "E011"
    DepthLimit(_limit, _offset) => "E020"
    SizeLimit(_limit, _offset) => "E021"
    TokenLimit(_limit, _offset) => "E022"
    InvalidState(_) => "E030"
    InvalidUtf8(_) => "E040"
    InvalidOption(_) => "E050"
    InvalidEvent(_) => "E060"
    SchemaError(_) => "E070"
  }
}

///|
/// One-line diagnostic, e.g. `E020 DepthLimit: limit 3 exceeded at offset 3`.
pub fn MendError::to_string(self : MendError) -> String {
  let sb = StringBuilder()
  match self {
    EmptyInput =>
      sb.write_string("E001 EmptyInput: input is empty or whitespace only")
    NoJsonFound(offset) => {
      sb.write_string("E002 NoJsonFound: no JSON value found at offset ")
      sb.write_string(offset.to_string())
    }
    StrictViolation(code, pos, msg) => {
      sb.write_string("E010 StrictViolation at ")
      sb.write_string(pos.line.to_string())
      sb.write_string(":")
      sb.write_string(pos.column.to_string())
      sb.write_string(" (offset ")
      sb.write_string(pos.offset.to_string())
      sb.write_string("): ")
      sb.write_string(code.code())
      sb.write_string(" ")
      sb.write_string(code.name())
      sb.write_string(": ")
      sb.write_string(msg)
    }
    AmbiguousRepair(code, pos, ambiguity) => {
      sb.write_string("E011 AmbiguousRepair at ")
      sb.write_string(pos.line.to_string())
      sb.write_string(":")
      sb.write_string(pos.column.to_string())
      sb.write_string(" (offset ")
      sb.write_string(pos.offset.to_string())
      sb.write_string("): ")
      sb.write_string(code.code())
      sb.write_string(" ")
      sb.write_string(code.name())
      sb.write_string(": ")
      sb.write_string(ambiguity.message())
    }
    DepthLimit(limit, offset) => {
      sb.write_string("E020 DepthLimit: nesting limit ")
      sb.write_string(limit.to_string())
      sb.write_string(" exceeded at offset ")
      sb.write_string(offset.to_string())
    }
    SizeLimit(limit, offset) => {
      sb.write_string("E021 SizeLimit: input limit ")
      sb.write_string(limit.to_string())
      sb.write_string(" exceeded at offset ")
      sb.write_string(offset.to_string())
    }
    TokenLimit(limit, offset) => {
      sb.write_string("E022 TokenLimit: token limit ")
      sb.write_string(limit.to_string())
      sb.write_string(" exceeded at offset ")
      sb.write_string(offset.to_string())
    }
    InvalidState(msg) => {
      sb.write_string("E030 InvalidState: ")
      sb.write_string(msg)
    }
    InvalidUtf8(offset) => {
      sb.write_string("E040 InvalidUtf8: invalid UTF-8 byte at offset ")
      sb.write_string(offset.to_string())
    }
    InvalidOption(msg) => {
      sb.write_string("E050 InvalidOption: ")
      sb.write_string(msg)
    }
    InvalidEvent(msg) => {
      sb.write_string("E060 InvalidEvent: ")
      sb.write_string(msg)
    }
    SchemaError(msg) => {
      sb.write_string("E070 SchemaError: ")
      sb.write_string(msg)
    }
  }
  sb.to_string()
}