/*
 * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
 * @generated-id: f6fbdf36f7d2
 */

// Not needed if lax mode
import * as z from "zod/v4";
import { isUnknown as isDiscriminatedUnionUnknown } from "./discriminatedUnion.js";
import { RFCDate } from "./rfcdate.js";
import { startCountingUnrecognized } from "./unrecognized.js";

interface Candidate {
  data: unknown;
  /** Total number of fields in the parsed data */
  fieldCount: number;
  /** Number of fields which only matched due to lax parsing */
  inexactCount: number;
  /** Number of fields which defaulted to zero values */
  zeroDefaultCount: number;
}

/**
 * Smart union parser that tries all schemas and returns the best match
 * based on the number of populated fields.
 */
export function smartUnion<
  Options extends readonly [z.ZodType, z.ZodType, ...z.ZodType[]],
>(
  options: Options,
): z.ZodType<z.output<Options[number]>, z.input<Options[number]>> {
  return z.unknown().transform((input, ctx) => {
    const candidates: Candidate[] = [];
    const errors: z.ZodIssue[][] = options.map(() => []);

    const parentUnrecognizedCtr = startCountingUnrecognized();

    // Filter out invalid options
    for (const [i, option] of options.entries()) {
      const unrecognizedCtr = startCountingUnrecognized();

      const result = option.safeParse(input);
      const inexactCount = unrecognizedCtr.end();
      const zeroDefaultCount = 0;
      if (result.success) {
        candidates.push({
          data: result.data,
          inexactCount,
          zeroDefaultCount,
          fieldCount: -1, // We'll count this later if needed
        });
        continue;
      }
      errors[i]!.push(...result.error.issues);
    }

    // No valid options
    if (candidates.length === 0) {
      parentUnrecognizedCtr.end(0);
      ctx.addIssue({ input: input, code: "invalid_union", errors: errors });
      return z.NEVER;
    }

    let best = candidates[0]!;

    // Find the best option
    for (const candidate of candidates) {
      // Minor optimization to avoid counting fields if there's only one candidate
      if (candidates.length > 1) {
        candidate.fieldCount = countFieldsRecursive(candidate.data);
      }
      best = better(candidate, best);
    }

    // The cost of this union should be the cost of the best candidate not all the candidates
    parentUnrecognizedCtr.end(best.inexactCount);

    return best.data;
  }) as any;
}

function better(a: Candidate, b: Candidate): Candidate {
  // First prefer exact matches over inexact ones
  const aIsExact = a.inexactCount === 0;
  const bIsExact = b.inexactCount === 0;
  if (aIsExact !== bIsExact) {
    return aIsExact ? a : b;
  }

  // Then compare field counts
  const actualFieldCountA = a.fieldCount - a.zeroDefaultCount;
  const actualFieldCountB = b.fieldCount - b.zeroDefaultCount;
  if (actualFieldCountA !== actualFieldCountB) {
    return actualFieldCountA > actualFieldCountB ? a : b;
  }
  return a.inexactCount < b.inexactCount ? a : b;
}

/**
 * Counts the number of fields in a parsed value recursively.
 * @param `parsed` assumed to *not* contain cycles
 * fieldCount: total number of fields found
 * inexactCount: number of primitive values that are not unrecognized enum values
 */
function countFieldsRecursive(parsed: unknown): number {
  let fieldCount = 0;

  const queue: unknown[] = [parsed];
  let index = 0;

  while (index < queue.length) {
    const value = queue[index++];
    if (value === undefined || isDiscriminatedUnionUnknown(value)) {
      continue;
    }

    // Check if it's a primitive value
    const type = typeof value;
    if (
      value === null
      || type === "number"
      || type === "string"
      || type === "boolean"
      || type === "bigint"
      || value instanceof Date
      || value instanceof RFCDate
    ) {
      fieldCount++;
      continue;
    }

    // Handle arrays
    if (Array.isArray(value)) {
      queue.push(...value);
      continue;
    }

    // Handle objects
    if (type === "object") {
      queue.push(...Object.values(value));
    }
  }

  return fieldCount;
}
