///|
/// Create or get a nested table structure from a path
fn create_nested_table(
  root_table : Map[String, TomlValue],
  path : Array[String],
) -> Map[String, TomlValue] raise TableConflicts {
  let mut current_table = root_table
  for i, key in path {
    let is_final = i == path.length() - 1
    match current_table.get(key) {
      Some(TomlTable(existing_table)) => {
        if existing_table.contains("\u0000__inline__") {
          raise InlineTableImmutable(key)
        }
        current_table = existing_table
      }
      Some(TomlArray(arr)) if arr.length() > 0 && !is_final =>
        // Navigate into the last element of an array of tables
        // (only for intermediate path segments, not the final key)
        match arr[arr.length() - 1] {
          TomlTable(last_table) => {
            if last_table.contains("\u0000__inline__") {
              raise InlineTableImmutable(key)
            }
            current_table = last_table
          }
          _ => raise ExpectedTable(key, TomlArray(arr))
        }
      Some(value) => raise ExpectedTable(key, value)
      None => {
        // Reject adding new keys to inline tables
        if current_table.contains("\u0000__inline__") {
          raise InlineTableImmutable(key)
        }
        let new_table = Map([])
        current_table[key] = TomlTable(new_table)
        current_table = new_table
      }
    }
  }
  current_table
}

///|
priv suberror TableConflicts {
  ExpectedTable(String, TomlValue)
  ExpectedArray(String, TomlValue)
  DuplicateKey(String)
  InlineTableImmutable(String)
} derive(Debug)

///|
/// Show impl for error messages
impl Show for TableConflicts with fn to_string(self) {
  match self {
    ExpectedTable(key, value) =>
      (
        $|ExpectedTable("\{key}", "\{value.type_name()}")
      )
    ExpectedArray(key, value) =>
      (
        $|ExpectedArray("\{key}", "\{value.type_name()}")
      )
    DuplicateKey(key) =>
      (
        $|DuplicateKey("\{key}")
      )
    InlineTableImmutable(key) =>
      (
        $|InlineTableImmutable("\{key}")
      )
  }
}

///|
/// Create or append to an array of tables structure in TOML.
/// 
/// This function handles the `[[table]]` syntax in TOML, which creates an array
/// of tables. If the array doesn't exist, it creates it with the first table.
/// If the array exists, it appends a new table to it.
/// 
/// ## Parameters
/// - `root_table`: The root table where the array of tables will be created/modified
/// - `path`: The dotted key path to the array (e.g., ["servers", "alpha"] for [[servers.alpha]])
/// - `error_tokens`: Token view for error reporting
/// 
/// ## Returns
/// The newly created empty table that was added to the array
/// 
/// ## Errors
/// - Raises if the path conflicts with existing non-table values
/// - Raises if the final key exists but is not an array
/// 
/// ## Example
/// ```
/// // For TOML: [[servers.alpha]]
/// // Creates: { servers: { alpha: [{}] } }
/// // Second call appends: { servers: { alpha: [{}, {}] } }
/// ```
fn create_array_of_tables(
  root_table : Map[String, TomlValue],
  path : Array[String],
) -> Map[String, TomlValue] raise TableConflicts {
  let mut current_table = root_table

  // Navigate to the parent of the final key
  for key in path[:path.length() - 1] {
    match current_table.get(key) {
      Some(TomlTable(existing_table)) => {
        if existing_table.contains("\u0000__inline__") {
          raise InlineTableImmutable(key)
        }
        current_table = existing_table
      }
      Some(TomlArray(arr)) if arr.length() > 0 =>
        // Navigate into the last element of an array of tables
        match arr[arr.length() - 1] {
          TomlTable(last_table) => {
            if last_table.contains("\u0000__inline__") {
              raise InlineTableImmutable(key)
            }
            current_table = last_table
          }
          _ => raise ExpectedTable(key, TomlArray(arr))
        }
      Some(value) => raise ExpectedTable(key, value)
      None => {
        let new_table = Map([])
        current_table[key] = TomlTable(new_table)
        current_table = new_table
      }
    }
  }

  // Handle the final key as an array of tables
  let final_key = path[path.length() - 1]
  let new_table = Map([])
  match current_table.get(final_key) {
    Some(TomlArray(existing_array)) => {
      // Verify this is an array-of-tables (created by [[ ]]), not a static array (= [...])
      // Static arrays are either empty or contain non-table values
      let is_array_of_tables = existing_array.length() > 0 &&
        existing_array.iter().all(fn(v) { v is TomlTable(_) })
      if !is_array_of_tables {
        raise ExpectedArray(final_key, TomlArray(existing_array))
      }
      existing_array.push(TomlTable(new_table))
      new_table
    }
    Some(value) => raise ExpectedArray(final_key, value)
    None => {
      // Create new array with the first table
      let array = Array::new()
      array.push(TomlTable(new_table))
      current_table[final_key] = TomlArray(array)
      new_table
    }
  }
}

