///|
/// Version stamped into every snapshot file.
///
/// Bumped when the JSON shape changes, so an old snapshot is rejected with a
/// clear message instead of being half-understood by a newer generator.
pub const SNAPSHOT_VERSION = "1"

///|
/// The database schema as aya understands it, at one point in time.
///
/// This is the artefact that gets committed next to the migration it belongs
/// to: the next `generate` diffs against it rather than against the database,
/// so generating a migration needs no connection.
pub struct Snapshot {
  version : String
  dialect : @aya.Dialect
  /// Sorted by name, so that reordering entity files cannot produce a diff.
  tables : Array[TableDef]
} derive(Debug, Eq)

///|
pub struct TableDef {
  name : String
  /// Declaration order, which is the order `CREATE TABLE` will use.
  columns : Array[ColumnDef]
  /// Sorted by name.
  indexes : Array[IndexDef]
} derive(Debug, Eq)

///|
/// One column, with its type already resolved for the snapshot's dialect.
///
/// The snapshot stores SQL, not MoonBit: two dialects disagree about what
/// `Int64` is called, and the diff has to compare what the database will
/// actually hold.
pub struct ColumnDef {
  name : String
  ty : String
  not_null : Bool
  primary_key : Bool
  autoincrement : Bool
  unique : Bool
  /// Raw SQL expression, emitted verbatim after `DEFAULT`.
  default_sql : String?
  references : ForeignKey?
} derive(Debug, Eq)

///|
pub struct ForeignKey {
  table : String
  column : String
  on_delete : String?
  on_update : String?
} derive(Debug, Eq)

///|
pub struct IndexDef {
  name : String
  columns : Array[String]
  unique : Bool
} derive(Debug, Eq)

///|
pub(all) suberror DdlError {
  /// Two entities claim the same table name.
  DuplicateTable(table~ : String)
  /// A column's MoonBit type has no built-in SQL mapping and the field did not
  /// say what it stores.
  UnknownColumnType(table~ : String, column~ : String, ty~ : String)
  /// The requested dialect cannot be used for what was asked.
  UnsupportedDialect(dialect~ : String, reason~ : String)
  /// The change is valid SQL nowhere, or valid only against an empty table.
  UnsafeChange(table~ : String, reason~ : String)
  /// A snapshot file could not be read.
  BadSnapshot(reason~ : String)
} derive(Debug)

///|
/// An empty schema: what the first migration is diffed against.
pub fn Snapshot::empty(dialect : @aya.Dialect) -> Snapshot {
  { version: SNAPSHOT_VERSION, dialect, tables: [] }
}

///|
pub fn Snapshot::table(self : Snapshot, name : String) -> TableDef? {
  match self.tables.search_by(t => t.name == name) {
    Some(i) => Some(self.tables[i])
    None => None
  }
}

///|
pub fn TableDef::column(self : TableDef, name : String) -> ColumnDef? {
  match self.columns.search_by(c => c.name == name) {
    Some(i) => Some(self.columns[i])
    None => None
  }
}

///|
pub fn TableDef::index(self : TableDef, name : String) -> IndexDef? {
  match self.indexes.search_by(i => i.name == name) {
    Some(i) => Some(self.indexes[i])
    None => None
  }
}

///|
/// The name a dialect goes by in config files and snapshots.
pub fn dialect_name(d : @aya.Dialect) -> String {
  match d {
    Sqlite => "sqlite"
    Postgres => "postgres"
  }
}

///|
/// Parse a dialect name, rejecting anything aya does not model.
pub fn dialect_of_name(name : String) -> @aya.Dialect raise DdlError {
  match name {
    "sqlite" => Sqlite
    "postgres" | "postgresql" | "pg" => Postgres
    _ =>
      raise UnsupportedDialect(
        dialect=name,
        reason="known dialects are `sqlite` and `postgres`",
      )
  }
}

///|
/// A sentence, not a constructor dump.
///
/// These errors are read by whoever ran `aya-kit`, usually right after they
/// changed an entity, so the message says what to change back.
pub impl Show for DdlError with fn output(self, logger) {
  logger.write_string(
    match self {
      DuplicateTable(table~) => "two entities both map to table `\{table}`"
      UnknownColumnType(table~, column~, ty~) =>
        "\{table}.\{column}: no built-in SQL type for `\{ty}`; add `#aya.column(type=\"...\")` to say what it stores"
      UnsupportedDialect(dialect~, reason~) => "dialect `\{dialect}`: \{reason}"
      UnsafeChange(table~, reason~) => "table `\{table}`: \{reason}"
      BadSnapshot(reason~) => "snapshot: \{reason}"
    },
  )
}