///|
fn xlsx_batch_failure(message : String, script : String) -> CliFailure {
  CliFailure(
    @lib.protocol_error(
      "office.xlsx.invalid_batch_script",
      bounded_text(message, 480),
      details=Json::object({ "script": Json::string(bounded_text(script, 160)) }),
    ),
  )
}

///|
fn xlsx_batch_stats_json(stats : @batch.BatchPlanStats) -> Json {
  Json::object({
    "operation_count": Json::number(stats.operation_count.to_double()),
    "touched_cells": Json::number(stats.touched_cells.to_double()),
    "style_cells": Json::number(stats.style_cells.to_double()),
    "row_column_lines": Json::number(stats.row_column_lines.to_double()),
    "new_style_records": Json::number(stats.new_style_records.to_double()),
  })
}

///|
fn xlsx_batch_result_json(result : @office_xlsx.XlsxBatchResult) -> Json {
  Json::object({
    "schema": Json::string(@lib.SCHEMA_XLSX_BATCH_RESULT),
    "stats": xlsx_batch_stats_json(result.stats),
    "transaction": result.transaction.to_json(),
  })
}

///|
fn xlsx_create_result_json(result : @office_xlsx.XlsxCreateResult) -> Json {
  Json::object({
    "schema": Json::string(@lib.SCHEMA_XLSX_CREATE_RESULT),
    "sheet": Json::string(result.sheet),
    "transaction": result.transaction.to_json(),
  })
}

///|
fn transaction_warnings(
  report : @transaction.TransactionReport,
) -> Array[@lib.ProtocolWarning] {
  let warnings = []
  for warning in report.warning_records() {
    warnings.push(warning)
  }
  warnings
}

///|
fn xlsx_operation_count_text(count : Int) -> String {
  if count == 1 {
    "1 operation"
  } else {
    "\{count} operations"
  }
}

///|
fn xlsx_cell_count_text(count : Int64) -> String {
  if count == 1L {
    "1 touched cell"
  } else {
    "\{count} touched cells"
  }
}

///|
fn print_transaction_warnings(report : @transaction.TransactionReport) -> Unit {
  for warning in report.warning_records() {
    println(
      "warning [\{human_text(warning.code, 160)}]: \{human_text(warning.message, 320)}",
    )
  }
}

///|
fn print_xlsx_create_result(
  matches : @argparse.Matches,
  output : String,
  result : @office_xlsx.XlsxCreateResult,
) -> Unit {
  if matches.flags.get_or_default("json", false) {
    println(
      @lib.output_success(
        xlsx_create_result_json(result),
        warnings=transaction_warnings(result.transaction),
      ).stringify(indent=2),
    )
  } else {
    let subject = if result.transaction.committed {
      "created XLSX sheet"
    } else {
      "XLSX sheet"
    }
    println(
      "\{human_transaction_status(result.transaction)}: \{subject} \"\{human_text(result.sheet, 80)}\" -> \{human_text(output, 160)}",
    )
    print_transaction_warnings(result.transaction)
  }
}

///|
fn print_xlsx_batch_result(
  matches : @argparse.Matches,
  destination : String,
  result : @office_xlsx.XlsxBatchResult,
) -> Unit {
  if matches.flags.get_or_default("json", false) {
    println(
      @lib.output_success(
        xlsx_batch_result_json(result),
        warnings=transaction_warnings(result.transaction),
      ).stringify(indent=2),
    )
  } else {
    println(
      "\{human_transaction_status(result.transaction)}: \{xlsx_operation_count_text(result.stats.operation_count)}, \{xlsx_cell_count_text(result.stats.touched_cells)} -> \{human_text(destination, 160)}",
    )
    print_transaction_warnings(result.transaction)
  }
}

///|
fn checked_xlsx_create_options(
  matches : @argparse.Matches,
) -> @transaction.CreateTransactionOptions raise CliFailure {
  @transaction.create_transaction_options(
    required_value(matches, "output"),
    dry_run=matches.flags.get_or_default("dry-run", false),
    overwrite=matches.flags.get_or_default("overwrite", false),
  ) catch {
    @transaction.TransactionError(..) as error =>
      raise transaction_failure(error)
    error =>
      raise unexpected_cli_failure(
        "office.xlsx.create_failed", "XLSX creation option validation", error,
      )
  }
}

///|
async fn run_create_xlsx(matches : @argparse.Matches) -> Unit {
  let output = required_value(matches, "output")
  let sheet = optional_value(matches, "sheet").unwrap_or("Sheet1")
  // Keep normalized option failures outside the operation catch so its generic
  // arm cannot rewrite an intentional `CliFailure` as a create failure.
  let options = checked_xlsx_create_options(matches)
  let result = @office_xlsx.create_workbook(options, sheet) catch {
    @transaction.TransactionError(..) as error =>
      raise transaction_failure(error)
    error if @async.is_being_cancelled() => raise error
    error =>
      raise unexpected_cli_failure(
        "office.xlsx.create_failed", "XLSX creation", error,
      )
  }
  print_xlsx_create_result(matches, output, result)
}

