///|
let read_office = "urn:oasis:names:tc:opendocument:xmlns:office:1.0#"

///|
let read_table = "urn:oasis:names:tc:opendocument:xmlns:table:1.0#"

///|
let read_text = "urn:oasis:names:tc:opendocument:xmlns:text:1.0#"

///|
/// Coordinates are zero-based. -1 means workbook/sheet scope.
#derive(Eq, Debug)
pub(all) struct ImportDiagnostic {
  sheet : String
  row : Int32
  col : Int32
  code : String
  message : String
}

///|
priv struct ReadNotices {
  items : Array[ImportDiagnostic]
  counts : Map[(String, String), Int32]
  summaries : Map[(String, String), (Int32, Int32, Int32)]
}

///|
fn ReadNotices::new() -> ReadNotices {
  { items: [], counts: {}, summaries: {}, }
}

///|
struct ImportResult {
  book : Workbook
  notices : ReadNotices
  xml_id_assignments : Array[XmlIdAssignment]
  xml_id_errors : Array[XmlIdDiagnostic]
  processor_events : Array[XmlProcessorEvent]
}

///|
pub fn ImportResult::workbook(self : ImportResult) -> Workbook {
  self.book
}

///|
/// A copy: callers cannot mutate the reader's diagnostic record.
pub fn ImportResult::diagnostics(
  self : ImportResult,
) -> Array[ImportDiagnostic] {
  self.notices.items.copy()
}

///|
pub fn ImportResult::xml_ids(self : ImportResult) -> Array[XmlIdAssignment] {
  self.xml_id_assignments.copy()
}

///|
pub fn ImportResult::xml_id_diagnostics(
  self : ImportResult,
) -> Array[XmlIdDiagnostic] {
  self.xml_id_errors.copy()
}

///|
pub fn Workbook::sheet(self : Workbook, index : Int32) -> Sheet? {
  if index < 0 || index >= self.sheets.length() {
    None
  } else {
    Some(self.sheets[index])
  }
}

///|
pub fn Sheet::dimensions(self : Sheet) -> (Int32, Int32) {
  (self.rows, self.cols)
}

///|
pub fn Sheet::merge_span(
  self : Sheet,
  row : Int32,
  col : Int32,
) -> (Int32, Int32)? {
  self.spans.get((row, col))
}

///|
pub fn Sheet::is_covered(self : Sheet, row : Int32, col : Int32) -> Bool {
  self.covered.contains((row, col))
}

///|
fn ReadElement::elements(
  self : ReadElement,
) -> Array[ReadElement] raise OdsError {
  let result = []
  for part in self.parts {
    match part {
      ReadText(t) =>
        if t.trim() != "" {
          raise Invalid("unexpected XML character data")
        }
      ReadElement(e) => result.push(e)
    }
  }
  result
}

///|
fn ReadElement::attribute(
  self : ReadElement,
  ns : String,
  key : String,
) -> String? {
  self.attrs.get(ns + key)
}

///|
fn ReadElement::allowed(
  self : ReadElement,
  names : Array[String],
) -> Unit raise OdsError {
  for k, _ in self.attrs {
    if !names.contains(k) && k != xml_id_expanded_name {
      raise Invalid("unsupported attribute: \{k}")
    }
  }
}

///|
fn read_positive(raw : String, max : Int32) -> Int32 raise OdsError {
  if raw == "" {
    raise Invalid("empty repeat/span count")
  }
  let mut n = 0
  for c in raw {
    let d = c.to_int() - 48
    if d < 0 || d > 9 || n > (max - d) / 10 {
      raise Invalid("repeat/span exceeds budget")
    }
    n = n * 10 + d
    if n > max {
      raise Invalid("repeat/span exceeds budget")
    }
  }
  if n < 1 {
    raise Invalid("repeat/span must be positive")
  }
  n
}

///|
fn ReadElement::count(
  self : ReadElement,
  ns : String,
  key : String,
  max : Int32,
) -> Int32 raise OdsError {
  match self.attribute(ns, key) {
    Some(s) => read_xsd_positive(s, max)
    None => 1
  }
}

///|
fn read_text_parts(node : ReadElement) -> String raise OdsError {
  let state : ReadParagraph = {
    output: StringBuilder(),
    units: 0,
    seen: false,
    pending_space: false,
  }
  state.append_parts(node)
  state.output.to_string()
}

///|
fn read_cell_text(node : ReadElement) -> String raise OdsError {
  let b = StringBuilder()
  let mut first = true
  let mut units = 0
  for e in node.elements() {
    if e.name != read_text + "p" {
      raise Invalid("unsupported cell child: \{e.name}")
    }
    e.allowed([read_text + "style-name"])
    if !first {
      b.write_char('\n')
      units += 1
    }
    first = false
    let s = read_text_parts(e)
    units += s.length()
    if units > 32767 {
      raise Invalid("expanded cell text exceeds budget")
    }
    b.write_string(s)
  }
  b.to_string()
}

