///|
/// 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`.
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`.
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.
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}

///|
fn json_cell_expectation(expected : DataType) -> String {
  match expected {
    Int => "an Int"
    Float => "a number"
    dtype => "a \{dtype}"
  }
}

///|
/// Human-readable message describing the error.
pub fn DataError::message(self : DataError) -> String {
  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.write_string("ColumnNotFound(\"\{@text.escape_debug(name)}\")")
    DuplicateColumn(name) =>
      logger.write_string("DuplicateColumn(\"\{@text.escape_debug(name)}\")")
    TypeMismatch(detail) =>
      match detail {
        Message(message) =>
          logger.write_string(
            "TypeMismatch(Message(\"\{@text.escape_debug(message)}\"))",
          )
        Expected(expected, got, column) =>
          logger.write_string(
            "TypeMismatch(Expected(\{expected}, \{got}, \"\{@text.escape_debug(column)}\"))",
          )
        Operation(operation, left, right) =>
          logger.write_string(
            "TypeMismatch(Operation(\"\{@text.escape_debug(operation)}\", \{left}, \{right}))",
          )
      }
    LengthMismatch => logger.write_string("LengthMismatch")
    IndexOutOfBounds(i) => logger.write_string("IndexOutOfBounds(\{i})")
    ParseError(detail) =>
      match detail {
        Message(message) =>
          logger.write_string(
            "ParseError(Message(\"\{@text.escape_debug(message)}\"))",
          )
        Cell(location, column, position, expected, value) => {
          let location_name = match location {
            Row => "Row"
            Record => "Record"
            Line => "Line"
          }
          logger.write_string(
            "ParseError(Cell(\{location_name}, \"\{@text.escape_debug(column)}\", \{position}, \{expected}, \"\{@text.escape_debug(value)}\"))",
          )
        }
      }
    InvalidOperation(detail) =>
      logger.write_string("InvalidOperation(\"\{@text.escape_debug(detail)}\")")
    IoError(detail) =>
      logger.write_string("IoError(\"\{@text.escape_debug(detail)}\")")
    Unsupported(detail) =>
      logger.write_string("Unsupported(\"\{@text.escape_debug(detail)}\")")
    NullInNonNullable(name) =>
      logger.write_string("NullInNonNullable(\"\{@text.escape_debug(name)}\")")
  }
}