///|
/// The `office annotate` command (#164 D4): preservation-safe comment
/// mutation of an existing DOCX through a strict docx.annotation-batch/1
/// script. Distinct from `office batch` (XLSX cell mutation) and from
/// fresh DOCX authoring: the body text is preserved (the document part
/// gains only narrow comment-anchor markers), while the comment,
/// content-type, and relationship parts are added or updated as needed.

///|
let office_annotation_max_script_bytes : Int = 4 * 1024 * 1024

///|
/// The office.docx.annotation-batch/1 result record.
fn annotation_report_json(
  file : String,
  output : String,
  report : @office_docx.DocxAnnotationReport,
  transaction? : Json,
) -> Json {
  let results = report.results.map(entry => {
    let fields : Map[String, Json] = {
      "op": Json::string(entry.op),
      "comment_id": Json::string(bounded_text(entry.comment_id, 80)),
      "done": match entry.done {
        Some(flag) => Json::boolean(flag)
        None => Json::null()
      },
    }
    match entry.anchor {
      Some(anchor) => fields["anchor"] = Json::string(bounded_text(anchor, 160))
      None => ()
    }
    match entry.anchor_to {
      Some(anchor_to) =>
        fields["anchor_to"] = Json::string(bounded_text(anchor_to, 160))
      None => ()
    }
    match entry.target {
      Some(target) => fields["target"] = Json::string(bounded_text(target, 80))
      None => ()
    }
    Json::object(fields)
  })
  let labels = report.labels.map(pair => {
    let (label, id) = pair
    Json::object({
      "label": Json::string(bounded_text(label, 80)),
      "comment_id": Json::string(bounded_text(id, 80)),
    })
  })
  let fields : Map[String, Json] = {
    "schema": Json::string(@office_docx.SCHEMA_ANNOTATION_RESULT),
    "file": Json::string(bounded_text(file, 160)),
    "format": Json::string("docx"),
    "output": Json::string(bounded_text(output, 160)),
    "ops_applied": Json::number(report.ops_applied.to_double()),
    "results": Json::array(results),
    "labels": Json::array(labels),
    "changed_parts": Json::array(
      report.changed_parts.map(part => Json::string(bounded_text(part, 160))),
    ),
  }
  match transaction {
    Some(value) => fields["transaction"] = value
    None => ()
  }
  Json::object(fields)
}

///|
async fn run_annotate(matches : @argparse.Matches) -> Unit {
  let file = required_value(matches, "file")
  let script_file = required_value(matches, "script")
  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 annotate requires a .docx template",
      ),
    )
  }
  guard output.to_lower().has_suffix(".docx") else {
    raise CliFailure(
      @lib.protocol_error(
        "office.invalid_arguments", "--out must end in .docx to match the annotated format",
      ),
    )
  }
  let script_bytes = read_bounded_file(
    script_file,
    office_annotation_max_script_bytes,
    "office.annotate.script_read_failed",
    "annotation script",
    limit_code="office.annotate.resource_limit",
  )
  let script_text = @utf8.decode(script_bytes, ignore_bom=true) catch {
    _ =>
      raise CliFailure(
        @lib.protocol_error(
          "office.annotate.invalid_script", "annotation script is not valid UTF-8",
        ),
      )
  }
  let batch = @office_docx.parse_annotation_batch(script_text) catch {
    DocxAnnotationParseError(reason) =>
      raise CliFailure(
        @lib.protocol_error(
          "office.annotate.invalid_script",
          "invalid annotation script: " + bounded_text(reason, 200),
        ),
      )
  }
  let options = @transaction.transaction_options(
    file,
    output_path=output,
    dry_run=matches.flags.get_or_default("dry-run", false),
    overwrite=matches.flags.get_or_default("overwrite", false),
  ) catch {
    error => raise transaction_failure(error)
  }
  let result = @office_docx.transact_docx_annotations(options, batch) catch {
    @transaction.TransactionError(..) as error =>
      raise transaction_failure(error)
    error if @async.is_being_cancelled() => raise error
    error =>
      raise unexpected_cli_failure(
        "office.annotate.failed", "annotation transaction", error,
      )
  }
  let record = annotation_report_json(
    file,
    output,
    result.report,
    transaction=result.transaction.to_json(),
  )
  match mode {
    JsonDocument => println(@lib.output_success(record).stringify(indent=2))
    JsonLines => println(@lib.output_success(record).stringify())
    Human =>
      println(
        "annotate: \{result.report.ops_applied} op(s) -> " +
        human_text(output, 160),
      )
  }
}

///|
fn annotate_command() -> @argparse.Command {
  let summary = match @lib.find_capability_command("annotate") {
    Some(command) => command.summary
    None => "Mutate DOCX comments through a strict annotation script"
  }
  Command(
    "annotate",
    about=summary,
    positionals=[
      PositionArg(
        "file",
        about="existing .docx package (never modified in place)",
        num_args=@argparse.ValueRange::single(),
      ),
      PositionArg(
        "script",
        about="docx.annotation-batch/1 JSON document",
        num_args=@argparse.ValueRange::single(),
      ),
    ],
    options=[
      OptionArg(
        "out",
        long="out",
        about="destination .docx path (never modified in place)",
      ),
    ],
    flags=[
      FlagArg("dry-run", long="dry-run", about="validate without publishing"),
      FlagArg(
        "overwrite",
        long="overwrite",
        about="replace an existing destination",
      ),
      FlagArg("json", long="json", about="print office.output/1 JSON"),
      FlagArg("jsonl", long="jsonl", about="print one office.output/1 line"),
    ],
  )
}