///|
pub(all) enum CliMode {
  CliCheck
  CliList
} derive(Eq, Debug)

///|
pub(all) struct CliOutput {
  stdout : String
  stderr : String
  exit_code : Int
} derive(Eq, Debug)

///|
fn cli_dbc_id_text(id : UInt) -> String {
  match dbc_message_id_parts(id) {
    Some((frame_id, extended)) => {
      let chars : Array[Char] = []
      append_padded_identifier(chars, frame_id, extended)
      if extended {
        "\{String::from_array(chars)} extended"
      } else {
        "\{String::from_array(chars)} standard"
      }
    }
    None => id.to_string()
  }
}

///|
fn cli_signal_count(database : Database) -> Int {
  let mut count = 0
  for message in database.messages {
    count += message.signals.length()
  }
  count
}

///|
fn cli_diagnostics(path : String, diagnostics : Array[Diagnostic]) -> String {
  let lines : Array[String] = []
  for diagnostic in diagnostics {
    lines.push(
      "\{path}:\{diagnostic.line}:\{diagnostic.column} [\{diagnostic.code}] \{diagnostic.message}",
    )
  }
  "\{lines.join("\n")}\n"
}

///|
/// Human-readable explanation of a semantic DBC validation issue.
pub fn ValidationIssue::description(self : ValidationIssue) -> String {
  match self {
    InvalidMessageIdentifierIssue(id) =>
      "message \{id} has an invalid DBC identifier"
    DuplicateMessageIdIssue(id) =>
      "message \{cli_dbc_id_text(id)} is declared more than once"
    InvalidPayloadSizeIssue(id, size) =>
      "message \{cli_dbc_id_text(id)} has invalid payload size \{size}"
    DuplicateSignalNameIssue(id, signal) =>
      "message \{cli_dbc_id_text(id)} repeats signal \{signal}"
    InvalidSignalLayoutIssue(id, signal, start, length) =>
      "signal \{signal} in message \{cli_dbc_id_text(id)} has invalid layout \{start}|\{length}"
    InvalidSignalValueTypeIssue(id, signal, value_type, length) =>
      "signal \{signal} in message \{cli_dbc_id_text(id)} declares \{cli_signal_value_type(value_type)} with \{length} bits"
    FloatingMultiplexerIssue(id, signal, value_type) =>
      "multiplexer \{signal} in message \{cli_dbc_id_text(id)} cannot use \{cli_signal_value_type(value_type)}"
    SignalOutsidePayloadIssue(id, signal) =>
      "signal \{signal} in message \{cli_dbc_id_text(id)} exceeds its payload"
    InvalidScaleIssue(id, signal, factor, offset) =>
      "signal \{signal} in message \{cli_dbc_id_text(id)} has invalid scale (\{factor},\{offset})"
    InvalidPhysicalRangeIssue(id, signal, minimum, maximum) =>
      "signal \{signal} in message \{cli_dbc_id_text(id)} has invalid range [\{minimum}|\{maximum}]"
    MultipleMultiplexersIssue(id) =>
      "message \{cli_dbc_id_text(id)} has multiple multiplexers"
    MissingMultiplexerIssue(id, signal) =>
      "signal \{signal} in message \{cli_dbc_id_text(id)} has no multiplexer"
    MultiplexSelectorOutOfRangeIssue(id, signal, selector) =>
      "signal \{signal} in message \{cli_dbc_id_text(id)} has out-of-range selector \{selector}"
    InvalidMultiplexRangeIssue(id, signal, start, end) =>
      "signal \{signal} in message \{cli_dbc_id_text(id)} has invalid multiplex range \{start}-\{end}"
    UnknownMultiplexSelectorIssue(id, signal, selector) =>
      "signal \{signal} in message \{cli_dbc_id_text(id)} refers to unknown multiplexer \{selector}"
    OverlappingSignalsIssue(id, left, right) =>
      "signals \{left} and \{right} overlap in message \{cli_dbc_id_text(id)}"
    DuplicateCommentIssue(_) => "a DBC target has more than one comment"
    UnknownCommentNodeIssue(node) => "comment refers to unknown node \{node}"
    UnknownCommentMessageIssue(id) =>
      "comment refers to unknown message \{cli_dbc_id_text(id)}"
    UnknownCommentSignalIssue(id, signal) =>
      "comment refers to unknown signal \{signal} in message \{cli_dbc_id_text(id)}"
    UnknownCommentEnvironmentIssue(name) =>
      "comment refers to unknown environment variable \{name}"
    DuplicateMessageTransmitterIssue(id, transmitter) =>
      "message \{cli_dbc_id_text(id)} repeats transmitter \{transmitter}"
    DuplicateAttributeDefinitionIssue(name) =>
      "attribute \{name} has multiple definitions"
    DuplicateAttributeDefaultIssue(name) =>
      "attribute \{name} has multiple defaults"
    DuplicateAttributeAssignmentIssue(name, _) =>
      "attribute \{name} is assigned more than once to the same target"
    UnknownAttributeDefinitionIssue(name) =>
      "attribute \{name} has no definition"
    InvalidAttributeTargetIssue(name, _) =>
      "attribute \{name} refers to an unknown target"
    AttributeScopeMismatchIssue(name, _) =>
      "attribute \{name} is assigned to the wrong target type"
    InvalidAttributeValueIssue(name) =>
      "attribute \{name} has an invalid value or type"
    DuplicateSignalGroupIssue(id, name) =>
      "signal group \{name} in message \{cli_dbc_id_text(id)} is declared more than once"
    InvalidSignalGroupIssue(id, name) =>
      "signal group \{name} in message \{cli_dbc_id_text(id)} is invalid"
    UnknownSignalGroupMessageIssue(id, name) =>
      "signal group \{name} refers to unknown message \{cli_dbc_id_text(id)}"
    UnknownSignalGroupMemberIssue(id, name, signal) =>
      "signal group \{name} in message \{cli_dbc_id_text(id)} refers to unknown signal \{signal}"
    DuplicateGlobalValueTableIssue(name) =>
      "global value table \{name} is declared more than once"
    InvalidGlobalValueTableIssue(name) =>
      "global value table \{name} is invalid"
    DuplicateEnvironmentVariableIssue(name) =>
      "environment variable \{name} is declared more than once"
    InvalidEnvironmentVariableIssue(name) =>
      "environment variable \{name} is invalid"
  }
}