///|
test "create_array_of_tables - first table creation" {
  // Test creating the first table in an array of tables
  let root_table : Map[String, TomlValue] = Map([])

  // Create first table in array [[servers]]
  let new_table = create_array_of_tables(root_table, ["servers"])

  // Add some data to the new table
  new_table["name"] = TomlString("alpha")
  new_table["ip"] = TomlString("10.0.0.1")
  inspect(
    TomlTable(root_table),
    content=(
      #|[[servers]]
      #|name = "alpha"
      #|ip = "10.0.0.1"
      #|
    ),
  )
}

///|
test "create_array_of_tables - append to existing array" {
  // Test appending to an existing array of tables
  let root_table : Map[String, TomlValue] = Map([])

  // Create first table
  let table1 = create_array_of_tables(root_table, ["products"])
  table1["name"] = TomlString("Hammer")
  table1["sku"] = TomlInteger(738594937)

  // Append second table to same array
  let table2 = create_array_of_tables(root_table, ["products"])
  table2["name"] = TomlString("Nail")
  table2["sku"] = TomlInteger(284758393)

  // Append third table
  let table3 = create_array_of_tables(root_table, ["products"])
  table3["name"] = TomlString("Screwdriver")
  table3["sku"] = TomlInteger(987654321)
  inspect(
    TomlTable(root_table),
    content=(
      #|[[products]]
      #|name = "Hammer"
      #|sku = 738594937
      #|
      #|[[products]]
      #|name = "Nail"
      #|sku = 284758393
      #|
      #|[[products]]
      #|name = "Screwdriver"
      #|sku = 987654321
      #|
    ),
  )
}

///|
test "create_array_of_tables - nested path creation" {
  // Test creating array of tables with nested path
  let root_table : Map[String, TomlValue] = Map([])

  // Create [[fruit.physical.color]]
  let color_table1 = create_array_of_tables(root_table, [
    "fruit", "physical", "color",
  ])
  color_table1["name"] = TomlString("red")
  color_table1["code"] = TomlString("#FF0000")

  // Add another color
  let color_table2 = create_array_of_tables(root_table, [
    "fruit", "physical", "color",
  ])
  color_table2["name"] = TomlString("green")
  color_table2["code"] = TomlString("#00FF00")
  inspect(
    TomlTable(root_table),
    content=(
      #|[fruit]
      #|[fruit.physical]
      #|[[fruit.physical.color]]
      #|name = "red"
      #|code = "#FF0000"
      #|
      #|[[fruit.physical.color]]
      #|name = "green"
      #|code = "#00FF00"
      #|
    ),
  )
}

///|
test "create_array_of_tables - mixed with regular tables" {
  // Test array of tables mixed with regular tables
  let root_table : Map[String, TomlValue] = Map([])

  // First create a regular table structure
  set_dotted_key_value(root_table, ["owner", "name"], TomlString("Tom"))
  set_dotted_key_value(root_table, ["owner", "dob"], TomlString("1979-05-27"))

  // Now create array of tables at a different path
  let db1 = create_array_of_tables(root_table, ["database", "servers"])
  db1["host"] = TomlString("192.168.1.1")
  db1["port"] = TomlInteger(5432)
  let db2 = create_array_of_tables(root_table, ["database", "servers"])
  db2["host"] = TomlString("192.168.1.2")
  db2["port"] = TomlInteger(5433)
  debug_inspect(
    root_table,
    content=(
      #|{
      #|  "owner": TomlTable({ "name": TomlString("Tom"), "dob": TomlString("1979-05-27") }),
      #|  "database": TomlTable(
      #|    {
      #|      "servers": TomlArray(
      #|        [
      #|          TomlTable({ "host": TomlString("192.168.1.1"), "port": TomlInteger(5432) }),
      #|          TomlTable({ "host": TomlString("192.168.1.2"), "port": TomlInteger(5433) }),
      #|        ],
      #|      ),
      #|    },
      #|  ),
      #|}
    ),
  )
}

