///|
fn legacy_selector_error(path : String, message : String) -> SelectorError {
  SelectorError(
    code="office.selector.legacy_docx",
    offset=0,
    input=bounded_selector_text(path, 160),
    message=bounded_selector_text(message, 320),
  )
}

///|
fn check_legacy_selector_length(path : String) -> Unit raise SelectorError {
  match scan_selector_unicode(path, SELECTOR_MAX_LENGTH) {
    Invalid(offset) =>
      raise SelectorError(
        code="office.selector.invalid_unicode",
        offset~,
        input="",
        message="selector contains an isolated UTF-16 surrogate",
      )
    TooLong =>
      raise SelectorError(
        code="office.selector.too_long",
        offset=SELECTOR_MAX_LENGTH,
        input=bounded_selector_text(path, 160),
        message="selector exceeds \{SELECTOR_MAX_LENGTH} characters",
      )
    Valid => ()
  }
}

///|
let selector_max_coordinate_length = 21

///|
fn check_xlsx_adapter_argument(
  value : String,
  label : String,
  limit : Int,
) -> Unit raise SelectorError {
  match scan_selector_unicode(value, limit) {
    Invalid(offset) =>
      raise SelectorError(
        code="office.selector.invalid_unicode",
        offset~,
        input="",
        message="\{label} contains an isolated UTF-16 surrogate",
      )
    TooLong =>
      raise SelectorError(
        code="office.selector.adapter_input_too_long",
        offset=limit,
        input=bounded_selector_text(value, 160),
        message="\{label} exceeds \{limit} characters",
      )
    Valid => ()
  }
}

///|
fn append_docx_root(output : StringBuilder, root : @docxpaths.PathRoot) -> Unit {
  match root {
    Body => output.write_string("/docx/body") |> ignore
    Header(index) => output.write_string("/docx/header[\{index}]") |> ignore
    Footer(index) => output.write_string("/docx/footer[\{index}]") |> ignore
    Footnotes => output.write_string("/docx/footnotes") |> ignore
    Endnotes => output.write_string("/docx/endnotes") |> ignore
    Comments => output.write_string("/docx/comments") |> ignore
  }
}

///|
fn append_docx_step(output : StringBuilder, step : @docxpaths.PathStep) -> Unit {
  output.write_char('/') |> ignore
  output.write_string(step.kind) |> ignore
  match step.selector {
    Ordinal(index) => output.write_string("[\{index}]") |> ignore
    ById(id) => {
      output.write_string("[id=") |> ignore
      output.write_string(Json::string(id).stringify()) |> ignore
      output.write_char(']') |> ignore
    }
  }
}

///|
/// Converts the DOCX projection paths already emitted by `docx text` and
/// `docx get` into the shared canonical selector AST. This adapts syntax only;
/// it does not load a package or resolve the path.
pub fn selector_from_docx_projection_path(
  path : String,
) -> OfficeSelector raise SelectorError {
  check_legacy_selector_length(path)
  let parsed = @docxpaths.parse_path(path) catch {
    PathError(message) => raise legacy_selector_error(path, message)
  }
  let canonical = StringBuilder::new()
  append_docx_root(canonical, parsed.root)
  for step in parsed.steps {
    append_docx_step(canonical, step)
  }
  parse_selector(canonical.to_string())
}

///|
fn selector_for_xlsx_coordinate(
  sheet_name : String,
  leaf : String,
  reference : String,
) -> OfficeSelector raise SelectorError {
  check_xlsx_adapter_argument(
    sheet_name,
    "worksheet name",
    SELECTOR_MAX_VALUE_LENGTH,
  )
  check_xlsx_adapter_argument(
    reference, "A1 coordinate", selector_max_coordinate_length,
  )
  parse_selector(
    "/xlsx/sheet[name=\{Json::string(sheet_name).stringify()}]/\{leaf}[\{reference}]",
  )
}

///|
/// Builds the canonical selector for the workbook singleton.
pub fn selector_for_xlsx_workbook() -> OfficeSelector raise SelectorError {
  parse_selector("/xlsx/workbook")
}

///|
/// Builds a stable canonical selector for one named worksheet or chart sheet.
/// It validates and quotes the name but does not look it up in a workbook.
pub fn selector_for_xlsx_sheet(
  sheet_name : String,
) -> OfficeSelector raise SelectorError {
  check_xlsx_adapter_argument(
    sheet_name,
    "worksheet name",
    SELECTOR_MAX_VALUE_LENGTH,
  )
  parse_selector("/xlsx/sheet[name=\{Json::string(sheet_name).stringify()}]")
}

///|
/// Builds a canonical, syntax-validated selector for one cell on a named
/// worksheet. It does not look up the sheet or cell in a workbook.
pub fn selector_for_xlsx_cell(
  sheet_name : String,
  address : String,
) -> OfficeSelector raise SelectorError {
  selector_for_xlsx_coordinate(sheet_name, "cell", address)
}

///|
/// Builds a canonical, normalized selector for one rectangular range on a
/// named worksheet. It does not look up the sheet or range in a workbook.
pub fn selector_for_xlsx_range(
  sheet_name : String,
  range : String,
) -> OfficeSelector raise SelectorError {
  selector_for_xlsx_coordinate(sheet_name, "range", range)
}