// `office delete-paragraph` — the N3b structural verb. The CLI half
// owns REQUEST policy: the target flag, the expect-text guard, and the
// output contract. Every structural refusal, the identity readback,
// and atomic publication live in the engine and the transaction.

///|
fn delete_paragraph_command() -> @argparse.Command {
  let summary = match @lib.find_capability_command("delete-paragraph") {
    Some(command) => command.summary
    None =>
      "Delete one direct body paragraph, refusing wherever the removal would dangle document state"
  }
  Command(
    "delete-paragraph",
    about=summary,
    positionals=[
      PositionArg(
        "file",
        about="existing .docx package (never modified in place)",
        num_args=@argparse.ValueRange::single(),
      ),
      PositionArg(
        "out",
        about="destination .docx; created atomically, nothing written on refusal",
        num_args=@argparse.ValueRange::single(),
      ),
    ],
    options=[
      OptionArg(
        "at",
        long="at",
        action=Append,
        about="the DIRECT body paragraph to delete: p[3], or stable p[id=\"…\"] resolved against the transaction snapshot",
      ),
      OptionArg(
        "expect-text",
        long="expect-text",
        action=Append,
        about="assert the target's full projection equals this text before deleting; a mismatch refuses with nothing written — the guard against deleting the wrong paragraph by ordinal",
      ),
    ],
    flags=[
      FlagArg(
        "dry-run",
        long="dry-run",
        about="run the identical resolve/plan/validate pipeline, exit as the real run would, write nothing",
      ),
      FlagArg(
        "overwrite",
        long="overwrite",
        about="replace an existing destination",
      ),
      docx_json_flag(),
    ],
  )
}

///|
fn delete_paragraph_report_json(
  file : String,
  output : String,
  at : String,
  expect_text : String?,
  dry_run : Bool,
  report : @office_docx.DocxDeleteParagraphReport,
  changed : Bool,
  transaction~ : Json,
) -> Json {
  Json::object({
    "schema": Json::string("office.docx.delete-paragraph/1"),
    "file": Json::string(file),
    "format": Json::string("docx"),
    "output": Json::string(output),
    "at": Json::string(at),
    "expect_text": match expect_text {
      Some(text) => Json::string(bounded_text(text, DELETE_ECHO_LIMIT))
      None => Json::null()
    },
    // The assertion is compared in FULL; the echo is bounded like every
    // other text field, so the record says when replaying it verbatim
    // would not reproduce the run.
    "expect_text_truncated": match expect_text {
      Some(text) => Json::boolean(echo_truncated(text))
      None => Json::boolean(false)
    },
    "dry_run": Json::boolean(dry_run),
    "changed": Json::boolean(changed),
    "deleted": Json::object({
      "path": Json::string(report.path()),
      "para_id": match report.para_id() {
        Some(id) => Json::string(id)
        None => Json::null()
      },
      "paragraph_anchor_status": Json::string(report.anchor_status()),
      "text": Json::string(bounded_text(report.text(), DELETE_ECHO_LIMIT)),
      // The record of what was destroyed must never look complete when
      // it is not: --expect-text compares the FULL projection, so a
      // caller feeding this field back needs to know it was cut.
      // Counted the way `bounded_text` cuts — by Unicode scalars, not
      // UTF-16 units — so the flag cannot disagree with the string it
      // describes.
      "text_truncated": Json::boolean(echo_truncated(report.text())),
    }),
    // Re-anchoring: after this publish the deleted path names the
    // SUCCESSOR — its stable identity, when it has one, is how an
    // agent keeps addressing it.
    "successor_para_id": match report.successor_para_id() {
      Some(id) => Json::string(id)
      None => Json::null()
    },
    // The re-anchor that always speaks: most producers other than Word
    // emit no paraIds, and this was verified against the published
    // candidate.
    "successor_text": match report.successor_text() {
      Some(text) => Json::string(bounded_text(text, DELETE_ECHO_LIMIT))
      None => Json::null()
    },
    // The field an agent feeds to the NEXT --expect-text, so it must
    // say when feeding it back would not reproduce the run.
    "successor_text_truncated": match report.successor_text() {
      Some(text) => Json::boolean(echo_truncated(text))
      None => Json::boolean(false)
    },
    "stories_scanned": Json::array([Json::string("/body")]),
    "transaction": transaction,
  })
}

///|
/// Every text echo in the record is cut at this many Unicode scalars,
/// and every `_truncated` flag says whether that cut happened — ONE
/// limit and ONE judgment, so a flag cannot disagree with the string
/// it describes.
const DELETE_ECHO_LIMIT : Int = 160

///|
fn echo_truncated(text : String) -> Bool {
  text.iter().count() > DELETE_ECHO_LIMIT
}