///|
#skip("error message is not correct")
test "create_array_of_tables - path conflicts with non-table" {
  // Test error when intermediate path conflicts with non-table value
  let root_table : Map[String, TomlValue] = Map([])

  // Set a string value at "products"
  root_table["products"] = TomlString("not a table")

  // Try to create array of tables at ["products", "items"]
  // This should fail because "products" is not a table
  let result = try
    create_array_of_tables(root_table, ["products", "items"])
  catch {
    err => Err(err)
  } noraise {
    value => Ok(value)
  }
  debug_inspect(
    result,
    content=(
      #|Err(Failure("/Users/hongbozhang/git/toml-parser/parser.mbt:1038:11-1038:45 FAILED: Array of tables path conflicts with existing value: products at the end of input"))
    ),
  )
}

///|
#skip("error message is not correct")
test "create_array_of_tables - final key conflicts with non-array" {
  // Test error when final key exists but is not an array
  let root_table : Map[String, TomlValue] = Map([])

  // Create a regular table at servers.alpha
  set_dotted_key_value(
    root_table,
    ["servers", "alpha"],
    TomlTable(Map::from_array([("name", TomlString("main"))])),
  )

  // Try to treat servers.alpha as array of tables
  let result = try
    create_array_of_tables(root_table, ["servers", "alpha"])
  catch {
    err => Err(err)
  } noraise {
    value => Ok(value)
  }
  debug_inspect(
    result,
    content=(
      #|Err(Failure("/Users/hongbozhang/git/toml-parser/parser.mbt:1049:11-1049:45 FAILED: Array of tables conflicts with existing non-array value: alpha at the end of input"))
    ),
  )
}

///|
test "create_array_of_tables - multiple nested arrays" {
  // Test creating multiple different arrays of tables
  let root_table : Map[String, TomlValue] = Map([])

  // Create first [[fruits]]
  let fruit1 = create_array_of_tables(root_table, ["fruits"])
  fruit1["name"] = TomlString("apple")

  // Create second [[fruits]]
  let fruit2 = create_array_of_tables(root_table, ["fruits"])
  fruit2["name"] = TomlString("banana")

  // Create [[vegetables]]
  let veg1 = create_array_of_tables(root_table, ["vegetables"])
  veg1["name"] = TomlString("carrot")
  veg1["color"] = TomlString("orange")
  let veg2 = create_array_of_tables(root_table, ["vegetables"])
  veg2["name"] = TomlString("lettuce")
  veg2["color"] = TomlString("green")
  debug_inspect(
    root_table,
    content=(
      #|{
      #|  "fruits": TomlArray(
      #|    [
      #|      TomlTable({ "name": TomlString("apple") }),
      #|      TomlTable({ "name": TomlString("banana") }),
      #|    ],
      #|  ),
      #|  "vegetables": TomlArray(
      #|    [
      #|      TomlTable({ "name": TomlString("carrot"), "color": TomlString("orange") }),
      #|      TomlTable({ "name": TomlString("lettuce"), "color": TomlString("green") }),
      #|    ],
      #|  ),
      #|}
    ),
  )
}

///|
test "create_array_of_tables - empty table in array" {
  // Test that empty tables are correctly added to arrays
  let root_table : Map[String, TomlValue] = Map([])

  // Create empty table in array
  let _empty_table1 = create_array_of_tables(root_table, ["empty", "tables"])
  // Don't add any fields

  let _empty_table2 = create_array_of_tables(root_table, ["empty", "tables"])
  // Also empty

  let table3 = create_array_of_tables(root_table, ["empty", "tables"])
  table3["has_content"] = TomlBoolean(true)
  debug_inspect(
    root_table,
    content=(
      #|{
      #|  "empty": TomlTable(
      #|    {
      #|      "tables": TomlArray(
      #|        [
      #|          TomlTable({}),
      #|          TomlTable({}),
      #|          TomlTable({ "has_content": TomlBoolean(true) }),
      #|        ],
      #|      ),
      #|    },
      #|  ),
      #|}
    ),
  )
}