///|
async fn run_xlsx_batch(matches : @argparse.Matches) -> Unit {
  let file = required_value(matches, "target")
  let script = required_value(matches, "script")
  // Reject structurally invalid CLI combinations before any script I/O so
  // argument failures are deterministic and independent of the script path.
  let options = checked_transaction_options(matches, positional="target")
  let encoded = read_bounded_file(
    script,
    @batch.MAX_BATCH_SCRIPT_BYTES,
    "office.xlsx.batch_script_read_failed",
    "batch script",
    limit_code="office.xlsx.resource_limit",
  )
  let plan = @office_xlsx.parse_batch(encoded) catch {
    BatchError(message) => raise xlsx_batch_failure(message, script)
  }
  let result = @office_xlsx.transact_batch(options, plan) catch {
    @transaction.TransactionError(..) as error =>
      raise transaction_failure(error)
    error if @async.is_being_cancelled() => raise error
    error =>
      raise unexpected_cli_failure(
        "office.xlsx.batch_failed", "XLSX batch transaction", error,
      )
  }
  let destination = optional_value(matches, "out").unwrap_or(file)
  print_xlsx_batch_result(matches, destination, result)
}

///|
fn create_command() -> @argparse.Command {
  Command(
    "create",
    about="Create a validated Office document atomically",
    subcommand_required=true,
    subcommands=[
      Command(
        "xlsx",
        about="Create a validated workbook with one worksheet",
        positionals=[
          PositionArg(
            "output",
            about="new .xlsx destination",
            num_args=@argparse.ValueRange::single(),
          ),
        ],
        options=[
          OptionArg(
            "sheet",
            long="sheet",
            about="name of the first worksheet",
            default_values=["Sheet1"],
          ),
        ],
        flags=[
          FlagArg(
            "dry-run",
            long="dry-run",
            about="validate without publishing",
          ),
          FlagArg(
            "overwrite",
            long="overwrite",
            about="replace an existing regular-file destination",
          ),
          FlagArg("json", long="json", about="print office.output/1 JSON"),
        ],
      ),
      Command(
        "docx",
        about="Create a validated blank Word document",
        positionals=[
          PositionArg(
            "output",
            about="new .docx destination",
            num_args=@argparse.ValueRange::single(),
          ),
        ],
        flags=[
          FlagArg(
            "dry-run",
            long="dry-run",
            about="validate without publishing",
          ),
          FlagArg(
            "overwrite",
            long="overwrite",
            about="replace an existing regular-file destination",
          ),
          FlagArg("json", long="json", about="print office.output/1 JSON"),
        ],
      ),
    ],
  )
}

///|
fn batch_command() -> @argparse.Command {
  Command(
    "batch",
    about="Apply a strict operation script transactionally (XLSX mutate, DOCX fresh author)",
    positionals=[
      PositionArg(
        "target",
        about="XLSX: existing .xlsx package to mutate; DOCX (--format docx): new .docx destination",
        num_args=@argparse.ValueRange::single(),
      ),
      PositionArg(
        "script",
        about="strict xlsx.batch/2 preferred (XLSX; v1 retained) or docx.batch/2 (DOCX) JSON script",
        num_args=@argparse.ValueRange::single(),
      ),
    ],
    options=[
      OptionArg(
        "format",
        long="format",
        about="xlsx (default, mutate existing) or docx (fresh authoring)",
        default_values=["xlsx"],
      ),
      OptionArg(
        "out",
        long="out",
        about="XLSX only: publish to a separate .xlsx path instead of replacing the input",
      ),
    ],
    flags=[
      FlagArg("dry-run", long="dry-run", about="validate without publishing"),
      FlagArg(
        "overwrite",
        long="overwrite",
        about="allow replacement of an existing destination",
      ),
      FlagArg("json", long="json", about="print office.output/1 JSON"),
    ],
  )
}

///|
/// Dispatches `office batch` by --format: docx fresh authoring vs the default
/// XLSX mutation path.
async fn run_batch(matches : @argparse.Matches) -> Unit {
  match optional_value(matches, "format").unwrap_or("xlsx") {
    "docx" => run_docx_batch(matches)
    "xlsx" => run_xlsx_batch(matches)
    other =>
      raise CliFailure(
        @lib.protocol_error(
          "office.invalid_arguments",
          "unsupported --format '" +
          bounded_text(other, 40) +
          "' (expected xlsx or docx)",
        ),
      )
  }
}

///|
async fn run_create(matches : @argparse.Matches) -> Unit {
  match matches.subcommand {
    Some(("xlsx", values)) => run_create_xlsx(values)
    Some(("docx", values)) => run_create_docx(values)
    _ =>
      raise CliFailure(
        @lib.protocol_error(
          "office.invalid_arguments", "create requires a document format (xlsx or docx)",
        ),
      )
  }
}