import { z } from "zod";

export function parseBody<T>(
  schema: z.ZodType<T>,
  body: unknown,
): { data: T } | { error: string } {
  try {
    return { data: schema.parse(body) };
  } catch (e) {
    if (e instanceof z.ZodError) {
      return {
        error: e.issues.map((err: z.ZodIssue) => err.message).join(", "),
      };
    }
    return { error: "Neplatný požadavek" };
  }
}

// ── Shared form-coercion helpers ──────────────────────────────────────────
// HTTP bodies arrive as strings (multipart) or numbers (JSON). These helpers
// coerce number|string → number with a NaN/Infinity guard, replacing the
// `z.union([z.number(), z.string()]).transform(Number)` idiom that was
// duplicated ~150× across schemas and silently produced NaN on bad input
// (which then flowed into Prisma / business math). User-facing messages Czech.

const coerceNum = (v: unknown): number =>
  typeof v === "number"
    ? v
    : typeof v === "string" && v.trim() !== ""
      ? Number(v)
      : NaN;

/** number|string → number; rejects NaN/Infinity/empty. */
export const numberFromForm = z
  .union([z.number(), z.string()])
  .transform(coerceNum)
  .refine((n) => Number.isFinite(n), { message: "Neplatné číslo" });

/** number|string → number within [min,max]; rejects NaN/out-of-range. */
export const numberInRange = (min: number, max: number, message?: string) =>
  z
    .union([z.number(), z.string()])
    .transform(coerceNum)
    .refine((n) => Number.isFinite(n) && n >= min && n <= max, {
      message: message ?? `Číslo musí být mezi ${min} a ${max}`,
    });

/** number|string → non-negative number (e.g. quantities, prices, odometers). */
export const nonNegativeNumberFromForm = z
  .union([z.number(), z.string()])
  .transform(coerceNum)
  .refine((n) => Number.isFinite(n) && n >= 0, {
    message: "Číslo nesmí být záporné",
  });

/** number|string → positive number ( > 0 ). */
export const positiveNumberFromForm = z
  .union([z.number(), z.string()])
  .transform(coerceNum)
  .refine((n) => Number.isFinite(n) && n > 0, {
    message: "Číslo musí být kladné",
  });

/** number|string → non-negative integer (e.g. list positions). */
export const nonNegativeIntFromForm = z
  .union([z.number(), z.string()])
  .transform(coerceNum)
  .refine((n) => Number.isInteger(n) && n >= 0, {
    message: "Číslo musí být nezáporné celé číslo",
  });

/** number|string → positive integer id; rejects NaN/≤0/non-integer. */
export const intIdFromForm = z
  .union([z.number(), z.string()])
  .transform(coerceNum)
  .refine((n) => Number.isInteger(n) && n > 0, { message: "Neplatné ID" });

/** number|string|null → number|null; rejects NaN when a value is present. */
export const nullableNumberFromForm = z
  .union([z.number(), z.string(), z.null()])
  .transform((v) => (v === null || v === "" ? null : coerceNum(v)))
  .refine((n) => n === null || Number.isFinite(n), {
    message: "Neplatné číslo",
  });

/** number|string|null → positive-int id | null; rejects NaN/≤0 when present. */
export const nullableIntIdFromForm = z
  .union([z.number(), z.string(), z.null()])
  .transform((v) => (v === null || v === "" ? null : coerceNum(v)))
  .refine((n) => n === null || (Number.isInteger(n) && n > 0), {
    message: "Neplatné ID",
  });

/** Truthy form flag → boolean: true | 1 | "1" | "true" → true. */
export const booleanFromForm = z.preprocess(
  (v) => v === true || v === 1 || v === "1" || v === "true",
  z.boolean(),
);

/**
 * YYYY-MM-DD date string. Tolerant of a trailing time component: an edit form
 * round-trips a `@db.Date` that the `Date.toJSON` override serialises as
 * "YYYY-MM-DDT00:00:00", so we strip anything after the date before validating
 * (and the validated output is always the date-only `YYYY-MM-DD`).
 */
export const isoDateString = z.preprocess(
  (v) => (typeof v === "string" ? v.slice(0, 10) : v),
  z
    .string()
    .regex(/^\d{4}-\d{2}-\d{2}$/, "Datum musí být ve formátu YYYY-MM-DD"),
);

/** HH:MM (24h) time string. Tolerant of a trailing ":ss" seconds component. */
export const timeString = z.preprocess(
  (v) =>
    typeof v === "string" && /^\d{2}:\d{2}:\d{2}/.test(v) ? v.slice(0, 5) : v,
  z
    .string()
    .regex(/^([01]\d|2[0-3]):[0-5]\d$/, "Čas musí být ve formátu HH:MM"),
);

/**
 * Combined local datetime string "YYYY-MM-DDTHH:MM" — the wire format the
 * attendance forms produce by joining a date input and a time input
 * (`${date}T${time}:00`). Tolerant of a trailing seconds component (":ss" —
 * stripped before validating), because the client appends ":00" and an edit
 * form round-trips a value the `Date.toJSON` override serialised as
 * "YYYY-MM-DDTHH:MM:SS". The service parses the value with `new Date(...)`
 * as LOCAL time (Europe/Prague) — no timezone suffix is part of the format.
 */
export const dateTimeString = z.preprocess(
  (v) =>
    typeof v === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(v)
      ? v.slice(0, 16)
      : v,
  z
    .string()
    .regex(
      /^\d{4}-\d{2}-\d{2}T([01]\d|2[0-3]):[0-5]\d$/,
      "Datum a čas musí být ve formátu YYYY-MM-DD HH:MM",
    ),
);

/**
 * Optional-datetime variant: ""/null mean "not set" → null (the attendance
 * forms historically submitted empty strings for unfilled time fields, and a
 * hard reject would 400 the whole form — including leave records that have no
 * times at all). Pair with `.optional()` at the call site for fields that may
 * be absent entirely (absent stays `undefined`, e.g. "don't change" on update).
 */
export const nullableDateTimeString = z.preprocess(
  (v) => (v === "" || v === null ? null : v),
  z.union([z.null(), dateTimeString]),
);

/**
 * An email field that also accepts an empty string (meaning "not set"). Many
 * settings/supplier forms submit "" for un-filled optional emails; a bare
 * `.email()` would 400 the whole form. Pair with `.nullish()` at the call site.
 */
export const emailOrEmpty = z.union([
  z.literal(""),
  z.string().max(255).email("Neplatný email"),
]);

/**
 * Shared rich-text document section — offers' scope_sections and issued
 * orders' issued_order_sections are 1:1 mirror tables, so both schemas use
 * this. Limits are DB-aligned: title/title_cz are VarChar(500) (the old
 * offers-local copy under-shot at 255), content is Text with the app-level
 * 8000 cap used by the other rich-text fields.
 */
export const DocumentSectionSchema = z.object({
  title: z.string().max(500).nullish(),
  title_cz: z.string().max(500).nullish(),
  content: z.string().max(8000).nullish(),
  position: nonNegativeIntFromForm.optional(),
});
