// Copyright 2026 OfficeCLI (https://OfficeCLI.AI)
// SPDX-License-Identifier: Apache-2.0
using System.Text.Json;
namespace OfficeCli.Core;
///
/// Public entry point for running a batch against an already-open
/// without going through a CLI process or
/// resident pipe — for embedders that link officecli directly (in-process
/// hosts). Reuses the same batch-item dispatch and JSON
/// envelope shape as the CLI `batch` command and the Node/Python SDKs, so a
/// caller sees one consistent protocol regardless of transport.
///
public static class BatchExecutor
{
///
/// Runs a batch and returns EXACTLY what the CLI `batch` command writes to
/// stdout, so an in-process host is byte-identical to the CLI (and to
/// what the watch server relays): with the
/// `{success, data:{results, summary}}` envelope, without it the
/// `[i] ...\n\nBatch complete: ...` text. This is the same output path the
/// CLI command uses (RunNonResidentBatch → PrintBatchResults → WrapEnvelope,
/// see CommandBuilder.Batch.cs); the earlier shortcut here emitted a bare
/// results array with `json` hardcoded, diverging from every other transport.
///
/// A JSON array of batch items — the same shape documented
/// for the CLI `batch` command and the `BatchItem` SDK type.
/// Mirrors the CLI `--json` toggle: structured envelope vs plain text.
public static string ExecuteBatch(IDocumentHandler handler, string itemsJson, bool json, bool stopOnError = false)
{
try
{
var items = JsonSerializer.Deserialize(itemsJson, BatchJsonContext.Default.ListBatchItem) ?? new List();
var unrecognizedLatex = new List();
var results = CommandBuilder.RunNonResidentBatch(handler, items, stopOnError, json, unrecognizedLatex);
// Reuse the CLI's own body formatter so both modes are byte-identical.
using var sw = new System.IO.StringWriter();
CommandBuilder.PrintBatchResults(results, json, items.Count, sw);
var inner = sw.ToString().TrimEnd('\n', '\r');
if (!json) return inner;
// Same outer envelope the CLI batch command applies: unrecognized-LaTeX
// warnings + outer success true only when every step succeeded
// (CommandBuilder.Batch.cs). Judgment command — a single failed step
// flips outer success to false.
var warnings = new List();
foreach (var tok in unrecognizedLatex)
warnings.Add(new CliWarning
{
Message = $"unrecognized_latex_command: {tok}",
Code = "unrecognized_latex_command",
Suggestion = "Check the command spelling; see https://katex.org/docs/supported.html for supported syntax.",
});
var success = results.Count == 0 || !results.Any(r => !r.Success);
return OutputFormatter.WrapEnvelope(inner, warnings, success);
}
catch (Exception ex)
{
return RenderTopLevelError(ex, json);
}
}
///
/// Mirrors the SDKs' send(item) (as distinct from batch(items)):
/// runs ONE batch-shaped item and returns EXACTLY what the CLI single command
/// writes to stdout — with the standalone command's
/// envelope (WrapEnvelopeText's `{success,data,message}` for text commands
/// like set/add/remove, WrapEnvelope's `{success,data}` for data commands like
/// get/query), without it the plain text. A failure throws instead of being
/// captured per-item, matching the standalone command's exit contract.
///
/// A single batch item object — the same shape as one
/// entry in 's array, or the SDKs' `send(item)` argument.
/// Mirrors the CLI `--json` toggle: structured envelope vs plain text.
public static string ExecuteSend(IDocumentHandler handler, string itemJson, bool json)
{
try
{
var item = JsonSerializer.Deserialize(itemJson, BatchJsonContext.Default.BatchItem)
?? throw new ArgumentException("send: empty item");
var inner = CommandBuilder.ExecuteBatchItem(handler, item, json);
if (!json) return inner;
// Match the standalone command's envelope by inner shape — a JSON payload
// (get/query/view/validate/…) wraps with WrapEnvelope; a plain-text
// message (set/add/remove/…) wraps with WrapEnvelopeText (which adds the
// `message` field the CLI emits). Content-based so a new command inherits
// the right envelope without a hand-kept command→envelope map.
var trimmed = inner.TrimStart();
return trimmed.StartsWith('{') || trimmed.StartsWith('[')
? OutputFormatter.WrapEnvelope(inner)
: OutputFormatter.WrapEnvelopeText(inner);
}
catch (Exception ex)
{
return RenderTopLevelError(ex, json);
}
}
///
/// Mirror of the CLI's shared WriteError(ex, json) (CommandBuilder):
/// in JSON mode a failed command puts the {success:false, error:{…}}
/// envelope on stdout — return it byte-identically. In text mode the CLI
/// writes to stderr and leaves stdout empty; with no stderr channel in-process
/// we surface the error by propagating it (the embedder host's failure
/// signal), matching the prior throw-on-failure contract.
///
private static string RenderTopLevelError(Exception ex, bool json)
{
// CONSISTENCY(error-wrap): same friendlier rendering the CLI applies to a
// bare XmlException from an externally-corrupted OOXML part.
var rendered = ex is System.Xml.XmlException xe
? new System.IO.InvalidDataException(
$"Malformed XML in document part: {xe.Message} " +
$"(the file appears to have a corrupted OOXML part).", xe)
: ex;
if (json) return OutputFormatter.WrapErrorEnvelope(rendered);
throw rendered;
}
}