// Schema — introspected table metadata for dynamic query building.
//
// Usage:
//   1. Query information_schema.columns on startup.
//   2. Build SchemaColumn values, pass to Schema::load.
//   3. schema.table("users") returns a SchemaTable.
//   4. Use .col("name"), .select(), .insert(), etc. just like a static proxy.

// ---------------------------------------------------------------------------
// FieldResolver — map string field names to ColumnRefs
// ---------------------------------------------------------------------------

///|
/// Trait for resolving string field names to ColumnRefs.
/// Used by queryx and other dynamic query builders.
pub(open) trait FieldResolver {
  fn resolve_column(Self, field : String) -> @ast.ColumnRef?
  fn table_name(Self) -> String
}

// ---------------------------------------------------------------------------
// SchemaColumn — one column's metadata
// ---------------------------------------------------------------------------

///|
/// One column's metadata, typically from information_schema.columns.
pub(all) struct SchemaColumn {
  table_name : String
  column_name : String
  sql_type : @types.SqlType
  is_primary_key : Bool
} derive(Debug, Eq)

// ---------------------------------------------------------------------------
// Schema — table → SchemaTable index
// ---------------------------------------------------------------------------

///|
/// Introspected schema.  Build with Schema::load.
pub(all) struct Schema {
  tables : Map[String, SchemaTable]
}

///|
/// Load schema from a flat list of column descriptors.
/// Columns must be grouped by table; primary_key is derived from the first
/// column with is_primary_key == true in each table group.
pub fn Schema::load(columns : Array[SchemaColumn]) -> Schema {
  // Group columns by table, preserving input order.
  let groups : Map[String, Array[SchemaColumn]] = Map([])
  for c in columns {
    match groups.get(c.table_name) {
      Some(arr) => arr.push(c)
      None => {
        let arr : Array[SchemaColumn] = [c]
        groups.set(c.table_name, arr)
      }
    }
  }

  // Build a SchemaTable from each group.
  let tables : Map[String, SchemaTable] = Map([])
  for table_name, cols in groups {
    let cols_map : Map[String, SchemaColumn] = Map([])
    let names : Array[String] = []
    let mut pk_col : String? = None
    for c in cols {
      cols_map.set(c.column_name, c)
      names.push(c.column_name)
      if c.is_primary_key && pk_col is None {
        pk_col = Some(c.column_name)
      }
    }
    tables.set(table_name, SchemaTable::{
      table_name,
      columns: cols_map,
      column_names: names,
      primary_key: pk_col,
    })
  }

  Schema::{ tables, }
}

///|
/// Get a dynamic table reference.  Panics if the table is not in the schema.
pub fn Schema::table(self : Schema, name : String) -> SchemaTable {
  match self.tables.get(name) {
    Some(t) => t
    None => abort("table not found in schema: \{name}")
  }
}

// ---------------------------------------------------------------------------
// SchemaTable — a dynamic table that exposes the same builders as a static proxy
// ---------------------------------------------------------------------------

///|
/// A dynamic table reference.  Exposes .select() / .insert() / .update() / .delete().
pub(all) struct SchemaTable {
  table_name : String
  columns : Map[String, SchemaColumn]
  column_names : Array[String]
  primary_key : String?
}

///|
pub impl FieldResolver for SchemaTable with fn resolve_column(
  self : SchemaTable,
  field : String,
) -> @ast.ColumnRef? {
  if self.has_column(field) {
    Some(self.col(field))
  } else {
    None
  }
}

///|
pub impl FieldResolver for SchemaTable with fn table_name(_self : SchemaTable) -> String {
  _self.table_name
}

///|
/// Get a ColumnRef for a column.  Panics if the column is not in the schema.
pub fn SchemaTable::col(self : SchemaTable, name : String) -> @ast.ColumnRef {
  match self.columns.get(name) {
    Some(_) => @ast.ColumnRef::{ name, table: self.table_name }
    None => abort("column not found in schema: \{self.table_name}.\{name}")
  }
}

///|
/// Check whether a column exists.
pub fn SchemaTable::has_column(self : SchemaTable, name : String) -> Bool {
  self.columns.get(name) is Some(_)
}

///|
/// Start building a SELECT query.  Omit columns for SELECT *.
pub fn SchemaTable::select(
  self : SchemaTable,
  columns? : Array[&@builder.AnyColumn],
) -> @builder.SelectBuilder {
  let cols = match columns {
    Some(arr) => arr.map(c => c.to_ref())
    None => []
  }
  @builder.SelectBuilder::new(@builder.Table::new(self.table_name), cols)
}

///|
/// Start building an INSERT query.
pub fn SchemaTable::insert(
  self : SchemaTable,
  columns : Array[&@builder.AnyColumn],
) -> @builder.InsertBuilder {
  @builder.InsertBuilder::new(@builder.Table::new(self.table_name), columns)
}

///|
/// Start building an UPDATE query.  Requires .where_() before .to_sql().
pub fn SchemaTable::update(self : SchemaTable) -> @builder.UpdateBuilder {
  @builder.UpdateBuilder::new(@builder.Table::new(self.table_name))
}

///|
/// Start building a DELETE query.  Requires .where_() before .to_sql().
pub fn SchemaTable::delete(self : SchemaTable) -> @builder.DeleteBuilder {
  @builder.DeleteBuilder::new(@builder.Table::new(self.table_name))
}