// src/utils/zodUtils.ts
import { z } from 'zod';

/**
 * A Zod schema for a required, non-empty string.
 * @param message The error message to display if the string is empty or missing.
 * @param maxLength Optional maximum length (defaults to 255).
 * @returns A Zod string schema.
 */
export const requiredString = (message: string, maxLength = 255) =>
  z.preprocess(
    // If the value is null or undefined, preprocess it to an empty string.
    // This ensures that the subsequent `.min(1)` check will catch missing required fields.
    (val) => val ?? '',
    // Now, validate that the (potentially preprocessed) value is a string that, after trimming, has at least 1 character.
    z.string().trim().min(1, message).max(maxLength, `Must be ${maxLength} characters or less.`),
  );

/**
 * Creates a Zod schema for a numeric ID in request parameters.
 * @param paramName The name of the parameter (e.g., 'id').
 * @param message The error message for invalid input.
 * @returns A Zod object schema for the params.
 */
export const numericIdParam = (
  paramName: string,
  message = `Invalid ID for parameter '${paramName}'. Must be a number.`,
) =>
  z.object({
    params: z.object({
      [paramName]: z.coerce.number().int(message).positive(message),
    }),
  });

/**
 * Creates a Zod schema for a UUID in request parameters.
 * @param paramName The name of the parameter (e.g., 'id').
 * @param message The error message for invalid input.
 * @returns A Zod object schema for the params.
 */
export const uuidParamSchema = (paramName: string, message: string) =>
  z.object({
    params: z.object({
      [paramName]: z.string().uuid(message),
    }),
  });

/**
 * Creates a Zod schema for an optional, numeric query parameter that is coerced from a string.
 * @param options Configuration for the validation like default value, min/max, and integer constraints.
 * @returns A Zod schema for the number.
 */
export const optionalNumeric = (
  options: {
    default?: number;
    min?: number;
    max?: number;
    integer?: boolean;
    positive?: boolean;
    nonnegative?: boolean;
  } = {},
) => {
  let numberSchema = z.coerce.number();

  if (options.integer) numberSchema = numberSchema.int();
  if (options.positive) numberSchema = numberSchema.positive();
  else if (options.nonnegative) numberSchema = numberSchema.nonnegative();

  if (options.min !== undefined) numberSchema = numberSchema.min(options.min);
  if (options.max !== undefined) numberSchema = numberSchema.max(options.max);

  // Make the number schema optional *before* preprocessing. This allows it to correctly handle
  // the `undefined` value that our preprocessor generates from `null`.
  const optionalNumberSchema = numberSchema.optional();

  // This is crucial because z.coerce.number(null) results in 0, which bypasses
  // the .optional() and .default() logic for null inputs. We want null to be
  // treated as "not provided", just like undefined.
  const schema = z.preprocess((val) => (val === null ? undefined : val), optionalNumberSchema);

  if (options.default !== undefined) return schema.default(options.default);

  return schema;
};

/**
 * Creates a Zod schema for an optional date string in YYYY-MM-DD format.
 * @param message Optional custom error message.
 * @returns A Zod schema for the date string.
 */
export const optionalDate = (message?: string) => z.string().date(message).optional();

/**
 * Creates a Zod schema for an optional boolean query parameter that is coerced from a string.
 * Handles 'true', '1' as true and 'false', '0' as false.
 * @param options Configuration for the validation like default value.
 * @returns A Zod schema for the boolean.
 */
export const optionalBoolean = (
  options: {
    default?: boolean;
  } = {},
) => {
  const schema = z.preprocess((val) => {
    if (val === 'true' || val === '1') return true;
    if (val === 'false' || val === '0') return false;
    if (val === '' || val === null) return undefined; // Treat empty string and null as not present
    return val;
  }, z.boolean().optional());

  if (options.default !== undefined) {
    return schema.default(options.default);
  }

  return schema;
};

/**
 * Creates a Zod schema for an optional string.
 * Treats null as undefined so it can be properly handled as optional.
 * @returns A Zod schema for an optional string.
 */
export const optionalString = () =>
  z.preprocess((val) => (val === null ? undefined : val), z.string().optional());

/**
 * Creates a Zod schema for a required HTTP/HTTPS URL.
 * Validates that the URL starts with http:// or https:// to match database constraints.
 * @param message Optional custom error message.
 * @returns A Zod schema for the URL string.
 */
export const httpUrl = (message = 'Must be a valid HTTP or HTTPS URL') =>
  z
    .string()
    .url(message)
    .regex(/^https?:\/\/.+/, message);

/**
 * Creates a Zod schema for an optional HTTP/HTTPS URL.
 * Validates that if provided, the URL starts with http:// or https://.
 * @param message Optional custom error message.
 * @returns A Zod schema for the optional URL string.
 */
export const optionalHttpUrl = (message = 'Must be a valid HTTP or HTTPS URL') =>
  z.preprocess(
    (val) => (val === null ? undefined : val),
    z
      .string()
      .url(message)
      .regex(/^https?:\/\/.+/, message)
      .optional(),
  );
