///| Diagnostic model.
///
/// Every analysis layer reports through the same `Diagnostic` value, so text
/// and JSON output can be rendered from one place. Codes and severities are
/// part of the public contract: new codes may be added, but the spelling and
/// meaning of an existing code must not change.

///|
/// How serious a finding is.
pub(all) enum Severity {
  /// Context that does not change the compatible/incompatible verdict.
  Info
  /// A change that is compatible but should be reviewed by a human.
  Warning
  /// A structural incompatibility or an unsupported input.
  Error
} derive(Eq, @debug.Debug)

///|
/// Lowercase name of the severity, shared by both renderers.
pub fn Severity::name(self : Severity) -> String {
  match self {
    Info => "info"
    Warning => "warning"
    Error => "error"
  }
}

///|
/// Rank used to order findings that share a location.
fn Severity::rank(self : Severity) -> Int {
  match self {
    Info => 0
    Warning => 1
    Error => 2
  }
}

///|
/// Stable code identifying one diagnostic rule.
///
/// Codes are namespaced by layer: `storage.entry.*` describes a storage
/// variable, and `storage.type.*` describes an entry of the compiler type
/// table that a storage type reference resolves to.
pub(all) enum DiagnosticCode {
  /// A storage variable is missing a required field.
  StorageEntryFieldMissing
  /// A storage slot is not a non-negative decimal integer.
  StorageEntrySlotInvalid
  /// A storage byte offset is outside the slot range `0..=31`.
  StorageEntryOffsetInvalid
  /// A storage variable references a type id that the layout does not define.
  StorageEntryTypeMissing
  /// A storage type is missing its compiler id.
  StorageTypeIdMissing
  /// A storage type id is defined more than once.
  StorageTypeIdDuplicate
  /// A storage type is missing a required field.
  StorageTypeFieldMissing
  /// A storage type uses an encoding this version does not support.
  StorageTypeEncodingUnsupported
  /// A `numberOfBytes` value is not a decimal integer.
  StorageTypeSizeInvalid
  /// A storage type references another type id that the layout does not
  /// define.
  StorageTypeReferenceMissing
  /// A fixed-array label does not carry a usable decimal element count.
  StorageTypeArrayLengthInvalid
  /// The artifact text is not valid JSON.
  ArtifactJsonInvalid
  /// The artifact root is not one of the recognized wrapper shapes.
  ArtifactShapeUnsupported
  /// The artifact matches more than one recognized wrapper shape.
  ArtifactShapeAmbiguous
  /// A recognized artifact field has the wrong JSON type or value.
  ArtifactFieldInvalid
  /// The artifact carries no storage layout.
  ArtifactLayoutMissing
  /// More than one contract in the artifact carries a storage layout.
  ArtifactContractAmbiguous
  /// No contract in the artifact matches the requested selector.
  ArtifactContractMissing
  /// An operation that compares ABI data received an artifact without an ABI.
  ArtifactAbiMissing
  /// A storage variable of the old layout is absent from the new layout.
  StorageEntryRemoved
  /// A storage variable appears only in the new layout.
  StorageEntryAdded
  /// A storage variable moved to a different slot.
  StorageEntrySlotChanged
  /// A storage variable moved within its slot.
  StorageEntryOffsetChanged
  /// A storage variable was renamed without moving or changing type.
  StorageEntryLabelChanged
  /// A storage variable kept its position but changed semantic type.
  StorageEntryTypeChanged
  /// A storage gap was resized, moved, or consumed while keeping its end
  /// position.
  StorageGapChanged
  /// The artifact mentions ERC-7201 namespaced storage, which cannot be
  /// located from compiler output.
  ArtifactNamespacedStorageUnsupported
  /// The command line names something the CLI does not support.
  CliArgumentInvalid
  /// The CLI could not read an input file.
  CliInputUnreadable
  /// A function signature of the old ABI is absent from the new ABI.
  AbiFunctionRemoved
  /// A function signature appears only in the new ABI.
  AbiFunctionAdded
  /// A function kept its signature but changed state mutability.
  AbiFunctionMutabilityChanged
  /// A function lost mutability behavior that existing callers rely on.
  AbiFunctionMutabilityIncompatible
  /// A function kept its signature but changed its output types.
  AbiFunctionOutputsChanged
  /// An event signature of the old ABI is absent from the new ABI.
  AbiEventRemoved
  /// An event signature appears only in the new ABI.
  AbiEventAdded
  /// An event kept its signature but changed which parameters are indexed.
  AbiEventIndexedChanged
  /// An event kept its signature but changed its anonymity.
  AbiEventAnonymousChanged
  /// A custom error signature of the old ABI is absent from the new ABI.
  AbiErrorRemoved
  /// A custom error signature appears only in the new ABI.
  AbiErrorAdded
  /// A selector or event topic is used by different signatures in the two
  /// versions.
  AbiSignatureCollision
} derive(Eq, @debug.Debug)

