///|
/// Severity attached to one audit diagnostic.
pub(all) enum DiagnosticLevel {
  Information
  Warning
  Failure
} derive(Eq, Debug)

///|
/// A stable diagnostic produced without raising an exception.
pub(all) struct AuditDiagnostic {
  level : DiagnosticLevel
  code : String
  message : String
  offset : Int
} derive(Eq, Debug)

///|
/// Conservative resource figures derived from a parsed delta.
pub(all) struct ResourceEstimate {
  input_size : Int
  declared_output_size : Int
  largest_window_size : Int
  largest_dictionary_size : Int
  total_instructions : Int
  estimated_peak_bytes : Int
} derive(Eq, Debug)

///|
/// Non-throwing validation result for untrusted VCDIFF bytes.
pub(all) struct AuditReport {
  structurally_valid : Bool
  decodable : Bool
  decoded_size : Int
  summary : DeltaSummary?
  resources : ResourceEstimate
  diagnostics : Array[AuditDiagnostic]
} derive(Eq, Debug)

///|
fn empty_resource_estimate(input_size : Int) -> ResourceEstimate {
  {
    input_size,
    declared_output_size: 0,
    largest_window_size: 0,
    largest_dictionary_size: 0,
    total_instructions: 0,
    estimated_peak_bytes: input_size,
  }
}

///|
fn diagnostic(
  level : DiagnosticLevel,
  code : String,
  message : String,
  offset : Int,
) -> AuditDiagnostic {
  { level, code, message, offset }
}

///|
fn checked_estimate_add(current : Int, amount : Int) -> Int {
  if amount < 0 || current > MAX_PORTABLE_INT - amount {
    MAX_PORTABLE_INT
  } else {
    current + amount
  }
}

///|
fn estimate_resources(summary : DeltaSummary) -> ResourceEstimate {
  let mut largest_window_size = 0
  let mut largest_dictionary_size = 0
  let mut total_instructions = 0
  for window in summary.windows {
    if window.target_size > largest_window_size {
      largest_window_size = window.target_size
    }
    if window.source_size > largest_dictionary_size {
      largest_dictionary_size = window.source_size
    }
    total_instructions = checked_estimate_add(
      total_instructions,
      window.add_count + window.run_count + window.copy_count,
    )
  }
  let working = checked_estimate_add(
    largest_window_size, largest_dictionary_size,
  )
  {
    input_size: summary.file_size,
    declared_output_size: summary.target_size,
    largest_window_size,
    largest_dictionary_size,
    total_instructions,
    estimated_peak_bytes: checked_estimate_add(summary.file_size, working),
  }
}

///|
fn append_policy_diagnostics(
  summary : DeltaSummary,
  limits : DecodeLimits,
  diagnostics : Array[AuditDiagnostic],
) -> Unit {
  if summary.window_count == 0 {
    diagnostics.push(
      diagnostic(
        Information,
        "empty_delta",
        "the delta contains no target windows",
        summary.header_size,
      ),
    )
  }
  if summary.file_size >= summary.target_size && summary.target_size > 0 {
    diagnostics.push(
      diagnostic(
        Information,
        "no_size_reduction",
        "the delta is not smaller than its declared target",
        0,
      ),
    )
  }
  if summary.window_count > limits.max_windows {
    diagnostics.push(
      diagnostic(
        Failure,
        "window_limit",
        "declared window count exceeds the configured limit",
        summary.header_size,
      ),
    )
  }
  if summary.target_size > limits.max_output_size {
    diagnostics.push(
      diagnostic(
        Failure,
        "output_limit",
        "declared output size exceeds the configured limit",
        summary.header_size,
      ),
    )
  }
  for window in summary.windows {
    if window.target_size > limits.max_window_size {
      diagnostics.push(
        diagnostic(
          Failure,
          "window_size_limit",
          "declared target window exceeds the configured limit",
          window.offset,
        ),
      )
    }
    let operations = window.add_count + window.run_count + window.copy_count
    if operations > limits.max_instructions_per_window {
      diagnostics.push(
        diagnostic(
          Failure,
          "instruction_limit",
          "window instruction count exceeds the configured limit",
          window.offset,
        ),
      )
    }
    if window.source_kind == SourceDictionary && window.source_size == 0 {
      diagnostics.push(
        diagnostic(
          Warning,
          "empty_source_segment",
          "VCD_SOURCE selects an empty dictionary segment",
          window.offset,
        ),
      )
    }
    if window.copy_count > 0 && window.address_size == 0 {
      diagnostics.push(
        diagnostic(
          Failure,
          "missing_addresses",
          "COPY instructions exist but the address section is empty",
          window.offset,
        ),
      )
    }
  }
}

///|
fn failed_audit(input_size : Int, error : VcdiffError) -> AuditReport {
  {
    structurally_valid: false,
    decodable: false,
    decoded_size: 0,
    summary: None,
    resources: empty_resource_estimate(input_size),
    diagnostics: [
      diagnostic(Failure, error.code(), error.message(), error.offset()),
    ],
  }
}

///|
fn decoding_failed_audit(
  input_size : Int,
  summary : DeltaSummary,
  diagnostics : Array[AuditDiagnostic],
  error : VcdiffError,
) -> AuditReport {
  ignore(input_size)
  diagnostics.push(
    diagnostic(Failure, error.code(), error.message(), error.offset()),
  )
  {
    structurally_valid: true,
    decodable: false,
    decoded_size: 0,
    summary: Some(summary),
    resources: estimate_resources(summary),
    diagnostics,
  }
}