///|
// XML parsing already bounds depth (32), nodes (65536) and input size.
// Runs are flattened only after validation by read_cell_text/read_text_parts.
fn ReadElement::has_text_formatting(self : ReadElement) -> Bool {
  if self.name == read_text + "span" ||
    self.attribute(read_text, "style-name") is Some(_) {
    return true
  }
  for part in self.parts {
    if part is ReadElement(e) && e.has_text_formatting() {
      return true
    }
  }
  false
}

///|
fn read_decimal_identity(raw : String) -> String raise OdsError {
  let raw = read_xsd_collapse(raw)
  // Exact decimal normalization for already lexically validated finite inputs.
  // This compares source/output decimals; it does not claim decimal arithmetic.
  let digits : Array[Char] = []
  let mut negative = false
  let mut fractional = false
  let mut fraction_digits = 0
  let mut exponent = false
  let mut exponent_negative = false
  let mut power = 0
  for i, c in raw {
    if !exponent {
      if i == 0 && c == '-' {
        negative = true
      } else if c == '+' {
        ()
      } else if c == '.' {
        fractional = true
      } else if c == 'e' || c == 'E' {
        exponent = true
      } else {
        digits.push(c)
        if fractional {
          fraction_digits += 1
        }
      }
    } else if c == '-' {
      exponent_negative = true
    } else if c == '+' {
      ()
    } else {
      if power > 100000 {
        raise Invalid("numeric exponent outside supported decimal range")
      }
      power = power * 10 + c.to_int() - 48
    }
  }
  let mut first = 0
  while first < digits.length() && digits[first] == '0' {
    first += 1
  }
  if first == digits.length() {
    return if negative { "-0" } else { "0" }
  }
  let mut last = digits.length()
  while last > first && digits[last - 1] == '0' {
    last -= 1
  }
  let scale = (if exponent_negative { -power } else { power }) -
    fraction_digits +
    digits.length() -
    last
  let b = StringBuilder()
  if negative {
    b.write_char('-')
  }
  for i in first.. Double raise OdsError {
  if raw.length() > 1024 {
    raise Invalid("numeric literal exceeds 1024 units")
  }
  let raw = read_xsd_collapse(raw)
  if raw == "INF" || raw == "-INF" || raw == "NaN" {
    raise Invalid(
      "unsupported non-finite ODF numeric value; finite numbers required",
    )
  }
  // XML Schema decimal/exponent lexical subset; core parse_double also accepts
  // language-specific underscores, which must not be admitted as ODF numbers.
  let chars : Array[Char] = []
  for c in raw {
    chars.push(c)
  }
  let n = chars.length()
  let mut i = 0
  if i < n && (chars[i] == '+' || chars[i] == '-') {
    i += 1
  }
  let mut digits = 0
  while i < n && chars[i] >= '0' && chars[i] <= '9' {
    i += 1
    digits += 1
  }
  if i < n && chars[i] == '.' {
    i += 1
    while i < n && chars[i] >= '0' && chars[i] <= '9' {
      i += 1
      digits += 1
    }
  }
  if digits == 0 {
    raise Invalid("invalid ODF numeric lexical form")
  }
  if i < n && (chars[i] == 'e' || chars[i] == 'E') {
    i += 1
    if i < n && (chars[i] == '+' || chars[i] == '-') {
      i += 1
    }
    let start = i
    while i < n && chars[i] >= '0' && chars[i] <= '9' {
      i += 1
    }
    if start == i {
      raise Invalid("invalid ODF numeric exponent")
    }
  }
  if i != n {
    raise Invalid("invalid ODF numeric lexical form")
  }
  let result = @string.parse_double(raw[:]) catch {
    _ => raise Invalid("invalid numeric cell")
  }
  if result.is_nan() || result.is_inf() {
    raise Invalid("number must be finite")
  }
  if result == 0.0 {
    let mut mantissa = true
    for c in raw {
      if c == 'e' || c == 'E' {
        mantissa = false
      }
      if mantissa && c >= '1' && c <= '9' {
        raise Invalid("nonzero numeric input underflows to zero")
      }
    }
  }
  result
}

///|
fn read_value(node : ReadElement, extended : Bool) -> Value raise OdsError {
  // Validate even display paragraphs on non-text values; never silently discard
  // annotations, drawings, links, or unsupported text nodes in numeric cells.
  let text = read_cell_text(node)
  let kind = node.attribute(read_office, "value-type").map(read_xsd_collapse)
  match
    node.attrs.get(
      "urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0#value-type",
    ) {
    Some(ext) =>
      if Some(read_xsd_collapse(ext)) != kind {
        raise Invalid("unsupported or conflicting LibreOffice value type")
      }
    None => ()
  }
  let value = match kind {
    None => {
      if text != "" {
        raise Invalid("untyped nonempty cell")
      }
      Empty
    }
    Some("string") =>
      match node.attribute(read_office, "string-value") {
        // ODF 1.3 Part 3 section 19.389: a supplied value attribute is
        // authoritative even when the validated display content differs.
        Some(s) => Text(s)
        None => Text(text)
      }
    Some("percentage") if extended =>
      Percentage(
        read_finite_double(node.attribute(read_office, "value").unwrap_or("")),
      )
    Some("currency") if extended => {
      let raw = node.attribute(read_office, "value").unwrap_or("")
      let amount = read_finite_double(raw)
      if read_decimal_identity(raw) !=
        read_decimal_identity(finite_number_text(amount)) {
        raise Invalid(
          "currency decimal cannot roundtrip through binary64 without changing its decimal value",
        )
      }
      Currency(amount, node.attribute(read_office, "currency").unwrap_or(""))
    }
    Some("time") if extended =>
      Duration(
        read_xsd_collapse(
          node.attribute(read_office, "time-value").unwrap_or(""),
        ),
      )
    Some("float") => {
      let raw = node.attribute(read_office, "value").unwrap_or("")
      Number(read_finite_double(raw))
    }
    Some("boolean") =>
      match
        node.attribute(read_office, "boolean-value").map(read_xsd_collapse) {
        Some("true") => Boolean(true)
        Some("false") => Boolean(false)
        _ => raise Invalid("invalid boolean cell")
      }
    Some("date") => {
      let raw = read_xsd_collapse(
        node.attribute(read_office, "date-value").unwrap_or(""),
      )
      if extended && raw.contains("T") {
        DateTime(raw)
      } else {
        let p = raw.split("-").map(x => x.to_owned()).collect()
        if p.length() != 3 ||
          p[0].length() != 4 ||
          p[1].length() != 2 ||
          p[2].length() != 2 {
          raise Invalid("only Gregorian YYYY-MM-DD dates supported")
        }
        Date(
          read_positive(p[0], 9999),
          read_positive(p[1], 12),
          read_positive(p[2], 31),
        )
      }
    }
    Some(other) => raise Invalid("unsupported ODF value type: \{other}")
  }
  // Reject conflicting alternate typed attributes instead of coercing them.
  for
    field in [
      "value", "boolean-value", "date-value", "string-value", "currency", "time-value",
    ] {
    if node.attribute(read_office, field) is Some(_) {
      let expected = match kind {
        Some("float") | Some("percentage") => "value"
        Some("currency") =>
          if field == "currency" {
            "currency"
          } else {
            "value"
          }
        Some("time") => "time-value"
        Some("boolean") => "boolean-value"
        Some("date") => "date-value"
        Some("string") => "string-value"
        _ => ""
      }
      if field != expected {
        raise Invalid("conflicting typed value attributes")
      }
    }
  }
  validate_value(value)
  value
}

///|
fn read_notice(
  notices : ReadNotices,
  diagnostic : ImportDiagnostic,
) -> Unit raise OdsError {
  // Count every repeated loss. Keep the first 32 exact locations per sheet/code
  // and one explicit summary rather than rejecting an otherwise valid grid.
  // At most 32 sheets * 5 grouped codes * 33 entries, below the old hard cap.
  if [
      "FORMATTING_DROPPED", "RICH_TEXT_FORMATTING_DROPPED", "FORMULA_CACHE_NOT_RECALCULATED",
      "NUMBER_PRECISION_CHANGED", "STRING_DISPLAY_TEXT_DROPPED",
    ].contains(diagnostic.code) {
    let key = (diagnostic.sheet, diagnostic.code)
    let count = notices.counts.get(key).unwrap_or(0) + 1
    notices.counts.set(key, count)
    if count > 32 {
      let (index, first_row, first_col) = match notices.summaries.get(key) {
        Some(data) => data
        None => {
          if notices.items.length() >= 8192 {
            raise Invalid("import diagnostic budget exceeded")
          }
          let data = (notices.items.length(), diagnostic.row, diagnostic.col)
          notices.items.push(diagnostic)
          notices.summaries.set(key, data)
          data
        }
      }
      notices.items[index] = {
        sheet: diagnostic.sheet,
        row: -1,
        col: -1,
        code: diagnostic.code + "_SUMMARY",
        message: "\{count - 32} additional occurrences of \{diagnostic.code}; first additional row \{first_row} col \{first_col}, last row \{diagnostic.row} col \{diagnostic.col}. The first 32 occurrences retain exact details; all occurrences are counted, not silently truncated.",
      }
      return
    }
  }
  if notices.items.length() >= 8192 {
    raise Invalid("import diagnostic budget exceeded")
  }
  notices.items.push(diagnostic)
}

///|
fn read_preview(text : String) -> String {
  if text.length() <= 96 {
    text
  } else {
    text[:96].to_owned() + "…"
  }
}

///|
fn ReadElement::style_notice(
  self : ReadElement,
  notices : ReadNotices,
  sheet : String,
  row : Int32,
  col : Int32,
) -> Unit raise OdsError {
  if self.attribute(read_table, "style-name") is Some(_) ||
    self.attribute(read_table, "default-cell-style-name") is Some(_) {
    read_notice(notices, {
      sheet,
      row,
      col,
      code: "FORMATTING_DROPPED",
      message: "Source \{if self.name == read_table + "table-column" { "column style/width and default cell style" } else if self.name == read_table + "table-row" { "row style/height and default cell style" } else if self.name == read_table + "table" { "sheet style/view settings" } else { "cell style/number-format" }} is not retained by data-only import (style-name=\{read_preview(self.attribute(read_table, "style-name").unwrap_or(""))}, default-cell-style-name=\{read_preview(self.attribute(read_table, "default-cell-style-name").unwrap_or(""))}).",
    })
  }
}

///|
fn read_workbook_budget(book : Workbook) -> Unit raise OdsError {
  let mut grids = 0
  let mut units = 0
  for s in book.sheets {
    if s.rows * s.cols > 65536 {
      raise Invalid("sheet grid exceeds 65536 cells")
    }
    grids += s.rows * s.cols
    units += s.text_units
    if grids > 262144 || units > 4194304 {
      raise Invalid("workbook resource budget exceeded during import")
    }
  }
}

///|
// Recognize only physically empty suffix rows; no values, formulas, spans,
// covered cells, annotations or unknown attributes may be discarded.
fn read_empty_tail_row(node : ReadElement) -> (Int32, Int32)? raise OdsError {
  if node.name != read_table + "table-row" {
    return None
  }
  for key, _ in node.attrs {
    if ![
        read_table + "number-rows-repeated",
        read_table + "style-name",
        read_table + "default-cell-style-name",
      ].contains(key) {
      return None
    }
  }
  let mut cols = 0
  for cell in node.elements() {
    if cell.name != read_table + "table-cell" || !cell.parts.is_empty() {
      return None
    }
    for key, _ in cell.attrs {
      if ![read_table + "number-columns-repeated", read_table + "style-name"].contains(
          key,
        ) {
        return None
      }
    }
    let n = cell.count(read_table, "number-columns-repeated", 128)
    if cols > 128 - n {
      raise Invalid("column repeat exceeds grid")
    }
    cols += n
  }
  Some((node.count(read_table, "number-rows-repeated", 1048576), cols))
}

///|
fn read_table_children(
  node : ReadElement,
  notices : ReadNotices,
  sheet : String,
) -> Array[ReadElement] raise OdsError {
  let children = []
  for child in node.elements() {
    let expected = if [
        read_table + "table-header-rows",
        read_table + "table-rows",
      ].contains(child.name) {
      read_table + "table-row"
    } else if [
        read_table + "table-header-columns",
        read_table + "table-columns",
      ].contains(child.name) {
      read_table + "table-column"
    } else {
      ""
    }
    if expected == "" {
      children.push(child)
    } else {
      child.allowed([])
      let contents = child.elements()
      if contents.is_empty() {
        raise Invalid("empty table row/column container")
      }
      for content in contents {
        if content.name != expected {
          raise Invalid("unsupported table row/column container child")
        }
        children.push(content)
      }
      if child.name == read_table + "table-header-rows" ||
        child.name == read_table + "table-header-columns" {
        read_notice(notices, {
          sheet,
          row: -1,
          col: -1,
          code: "TABLE_HEADER_METADATA_DROPPED",
          message: "Source \{child.name} cells retain their positions and typed data; print/repeated-header metadata is not retained.",
        })
      }
    }
  }
  children
}

///|
fn import_table(
  node : ReadElement,
  book : Workbook,
  notices : ReadNotices,
  extended : Bool,
  formatting : ReadFormatting?,
) -> Unit raise OdsError {
  node.allowed([read_table + "name", read_table + "style-name"])
  let name = node.attribute(read_table, "name").unwrap_or("")
  let sheet = book.add_sheet(name)
  node.style_notice(notices, name, -1, -1)
  let mut row = 0
  let mut declared_cols = 0
  let column_defaults : Map[Int32, String] = Map([])
  let children = read_table_children(node, notices, name)
  let mut tail_start = children.length()
  let mut tail_rows = 0
  let mut tail_cols = 0
  while tail_start > 0 {
    // Invalid suffixes use the normal parser so contextual errors are preserved.
    let empty = read_empty_tail_row(children[tail_start - 1]) catch {
      Invalid(_) => None
    }
    match empty {
      None => break
      Some((n, cols)) => {
        tail_cols = if cols > tail_cols { cols } else { tail_cols }
        if tail_rows > 1048576 - n {
          raise Invalid("empty tail exceeds row extent")
        }
        tail_rows += n
        tail_start -= 1
      }
    }
  }
  // Preserve ordinary in-budget empty rows. Large suffixes are never expanded.
  let mut trim_tail = false
  for index, child in children {
    if index == tail_start {
      if row > 1048576 - tail_rows {
        raise Invalid("empty tail exceeds row extent")
      }
      let total_rows = row + tail_rows
      let total_cols = if sheet.cols > tail_cols {
        sheet.cols
      } else {
        tail_cols
      }
      let mut other_grids = 0
      for s in book.sheets {
        if s.name != name {
          other_grids += s.rows * s.cols
        }
      }
      // Omit the whole physically empty suffix only if preserving it would
      // exceed the remaining row, sheet-grid or workbook-grid budget.
      trim_tail = total_rows > 1024 ||
        total_rows * total_cols > 65536 ||
        other_grids + total_rows * total_cols > 262144
      if trim_tail {
        if sheet.rows > row && !sheet.spans.is_empty() {
          raise Invalid("merge crosses omitted empty tail boundary")
        }
        break
      }
    }
    if child.name == read_table + "table-column" {
      child.allowed([
        read_table + "style-name",
        read_table + "default-cell-style-name",
        read_table + "number-columns-repeated",
      ])
      if !child.parts.is_empty() || row != 0 {
        raise Invalid("unsupported table column placement/content")
      }
      let n = child.count(read_table, "number-columns-repeated", 128)
      if declared_cols > 128 - n {
        raise Invalid("column declaration exceeds limit")
      }
      let width = match formatting {
        Some(format) =>
          format.column_width(child.attribute(read_table, "style-name"))
        None => None
      }
      for c in declared_cols..<(declared_cols + n) {
        if child.attribute(read_table, "default-cell-style-name") is Some(style) {
          column_defaults.set(c, style)
        }
        if width is Some(mm) {
          sheet.set_column_width_length(c, mm)
          if formatting is Some(format) {
            format.retained_widths += 1
          }
        }
      }
      if width is Some(_) {
        read_notice(notices, {
          sheet: name,
          row: -1,
          col: declared_cols,
          code: "FORMATTING_DROPPED",
          message: "Exact physical column width retained; source column style names, default references and other metadata are normalized, not copied verbatim.",
        })
      } else {
        child.style_notice(notices, name, -1, declared_cols)
      }
      declared_cols += n
      sheet.cols = declared_cols
      read_workbook_budget(book)
      continue
    }
    if child.name != read_table + "table-row" {
      raise Invalid("unsupported table child: \{child.name}")
    }
    child.allowed([
      read_table + "number-rows-repeated",
      read_table + "style-name",
      read_table + "default-cell-style-name",
    ])
    let rows = child.count(read_table, "number-rows-repeated", 1024) catch {
      Invalid(msg) => raise Invalid("sheet '\{name}' row \{row} col 0: \{msg}")
    }
    if row > 1024 - rows {
      raise Invalid("row repeat exceeds grid")
    }
    if row + rows > sheet.rows {
      sheet.rows = row + rows
    }
    read_workbook_budget(book)
    let cells = child.elements()
    let height = match formatting {
      Some(format) =>
        format.row_height(child.attribute(read_table, "style-name"))
      None => None
    }
    for r in row..<(row + rows) {
      if height is Some(length) {
        sheet.set_row_height_length(r, length)
        if formatting is Some(format) {
          format.retained_heights += 1
        }
        read_notice(notices, {
          sheet: name,
          row: r,
          col: -1,
          code: "ROW_HEIGHT_RETAINED",
          message: "Fixed physical row height retained; row style identity and other metadata are not copied verbatim.",
        })
      } else {
        child.style_notice(notices, name, r, -1)
      }
      let mut col = 0
      for node in cells {
        let is_covered = node.name == read_table + "covered-table-cell"
        if !is_covered && node.name != read_table + "table-cell" {
          raise Invalid("unsupported row child")
        }
        let cols = node.count(read_table, "number-columns-repeated", 128) catch {
          Invalid(msg) =>
            raise Invalid("sheet '\{name}' row \{r} col \{col}: \{msg}")
        }
        if col > 128 - cols {
          raise Invalid("column repeat exceeds grid")
        }
        for c in col..<(col + cols) {
          try {
            if is_covered {
              node.allowed([
                read_table + "number-columns-repeated",
                read_table + "style-name",
              ])
              if !node.parts.is_empty() || !sheet.is_covered(r, c) {
                raise Invalid("unexpected/nonempty covered cell")
              }
              node.style_notice(notices, name, r, c)
            } else {
              let allowed_attributes = [
                read_table + "number-columns-repeated",
                read_table + "number-columns-spanned",
                read_table + "number-rows-spanned",
                read_table + "formula",
                read_table + "style-name",
                read_office + "value-type",
                read_office + "value",
                read_office + "boolean-value",
                read_office + "date-value",
                read_office + "string-value",
                "urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0#value-type",
              ]
              if extended {
                allowed_attributes.push(read_office + "currency")
                allowed_attributes.push(read_office + "time-value")
              }
              node.allowed(allowed_attributes)
              let value = read_value(node, extended)
              if node.attribute(read_office, "string-value") is Some(stored) &&
                value is Text(_) &&
                node.parts
                .iter()
                .any(part => {
                  match part {
                    ReadElement(child) => child.name == read_text + "p"
                    _ => false
                  }
                }) &&
                read_cell_text(node) != stored {
                read_notice(notices, {
                  sheet: name,
                  row: r,
                  col: c,
                  code: "STRING_DISPLAY_TEXT_DROPPED",
                  message: "Explicit office:string-value is retained as the cell value; distinct source display text is not retained by this data model.",
                })
              }
              if node.has_text_formatting() {
                read_notice(notices, {
                  sheet: name,
                  row: r,
                  col: c,
                  code: "RICH_TEXT_FORMATTING_DROPPED",
                  message: "Supported text spans/paragraph styles are flattened; text whitespace and typed values are retained, run styling and numeric display text are not.",
                })
              }
              match value {
                Number(n) | Percentage(n) | Currency(n, _) => {
                  let raw = node.attribute(read_office, "value").unwrap_or("")
                  let stored = finite_number_text(n)
                  if read_decimal_identity(raw) != read_decimal_identity(stored) {
                    let preview = if raw.length() <= 96 {
                      raw
                    } else {
                      raw[:96].to_owned() + "…"
                    }
                    read_notice(notices, {
                      sheet: name,
                      row: r,
                      col: c,
                      code: "NUMBER_PRECISION_CHANGED",
                      message: "Source decimal \{preview} is stored as binary64 output \{stored}; exact decimal input is not retained.",
                    })
                  }
                }
                _ => ()
              }
              let cell = match node.attribute(read_table, "formula") {
                None => Cell::new(value)
                Some(f) => {
                  let normalized = read_formula_prefix(f, node.ns)
                  read_notice(notices, {
                    sheet: name,
                    row: r,
                    col: c,
                    code: "FORMULA_CACHE_NOT_RECALCULATED",
                    message: "Stored cache retained; edits do not recalculate this formula.",
                  })
                  Cell::formula(normalized, value)
                }
              }
              let selected_style = match
                node.attribute(read_table, "style-name") {
                Some(style) => Some(style)
                None =>
                  match child.attribute(read_table, "default-cell-style-name") {
                    Some(style) => Some(style)
                    None => column_defaults.get(c)
                  }
              }
              let normalized_style = match formatting {
                Some(format) => format.cell_style(selected_style, value~)
                None => None
              }
              if normalized_style is None {
                if formatting is Some(format) &&
                  (
                    selected_style is Some(_) ||
                    format.source.defaults.contains("table-cell")
                  ) {
                  read_notice(notices, {
                    sheet: name,
                    row: r,
                    col: c,
                    code: "FORMATTING_DROPPED",
                    message: "Effective cell formatting is outside the supported presets after resolving defaults/inheritance (source=\{read_preview(selected_style.unwrap_or("family default"))}); typed value retained, formatting dropped. No style-name guessing is used.",
                  })
                } else {
                  node.style_notice(notices, name, r, c)
                }
              }
              let cell = match normalized_style {
                Some(style) => {
                  if formatting is Some(format) {
                    format.retained_cells += 1
                  }
                  cell.styled(style)
                }
                None =>
                  match value {
                    Date(_, _, _) => cell.styled(DateISO)
                    _ => cell
                  }
              }
              sheet.set(r, c, cell)
              read_workbook_budget(book)
              let nr = node.count(read_table, "number-rows-spanned", 1024)
              let nc = node.count(read_table, "number-columns-spanned", 128)
              if nr > 1 || nc > 1 {
                // ODF 1.3 section 19.681 permits repeated rows with only
                // horizontal merges. Section 19.679.3 forbids repeating a
                // merged cell across columns, even for vertical-only spans.
                if cols != 1 || (rows != 1 && nr != 1) {
                  raise Invalid("repeated merged anchors unsupported")
                }
                sheet.merge(r, c, nr, nc)
                read_workbook_budget(book)
              }
            }
          } catch {
            Invalid(msg) =>
              raise Invalid("sheet '\{name}' row \{r} col \{c}: \{msg}")
          }
        }
        col += cols
      }
      // Missing covered positions must not be invented by the importer.
      for c in 0..= col {
          raise Invalid("missing covered cell")
        }
      }
    }
    row += rows
  }
  if formatting is Some(format) {
    for c in declared_cols.. 1048576 - tail_rows {
      raise Invalid("empty tail exceeds row extent")
    }
    read_notice(notices, {
      sheet: name,
      row,
      col: -1,
      code: "EMPTY_TAIL_ROWS_DROPPED",
      message: "Data-only import omits \{tail_rows} trailing empty rows and their row/cell formatting; source extent is not retained.",
    })
  }
  if sheet.rows > row && !sheet.spans.is_empty() {
    raise Invalid("merge extends beyond input rows")
  }
}