///|
fn cli_validation_issues(
  path : String,
  issues : Array[LocatedValidationIssue],
) -> String {
  let lines : Array[String] = []
  for located in issues {
    match located.source {
      Some(span) =>
        lines.push(
          "\{path}:\{span.start_line}:\{span.start_column}: \{located.issue.description()}",
        )
      None => lines.push("\{path}: \{located.issue.description()}")
    }
  }
  "\{lines.join("\n")}\n"
}

///|
fn cli_can_frame_id_text(id : UInt, extended : Bool) -> String {
  let chars : Array[Char] = []
  append_padded_identifier(chars, id, extended)
  String::from_array(chars)
}

///|
fn cli_frame_text_error(error : CanFrameTextError) -> String {
  match error {
    CanFrameMissingSeparator => "CAN frame is missing '#'"
    CanFrameEmptyIdentifier => "CAN frame identifier is empty"
    CanFrameIdentifierTooLong(length) =>
      "CAN frame identifier has \{length} hexadecimal digits"
    CanFrameInvalidIdentifier(index, _) =>
      "CAN frame identifier has an invalid character at offset \{index}"
    CanFrameIdentifierOutOfRange(id) =>
      "CAN frame identifier \{id} is out of range"
    CanFrameOddPayloadLength(length) =>
      "CAN payload has an odd hexadecimal length of \{length}"
    CanFramePayloadTooLong(length) =>
      "CAN payload has \{length} bytes; at most 64 are supported"
    CanFrameInvalidPayload(index, _) =>
      "CAN payload has an invalid character at offset \{index}"
  }
}

