/**
 * @fileoverview Helpers for the "partial success" tool-output pattern: a tool
 * that processes N items and returns the ones that succeeded plus a structured
 * list of the ones that failed (each with a stable `reason` enum). Generalizes
 * the pattern exemplified by pubmed-mcp-server's `unavailable[]` array.
 * @module src/utils/formatting/partialResult
 */
import { z } from 'zod';
/**
 * Builds a Zod schema for one entry in a partial-failure list.
 *
 * The shape is `{ [idKey]: string; reason: TReason; detail?: string }` —
 * the `idKey` field identifies the input the failure pertains to, `reason`
 * is a stable enum the caller can switch on, and `detail` is an optional
 * human-readable supplement.
 *
 * @example
 * ```ts
 * const UnavailableReason = z.enum([
 *   'no_pmc_fallback_disabled',
 *   'no_doi',
 *   'no_oa',
 *   'fetch_failed',
 *   'parse_failed',
 *   'service_error',
 * ]).describe('Why the PMID could not be returned');
 *
 * const Unavailable = failureEntrySchema({
 *   idKey: 'pmid',
 *   idDescription: 'PMID full text could not be returned for',
 *   reason: UnavailableReason,
 *   description: 'A PMID that could not be returned, with an explanation',
 * });
 * // → ZodObject<{ pmid: ZodString; reason: typeof UnavailableReason; detail: ZodOptional<ZodString> }>
 * ```
 */
export function failureEntrySchema(opts) {
    const idField = z.string().describe(opts.idDescription ?? `Identifier (${opts.idKey})`);
    const detailField = z
        .string()
        .optional()
        .describe(opts.detailDescription ?? 'Additional human-readable context, when available');
    // Build the shape with the dynamic id key. Cast through `unknown` because TS
    // can't narrow the index signature to the literal `TIdKey` without help.
    const shape = {
        [opts.idKey]: idField,
        reason: opts.reason,
        detail: detailField,
    };
    const schema = z.object(shape);
    return opts.description ? schema.describe(opts.description) : schema;
}
/**
 * Standard tool-output shape for batch operations that may partially fail.
 *
 * Composes a typed succeeded list, totals, and an optional failures list with
 * stable reason codes. Wraps `failureEntrySchema` so callers don't have to
 * compose three things.
 *
 * @example
 * ```ts
 * const ArticleSchema = z.object({ pmid: z.string(), title: z.string() });
 *
 * export const fetchTool = tool('fetch_articles', {
 *   // ...
 *   output: partialResultSchema({
 *     succeededKey: 'articles',
 *     succeededSchema: ArticleSchema,
 *     failedKey: 'unavailable',
 *     idKey: 'pmid',
 *     reason: z.enum(['not_found', 'withdrawn']),
 *   }),
 *   // → { articles: ArticleSchema[], totalReturned: number,
 *   //     unavailable?: [{pmid, reason, detail?}], totalFailed?: number }
 * });
 * ```
 */
export function partialResultSchema(opts) {
    const failureSchema = failureEntrySchema({
        idKey: opts.idKey,
        ...(opts.idDescription !== undefined && { idDescription: opts.idDescription }),
        reason: opts.reason,
        ...(opts.failureDescription !== undefined && { description: opts.failureDescription }),
    });
    const succeededField = z
        .array(opts.succeededSchema)
        .describe(opts.succeededDescription ?? `Successful items (${opts.succeededKey})`);
    const totalSucceededField = z
        .number()
        .int()
        .nonnegative()
        .describe(`Number of successful items in '${opts.succeededKey}'`);
    const failedField = z
        .array(failureSchema)
        .optional()
        .describe(`Per-input explanations for inputs that could not be returned. Absent when nothing failed.`);
    const baseShape = {
        [opts.succeededKey]: succeededField,
        totalSucceeded: totalSucceededField,
        [opts.failedKey]: failedField,
    };
    if (opts.includeTotalFailed) {
        baseShape.totalFailed = z
            .number()
            .int()
            .nonnegative()
            .optional()
            .describe(`Number of failed inputs in '${opts.failedKey}'`);
    }
    return z.object(baseShape);
}
/**
 * Constructs a partial-result object at runtime, conditionally including the
 * failed array (omits it when empty so the structuredContent stays clean).
 *
 * @example
 * ```ts
 * return partialResult({
 *   succeededKey: 'articles',
 *   succeeded: articles,
 *   failedKey: 'unavailable',
 *   failed: unavailable,
 * });
 * // → { articles: [...], totalSucceeded: 3, unavailable: [...] }
 * // or, when unavailable is empty:
 * // → { articles: [...], totalSucceeded: 3 }
 * ```
 */
export function partialResult(opts) {
    const out = {
        [opts.succeededKey]: opts.succeeded,
        totalSucceeded: opts.succeeded.length,
    };
    if (opts.failed.length > 0) {
        out[opts.failedKey] = opts.failed;
        if (opts.includeTotalFailed) {
            out.totalFailed = opts.failed.length;
        }
    }
    else if (opts.includeTotalFailed) {
        out.totalFailed = 0;
    }
    return out;
}
//# sourceMappingURL=partialResult.js.map