///|
/// Set a value using a dotted key path in a table
fn set_dotted_key_value(
  table : Map[String, TomlValue],
  key_path : Array[String],
  value : TomlValue,
  mark_implicit? : Bool = false,
) -> Unit raise TableConflicts {
  guard key_path.length() != 0
  if key_path.length() == 1 {
    // Simple key assignment — reject duplicates
    let key = key_path[0]
    if table.contains(key) {
      raise DuplicateKey(key)
    }
    table[key] = value
    return
  }

  // Navigate through the dotted path, creating tables as needed.
  // Mark newly created intermediate tables as "defined" so they can't
  // be reopened with [table] headers later.
  let mut current_table = table
  for i = 0; i < key_path.length() - 1; i = i + 1 {
    let key = key_path[i]
    match current_table.get(key) {
      Some(TomlTable(existing_table)) => {
        // Reject extending inline tables
        if existing_table.contains("\u0000__inline__") {
          raise InlineTableImmutable(key)
        }
        // Reject using dotted keys to extend a table that was explicitly
        // defined with a [table] header in a different section.
        // (Tables defined by dotted keys have __dotted__ marker, not __defined__)
        if mark_implicit && existing_table.contains("\u0000__header__") {
          raise DuplicateKey(key)
        }
        current_table = existing_table
      }
      Some(value) => raise ExpectedTable(key, value)
      None => {
        // Create new nested table
        let new_table = Map([])
        if mark_implicit {
          new_table["\u0000__defined__"] = TomlBoolean(true)
        }
        current_table[key] = TomlTable(new_table)
        current_table = new_table
      }
    }
  }

  // Set the final value — reject duplicates
  let final_key = key_path[key_path.length() - 1]
  if current_table.contains(final_key) {
    raise DuplicateKey(final_key)
  }
  current_table[final_key] = value
}

///|
test "set dotted key value - simple key" {
  // Test setting a simple key (path length 1)
  let table : Map[String, TomlValue] = Map([])
  set_dotted_key_value(table, ["name"], TomlString("Alice"))
  debug_inspect(
    table,
    content=(
      #|{ "name": TomlString("Alice") }
    ),
  )
}

///|
test "set dotted key value - nested keys creating tables" {
  // Test creating nested tables automatically
  let table : Map[String, TomlValue] = Map([])
  set_dotted_key_value(table, ["server", "host"], TomlString("localhost"))
  set_dotted_key_value(table, ["server", "port"], TomlInteger(8080))
  debug_inspect(
    table,
    content=(
      #|{ "server": TomlTable({ "host": TomlString("localhost"), "port": TomlInteger(8080) }) }
    ),
  )
}

///|
test "set dotted key value - deep nesting" {
  // Test deep nested table creation
  let table : Map[String, TomlValue] = Map([])
  set_dotted_key_value(
    table,
    ["a", "b", "c", "d", "e"],
    TomlString("deep value"),
  )
  debug_inspect(
    table,
    content=(
      #|{ "a": TomlTable({ "b": TomlTable({ "c": TomlTable({ "d": TomlTable({ "e": TomlString("deep value") }) }) }) }) }
    ),
  )
}

///|
test "set dotted key value - multiple values same nested table" {
  // Test adding multiple values to the same nested table
  let table : Map[String, TomlValue] = Map([])

  // Add multiple values at different levels
  set_dotted_key_value(table, ["database", "server"], TomlString("postgres"))
  set_dotted_key_value(table, ["database", "port"], TomlInteger(5432))
  set_dotted_key_value(
    table,
    ["database", "credentials", "user"],
    TomlString("admin"),
  )
  set_dotted_key_value(
    table,
    ["database", "credentials", "password"],
    TomlString("secret"),
  )
  set_dotted_key_value(table, ["database", "options", "ssl"], TomlBoolean(true))
  debug_inspect(
    table,
    content=(
      #|{
      #|  "database": TomlTable(
      #|    {
      #|      "server": TomlString("postgres"),
      #|      "port": TomlInteger(5432),
      #|      "credentials": TomlTable({ "user": TomlString("admin"), "password": TomlString("secret") }),
      #|      "options": TomlTable({ "ssl": TomlBoolean(true) }),
      #|    },
      #|  ),
      #|}
    ),
  )
}