///|
fn cli_can_log_text_error(error : CanLogTextError) -> String {
  match error {
    CanLogInvalidFieldCount(count) =>
      "CAN log record has \{count} fields; expected 1, 2, or 3"
    CanLogInvalidTimestamp(timestamp) =>
      "CAN log record has invalid timestamp \{timestamp}"
    CanLogInvalidInterface(interface_name) =>
      "CAN log record has invalid interface \{interface_name}"
    CanLogMissingFdFlags => "CAN FD record is missing its flags digit"
    CanLogInvalidFdFlags(flags) => "CAN FD record has invalid flags \{flags}"
    CanLogFdFlagsOutOfRange(flags) => "CAN FD flags \{flags} are out of range"
    CanLogTimestampWithoutInterface =>
      "CAN log timestamp requires an interface name"
    CanLogFrameTextError(error) => cli_frame_text_error(error)
  }
}

///|
fn cli_decode_error(error : DecodeError) -> String {
  match error {
    InvalidSignalLayout(start, length) =>
      "invalid signal layout \{start}|\{length}"
    FrameTooShort(required, available) =>
      "signal requires \{required} bits but the frame has \{available}"
  }
}

///|
fn cli_frame_decode_error(error : FrameDecodeError) -> String {
  match error {
    UnknownMessage(id) => "unknown DBC message \{id}"
    InvalidCanFrameIdentifier(id, extended) =>
      "invalid \{if extended { "extended" } else { "standard" }} CAN identifier \{id}"
    UnknownCanMessage(id, extended) =>
      "unknown \{if extended { "extended" } else { "standard" }} CAN message \{cli_can_frame_id_text(id, extended)}"
    PayloadTooShort(required, actual) =>
      "message requires \{required} payload bytes but the frame has \{actual}"
    MissingMultiplexer => "message has multiplexed signals but no multiplexer"
    MultipleMultiplexers => "message has more than one multiplexer"
    SignalDecodeFailed(signal, error) =>
      "signal \{signal} could not be decoded: \{cli_decode_error(error)}"
  }
}

///|
fn cli_raw_value(value : RawValue) -> String {
  match value {
    UnsignedRaw(value) => value.to_string()
    SignedRaw(value) => value.to_string()
    FloatRaw(value) => value.to_string()
  }
}

///|
fn cli_encode_error(error : EncodeError) -> String {
  match error {
    EncodeInvalidLayout(start, length) =>
      "invalid signal layout \{start}|\{length}"
    EncodeFrameTooShort(required, available) =>
      "signal requires \{required} bits but the frame has \{available}"
    ValueKindMismatch(expected, actual) =>
      "expected a \{cli_value_kind(expected)} raw value, received \{cli_raw_value(actual)}"
    SignalValueTypeMismatch(expected, actual) =>
      "expected a \{cli_signal_value_type(expected)} raw value, received \{cli_raw_value(actual)}"
    RawValueOutOfRange(value, length) =>
      "raw value \{cli_raw_value(value)} does not fit in \{length} bits"
    InvalidFactor(factor) => "signal has invalid factor \{factor}"
    NonFinitePhysicalValue(value) => "physical value \{value} is not finite"
    PhysicalValueOutOfRange(value, minimum, maximum) =>
      "physical value \{value} is outside [\{minimum},\{maximum}]"
    PhysicalRawOutOfRange(value, length) =>
      "scaled raw value \{value} does not fit in \{length} bits"
  }
}