///|
async fn run_delete_paragraph(matches : @argparse.Matches) -> Unit {
  reject_duplicate_scalar_options(matches, ["at", "expect-text"])
  let file = required_value(matches, "file")
  let output = required_value(matches, "out")
  let mode = office_validate_output_mode(matches)
  guard file.to_lower().has_suffix(".docx") else {
    raise CliFailure(
      @lib.protocol_error(
        "office.invalid_arguments", "office delete-paragraph requires a .docx document",
      ),
    )
  }
  guard output.to_lower().has_suffix(".docx") else {
    raise CliFailure(
      @lib.protocol_error(
        "office.invalid_arguments", "the destination must end in .docx to match the edited format",
      ),
    )
  }
  let at = match optional_value(matches, "at") {
    Some(value) if value != "" => value
    Some(_) | None =>
      raise CliFailure(
        @lib.protocol_error(
          "office.invalid_arguments", "delete-paragraph requires --at naming the DIRECT body paragraph: p[3], or stable p[id=\"…\"]",
        ),
      )
  }
  // The same request-boundary grammar the format verb settled: an
  // ordinal target must be exactly p[N]; a stable-looking target must
  // be exactly the single-segment stable shape. Everything else
  // refuses before any file is read. (The engine re-proves the ordinal
  // against the element tree; the snapshot resolves the stable form.)
  if at.has_prefix("p[id") {
    guard docx_stable_target_shape_valid(at) else {
      raise CliFailure(
        @lib.protocol_error(
          "office.invalid_arguments",
          "--at '\{bounded_text(at, 80)}' is not the stable shape p[id=\"…\"]; pass the exact para_id spelling the read surfaces report",
        ),
      )
    }
  } else {
    guard format_scope_segment_kind(at) is Some("p") else {
      raise CliFailure(
        @lib.protocol_error(
          "office.invalid_arguments",
          "--at '\{bounded_text(at, 80)}' is not a direct body paragraph; pass p[N] (the reader's own spelling) or a stable p[id=\"…\"]",
        ),
      )
    }
  }
  let expect_text = optional_value(matches, "expect-text")
  let dry_run = matches.flags.get_or_default("dry-run", false)
  let options = @transaction.transaction_options(
    file,
    output_path=output,
    dry_run~,
    overwrite=matches.flags.get_or_default("overwrite", false),
  ) catch {
    error => raise transaction_failure(error)
  }
  // NO in-transaction report preflight for this verb, unlike `format`.
  // The preflight exists where the record can grow without bound before
  // publication — format's embeds one entry per match — so refusing
  // must happen while nothing has been written. Every field of a
  // deletion record is bounded by construction: two paths, one address,
  // and three projections capped at 160 characters each. It could not
  // fire, and it cost a second full JSON build inside every `--json`
  // transaction. What remains is the ceiling check on the FULL payload
  // below, where the transaction report — whose preservation manifest
  // grows with the package's part count — is the part that can actually
  // be large.
  let result = @office_docx.transact_docx_delete_paragraph(
    options,
    at~,
    expect_text?,
  ) catch {
    error => raise map_delete_paragraph_failure(error)
  }
  let record = delete_paragraph_report_json(
    file,
    output,
    at,
    expect_text,
    dry_run,
    result.report(),
    result.transaction().changed,
    transaction=result.transaction().to_json(),
  )
  match mode {
    JsonDocument | JsonLines => {
      let warnings = []
      for warning in result.transaction().warning_records() {
        warnings.push(warning)
      }
      let payload = bounded_docx_payload(
        record, warnings, docx_cli_default_max_output_chars,
      ) catch {
        _ =>
          raise CliFailure(
            @lib.protocol_error(
              "office.delete.report_too_large",
              if dry_run {
                "the JSON report for this deletion exceeds the output ceiling — its transaction preservation manifest is too large for one document; this was a dry run, so nothing was written. Re-run without --json for the human summary"
              } else {
                "the JSON report for this deletion exceeds the output ceiling — its transaction preservation manifest is too large for one document. The deletion itself completed and \{output} is valid; re-run `office validate` or use the human summary"
              },
            ),
          )
      }
      println(checked_office_json_output(payload))
    }
    Human => {
      let verb = if dry_run { "would delete" } else { "deleted" }
      println(
        "delete-paragraph: " +
        verb +
        " \{result.report().path()} (\"\{human_text(result.report().text(), 80)}\") -> " +
        human_text(output, 160),
      )
      for warning in result.transaction().warning_records() {
        println(
          "warning [\{human_text(warning.code, 160)}]: \{human_text(warning.message, 320)}",
        )
      }
    }
  }
}

///|
/// Map a transaction-layer failure to the CLI contract, preserving the
/// typed codes the transaction raises (the planner's refusal slugs, the
/// stable para_id refusals, `office.delete.expect_text_mismatch`, and
/// the shared preflight's protection codes).
fn map_delete_paragraph_failure(error : Error) -> CliFailure {
  match error {
    CliFailure(_) as failure => failure
    @transaction.TransactionError(_) as failure => transaction_failure(failure)
    _ =>
      unexpected_cli_failure(
        "office.delete_paragraph.failed", "delete-paragraph", error,
      )
  }
}