///|
/// Package alias the generated code refers to.
///
/// Generated files are ordinary MoonBit source, so the consuming package must
/// import `Allianaab2m/aya`, whose default alias this is.
const CORE = "@aya"

///|
/// Header written at the top of every generated file.
const HEADER = "// Generated by aya-kit. DO NOT EDIT.\n// Edit the annotated entity struct and re-run the generator instead.\n"

///|
/// Emit a complete generated file for a set of entities.
pub fn emit_file(specs : Array[EntitySpec]) -> String {
  let buf = StringBuilder()
  buf.write_string(HEADER)
  for spec in specs {
    buf.write_string("\n")
    buf.write_string(emit_entity(spec))
  }
  buf.to_string()
}

///|
/// Emit the four declarations that make one entity usable as a table.
pub fn emit_entity(spec : EntitySpec) -> String {
  let buf = StringBuilder()
  buf.write_string(emit_cols_struct(spec))
  buf.write_string("\n")
  buf.write_string(emit_cols_fn(spec))
  buf.write_string("\n")
  buf.write_string(emit_selection_fn(spec))
  buf.write_string("\n")
  buf.write_string(emit_binding_fn(spec))
  buf.write_string("\n")
  buf.write_string(emit_table_fn(spec))
  buf.write_string("\n")
  buf.write_string(emit_table_of_fn(spec))
  buf.write_string("\n")
  buf.write_string(emit_primary_key_fn(spec))
  buf.to_string()
}

///|
/// `pub struct UserCols { id : @aya.Column[Int], ... }`
fn emit_cols_struct(spec : EntitySpec) -> String {
  let buf = StringBuilder()
  buf.write_string("///|\n")
  buf.write_string("/// Column handles for `\{spec.type_name}`.\n")
  buf.write_string("pub struct \{spec.cols_name} {\n")
  for f in spec.fields {
    buf.write_string("  \{f.name} : \{CORE}.Column[\{f.ty}]\n")
  }
  buf.write_string("}\n")
  buf.to_string()
}

///|
/// `pub fn User::cols() -> UserCols`
fn emit_cols_fn(spec : EntitySpec) -> String {
  let buf = StringBuilder()
  buf.write_string("///|\n")
  buf.write_string("pub fn \{spec.type_name}::cols() -> \{spec.cols_name} {\n")
  buf.write_string("  {\n")
  for f in spec.fields {
    buf.write_string(
      "    \{f.name}: \{CORE}.Column::new(tbl=\"\{spec.tbl}\", name=\"\{f.column}\"),\n",
    )
  }
  buf.write_string("  }\n}\n")
  buf.to_string()
}

///|
/// `pub fn User::all(cols) -> @aya.Selection[User]`
///
/// Projection and decoder are emitted from one loop over the same field list,
/// so the positional decode cannot drift out of step with the column order.
fn emit_selection_fn(spec : EntitySpec) -> String {
  let buf = StringBuilder()
  buf.write_string("///|\n")
  buf.write_string(
    "/// Select every column of `\{spec.type_name}`, in declaration order.\n",
  )
  buf.write_string(
    "pub fn \{spec.type_name}::all(cols : \{spec.cols_name}) -> \{CORE}.Selection[\{spec.type_name}] {\n",
  )
  buf.write_string("  \{CORE}.Selection::new(\n    [\n")
  for f in spec.fields {
    buf.write_string("      cols.\{f.name}.raw(),\n")
  }
  buf.write_string("    ],\n    row => \{spec.type_name}::{\n")
  for i, f in spec.fields {
    buf.write_string(
      "      \{f.name}: \{CORE}.SqlDecode::decode(row[\{i}], \"\{f.column}\"),\n",
    )
  }
  buf.write_string("    },\n  )\n}\n")
  buf.to_string()
}

///|
/// `pub fn User::binding() -> @aya.Binding[User]`
///
/// The write-side mirror of `all`: same fields, same order, so an INSERT and a
/// SELECT of the same entity agree on what each position means.
fn emit_binding_fn(spec : EntitySpec) -> String {
  let buf = StringBuilder()
  buf.write_string("///|\n")
  buf.write_string(
    "/// Write every column of `\{spec.type_name}`, in declaration order.\n",
  )
  buf.write_string(
    "pub fn \{spec.type_name}::binding() -> \{CORE}.Binding[\{spec.type_name}] {\n",
  )
  buf.write_string("  \{CORE}.Binding::new(\n    [")
  buf.write_string(spec.fields.map(f => "\"\{f.column}\"").join(", "))
  buf.write_string("],\n    self => [\n")
  for f in spec.fields {
    buf.write_string("      \{CORE}.SqlEncode::to_sql_value(self.\{f.name}),\n")
  }
  buf.write_string("    ],\n  )\n}\n")
  buf.to_string()
}

///|
/// `pub fn User::table() -> @aya.Table[UserCols, User]`
fn emit_table_fn(spec : EntitySpec) -> String {
  let buf = StringBuilder()
  buf.write_string("///|\n")
  buf.write_string(
    "pub fn \{spec.type_name}::table() -> \{CORE}.Table[\{spec.cols_name}, \{spec.type_name}] {\n",
  )
  buf.write_string("  let cols = \{spec.type_name}::cols()\n")
  buf.write_string(
    "  \{CORE}.Table::new(table_name=\"\{spec.table}\", tbl=\"\{spec.tbl}\", cols~, all=\{spec.type_name}::all(cols), write=\{spec.type_name}::binding())\n",
  )
  buf.write_string("}\n")
  buf.to_string()
}

///|
/// `pub fn[D] UserRow::table_of(to_domain, from_domain) -> Table[UserCols, D]`
///
/// The seam between the row type and a domain type. The table's name, alias
/// and columns come from the annotation; the two mapping functions are the
/// part only the programmer can write, so they are the only arguments.
///
/// `to_domain` may raise: reconstructing a domain value can discover that the
/// stored row does not represent a legal state. `from_domain` cannot.
fn emit_table_of_fn(spec : EntitySpec) -> String {
  let buf = StringBuilder()
  let row = spec.type_name
  buf.write_string("///|\n")
  buf.write_string(
    "/// Build a table over a domain type mapped to and from `\{row}`.\n",
  )
  buf.write_string(
    "pub fn[D] \{row}::table_of(\n  to_domain : (\{row}) -> D raise \{CORE}.DecodeError,\n  from_domain : (D) -> \{row},\n) -> \{CORE}.Table[\{spec.cols_name}, D] {\n",
  )
  buf.write_string("  let cols = \{row}::cols()\n")
  buf.write_string(
    "  \{CORE}.Table::new(\n    table_name=\"\{spec.table}\",\n    tbl=\"\{spec.tbl}\",\n    cols~,\n    all=\{row}::all(cols).map(to_domain),\n    write=\{row}::binding().contramap(from_domain),\n  )\n",
  )
  buf.write_string("}\n")
  buf.to_string()
}

///|
/// `pub fn User::primary_key_name() -> String?`
///
/// Not consumed by the query builder yet; it is what update- and delete-by-id
/// will key on once aya emits DML.
fn emit_primary_key_fn(spec : EntitySpec) -> String {
  let pk = spec.fields.filter(f => f.is_primary)
  let body = if pk.is_empty() { "None" } else { "Some(\"\{pk[0].column}\")" }
  let buf = StringBuilder()
  buf.write_string("///|\n")
  buf.write_string(
    "pub fn \{spec.type_name}::primary_key_name() -> String? {\n  \{body}\n}\n",
  )
  buf.to_string()
}