///|
fn cli_frame_encode_error(error : FrameEncodeError) -> String {
  match error {
    FrameEncodeUnknownMessage(id) => "unknown DBC message \{id}"
    FrameEncodeInvalidCanIdentifier(id, extended) =>
      "invalid \{if extended { "extended" } else { "standard" }} CAN identifier \{id}"
    FrameEncodeUnknownCanMessage(id, extended) =>
      "unknown \{if extended { "extended" } else { "standard" }} CAN message \{cli_can_frame_id_text(id, extended)}"
    FrameEncodeInvalidPayloadSize(size) =>
      "message has invalid payload size \{size}"
    FrameEncodeUnknownSignal(signal) => "unknown signal \{signal}"
    FrameEncodeDuplicateSignal(signal) =>
      "signal \{signal} is assigned more than once"
    FrameEncodeMissingSignal(signal) => "signal \{signal} is not assigned"
    FrameEncodeInactiveSignal(signal, selector) =>
      "signal \{signal} is inactive for multiplex selector \{selector}"
    FrameEncodeInactiveRangeSignal(signal) =>
      "signal \{signal} is inactive for the selected multiplex range"
    FrameEncodeMissingMultiplexer =>
      "message has multiplexed signals but no multiplexer"
    FrameEncodeMultipleMultiplexers => "message has more than one multiplexer"
    FrameSignalEncodeFailed(signal, error) =>
      "signal \{signal} could not be encoded: \{cli_encode_error(error)}"
    FrameMultiplexerDecodeFailed(signal, error) =>
      "multiplexer \{signal} could not be read after encoding: \{cli_decode_error(error)}"
  }
}

///|
fn cli_write_error(error : DbcWriteError) -> String {
  match error {
    DbcWriteInvalidToken(context, text) =>
      "\{context} '\{text}' is not a valid DBC token"
    DbcWriteInvalidQuotedText(context, _) =>
      "\{context} contains text that cannot be quoted in DBC syntax"
    DbcWriteInvalidPayloadSize(id, size) =>
      "message \{cli_dbc_id_text(id)} has invalid payload size \{size}"
    DbcWriteNonFiniteSignalNumber(id, signal, field) =>
      "signal \{signal} in message \{cli_dbc_id_text(id)} has a non-finite \{field}"
    DbcWriteNegativeMultiplexValue(id, signal, value) =>
      "signal \{signal} in message \{cli_dbc_id_text(id)} has negative multiplex value \{value}"
    DbcWriteInvalidMultiplexRange(id, signal, start, end) =>
      "signal \{signal} in message \{cli_dbc_id_text(id)} has invalid multiplex range \{start}-\{end}"
    DbcWriteEmptyValueTable(id, signal) =>
      "signal \{signal} in message \{cli_dbc_id_text(id)} has an empty value table"
    DbcWriteEmptyGlobalValueTable(name) =>
      "global value table \{name} has no entries"
    DbcWriteDuplicateTransmitter(id, transmitter) =>
      "message \{cli_dbc_id_text(id)} repeats transmitter \{transmitter}"
    DbcWriteInvalidMessageIdentifier(id) =>
      "message \{id} has an invalid DBC identifier"
    DbcWriteInvalidAttribute(message) => "invalid DBC attribute: \{message}"
    DbcWriteInvalidSignalGroup(id, name) =>
      "signal group \{name} in message \{cli_dbc_id_text(id)} cannot be written"
    DbcWriteInvalidEnvironmentVariable(name) =>
      "environment variable \{name} cannot be written"
  }
}

///|
fn cli_trace_error(error : TraceError) -> String {
  match error {
    TraceFrameTextError(error) => cli_frame_text_error(error)
    TraceCanLogTextError(error) => cli_can_log_text_error(error)
    TraceFrameDecodeError(error) => cli_frame_decode_error(error)
  }
}

///|
fn cli_trace_diagnostics(
  path : String,
  diagnostics : Array[TraceDiagnostic],
) -> String {
  let lines : Array[String] = []
  for diagnostic in diagnostics {
    lines.push(
      "\{path}:\{diagnostic.line}: \{cli_trace_error(diagnostic.error)}",
    )
  }
  if lines.is_empty() {
    ""
  } else {
    "\{lines.join("\n")}\n"
  }
}