///|
fn catch_decode_for_audit(
  source : Bytes,
  delta : Bytes,
  limits : DecodeLimits,
  summary : DeltaSummary,
  diagnostics : Array[AuditDiagnostic],
) -> AuditReport {
  let decoded = decode(source, delta, limits) catch {
    InvalidMagic(offset~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        InvalidMagic(offset~),
      )
    UnsupportedVersion(offset~, version~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        UnsupportedVersion(offset~, version~),
      )
    UnsupportedFeature(offset~, feature~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        UnsupportedFeature(offset~, feature~),
      )
    TruncatedInput(offset~, needed~, available~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        TruncatedInput(offset~, needed~, available~),
      )
    InvalidVarint(offset~, reason~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        InvalidVarint(offset~, reason~),
      )
    IntegerOverflow(offset~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        IntegerOverflow(offset~),
      )
    InvalidHeader(offset~, reason~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        InvalidHeader(offset~, reason~),
      )
    InvalidWindow(offset~, reason~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        InvalidWindow(offset~, reason~),
      )
    InvalidInstruction(offset~, reason~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        InvalidInstruction(offset~, reason~),
      )
    InvalidAddress(offset~, address~, limit~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        InvalidAddress(offset~, address~, limit~),
      )
    MissingSource(offset~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        MissingSource(offset~),
      )
    ResourceLimit(offset~, resource~, limit~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        ResourceLimit(offset~, resource~, limit~),
      )
    LengthMismatch(offset~, expected~, actual~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        LengthMismatch(offset~, expected~, actual~),
      )
    VerificationMismatch(offset~, expected_size~, actual_size~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        VerificationMismatch(offset~, expected_size~, actual_size~),
      )
    InvalidOption(option~, reason~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        InvalidOption(option~, reason~),
      )
    Io(path~, reason~) =>
      return decoding_failed_audit(
        delta.length(),
        summary,
        diagnostics,
        Io(path~, reason~),
      )
  }
  {
    structurally_valid: true,
    decodable: true,
    decoded_size: decoded.length(),
    summary: Some(summary),
    resources: estimate_resources(summary),
    diagnostics,
  }
}

///|
/// Audits untrusted delta bytes without raising VCDIFF errors.
///
/// Structural validity is reported separately from source-dependent
/// decodability, and successful reports include conservative memory figures.
pub fn audit(
  source : Bytes,
  delta : Bytes,
  limits : DecodeLimits,
) -> AuditReport {
  let summary = inspect(delta) catch {
    InvalidMagic(offset~) =>
      return failed_audit(delta.length(), InvalidMagic(offset~))
    UnsupportedVersion(offset~, version~) =>
      return failed_audit(delta.length(), UnsupportedVersion(offset~, version~))
    UnsupportedFeature(offset~, feature~) =>
      return failed_audit(delta.length(), UnsupportedFeature(offset~, feature~))
    TruncatedInput(offset~, needed~, available~) =>
      return failed_audit(
        delta.length(),
        TruncatedInput(offset~, needed~, available~),
      )
    InvalidVarint(offset~, reason~) =>
      return failed_audit(delta.length(), InvalidVarint(offset~, reason~))
    IntegerOverflow(offset~) =>
      return failed_audit(delta.length(), IntegerOverflow(offset~))
    InvalidHeader(offset~, reason~) =>
      return failed_audit(delta.length(), InvalidHeader(offset~, reason~))
    InvalidWindow(offset~, reason~) =>
      return failed_audit(delta.length(), InvalidWindow(offset~, reason~))
    InvalidInstruction(offset~, reason~) =>
      return failed_audit(delta.length(), InvalidInstruction(offset~, reason~))
    InvalidAddress(offset~, address~, limit~) =>
      return failed_audit(
        delta.length(),
        InvalidAddress(offset~, address~, limit~),
      )
    MissingSource(offset~) =>
      return failed_audit(delta.length(), MissingSource(offset~))
    ResourceLimit(offset~, resource~, limit~) =>
      return failed_audit(
        delta.length(),
        ResourceLimit(offset~, resource~, limit~),
      )
    LengthMismatch(offset~, expected~, actual~) =>
      return failed_audit(
        delta.length(),
        LengthMismatch(offset~, expected~, actual~),
      )
    VerificationMismatch(offset~, expected_size~, actual_size~) =>
      return failed_audit(
        delta.length(),
        VerificationMismatch(offset~, expected_size~, actual_size~),
      )
    InvalidOption(option~, reason~) =>
      return failed_audit(delta.length(), InvalidOption(option~, reason~))
    Io(path~, reason~) =>
      return failed_audit(delta.length(), Io(path~, reason~))
  }
  let diagnostics : Array[AuditDiagnostic] = []
  append_policy_diagnostics(summary, limits, diagnostics)
  catch_decode_for_audit(source, delta, limits, summary, diagnostics)
}

///|
fn DiagnosticLevel::name(self : DiagnosticLevel) -> String {
  match self {
    Information => "info"
    Warning => "warning"
    Failure => "error"
  }
}

///|
/// Renders a concise audit report for logs and command-line tools.
pub fn AuditReport::to_text(self : AuditReport) -> String {
  let builder = StringBuilder()
  builder <+ "structurally valid: \{self.structurally_valid}\n"
  builder <+ "decodable: \{self.decodable}\n"
  builder <+ "decoded size: \{self.decoded_size}\n"
  builder <+ "estimated peak bytes: \{self.resources.estimated_peak_bytes}\n"
  for item in self.diagnostics {
    builder <+ "[\{item.level.name()}] \{item.code}"
    if item.offset >= 0 {
      builder <+ " at byte \{item.offset}"
    }
    builder <+ ": \{item.message}\n"
  }
  builder.to_string()
}