import type { Alepha, Static, TObject } from "alepha";
import { useAlepha } from "alepha/react";
import { useEffect, useState } from "react";
import { useRouter } from "./useRouter.ts";

/**
 * Hook to manage query parameters in the URL using a defined schema.
 *
 * Two storage formats are supported via {@link UseQueryParamsHookOptions.format}:
 * - `"base64"` (default) packs the whole object into a single opaque param
 *   (named by `key`, default `q`) — e.g. `?q=eyJ0YWIiOiJzZWN1cml0eSJ9`.
 * - `"querystring"` spreads each schema field across its own readable param —
 *   e.g. `?tab=security`. In this mode `key` is ignored.
 *
 * By default the URL is updated with `replaceState` (no new history entry).
 * Pass `push: true` to add a history entry instead, so the browser back
 * button steps back through previous values.
 *
 * @example
 * const [params, setParams] = useQueryParams(
 *   z.object({ tab: z.string().optional() }),
 *   { format: "querystring" },
 * );
 * // params.tab reads from `?tab=…`; setParams({ tab: "security" }) writes it.
 */
export const useQueryParams = <T extends TObject>(
  schema: T,
  options: UseQueryParamsHookOptions = {},
): [Partial<Static<T>>, (data: Static<T>) => void] => {
  const alepha = useAlepha();
  const router = useRouter();

  const key = options.key ?? "q";
  const format = options.format ?? "base64";

  // The slice of the URL this hook reads from: a single opaque param in
  // base64 mode, or the schema's own field names in querystring mode.
  const read = (): Partial<Static<T>> | undefined =>
    format === "querystring"
      ? decodeQueryString(alepha, schema, router.query)
      : decodeBase64(alepha, schema, router.query[key]);

  // A primitive the effect can depend on so it re-syncs only when the
  // relevant part of the URL actually changes.
  const signature =
    format === "querystring"
      ? Object.keys(schema.properties)
          .map((name) => `${name}=${router.query[name] ?? ""}`)
          .join("&")
      : router.query[key];

  const [queryParams = {}, setQueryParams] = useState<
    Partial<Static<T>> | undefined
  >(read());

  useEffect(() => {
    setQueryParams(read());
  }, [signature]);

  return [
    queryParams,
    (next: Static<T>) => {
      setQueryParams(next);
      router.setQueryParams(
        (data) =>
          format === "querystring"
            ? writeQueryString(alepha, schema, data, next)
            : { ...data, [key]: encodeBase64(alepha, schema, next) },
        { push: options.push },
      );
    },
  ];
};

// ---------------------------------------------------------------------------------------------------------------------

export interface UseQueryParamsHookOptions {
  /**
   * How the params are stored in the URL.
   * - `"base64"` (default): the whole object in one opaque param (`key`).
   * - `"querystring"`: each schema field as its own readable param (`key`
   *   is ignored).
   */
  format?: "base64" | "querystring";
  /**
   * Param name used in `"base64"` format. Defaults to `"q"`. Ignored in
   * `"querystring"` format, where the schema's field names are used.
   */
  key?: string;
  /**
   * When `true`, updates push a new browser-history entry (`pushState`) so
   * the back button returns to the previous value. Defaults to `false`,
   * which replaces the current entry in place (`replaceState`).
   */
  push?: boolean;
}

const encodeBase64 = (alepha: Alepha, schema: TObject, data: any) => {
  return btoa(JSON.stringify(alepha.codec.decode(schema, data)));
};

const decodeBase64 = <T extends TObject>(
  alepha: Alepha,
  schema: T,
  data: any,
): Static<T> | undefined => {
  try {
    return alepha.codec.decode(
      schema,
      JSON.parse(atob(decodeURIComponent(data))),
    );
  } catch {
    return;
  }
};

/**
 * querystring read: each schema field is its own URL param. Mirrors the
 * server's per-key query decode — `codec.decode(propertySchema, rawString)`
 * coerces the raw string into the declared type (number, boolean, …).
 */
const decodeQueryString = <T extends TObject>(
  alepha: Alepha,
  schema: T,
  query: Record<string, any>,
): Partial<Static<T>> | undefined => {
  try {
    const out: Record<string, any> = {};
    for (const name of Object.keys(schema.properties)) {
      const raw = query[name];
      if (raw != null && raw !== "") {
        out[name] = alepha.codec.decode(schema.properties[name], raw);
      }
    }
    return out as Partial<Static<T>>;
  } catch {
    return;
  }
};

/**
 * querystring write: spread the decoded object across individual params,
 * dropping empty values so the URL stays clean. Unrelated params (not part
 * of the schema) are preserved.
 */
const writeQueryString = (
  alepha: Alepha,
  schema: TObject,
  current: Record<string, any>,
  next: any,
): Record<string, any> => {
  const decoded = alepha.codec.decode(schema, next) as Record<string, any>;
  const merged = { ...current };
  for (const name of Object.keys(schema.properties)) {
    const value = decoded?.[name];
    if (value == null || value === "") {
      delete merged[name];
    } else {
      merged[name] = String(value);
    }
  }
  return merged;
};
