///|
/// vcdb CLI Demo - Vector Database functionality demonstration
/// Run with `moon run cmd/main` from the project root.
fn run_async(f : async () -> Unit noraise) -> Unit = "%async.run"

///|
fn main {
  run_async(async fn() noraise {
    let _ = demo_bruteforce() catch { e => abort(e.to_string()) }
    let _ = demo_hnsw() catch { e => abort(e.to_string()) }
    let _ = demo_filtered_search() catch { e => abort(e.to_string()) }
    let _ = demo_serialization() catch { e => abort(e.to_string()) }
    let _ = demo_metrics() catch { e => abort(e.to_string()) }
    println("\n=== All demos completed successfully! ===")
  })
}

///|
async fn demo_bruteforce() -> Unit {
  println("--- Demo 1: Bruteforce Search ---")

  let db = @vcdb.PersistentDB::in_memory_bruteforce(3)

  // Add sample vectors representing documents/items
  // Simulating embeddings for: "apple", "banana", "orange", "grape", "melon"
  let items = [
    (1, "apple", [0.9, 0.1, 0.0]),
    (2, "banana", [0.1, 0.9, 0.0]),
    (3, "orange", [0.8, 0.2, 0.0]),
    (4, "grape", [0.2, 0.8, 0.0]),
    (5, "melon", [0.5, 0.5, 0.0]),
  ]

  for item in items {
    let (id, name, vec) = item
    let attrs = @types.empty_attrs()
    attrs.set("name", @types.String(name))
    let _ = db.add(@types.VectorId::from_int(id), vec, attrs, 0L)
  }

  println("Added \{db.size()} items to database")

  // Search for items similar to "apple" (query: [0.9, 0.1, 0.0])
  let query = [0.9, 0.1, 0.0]
  let results = db.search(query, 3, None)

  println("Query: similar to 'apple' [0.9, 0.1, 0.0]")
  println("Top 3 results:")
  for i, hit in results.iter2() {
    let name = match hit.attrs.get("name") {
      Some(@types.String(s)) => s
      _ => "unknown"
    }
    println("  \{i + 1}. \{name} (id=\{hit.id}, score=\{hit.score})")
  }
  println("")
}

///|
async fn demo_hnsw() -> Unit {
  println("--- Demo 2: HNSW Approximate Search ---")

  let db = @vcdb.PersistentDB::in_memory_hnsw(4, metric=@types.Dot)

  // Add 100 vectors
  for i in 0..<100 {
    let v = [
      (i % 10).to_double() / 10.0,
      (i / 10 % 10).to_double() / 10.0,
      (i % 7).to_double() / 7.0,
      (i % 13).to_double() / 13.0,
    ]
    let attrs = @types.empty_attrs()
    attrs.set("index", @types.Int(i.to_int64()))
    let _ = db.add(@types.VectorId::from_int(i), v, attrs, 0L)
  }

  println("Added \{db.size()} vectors with HNSW index")

  // Search
  let query = [0.5, 0.5, 0.5, 0.5]
  let results = db.search(query, 5, None)

  println("Query: [0.5, 0.5, 0.5, 0.5]")
  println("Top 5 results:")
  for i, hit in results.iter2() {
    let idx = match hit.attrs.get("index") {
      Some(@types.Int(n)) => n.to_string()
      _ => "?"
    }
    println(
      "  \{i + 1}. index=\{idx} (id=\{hit.id}, score=\{format_score(hit.score)})",
    )
  }
  println("")
}

///|
async fn demo_filtered_search() -> Unit {
  println("--- Demo 3: Search with Attribute Filter ---")

  let db = @vcdb.PersistentDB::in_memory_bruteforce(2)

  // Add products with categories
  let products = [
    (1, "iPhone", "electronics", [0.9, 0.1]),
    (2, "Galaxy", "electronics", [0.85, 0.15]),
    (3, "T-shirt", "clothing", [0.1, 0.9]),
    (4, "Jeans", "clothing", [0.15, 0.85]),
    (5, "MacBook", "electronics", [0.95, 0.05]),
    (6, "Sneakers", "clothing", [0.2, 0.8]),
  ]

  for product in products {
    let (id, name, category, vec) = product
    let attrs = @types.empty_attrs()
    attrs.set("name", @types.String(name))
    attrs.set("category", @types.String(category))
    let _ = db.add(@types.VectorId::from_int(id), vec, attrs, 0L)
  }

  println("Added \{db.size()} products")

  // Search only in electronics category
  let filter = fn(_id : @types.VectorId, attrs : @types.Attrs) -> Bool {
    match attrs.get("category") {
      Some(@types.String(s)) => s == "electronics"
      _ => false
    }
  }

  let query = [1.0, 0.0] // Looking for electronics-like vector
  let results = db.search_with_filter(query, 3, Some(filter))

  println("Query: [1.0, 0.0] with filter: category='electronics'")
  println("Results:")
  for i, hit in results.iter2() {
    let name = match hit.attrs.get("name") {
      Some(@types.String(s)) => s
      _ => "unknown"
    }
    println("  \{i + 1}. \{name} (score=\{format_score(hit.score)})")
  }
  println("")
}