///|
/// Import a bounded, data-only ODS subset. The explicit opt-in acknowledges
/// formatting, application metadata and view settings are not retained.
/// Unsupported semantic features fail, with sheet/cell context where known.
/// No partial workbook is returned on failure; formulas are never evaluated.
pub fn read_ods(
  data : Bytes,
  allow_formatting_loss~ : Bool = false,
  allow_extended_types~ : Bool = false,
  preserve_basic_formatting~ : Bool = false,
) -> ImportResult raise OdsError {
  if !allow_formatting_loss {
    raise Invalid(
      "data-only import requires allow_formatting_loss=true; source formatting and metadata will not be retained",
    )
  }
  let omitted : Array[String] = []
  let styles : Array[String] = []
  let package_ids : Array[XmlIdAssignment] = []
  let package_id_errors : Array[XmlIdDiagnostic] = []
  let package_events : Array[XmlProcessorEvent] = []
  let xml = read_ods_content_noticing(
    data,
    omitted,
    require_manifest=true,
    id_assignments=package_ids,
    id_diagnostics=package_id_errors,
    processor_events=package_events,
    style_parts=if preserve_basic_formatting { Some(styles) } else { None },
  )
  let result = import_content(
    xml,
    allow_extended_types~,
    preserve_basic_formatting~,
    styles~,
  )
  for event in package_events {
    result.processor_events.push(event)
  }
  for id in package_ids {
    result.xml_id_assignments.push(id)
  }
  for error in package_id_errors {
    result.xml_id_errors.push(error)
  }
  for event in result.processor_events {
    if event.kind == "MANIFEST_EXTENSION_NOT_INTERPRETED" {
      read_notice(result.notices, {
        sheet: "",
        row: -1,
        col: -1,
        code: "PACKAGE_EXTENSION_DROPPED",
        message: "Foreign manifest extension not interpreted or preserved: " +
        event.name,
      })
    }
  }
  for path in omitted {
    if path.has_prefix("META-INF/") && path.contains("signatures") {
      read_notice(result.notices, {
        sheet: "",
        row: -1,
        col: -1,
        code: "SIGNATURE_NOT_VERIFIED",
        message: "Signature XML parsed, but cryptographic validity not checked; signature omitted on data-only export: " +
        path,
      })
    }
    read_notice(result.notices, {
      sheet: "",
      row: -1,
      col: -1,
      code: "PACKAGE_PART_DROPPED",
      message: "Source package part \{path} is not preserved by data-only import.",
    })
  }
  result
}