///|
test "set dotted key value - reject duplicate key" {
  let table : Map[String, TomlValue] = Map([])
  set_dotted_key_value(table, ["config", "timeout"], TomlInteger(30))
  // Duplicate key should be rejected
  let result = try
    set_dotted_key_value(table, ["config", "timeout"], TomlInteger(60))
  catch {
    err => Err(err)
  } noraise {
    value => Ok(value)
  }
  debug_inspect(result, content="Err(DuplicateKey(\"timeout\"))")
}

///|
test "set dotted key value - add to existing table" {
  // Test adding values to an already existing table
  let table : Map[String, TomlValue] = Map([])

  // Create initial structure
  set_dotted_key_value(table, ["app", "name"], TomlString("MyApp"))
  set_dotted_key_value(table, ["app", "version"], TomlString("1.0.0"))

  // Add more values to the same table later
  set_dotted_key_value(table, ["app", "author"], TomlString("Developer"))
  set_dotted_key_value(table, ["app", "license"], TomlString("MIT"))
  debug_inspect(
    table,
    content=(
      #|{
      #|  "app": TomlTable(
      #|    {
      #|      "name": TomlString("MyApp"),
      #|      "version": TomlString("1.0.0"),
      #|      "author": TomlString("Developer"),
      #|      "license": TomlString("MIT"),
      #|    },
      #|  ),
      #|}
    ),
  )
}

///|
test "set dotted key value - conflict with non-table value" {
  let table : Map[String, TomlValue] = Map([])

  // Set a string value at "config"
  set_dotted_key_value(table, ["config"], TomlString("simple value"))

  // Try to treat "config" as a table - this should raise an error
  try
    set_dotted_key_value(table, ["config", "nested"], TomlString("value"))
  catch {
    err =>
      debug_inspect(
        err,
        content=(
          #|ExpectedTable("config", TomlString("simple value"))
        ),
      )
  } noraise {
    _ => fail("Expected error but got success")
  }
}

///|
test "set dotted key value - mixed types in path" {
  // Test with different value types at different levels
  let table : Map[String, TomlValue] = Map([])
  set_dotted_key_value(
    table,
    ["root", "array"],
    TomlArray([TomlInteger(1), TomlInteger(2), TomlInteger(3)]),
  )
  set_dotted_key_value(table, ["root", "float"], TomlFloat(3.14))
  set_dotted_key_value(table, ["root", "nested", "bool"], TomlBoolean(false))
  set_dotted_key_value(
    table,
    ["root", "nested", "datetime"],
    TomlDateTime(OffsetDateTime("2024-01-01T12:00:00Z")),
  )
  debug_inspect(
    table,
    content=(
      #|{
      #|  "root": TomlTable(
      #|    {
      #|      "array": TomlArray([TomlInteger(1), TomlInteger(2), TomlInteger(3)]),
      #|      "float": TomlFloat(3.14),
      #|      "nested": TomlTable(
      #|        {
      #|          "bool": TomlBoolean(false),
      #|          "datetime": TomlDateTime(OffsetDateTime("2024-01-01T12:00:00Z")),
      #|        },
      #|      ),
      #|    },
      #|  ),
      #|}
    ),
  )
}

///|
#skip("empty path edge case to fix later")
test "set dotted key value - empty path edge case" {
  // Test with empty path (edge case)
  let table : Map[String, TomlValue] = Map([])

  // Set a value first
  set_dotted_key_value(table, ["key"], TomlString("value"))

  // Try with empty path - currently does nothing
  set_dotted_key_value(table, [], TomlString("should not appear"))

  // Table should only have the first key
  debug_inspect(table, content="")
}