///|
/// Stable string code emitted in text and JSON output.
pub fn DiagnosticCode::name(self : DiagnosticCode) -> String {
  match self {
    StorageEntryFieldMissing => "storage.entry.field.missing"
    StorageEntrySlotInvalid => "storage.entry.slot.invalid"
    StorageEntryOffsetInvalid => "storage.entry.offset.invalid"
    StorageEntryTypeMissing => "storage.entry.type.missing"
    StorageTypeIdMissing => "storage.type.id.missing"
    StorageTypeIdDuplicate => "storage.type.id.duplicate"
    StorageTypeFieldMissing => "storage.type.field.missing"
    StorageTypeEncodingUnsupported => "storage.type.encoding.unsupported"
    StorageTypeSizeInvalid => "storage.type.size.invalid"
    StorageTypeReferenceMissing => "storage.type.reference.missing"
    StorageTypeArrayLengthInvalid => "storage.type.array-length.invalid"
    ArtifactJsonInvalid => "artifact.json.invalid"
    ArtifactShapeUnsupported => "artifact.shape.unsupported"
    ArtifactShapeAmbiguous => "artifact.shape.ambiguous"
    ArtifactFieldInvalid => "artifact.field.invalid"
    ArtifactLayoutMissing => "artifact.layout.missing"
    ArtifactContractAmbiguous => "artifact.contract.ambiguous"
    ArtifactContractMissing => "artifact.contract.missing"
    ArtifactAbiMissing => "artifact.abi.missing"
    StorageEntryRemoved => "storage.entry.removed"
    StorageEntryAdded => "storage.entry.added"
    StorageEntrySlotChanged => "storage.entry.slot.changed"
    StorageEntryOffsetChanged => "storage.entry.offset.changed"
    StorageEntryLabelChanged => "storage.entry.label.changed"
    StorageEntryTypeChanged => "storage.entry.type.changed"
    StorageGapChanged => "storage.gap.changed"
    ArtifactNamespacedStorageUnsupported =>
      "artifact.namespaced-storage.unsupported"
    CliArgumentInvalid => "cli.argument.invalid"
    CliInputUnreadable => "cli.input.unreadable"
    AbiFunctionRemoved => "abi.function.removed"
    AbiFunctionAdded => "abi.function.added"
    AbiFunctionMutabilityChanged => "abi.function.mutability.changed"
    AbiFunctionMutabilityIncompatible => "abi.function.mutability.incompatible"
    AbiFunctionOutputsChanged => "abi.function.outputs.changed"
    AbiEventRemoved => "abi.event.removed"
    AbiEventAdded => "abi.event.added"
    AbiEventIndexedChanged => "abi.event.indexed.changed"
    AbiEventAnonymousChanged => "abi.event.anonymous.changed"
    AbiErrorRemoved => "abi.error.removed"
    AbiErrorAdded => "abi.error.added"
    AbiSignatureCollision => "abi.signature.collision"
  }
}

