// Copyright (c) 2026 moonbit-multiindex contributors
// SPDX-License-Identifier: Apache-2.0

// moonbit-multiindex: a record store with N secondary indexes over stable
// generational IDs.
//
// A `Table[K, V]` stores records of type `V` and maintains any number of
// named secondary indexes over them. Each index projects a record to a key of
// type `K`; an index may be **unique** (one record per key, like one side of a
// `BiMap`), **non-unique** (many records per key), or **sorted** (ordered by
// key, supporting range queries).
//
// A `BiMap` is the special case of exactly two unique indexes that are mutual
// inverses; this library generalizes that to N indexes, each maintained
// consistently across insert / update / remove.
//
// # Quick Start
//
// (see README.md — the API is stabilized as of v0.2.0)

///|
/// Library version string.
pub const VERSION : String = "0.3.0"

///|
/// A stable handle to a stored record: a slot index plus a generation counter.
///
/// The generation is bumped every time a slot is recycled, so a handle that
/// outlives its record can never silently resolve to a *different* record
/// (the ABA problem). Operations on a stale handle return `None` / `false`
/// rather than touching the new occupant of the slot.
#derive(Eq, Hash, Debug)
pub struct RowId {
  slot : Int32
  gen : Int32
}

///|
/// Display form: `RowId(slot, gen)`.
pub impl Show for RowId with fn output(self : RowId, logger : &Logger) {
  logger.write_string("RowId(")
  logger.write_string(self.slot.to_string())
  logger.write_string(", ")
  logger.write_string(self.gen.to_string())
  logger.write_string(")")
}

///|
/// A uniqueness violation reported *before* any mutation happens.
///
/// Deliberately has **no** "no conflict" variant — the absence of a conflict is
/// spelled `None` at the call site. (A redundant nullary variant would force
/// every caller to match it and collides with `Option::None` in pattern
/// position; see docs/SPEC.md §Design lessons.)
///
/// - `UniqueViolation(index, key, holder)` — the unique index named `index`
///   already maps `key` to the live record `holder`.
/// - `DuplicateInBatch(index, key, at)` — one `insert_many` batch contains two
///   records that project to the same `key` on the unique index `index`; `at` is
///   the position of the **earlier** member in that batch. No record is live at
///   `key`: the whole batch is rejected atomically (docs/SPEC.md §11), so a handle
///   would have been a lie — the batch position is the honest answer.
#derive(Debug, Eq)
pub enum Conflict[K] {
  UniqueViolation(String, K, RowId)
  DuplicateInBatch(String, K, Int32)
}

///|
/// Display form: `UniqueViolation(index, key, holder)`.
pub impl[K : Show] Show for Conflict[K] with fn output(
  self : Conflict[K],
  logger : &Logger,
) {
  match self {
    UniqueViolation(index, key, holder) => {
      logger.write_string("UniqueViolation(")
      logger.write_string(index)
      logger.write_string(", ")
      Show::output(key, logger)
      logger.write_string(", ")
      Show::output(holder, logger)
      logger.write_string(")")
    }
    DuplicateInBatch(index, key, at) => {
      logger.write_string("DuplicateInBatch(")
      logger.write_string(index)
      logger.write_string(", ")
      Show::output(key, logger)
      logger.write_string(", ")
      logger.write_string(at.to_string())
      logger.write_string(")")
    }
  }
}

///|
/// A write that was refused because a unique index already holds the key —
/// carrying the record back to the caller.
///
/// MoonBit passes records by value, so an `insert(v)` that reported only
/// `Conflict[K]` would consume `v` for good. Returning the *record itself*
/// inside the error keeps the caller's data intact; the idiomatic retry is:
///
/// ```moonbit nocheck
/// match table.insert(record) {
///   Ok(id) => id
///   Err(rejected) => retry_with(rejected.record)
/// }
/// ```
#derive(Debug)
pub struct Rejected[K, V] {
  conflict : Conflict[K]
  record : V
}

///|
/// Display form: `Rejected(conflict=..., record=...)`.
pub impl[K : Show, V : Show] Show for Rejected[K, V] with fn output(
  self : Rejected[K, V],
  logger : &Logger,
) {
  logger.write_string("Rejected(conflict=")
  Show::output(self.conflict, logger)
  logger.write_string(", record=")
  Show::output(self.record, logger)
  logger.write_string(")")
}

///|
/// Why an index registration was refused (docs/SPEC.md §2.4).
///
/// Registration never half-succeeds: on `Err` the registry and the records are
/// exactly what they were.
///
/// - `DuplicateName(name)` — an index of that name is already registered; the
///   existing one is **not** overwritten.
/// - `UniqueViolation(name, key)` — backfilling the unique index `name` found two
///   live records projecting to the same key. The key travels as itself, so
///   registering a unique index adds **no** `Show` bound on `K` (it mirrors
///   `Conflict[K]`).
#derive(Debug, Eq)
pub enum RegisterError[K] {
  DuplicateName(String)
  UniqueViolation(String, K)
}

///|
/// Why an update was refused (docs/SPEC.md §2.5).
///
/// - `StaleHandle(id, pending)` — `id` no longer names a live record. `update`
///   gives the record it was handed back (`Some(value)`); `modify` has none to
///   give (`None`: with no record to transform, no new record exists).
/// - `Conflict(conflict, value)` — a unique index already holds one of the
///   record's keys; the record always comes back.
///
/// Same principle as `Rejected`: a refused write never swallows the caller's data.
#derive(Debug, Eq)
pub enum UpdateRejected[K, V] {
  StaleHandle(RowId, V?)
  Conflict(Conflict[K], V)
}

///|
/// How the buckets of one index are distributed (docs/SPEC.md §3.4).
///
/// A degenerating projection shows up here: `longest_bucket` climbing while
/// `average_bucket` stays near 1 means many records pile into one bucket. An index
/// with no entries reports zero for everything (including the average).
#derive(Debug, Eq)
pub struct IndexStats {
  buckets : Int32
  entries : Int32
  longest_bucket : Int32
  average_bucket : Double
}

///|
/// Which of the four quadrants a registered index lives in — the public shadow of
/// the private `Index[K, V]` enum (src/index.mbt).
///
/// `Table::index_entries` hands these out next to the index names so a caller can
/// tell a conflict-checked index (`HashUnique` / `SortedUnique`) from a fan-out
/// one (`HashMulti` / `SortedMulti`) before reasoning about what a write may hit.
///
/// Deliberately **not** folded into `IndexStats`: that struct's fields are public,
/// so adding a name or a kind to it would break every external construction site
/// (a MAJOR break — Issue #4). A separate enum keeps `IndexStats` at MINOR.
#derive(Debug)
pub(all) enum IndexKind {
  HashUnique
  SortedUnique
  HashMulti
  SortedMulti
}

///|
/// Display form: the bare variant name (`HashUnique`, `SortedMulti`, ...).
pub impl Show for IndexKind with fn output(self : IndexKind, logger : &Logger) {
  match self {
    IndexKind::HashUnique => logger.write_string("HashUnique")
    IndexKind::SortedUnique => logger.write_string("SortedUnique")
    IndexKind::HashMulti => logger.write_string("HashMulti")
    IndexKind::SortedMulti => logger.write_string("SortedMulti")
  }
}