///|
async fn demo_serialization() -> Unit {
  println("--- Demo 4: Serialization & Persistence ---")

  let db = @vcdb.PersistentDB::in_memory_hnsw(3, metric=@types.Cosine)

  // Add data
  for i in 1..<=10 {
    let attrs = @types.empty_attrs()
    attrs.set("label", @types.String("item-\{i}"))
    let _ = db.add(
      @types.VectorId::from_int(i),
      [i.to_double() / 10.0, (10 - i).to_double() / 10.0, 0.5],
      attrs,
      0L,
    )
  }

  // Serialize
  let bytes = db.serialize_snapshot()
  println(
    "Original DB: \{db.size()} vectors, serialized to \{bytes.length()} bytes",
  )

  // Deserialize
  let db2 = @vcdb.PersistentDB::deserialize(bytes)
  println("Restored DB: \{db2.size()} vectors")

  // Verify search works on restored DB
  let query = [1.0, 0.0, 0.5]
  let results = db2.search(query, 3, None)

  println("Search on restored DB for [1.0, 0.0, 0.5]:")
  for i, hit in results.iter2() {
    let label = match hit.attrs.get("label") {
      Some(@types.String(s)) => s
      _ => "?"
    }
    println("  \{i + 1}. \{label} (score=\{format_score(hit.score)})")
  }
  println("")
}

///|
async fn demo_metrics() -> Unit {
  println("--- Demo 5: Metric Comparison ---")

  // Same vectors, different metrics
  let vectors = [
    (1, [1.0, 0.0, 0.0]),
    (2, [2.0, 0.0, 0.0]), // Same direction as 1, different magnitude
    (3, [0.0, 1.0, 0.0]), // Orthogonal
  ]

  let query = [1.0, 0.0, 0.0]

  // Cosine: direction matters, not magnitude
  let db_cosine = @vcdb.PersistentDB::in_memory(
    3,
    metric=@types.Cosine,
    strategy=@types.Bruteforce,
  )

  // Dot: both direction and magnitude matter
  let db_dot = @vcdb.PersistentDB::in_memory(
    3,
    metric=@types.Dot,
    strategy=@types.Bruteforce,
  )

  // L2: distance matters
  let db_l2 = @vcdb.PersistentDB::in_memory(
    3,
    metric=@types.L2,
    strategy=@types.Bruteforce,
  )

  for vec in vectors {
    let (id, v) = vec
    let _ = db_cosine.add(
      @types.VectorId::from_int(id),
      v,
      @types.empty_attrs(),
      0L,
    )
    let _ = db_dot.add(
      @types.VectorId::from_int(id),
      v,
      @types.empty_attrs(),
      0L,
    )
    let _ = db_l2.add(
      @types.VectorId::from_int(id),
      v,
      @types.empty_attrs(),
      0L,
    )
  }

  println("Vectors: id=1 [1,0,0], id=2 [2,0,0], id=3 [0,1,0]")
  println("Query: [1,0,0]\n")

  println("Cosine (direction only):")
  let r_cos = db_cosine.search(query, 3, None)
  for hit in r_cos {
    println("  id=\{hit.id}, score=\{format_score(hit.score)}")
  }

  println("Dot (direction + magnitude):")
  let r_dot = db_dot.search(query, 3, None)
  for hit in r_dot {
    println("  id=\{hit.id}, score=\{format_score(hit.score)}")
  }

  println("L2 (distance, negated):")
  let r_l2 = db_l2.search(query, 3, None)
  for hit in r_l2 {
    println("  id=\{hit.id}, score=\{format_score(hit.score)}")
  }
}

///|
/// Format score for display
fn format_score(score : Double) -> String {
  // Simple formatting: truncate to 4 decimal places
  let scaled = (score * 10000.0).to_int64()
  let integer = scaled / 10000L
  let frac = if scaled >= 0L { scaled % 10000L } else { -scaled % 10000L }
  let sign = if score < 0.0 && integer == 0L { "-" } else { "" }
  let frac_str = frac.to_string()
  let padded = match frac_str.length() {
    1 => "000" + frac_str
    2 => "00" + frac_str
    3 => "0" + frac_str
    _ => frac_str
  }
  "\{sign}\{integer}.\{padded}"
}