///|
/// Canonical severity of a code.
///
/// The severity is fixed per code so that consumers can filter on it across
/// releases. `Diagnostic::new` uses this table; a future policy that wants a
/// different severity for the same code must document that choice.
pub fn DiagnosticCode::severity(self : DiagnosticCode) -> Severity {
  match self {
    StorageEntryFieldMissing => Error
    StorageEntrySlotInvalid => Error
    StorageEntryOffsetInvalid => Error
    StorageEntryTypeMissing => Error
    StorageTypeIdMissing => Error
    StorageTypeIdDuplicate => Error
    StorageTypeFieldMissing => Error
    StorageTypeEncodingUnsupported => Error
    StorageTypeSizeInvalid => Error
    StorageTypeReferenceMissing => Error
    StorageTypeArrayLengthInvalid => Error
    ArtifactJsonInvalid => Error
    ArtifactShapeUnsupported => Error
    ArtifactShapeAmbiguous => Error
    ArtifactFieldInvalid => Error
    ArtifactLayoutMissing => Error
    ArtifactContractAmbiguous => Error
    ArtifactContractMissing => Error
    ArtifactAbiMissing => Error
    StorageEntryRemoved => Error
    StorageEntryAdded => Info
    StorageEntrySlotChanged => Error
    StorageEntryOffsetChanged => Error
    StorageEntryLabelChanged => Warning
    StorageEntryTypeChanged => Error
    StorageGapChanged => Info
    ArtifactNamespacedStorageUnsupported => Error
    CliArgumentInvalid => Error
    CliInputUnreadable => Error
    AbiFunctionRemoved => Error
    AbiFunctionAdded => Info
    AbiFunctionMutabilityChanged => Warning
    AbiFunctionMutabilityIncompatible => Error
    AbiFunctionOutputsChanged => Error
    AbiEventRemoved => Error
    AbiEventAdded => Info
    AbiEventIndexedChanged => Error
    AbiEventAnonymousChanged => Error
    AbiErrorRemoved => Error
    AbiErrorAdded => Info
    AbiSignatureCollision => Error
  }
}

///|
/// Where a finding was located.
///
/// `path` is relative to the `storageLayout` object that was analyzed: for
/// example `storage[2].slot` or `types["t_uint8"].encoding`. Which artifact the
/// layout came from is a wrapper concern, so the file name is not part of the
/// path; `contract` carries the emitting contract as the artifact reports it.
pub(all) struct Location {
  /// Emitting contract, such as `fileA:A`, when the artifact provides one.
  contract : String?
  /// Path inside the `storageLayout` object.
  path : String
} derive(Eq, @debug.Debug)

///|
/// Which input produced a finding before the two artifacts were compared.
pub(all) enum InputSide {
  Old
  New
} derive(Eq, @debug.Debug)

///|
/// Stable name used in text and JSON reports.
pub fn InputSide::name(self : InputSide) -> String {
  match self {
    Old => "old"
    New => "new"
  }
}

///|
/// One finding.
pub(all) struct Diagnostic {
  /// Stable rule code.
  code : DiagnosticCode
  /// Severity of this finding.
  severity : Severity
  /// Input that produced the finding, if it came from one artifact alone.
  input : InputSide?
  /// Where the finding was located.
  location : Location
  /// Human-readable description. Messages are single-line.
  message : String
  /// Value in the old artifact, for findings that relate two artifacts.
  old_value : String?
  /// Value in the new artifact, for findings that relate two artifacts.
  new_value : String?
} derive(Eq, @debug.Debug)

///|
/// Builds a diagnostic with the canonical severity of `code`.
pub fn Diagnostic::new(
  code : DiagnosticCode,
  location : Location,
  message : String,
) -> Diagnostic {
  {
    code,
    severity: code.severity(),
    input: None,
    location,
    message,
    old_value: None,
    new_value: None,
  }
}