///|
fn cli_check(path : String, parsed : SourceParseResult) -> CliOutput {
  let issues = parsed.locate_validation_issues()
  if !issues.is_empty() {
    return {
      stdout: "",
      stderr: cli_validation_issues(path, issues),
      exit_code: 1,
    }
  }
  let database = parsed.database
  {
    stdout: [
      "OK \{path}",
      "Messages: \{database.messages.length()}",
      "Signals: \{cli_signal_count(database)}",
      "Nodes: \{database.nodes.length()}",
      "",
    ].join("\n"),
    stderr: "",
    exit_code: 0,
  }
}

///|
fn cli_byte_order(order : ByteOrder) -> String {
  match order {
    Intel => "intel"
    Motorola => "motorola"
  }
}

///|
fn cli_value_kind(kind : ValueKind) -> String {
  match kind {
    Unsigned => "unsigned"
    Signed => "signed"
  }
}

///|
fn cli_signal_value_type(value_type : SignalValueType) -> String {
  match value_type {
    Integer => "integer"
    Float32 => "float32"
    Float64 => "float64"
  }
}

///|
fn cli_signal_storage(signal : Signal) -> String {
  match signal.value_type {
    Integer => cli_value_kind(signal.value_kind)
    Float32 => "float32"
    Float64 => "float64"
  }
}

///|
fn cli_multiplex(role : MultiplexRole) -> String {
  match role {
    Plain => "plain"
    Multiplexer => "multiplexer"
    Multiplexed(value) => "multiplexed(\{value})"
    MultiplexedRanges(selector, ranges) => {
      let parts : Array[String] = []
      for range in ranges {
        parts.push("\{range.start}-\{range.end}")
      }
      "multiplexed(\{selector}: \{parts.join(",")})"
    }
  }
}

///|
fn cli_list(database : Database) -> String {
  let lines = ["Messages: \{database.messages.length()}"]
  for message in database.messages {
    let bytes = if message.payload_size == 1 { "byte" } else { "bytes" }
    lines.push(
      "\{cli_dbc_id_text(message.id)} \{message.name}, \{message.payload_size} \{bytes}, transmitters: \{message.transmitters().join(",")}",
    )
    for signal in message.signals {
      let unit = if signal.unit.is_empty() { "-" } else { signal.unit }
      lines.push(
        "  \{signal.name}: \{signal.start_bit}|\{signal.bit_length}, \{cli_byte_order(signal.byte_order)}, \{cli_signal_storage(signal)}, factor=\{signal.factor}, offset=\{signal.offset}, unit=\{unit}, \{cli_multiplex(signal.multiplex)}",
      )
    }
  }
  "\{lines.join("\n")}\n"
}

///|
/// Run a deterministic CLI operation against already-loaded DBC text. Keeping
/// file access outside this function makes command behavior portable and easy
/// to test independently from the native executable.
pub fn run_cli(mode : CliMode, path : String, source : StringView) -> CliOutput {
  let parsed = parse_with_source_map(source)
  if !parsed.diagnostics.is_empty() {
    return {
      stdout: "",
      stderr: cli_diagnostics(path, parsed.diagnostics),
      exit_code: 1,
    }
  }
  match mode {
    CliCheck => cli_check(path, parsed)
    CliList => {
      let issues = parsed.locate_validation_issues()
      {
        stdout: cli_list(parsed.database),
        stderr: if issues.is_empty() {
          ""
        } else {
          cli_validation_issues(path, issues)
        },
        exit_code: if issues.is_empty() {
          0
        } else {
          1
        },
      }
    }
  }
}

