// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// One row of a result set: the column names in projection order alongside the
/// decoded [`Value`] for each. `columns` and `values` are the same length and are
/// index-aligned — `columns[i]` names `values[i]`.
///
/// This is the shape drivers hand back from [`Driver::query`] and the shape a
/// query layer decodes. Read cells either positionally ([`get`]/[`int`]/…) or by
/// column name ([`by_name`]/[`int_by`]/…). The typed accessors *raise* on a type
/// or lookup mismatch rather than returning a sentinel, so a decode bug surfaces
/// as a `DbError` at the call site instead of silently reading a zero value.
pub(all) struct Row {
  columns : Array[String]
  values : Array[Value]
}

///|
/// The number of columns in the row.
pub fn Row::width(self : Row) -> Int {
  self.values.length()
}

///|
/// The raw [`Value`] at column `idx` (0-based, projection order). Raises
/// `QueryError` if `idx` is out of range.
pub fn Row::get(self : Row, idx : Int) -> Value raise DbError {
  if idx < 0 || idx >= self.values.length() {
    raise QueryError(
      "column index " +
      idx.to_string() +
      " out of range (row has " +
      self.values.length().to_string() +
      " columns)",
    )
  }
  self.values[idx]
}

///|
/// The [`Value`] of the column named `name`, or `None` if the row has no such
/// column. Names match exactly as the driver reported them (case-sensitive).
pub fn Row::by_name(self : Row, name : String) -> Value? {
  for i in 0.. Int raise DbError {
  for i in 0.. Bool raise DbError {
  self.get(idx).is_null()
}

///|
/// Whether the column named `name` is SQL `NULL`. Raises `QueryError` if the
/// column is absent.
pub fn Row::is_null_by(self : Row, name : String) -> Bool raise DbError {
  self.get(self.index_of(name)).is_null()
}

// --- positional typed accessors ------------------------------------------------
//
// Each reads column `idx` as a specific MoonBit type, raising `TypeError` when the
// stored value cannot be read that way (including `Null` — use `is_null` first for
// nullable columns) and `QueryError` when `idx` is out of range. Widening between
// integer types and from integer to double is allowed and lossless; `int` narrows
// an `Int64` and is documented as such.

///|
/// Read column `idx` as a `Bool`. Raises `TypeError` unless the cell is `Bool`.
pub fn Row::bool(self : Row, idx : Int) -> Bool raise DbError {
  match self.get(idx) {
    Bool(v) => v
    other =>
      raise TypeError(
        "column " + idx.to_string() + " is " + other.kind() + ", not Bool",
      )
  }
}

///|
/// Read column `idx` as an `Int`, narrowing an `Int64` to its low 32 bits. Raises
/// `TypeError` unless the cell is `Int` or `Int64`.
pub fn Row::int(self : Row, idx : Int) -> Int raise DbError {
  match self.get(idx) {
    Int(v) => v
    Int64(v) => v.to_int()
    other =>
      raise TypeError(
        "column " + idx.to_string() + " is " + other.kind() + ", not Int",
      )
  }
}

///|
/// Read column `idx` as an `Int64`, widening an `Int`. Raises `TypeError` unless
/// the cell is `Int` or `Int64`.
pub fn Row::int64(self : Row, idx : Int) -> Int64 raise DbError {
  match self.get(idx) {
    Int64(v) => v
    Int(v) => v.to_int64()
    other =>
      raise TypeError(
        "column " + idx.to_string() + " is " + other.kind() + ", not Int64",
      )
  }
}

///|
/// Read column `idx` as a `Double`, widening an integer. Raises `TypeError` unless
/// the cell is `Double`, `Int`, or `Int64`.
pub fn Row::double(self : Row, idx : Int) -> Double raise DbError {
  match self.get(idx) {
    Double(v) => v
    Int(v) => v.to_double()
    Int64(v) => v.to_double()
    other =>
      raise TypeError(
        "column " + idx.to_string() + " is " + other.kind() + ", not Double",
      )
  }
}

///|
/// Read column `idx` as text. Raises `TypeError` unless the cell is `Text`.
pub fn Row::text(self : Row, idx : Int) -> String raise DbError {
  match self.get(idx) {
    Text(v) => v
    other =>
      raise TypeError(
        "column " + idx.to_string() + " is " + other.kind() + ", not Text",
      )
  }
}

///|
/// Read column `idx` as an opaque blob. Raises `TypeError` unless the cell is
/// `Blob`.
pub fn Row::blob(self : Row, idx : Int) -> Bytes raise DbError {
  match self.get(idx) {
    Blob(v) => v
    other =>
      raise TypeError(
        "column " + idx.to_string() + " is " + other.kind() + ", not Blob",
      )
  }
}

// --- by-name typed accessors ---------------------------------------------------
//
// Convenience wrappers that resolve a column name through `index_of` (raising
// `QueryError` if absent) and then defer to the positional accessor.

///|
/// Read the column named `name` as a `Bool`.
pub fn Row::bool_by(self : Row, name : String) -> Bool raise DbError {
  self.bool(self.index_of(name))
}

///|
/// Read the column named `name` as an `Int`.
pub fn Row::int_by(self : Row, name : String) -> Int raise DbError {
  self.int(self.index_of(name))
}

///|
/// Read the column named `name` as an `Int64`.
pub fn Row::int64_by(self : Row, name : String) -> Int64 raise DbError {
  self.int64(self.index_of(name))
}

///|
/// Read the column named `name` as a `Double`.
pub fn Row::double_by(self : Row, name : String) -> Double raise DbError {
  self.double(self.index_of(name))
}

///|
/// Read the column named `name` as text.
pub fn Row::text_by(self : Row, name : String) -> String raise DbError {
  self.text(self.index_of(name))
}

///|
/// Read the column named `name` as an opaque blob.
pub fn Row::blob_by(self : Row, name : String) -> Bytes raise DbError {
  self.blob(self.index_of(name))
}

///|
/// The outcome of a non-query statement (INSERT/UPDATE/DELETE/DDL): how many rows
/// it changed and the id of the last inserted row.
///
/// `rows_affected` is 64-bit to hold bulk DML counts; `last_insert_id` is the
/// backend's auto-increment / rowid value and is backend-defined (`0` when the
/// statement produced none). This mirrors Go's `sql.Result` (`RowsAffected` /
/// `LastInsertId`) and DB-API's `cursor.rowcount` / `lastrowid`.
pub(all) struct ExecResult {
  rows_affected : Int64
  last_insert_id : Int64
} derive(Eq)