///|
fn import_content(
  xml : String,
  allow_extended_types~ : Bool = false,
  preserve_basic_formatting~ : Bool = false,
  styles~ : Array[String] = [],
) -> ImportResult raise OdsError {
  let xml_id_assignments : Array[XmlIdAssignment] = []
  let xml_id_errors : Array[XmlIdDiagnostic] = []
  let processor_events : Array[XmlProcessorEvent] = []
  let root = read_xml(
    xml,
    document_part="content.xml",
    id_assignments=xml_id_assignments,
    id_diagnostics=xml_id_errors,
    processor_events~,
  )
  if root.name != read_office + "document-content" {
    raise Invalid("expected ODF document-content namespace")
  }
  root.allowed([read_office + "version"])
  let book = Workbook::new()
  let notices = ReadNotices::new()
  let formatting = if preserve_basic_formatting {
    Some(read_formatting(root, styles))
  } else {
    None
  }
  read_notice(notices, {
    sheet: "",
    row: -1,
    col: -1,
    code: "DATA_ONLY_IMPORT",
    message: if preserve_basic_formatting {
      "Only recognized preset styles, exact physical column lengths and fixed row heights may be retained; other formatting, metadata, view settings and display text are not retained. Values, merges and formula/cache follow the selected type policy. BASIC_FORMATTING_SUBSET reports actual retained counts; repeated losses retain 32 exact occurrences plus counted summaries."
    } else {
      "Formatting, metadata, view settings, and numeric display text are not retained. Numbers use binary64; changed decimal representations are reported. Supported merges and formula text/cache are retained. Repeated per-sheet loss diagnostics retain 32 exact occurrences and an explicit counted summary."
    },
  })
  let mut bodies = 0
  for child in root.elements() {
    if child.name == read_office + "automatic-styles" ||
      child.name == read_office + "font-face-decls" {
      if !child.elements().is_empty() || !child.attrs.is_empty() {
        read_notice(notices, {
          sheet: "",
          row: -1,
          col: -1,
          code: "DOCUMENT_FORMATTING_DROPPED",
          message: "Source \{child.name} is not retained in full; only an explicitly enabled basic formatting subset may be normalized.",
        })
      }
      continue
    }
    if child.name == read_office + "scripts" {
      if !child.elements().is_empty() || !child.attrs.is_empty() {
        raise Invalid("scripts unsupported")
      }
      continue
    }
    if child.name != read_office + "body" {
      raise Invalid("unsupported document feature: \{child.name}")
    }
    bodies += 1
    if bodies != 1 {
      raise Invalid("duplicate office body")
    }
    child.allowed([])
    let contents = child.elements()
    if contents.length() != 1 || contents[0].name != read_office + "spreadsheet" {
      raise Invalid("expected one spreadsheet")
    }
    contents[0].allowed([])
    for table in contents[0].elements() {
      if table.name == read_table + "named-expressions" &&
        table.elements().is_empty() &&
        table.attrs.is_empty() {
        continue
      }
      if table.name != read_table + "table" {
        raise Invalid("unsupported spreadsheet feature: \{table.name}")
      }
      import_table(table, book, notices, allow_extended_types, formatting)
    }
  }
  if bodies != 1 || book.sheet_count() == 0 {
    raise Invalid("missing spreadsheet body or sheets")
  }
  if formatting is Some(format) {
    read_notice(notices, {
      sheet: "",
      row: -1,
      col: -1,
      code: "BASIC_FORMATTING_SUBSET",
      message: "Retained \{format.retained_cells} normalized preset cell styles and \{format.retained_widths} exact physical column widths and \{format.retained_heights} fixed row heights. Other definitions, names, fonts, automatic sizing, layout, themes and metadata are not retained; arbitrary style XML is never copied.",
    })
  }
  // Reuse writer preflight to enforce aggregate grid/text budgets as well.
  book.content_xml() |> ignore
  { book, notices, xml_id_assignments, xml_id_errors, processor_events, }
}

///|
pub extend ImportDiagnostic with Eq::{equal, not_equal}

///|
pub extend ImportDiagnostic with @debug.Debug::{to_repr}

///|
// Normalize only the outer OpenFormula namespace prefix. The body is opaque;
// extension-function prefix semantics and formula correctness are not validated.
fn read_formula_prefix(
  f : String,
  ns : Map[String, String],
) -> String raise OdsError {
  let prefix = StringBuilder()
  let mut first = true
  for c in f {
    if c == ':' {
      break
    }
    if !(if first { xml_name_start(c) } else { xml_name_char(c) }) {
      raise Invalid("unsupported formula namespace prefix")
    }
    prefix.write_char(c)
    first = false
  }
  let name = prefix.to_string()
  if first || !f.has_prefix(name + ":=") || f.length() <= name.length() + 2 {
    raise Invalid("formula requires namespace prefix and nonempty body")
  }
  if ns.get(name) != Some("urn:oasis:names:tc:opendocument:xmlns:of:1.2") {
    raise Invalid("formula namespace unsupported")
  }
  "of:" + f[name.length() + 1:].to_owned()
}

///|
pub fn ImportResult::xml_events(
  self : ImportResult,
) -> Array[XmlProcessorEvent] {
  self.processor_events.copy()
}