///|
/// Parse and validate a DBC database, decode a SocketCAN or candump trace, and
/// return decoded signal rows as CSV. Valid records are retained when another
/// trace line is malformed; diagnostics and a non-zero exit code report the
/// partial failure.
pub fn run_decode_cli(
  dbc_path : String,
  dbc_source : StringView,
  trace_path : String,
  trace_source : StringView,
) -> CliOutput {
  let parsed = parse_with_source_map(dbc_source)
  if !parsed.diagnostics.is_empty() {
    return {
      stdout: "",
      stderr: cli_diagnostics(dbc_path, parsed.diagnostics),
      exit_code: 1,
    }
  }
  let issues = parsed.locate_validation_issues()
  if !issues.is_empty() {
    return {
      stdout: "",
      stderr: cli_validation_issues(dbc_path, issues),
      exit_code: 1,
    }
  }
  let decoded = parsed.database.decode_trace(trace_source)
  let csv = match decoded.to_csv() {
    Ok(value) => value
    Err(error) =>
      return {
        stdout: "",
        stderr: "\{trace_path}: unable to render CSV: \{cli_can_log_text_error(error)}\n",
        exit_code: 1,
      }
  }
  {
    stdout: "\{csv}\r\n",
    stderr: cli_trace_diagnostics(trace_path, decoded.diagnostics),
    exit_code: if decoded.diagnostics.is_empty() {
      0
    } else {
      1
    },
  }
}

///|
/// Summarize how much of a trace a DBC covers without emitting one diagnostic
/// per unknown frame. Unknown identifiers are informational; malformed records
/// and failed decodes still make the command fail.
pub fn run_summary_cli(
  dbc_path : String,
  dbc_source : StringView,
  trace_path : String,
  trace_source : StringView,
) -> CliOutput {
  let parsed = parse_with_source_map(dbc_source)
  if !parsed.diagnostics.is_empty() {
    return {
      stdout: "",
      stderr: cli_diagnostics(dbc_path, parsed.diagnostics),
      exit_code: 1,
    }
  }
  let issues = parsed.locate_validation_issues()
  if !issues.is_empty() {
    return {
      stdout: "",
      stderr: cli_validation_issues(dbc_path, issues),
      exit_code: 1,
    }
  }
  let summary = parsed.database.decode_trace(trace_source).summary()
  {
    stdout: [
      "Trace: \{trace_path}",
      "Records: \{summary.records}",
      "Decoded frames: \{summary.decoded_frames}",
      "Decoded signals: \{summary.decoded_signals}",
      "Unknown frame IDs: \{summary.unknown_frames}",
      "Malformed records: \{summary.malformed_records}",
      "Short payloads: \{summary.short_payload_frames}",
      "Other decode failures: \{summary.failed_frames}",
      "",
    ].join("\n"),
    stderr: "",
    exit_code: if summary.malformed_records == 0 &&
      summary.short_payload_frames == 0 &&
      summary.failed_frames == 0 {
      0
    } else {
      1
    },
  }
}

///|
/// Parse and validate two DBC sources, then render their compatibility report
/// as Markdown. Breaking changes use exit code 3 so CI can distinguish an
/// incompatible database revision from malformed input.
pub fn run_diff_cli(
  before_path : String,
  before_source : StringView,
  after_path : String,
  after_source : StringView,
) -> CliOutput {
  let before = parse_with_source_map(before_source)
  if !before.diagnostics.is_empty() {
    return {
      stdout: "",
      stderr: cli_diagnostics(before_path, before.diagnostics),
      exit_code: 1,
    }
  }
  let before_issues = before.locate_validation_issues()
  if !before_issues.is_empty() {
    return {
      stdout: "",
      stderr: cli_validation_issues(before_path, before_issues),
      exit_code: 1,
    }
  }
  let after = parse_with_source_map(after_source)
  if !after.diagnostics.is_empty() {
    return {
      stdout: "",
      stderr: cli_diagnostics(after_path, after.diagnostics),
      exit_code: 1,
    }
  }
  let after_issues = after.locate_validation_issues()
  if !after_issues.is_empty() {
    return {
      stdout: "",
      stderr: cli_validation_issues(after_path, after_issues),
      exit_code: 1,
    }
  }
  let report = before.database.compatibility_report(after.database)
  let location_suffixes = before
    .diff_with_locations(after)
    .map(change => {
      let positions : Array[String] = []
      match change.before {
        Some(span) =>
          positions.push("old line \{span.start_line}:\{span.start_column}")
        None => ()
      }
      match change.after {
        Some(span) =>
          positions.push("new line \{span.start_line}:\{span.start_column}")
        None => ()
      }
      if positions.is_empty() {
        ""
      } else {
        " (\{positions.join("; ")})"
      }
    })
  {
    stdout: "\{report.render_markdown(location_suffixes)}\n",
    stderr: if report.breaking_count == 0 {
      ""
    } else {
      "\{after_path}: \{report.breaking_count} breaking change(s) compared with \{before_path}\n"
    },
    exit_code: if report.breaking_count == 0 {
      0
    } else {
      3
    },
  }
}

