import { z } from "zod";
import type { FacetDef, FilterValue, SearchParams } from "./types";

/**
 * Rohe searchParams, wie Next.js sie liefert: jeder Wert kann fehlen, ein
 * String oder ein String-Array sein.
 */
export type RawSearchParams = Record<
  string,
  string | string[] | undefined
>;

const uuidRe =
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

const numberRe = /^-?\d+(\.\d+)?$/;

/** Nimmt bei String-Array den ersten Eintrag; normalisiert undefined/"". */
function first(v: string | string[] | undefined): string | undefined {
  const s = Array.isArray(v) ? v[0] : v;
  return s ?? undefined;
}

function parseNum(s: string): number | undefined {
  if (!numberRe.test(s)) return undefined;
  const n = Number(s);
  return Number.isFinite(n) ? n : undefined;
}

/**
 * Parst eine `lo..hi`-Range. Beide Seiten optional, aber mindestens eine muss
 * gesetzt und gültig sein. Liefert `undefined`, wenn nichts Gültiges entsteht.
 */
function parseRange(raw: string): { gte?: number; lte?: number } | undefined {
  if (!raw.includes("..")) return undefined;
  const parts = raw.split("..");
  const loRaw = parts[0] ?? "";
  const hiRaw = parts[1] ?? "";
  const out: { gte?: number; lte?: number } = {};
  if (loRaw !== "") {
    const lo = parseNum(loRaw);
    if (lo === undefined) return undefined;
    out.gte = lo;
  }
  if (hiRaw !== "") {
    const hi = parseNum(hiRaw);
    if (hi === undefined) return undefined;
    out.lte = hi;
  }
  if (out.gte === undefined && out.lte === undefined) return undefined;
  if (out.gte !== undefined && out.lte !== undefined && out.gte > out.lte) {
    return undefined;
  }
  return out;
}

function parseBool(raw: string): boolean | undefined {
  if (raw === "ja") return true;
  if (raw === "nein") return false;
  return undefined;
}

/**
 * Parst die rohen searchParams in typisierte `SearchParams`. Die `facets`
 * liefern Typ und (für enum) gültige Optionen je `merkmalId`. ALLES Ungültige
 * wird STILL verworfen (kein Throw) — die UI bleibt robust gegen URL-Manipulation.
 *
 * Konvention: `q` = Freitext, `bereit=1` = nur einsatzbereit,
 * `f.<merkmalId>=...` = Filter (number `lo..hi`, enum CSV, boolean `ja/nein`).
 */
export function parseSearchParams(
  raw: RawSearchParams,
  facets: FacetDef[],
): SearchParams {
  const facetById = new Map<string, FacetDef>(
    facets.map((f) => [f.merkmalId, f]),
  );

  const qRaw = first(raw.q)?.trim();
  const q = qRaw ? qRaw : undefined;
  const nurEinsatzbereit = first(raw.bereit) === "1";

  const filter: FilterValue[] = [];
  for (const [key, value] of Object.entries(raw)) {
    if (!key.startsWith("f.")) continue;
    const merkmalId = key.slice(2);
    if (!uuidRe.test(merkmalId)) continue;
    const facet = facetById.get(merkmalId);
    if (!facet) continue;
    const v = first(value);
    if (v === undefined) continue;

    if (facet.typ === "number") {
      const range = parseRange(v);
      if (range) filter.push({ typ: "number", merkmalId, ...range });
    } else if (facet.typ === "boolean") {
      const b = parseBool(v);
      if (b !== undefined) filter.push({ typ: "boolean", merkmalId, eq: b });
    } else if (facet.typ === "enum") {
      const allowed = new Set((facet.optionen ?? []).map((o) => o.wert));
      const chosen = v
        .split(",")
        .map((s) => s.trim())
        .filter((s) => s !== "" && allowed.has(s));
      if (chosen.length > 0) filter.push({ typ: "enum", merkmalId, in: chosen });
    }
    // typ === "text" wird im Filter-UI nicht angeboten -> ignoriert.
  }

  return { q, nurEinsatzbereit, filter };
}

/**
 * Zod-Schema für den Geocode-Standort, der optional aus der URL kommen kann
 * (lat/lng). Bleibt streng, damit kein 500 entsteht.
 */
export const standortSchema = z
  .object({
    lat: z.coerce.number().min(-90).max(90),
    lng: z.coerce.number().min(-180).max(180),
  })
  .partial();
