///|
/// Foreign-key relationships with explicit eager loading — MoonBit has no attribute
/// interception, so a related load is a call, not a silent lazy attribute access
/// (Diesel's preload / GORM's `Preload`). `has_many` (1:N), `belongs_to` (N:1), and
/// `many_to_many` each build the resolving `Select`; `Session::load` / `load_one` /
/// `load_many` run one, and `load_batch` / `load_one_batch` fetch related rows for many
/// sources in a single query (the N+1-avoiding `selectinload`), bucketing them back to
/// their sources in memory. The bound-value match keeps eager loading injection-safe.
/// Execution runs over a small fixed-rows driver standing in for the backend.
///
///     moon run examples/13-relationships
struct Team {
  id : Int32
  name : String
}

///|
struct Hero {
  id : Int32
  name : String
  team_id : Int32
}

///|
struct Tag {
  id : Int32
  label : String
}

///|
let team_model : @moonorm.Model[Team] = @moonorm.Model::new(
  "teams",
  [
    @moonorm.column("id", IntType, primary_key=true),
    @moonorm.column("name", TextType, nullable=false),
  ],
  r => { id: r.int(0), name: r.text(1), },
  t => [("id", @moondb.Int(t.id)), ("name", @moondb.Text(t.name))],
)

///|
let hero_model : @moonorm.Model[Hero] = @moonorm.Model::new(
  "heroes",
  [
    @moonorm.column("id", IntType, primary_key=true),
    @moonorm.column("name", TextType, nullable=false),
    @moonorm.column("team_id", IntType, references=("teams", "id")),
  ],
  r => { id: r.int(0), name: r.text(1), team_id: r.int(2), },
  h => {
    [
      ("id", @moondb.Int(h.id)),
      ("name", @moondb.Text(h.name)),
      ("team_id", @moondb.Int(h.team_id)),
    ]
  },
)

///|
let tag_model : @moonorm.Model[Tag] = @moonorm.Model::new(
  "tags",
  [
    @moonorm.column("id", IntType, primary_key=true),
    @moonorm.column("label", TextType, nullable=false),
  ],
  r => { id: r.int(0), label: r.text(1), },
  t => [("id", @moondb.Int(t.id)), ("label", @moondb.Text(t.label))],
)

///|
fn render(v : @moondb.Value) -> String {
  match v {
    Null => "Null"
    Bool(b) => "Bool(\{b})"
    Int(n) => "Int(\{n})"
    Int64(n) => "Int64(\{n})"
    Double(d) => "Double(\{d})"
    Text(s) => "Text(\"\{s}\")"
    Blob(b) => "Blob(\{b.length()} bytes)"
    Time(s) => "Time(\"\{s}\")"
    Numeric(s) => "Numeric(\{s})"
    Uuid(b) => "Uuid(\{b.length()} bytes)"
    Json(s) => "Json(\{s})"
    Array(items) => "Array(\{items.length()} items)"
  }
}

///|
fn params_str(vals : Array[@moondb.Value]) -> String {
  "[" + vals.map(render).join(", ") + "]"
}

///|
/// A driver that returns a fixed result set for every query — enough to drive the
/// load / bucket logic without a real backend. Seed it with the rows a real WHERE
/// would return.
struct Rows {
  rows : Array[@moondb.Row]
}

///|
impl @moondb.Driver for Rows with fn execute(
  _self : Rows,
  _sql : String,
  _params : Array[@moondb.Value],
) -> @moondb.ExecResult raise @moondb.DbError {
  { rows_affected: 0L, last_insert_id: 0L, }
}

///|
impl @moondb.Driver for Rows with fn query(
  self : Rows,
  _sql : String,
  _params : Array[@moondb.Value],
) -> Array[@moondb.Row] raise @moondb.DbError {
  self.rows
}

///|
impl @moondb.Driver for Rows with fn begin(_self : Rows) -> Unit raise @moondb.DbError {

}

///|
impl @moondb.Driver for Rows with fn commit(_self : Rows) -> Unit raise @moondb.DbError {

}

///|
impl @moondb.Driver for Rows with fn rollback(_self : Rows) -> Unit raise @moondb.DbError {

}

///|
impl @moondb.Driver for Rows with fn close(_self : Rows) -> Unit {

}

///|
fn hero_row(id : Int32, name : String, team_id : Int32) -> @moondb.Row {
  {
    columns: ["id", "name", "team_id"],
    values: [@moondb.Int(id), @moondb.Text(name), @moondb.Int(team_id)],
  }
}