///|
/// Parse and validate a DBC source, then generate standalone MoonBit constants
/// for build-time integration.
pub fn run_generate_cli(path : String, source : StringView) -> CliOutput {
  let parsed = parse_with_source_map(source)
  if !parsed.diagnostics.is_empty() {
    return {
      stdout: "",
      stderr: cli_diagnostics(path, parsed.diagnostics),
      exit_code: 1,
    }
  }
  let issues = parsed.locate_validation_issues()
  if !issues.is_empty() {
    return {
      stdout: "",
      stderr: cli_validation_issues(path, issues),
      exit_code: 1,
    }
  }
  {
    stdout: "\{parsed.database.generate_moonbit_constants()}\n",
    stderr: "",
    exit_code: 0,
  }
}

///|
/// Generate named signal encode/decode functions from a valid DBC source.
pub fn run_generate_codecs_cli(path : String, source : StringView) -> CliOutput {
  let parsed = parse_with_source_map(source)
  if !parsed.diagnostics.is_empty() {
    return {
      stdout: "",
      stderr: cli_diagnostics(path, parsed.diagnostics),
      exit_code: 1,
    }
  }
  let issues = parsed.locate_validation_issues()
  if !issues.is_empty() {
    return {
      stdout: "",
      stderr: cli_validation_issues(path, issues),
      exit_code: 1,
    }
  }
  match parsed.database.generate_moonbit_signal_codecs() {
    Ok(source) => { stdout: "\{source}\n", stderr: "", exit_code: 0, }
    Err(message) =>
      { stdout: "", stderr: "\{path}: \{message}\n", exit_code: 1, }
  }
}

///|
fn cli_format_line_is_supported(line : StringView) -> Bool {
  line.is_empty() ||
  line.has_prefix("VERSION") ||
  line.has_prefix("BU_:") ||
  line.has_prefix("BO_TX_BU_ ") ||
  line.has_prefix("SIG_VALTYPE_ ") ||
  line.has_prefix("SG_MUL_VAL_ ") ||
  line.has_prefix("BA_DEF_DEF_ ") ||
  line.has_prefix("BA_DEF_ ") ||
  line.has_prefix("BA_ ") ||
  line.has_prefix("BO_ ") ||
  line.has_prefix("SG_ ") ||
  line.has_prefix("CM_ ") ||
  line.has_prefix("VAL_ ") ||
  line.has_prefix("VAL_TABLE_ ") ||
  line.has_prefix("SIG_GROUP_ ") ||
  line.has_prefix("EV_ ") ||
  line.has_prefix("ENVVAR_DATA_ ")
}

///|
fn cli_first_unsupported_format_line(source : StringView) -> Int? {
  let mut line_number = 0
  for raw_line in source.split("\n") {
    line_number += 1
    let line = raw_line.trim(chars=" \t\r")
    if !cli_format_line_is_supported(line) {
      return Some(line_number)
    }
  }
  None
}

