///|
/// Identifies how a typed cell's 1-based source position is reported.
/// `Row` is used by CSV, `Record` by JSON arrays, and `Line` by NDJSON.
pub(all) enum CellParseLocation {
  Row
  Record
  Line
} derive(Eq, Debug)

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

///|
pub extend CellParseLocation with Debug::{to_repr}

///|
/// Structured details carried by `DataError::ParseError`. `#non_exhaustive`:
/// a new diagnostic shape here is not a breaking change — a caller matching
/// these details carries a `..` arm for the shapes it does not know (the
/// contract the `DataError` variants naming them inherit).
#non_exhaustive
pub(all) enum ParseErrorDetail {
  /// A parser or shape diagnostic that does not have a shared typed shape.
  Message(String)
  /// A non-null cell did not fit the dtype locked in during inference. Fields
  /// are `(location, column, position, expected, value)`; `position` is
  /// 1-based and its meaning is carried by `location`.
  Cell(CellParseLocation, String, Int, DataType, String)
} derive(Eq, Debug)

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

///|
pub extend ParseErrorDetail with Debug::{to_repr}

///|
/// Structured details carried by `DataError::TypeMismatch`. `#non_exhaustive`:
/// a new diagnostic shape here is not a breaking change — a caller matching
/// these details carries a `..` arm for the shapes it does not know (the
/// contract the `DataError` variants naming them inherit).
#non_exhaustive
pub(all) enum TypeMismatchDetail {
  /// A dtype diagnostic that does not have a shared typed shape.
  Message(String)
  /// A value had the wrong dtype. Fields are `(expected, got, column)`, where
  /// `column` is empty when the mismatch is not tied to a named column.
  Expected(DataType, DataType, String)
  /// A binary operation rejected its operands. Fields are
  /// `(operation, left, right)`.
  Operation(String, DataType, DataType)
} derive(Eq, Debug)

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

///|
pub extend TypeMismatchDetail with Debug::{to_repr}

///|
/// Unified error type for all MoonFrame operations.
///
/// The *diagnostic shapes* it carries — `ParseErrorDetail`,
/// `TypeMismatchDetail` — are `#non_exhaustive`: a new shape is not a
/// breaking change, and a caller matching those details carries a `..` arm
/// for the shapes it does not know. `DataError` itself stays exhaustive:
/// `#non_exhaustive` is not supported on a `suberror` (MoonBit v0.10.14),
/// so a new error *kind* remains a deliberate, breaking change under the
/// enum-surface lock.
pub(all) suberror DataError {
  ColumnNotFound(String)
  DuplicateColumn(String)
  TypeMismatch(TypeMismatchDetail)
  LengthMismatch
  IndexOutOfBounds(Int)
  ParseError(ParseErrorDetail)
  InvalidOperation(String)
  IoError(String)
  Unsupported(String)
  NullInNonNullable(String)
} derive(Eq, Debug)

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

///|
pub extend DataError with Show::{to_string, output}

///|
pub extend DataError with Debug::{to_repr}

///|
/// Human-readable message describing the error.
pub fn DataError::message(self : DataError) -> String {
  // The wording the JSON `Cell` diagnostics render their `expected` dtype
  // with — the message half of the JSON readers' per-dtype number acceptance
  // (`json_value_as_int` / `json_value_as_float`): an `Int`-locked column
  // still rejects fractional numbers, so its diagnostic names `an Int`, while
  // a `Float`-locked column accepts any JSON number shape, so a rejection can
  // only be of a non-number and says `a number`. The policy and this wording
  // change together — nothing mechanical ties them.
  fn json_cell_expectation(expected : DataType) -> String {
    match expected {
      Int => "an Int"
      Float => "a number"
      dtype => "a \{dtype}"
    }
  }

  match self {
    ColumnNotFound(name) => "Column not found: \{name}"
    DuplicateColumn(name) => "Duplicate column: \{name}"
    TypeMismatch(detail) =>
      match detail {
        Message(message) => "Type mismatch: \{message}"
        Expected(expected, got, column) =>
          if column == "" {
            "Type mismatch: expected \{expected}, got \{got}"
          } else {
            "Type mismatch: column \"\{column}\" expected \{expected}, got \{got}"
          }
        Operation(operation, left, right) =>
          "Type mismatch: cannot \{operation} \{left} and \{right}"
      }
    LengthMismatch => "Length mismatch between columns"
    IndexOutOfBounds(i) => "Index out of bounds: \{i}"
    ParseError(detail) =>
      match detail {
        Message(message) => "Parse error: \{message}"
        Cell(location, column, position, expected, value) =>
          match location {
            Row =>
              "Parse error: column \"\{column}\" row \{position}: cannot parse \"\{value}\" as \{expected}"
            Record => {
              let expectation = json_cell_expectation(expected)
              "Parse error: column \"\{column}\" record \{position}: JSON value is not \{expectation} (\"\{value}\")"
            }
            Line => {
              let expectation = json_cell_expectation(expected)
              "Parse error: column \"\{column}\" line \{position}: JSON value is not \{expectation} (\"\{value}\")"
            }
          }
      }
    InvalidOperation(detail) => "Invalid operation: \{detail}"
    IoError(detail) => "I/O error: \{detail}"
    Unsupported(detail) => "Unsupported: \{detail}"
    NullInNonNullable(name) => "Null in non-nullable column: \{name}"
  }
}

///|
/// `Show` renders the variant form (`ColumnNotFound("age")`), useful for
/// assertions and snapshots. The user-facing description is `message()`.
pub impl Show for DataError with fn output(self, logger) {
  match self {
    ColumnNotFound(name) =>
      logger <+ "ColumnNotFound(\"\{@text.escape_debug(name)}\")"
    DuplicateColumn(name) =>
      logger <+ "DuplicateColumn(\"\{@text.escape_debug(name)}\")"
    TypeMismatch(detail) =>
      match detail {
        Message(message) =>
          logger <+ "TypeMismatch(Message(\"\{@text.escape_debug(message)}\"))"
        Expected(expected, got, column) =>
          logger <+
            "TypeMismatch(Expected(\{expected}, \{got}, \"\{@text.escape_debug(column)}\"))"
        Operation(operation, left, right) =>
          logger <+
            "TypeMismatch(Operation(\"\{@text.escape_debug(operation)}\", \{left}, \{right}))"
      }
    LengthMismatch => logger.write_string("LengthMismatch")
    IndexOutOfBounds(i) => logger <+ "IndexOutOfBounds(\{i})"
    ParseError(detail) =>
      match detail {
        Message(message) =>
          logger <+ "ParseError(Message(\"\{@text.escape_debug(message)}\"))"
        Cell(location, column, position, expected, value) => {
          let location_name = match location {
            Row => "Row"
            Record => "Record"
            Line => "Line"
          }
          logger <+
            "ParseError(Cell(\{location_name}, \"\{@text.escape_debug(column)}\", \{position}, \{expected}, \"\{@text.escape_debug(value)}\"))"
        }
      }
    InvalidOperation(detail) =>
      logger <+ "InvalidOperation(\"\{@text.escape_debug(detail)}\")"
    IoError(detail) => logger <+ "IoError(\"\{@text.escape_debug(detail)}\")"
    Unsupported(detail) =>
      logger <+ "Unsupported(\"\{@text.escape_debug(detail)}\")"
    NullInNonNullable(name) =>
      logger <+ "NullInNonNullable(\"\{@text.escape_debug(name)}\")"
  }
}