///|
fn team_row(id : Int32, name : String) -> @moondb.Row {
  { columns: ["id", "name"], values: [@moondb.Int(id), @moondb.Text(name)], }
}

///|
fn tag_row(id : Int32, label : String) -> @moondb.Row {
  { columns: ["id", "label"], values: [@moondb.Int(id), @moondb.Text(label)], }
}

///|
#coverage.skip
fn main {
  let team1 : Team = { id: 1, name: "lfanke", }
  let hero1 : Hero = { id: 233, name: "qwq", team_id: 1, }

  // 1:N — a team to its heroes, matched by heroes.team_id.
  let children = @moonorm.has_many(hero_model, "team_id", (t : Team) => {
    @moondb.Int(t.id)
  })
  let (hsql, hparams) = children.query(team1).build()
  println("-- has_many query (team -> heroes)")
  println(hsql)
  println("params: " + params_str(hparams))
  println("source_value(team1): " + render(children.source_value(team1)))

  // N:1 — a hero to its single team, matched by teams.id.
  let parent = @moonorm.belongs_to(team_model, "id", (h : Hero) => {
    @moondb.Int(h.team_id)
  })
  let (psql, pparams) = parent.query(hero1).build()
  println("")
  println("-- belongs_to query (hero -> team)")
  println(psql)
  println("params: " + params_str(pparams))

  // The single batch query behind selectinload: WHERE team_id IN (…distinct keys…).
  let teams = [team1, { id: 2, name: "heke1228", }]
  let (bsql, bparams) = children.batch_query(teams).build()
  println("")
  println("-- batch_query (one round trip for many sources)")
  println(bsql)
  println("params: " + params_str(bparams))

  // N:M — heroes to tags through a junction table.
  let tagged = @moonorm.many_to_many(
    tag_model,
    junction="hero_tags",
    source_fk="hero_id",
    target_fk="tag_id",
    target_key="id",
    source_key=(h : Hero) => @moondb.Int(h.id),
  )
  let (msql, mparams) = tagged.query(hero1).build()
  println("")
  println("-- many_to_many query (hero -> tags via junction)")
  println(msql)
  println("params: " + params_str(mparams))

  // Now execute the loads against fixed result sets.
  try {
    // load: the team's children (the double returns the rows the WHERE would match).
    let s1 = @moonorm.Session::new(Rows::{
      rows: [hero_row(10, "qwq", 1), hero_row(11, "emm", 1)],
    })
    println("")
    println(
      "-- load (team1 children): " +
      s1.load(team1, children).map(fn(h) { h.name }).join(", "),
    )

    // load_batch: one query, children bucketed to their parents in memory.
    let s2 = @moonorm.Session::new(Rows::{
      rows: [
        hero_row(10, "qwq", 1),
        hero_row(11, "emm", 1),
        hero_row(12, "hhh", 2),
      ],
    })
    let buckets = s2.load_batch(teams, children)
    println("-- load_batch:")
    for i, t in teams {
      println(
        "     \{t.name} -> " + buckets[i].map(fn(h) { h.name }).join(", "),
      )
    }

    // load_one: a hero's single team.
    let s3 = @moonorm.Session::new(Rows::{ rows: [team_row(1, "lfanke")], })
    match s3.load_one(hero1, parent) {
      Some(t) => println("-- load_one (hero1's team): \{t.name}")
      None => println("-- load_one: none")
    }

    // load_one_batch: each hero's team, one query, matched in memory.
    let heroes = [hero1, { id: 234, name: "emm", team_id: 2, }]
    let s4 = @moonorm.Session::new(Rows::{
      rows: [team_row(1, "lfanke"), team_row(2, "heke1228")],
    })
    let parents = s4.load_one_batch(heroes, parent)
    println("-- load_one_batch:")
    for i, h in heroes {
      let pn = match parents[i] {
        Some(t) => t.name
        None => "-"
      }
      println("     \{h.name} -> \{pn}")
    }

    // load_many: a hero's tags through the junction.
    let s5 = @moonorm.Session::new(Rows::{
      rows: [tag_row(1, "qwe"), tag_row(2, "hhh")],
    })
    println(
      "-- load_many (hero1's tags): " +
      s5.load_many(hero1, tagged).map(fn(t) { t.label }).join(", "),
    )
  } catch {
    e => println("db error: " + e.to_string())
  }
}