///|
/// Total order over diagnostics: contract, path, code, severity, input,
/// message, and finally the compared values. Text fields compare with MoonBit's
/// `String::compare`, which orders shorter strings first and then by code
/// unit.
///
/// Both renderers sort with this order, so output never depends on the order
/// in which a caller collected findings.
pub impl Compare for Diagnostic with fn compare(
  self : Diagnostic,
  other : Diagnostic,
) -> Int {
  let c = compare_optional(self.location.contract, other.location.contract)
  if c != 0 {
    return c
  }
  let c = self.location.path.compare(other.location.path)
  if c != 0 {
    return c
  }
  let c = self.code.name().compare(other.code.name())
  if c != 0 {
    return c
  }
  let c = self.severity.rank() - other.severity.rank()
  if c != 0 {
    return c
  }
  let c = compare_optional_input(self.input, other.input)
  if c != 0 {
    return c
  }
  let c = self.message.compare(other.message)
  if c != 0 {
    return c
  }
  let c = compare_optional(self.old_value, other.old_value)
  if c != 0 {
    return c
  }
  compare_optional(self.new_value, other.new_value)
}

///|
/// Compares two optional strings. An absent value sorts before a present one.
fn compare_optional(a : String?, b : String?) -> Int {
  match (a, b) {
    (None, None) => 0
    (None, Some(_)) => -1
    (Some(_), None) => 1
    (Some(a), Some(b)) => a.compare(b)
  }
}

///|
/// Input findings at the same location list the old side first.
fn compare_optional_input(a : InputSide?, b : InputSide?) -> Int {
  match (a, b) {
    (None, None) => 0
    (None, Some(_)) => -1
    (Some(_), None) => 1
    (Some(Old), Some(New)) => -1
    (Some(New), Some(Old)) => 1
    (Some(_), Some(_)) => 0
  }
}

///|
/// Whether a diagnostic set blocks an upgrade.
///
/// Only `Error` findings block. They mark a storage or ABI change that breaks
/// existing storage or existing callers. `Warning` and `Info` findings are
/// reported without changing the verdict, so they never make a checker fail.
pub fn blocks_upgrade(diagnostics : Array[Diagnostic]) -> Bool {
  for diagnostic in diagnostics {
    if diagnostic.severity == Error {
      return true
    }
  }
  false
}

///|
/// Returns a copy of the diagnostic with the compared values set.
///
/// A value that is not passed keeps the value already on the diagnostic, so a
/// finding that only has an old value can be built with one argument.
pub fn Diagnostic::with_values(
  self : Diagnostic,
  old_value? : String,
  new_value? : String,
) -> Diagnostic {
  {
    ..self,
    old_value: match old_value {
      Some(value) => Some(value)
      None => self.old_value
    },
    new_value: match new_value {
      Some(value) => Some(value)
      None => self.new_value
    },
  }
}

///|
/// Identifies the artifact that produced an input finding.
pub fn Diagnostic::with_input(
  self : Diagnostic,
  input : InputSide,
) -> Diagnostic {
  { ..self, input: Some(input), }
}

///|
pub extend Severity with Eq::{not_equal, equal}

///|
pub extend Severity with @debug.Debug::{to_repr}

///|
pub extend DiagnosticCode with Eq::{not_equal, equal}

///|
pub extend DiagnosticCode with @debug.Debug::{to_repr}

///|
pub extend Location with Eq::{not_equal, equal}

///|
pub extend Location with @debug.Debug::{to_repr}

///|
pub extend InputSide with Eq::{not_equal, equal}

///|
pub extend InputSide with @debug.Debug::{to_repr}

///|
pub extend Diagnostic with Eq::{not_equal, equal}

///|
pub extend Diagnostic with @debug.Debug::{to_repr}

///|
pub extend Diagnostic with Compare::{op_lt, op_le, op_ge, compare, op_gt}