///|
/// Parse and validate a DBC source, then emit the deterministic representation
/// used by `Database::to_dbc`. Check mode produces no standard output and uses
/// exit code 4 when the input differs from that representation. Unsupported
/// declarations are rejected so formatting never silently removes source data.
pub fn run_format_cli(
  path : String,
  source : StringView,
  check_only : Bool,
) -> CliOutput {
  match cli_first_unsupported_format_line(source) {
    Some(line) =>
      return {
        stdout: "",
        stderr: "\{path}:\{line}:1 [DBC019] unsupported declaration cannot be formatted safely\n",
        exit_code: 1,
      }
    None => ()
  }
  let parsed = parse_with_source_map(source)
  if !parsed.diagnostics.is_empty() {
    return {
      stdout: "",
      stderr: cli_diagnostics(path, parsed.diagnostics),
      exit_code: 1,
    }
  }
  let issues = parsed.locate_validation_issues()
  if !issues.is_empty() {
    return {
      stdout: "",
      stderr: cli_validation_issues(path, issues),
      exit_code: 1,
    }
  }
  let formatted = match parsed.database.to_dbc() {
    Ok(text) => "\{text}\n"
    Err(error) =>
      return {
        stdout: "",
        stderr: "\{path}: unable to format DBC: \{cli_write_error(error)}\n",
        exit_code: 1,
      }
  }
  if check_only {
    if source == formatted {
      { stdout: "", stderr: "", exit_code: 0, }
    } else {
      {
        stdout: "",
        stderr: "\{path}: formatting changes required\n",
        exit_code: 4,
      }
    }
  } else {
    { stdout: formatted, stderr: "", exit_code: 0, }
  }
}

///|
fn cli_parse_assignment(text : String) -> Result[SignalAssignment, String] {
  let (name_text, value_text) = match text.view().split_once("=") {
    Some(parts) => parts
    None => return Err("invalid assignment '\{text}'; expected NAME=NUMBER")
  }
  let name = name_text.trim()
  let value = value_text.trim()
  if name.is_empty() || value.is_empty() {
    return Err("invalid assignment '\{text}'; expected NAME=NUMBER")
  }
  let physical = match parse_double(value) {
    Some(value) => value
    None => return Err("assignment '\{text}' has an invalid number")
  }
  Ok({ name: name.to_owned(), physical, })
}

///|
/// Parse and validate a DBC source, encode named physical values into a CAN
/// frame, and render canonical SocketCAN text. The CAN identifier uses one to
/// three hexadecimal digits for a standard frame and four to eight for an
/// extended frame.
pub fn run_encode_cli(
  path : String,
  source : StringView,
  identifier : String,
  assignment_texts : Array[String],
) -> CliOutput {
  let parsed = parse_with_source_map(source)
  if !parsed.diagnostics.is_empty() {
    return {
      stdout: "",
      stderr: cli_diagnostics(path, parsed.diagnostics),
      exit_code: 1,
    }
  }
  let issues = parsed.locate_validation_issues()
  if !issues.is_empty() {
    return {
      stdout: "",
      stderr: cli_validation_issues(path, issues),
      exit_code: 1,
    }
  }
  let selector = match parse_can_frame("\{identifier}#") {
    Ok(frame) => frame
    Err(error) =>
      return {
        stdout: "",
        stderr: "moondbc: invalid CAN identifier '\{identifier}': \{cli_frame_text_error(error)}\n",
        exit_code: 1,
      }
  }
  let assignments : Array[SignalAssignment] = []
  for text in assignment_texts {
    match cli_parse_assignment(text) {
      Ok(assignment) => assignments.push(assignment)
      Err(message) =>
        return { stdout: "", stderr: "moondbc: \{message}\n", exit_code: 1, }
    }
  }
  let frame = match
    parsed.database.encode_can_frame(
      selector.id,
      selector.is_extended_frame,
      assignments,
    ) {
    Ok(frame) => frame
    Err(error) =>
      return {
        stdout: "",
        stderr: "moondbc: \{cli_frame_encode_error(error)}\n",
        exit_code: 1,
      }
  }
  match frame.to_text() {
    Ok(text) => { stdout: "\{text}\n", stderr: "", exit_code: 0, }
    Err(error) =>
      {
        stdout: "",
        stderr: "moondbc: unable to render CAN frame: \{cli_frame_text_error(error)}\n",
        exit_code: 1,
      }
  }
}