import * as uuid from "uuid";
import { z } from "zod";

import {
  get_empty_datum_for,
  type Presentation,
} from "./presentation.svelte.ts";

type Assert<_T extends true> = void;

export const all_datum_tags = [
  "Numeric",
  "Text",
  "Timestamp",
  "Uuid",
] as const;

// Type checking to ensure that all valid enum tags are included.
type _DatumTagsAssertionA = Assert<
  Datum["t"] extends (typeof all_datum_tags)[number] ? true : false
>;
type _DatumTagsAssertionB = Assert<
  (typeof all_datum_tags)[number] extends Datum["t"] ? true : false
>;

const NUMERIC_REGEX = /^-?[0-9]+(\.[0-9]+)?$/;

const datum_numeric_schema = z.object({
  t: z.literal("Numeric"),
  // The `bigdecimal` Rust library serializes values to strings by default.
  // Doing the same here helps to avoid weird floating point conversion errors
  // when displaying.
  c: z.string().regex(NUMERIC_REGEX).nullish().transform((x) => x ?? undefined),
});

const datum_text_schema = z.object({
  t: z.literal("Text"),
  c: z.string().nullish().transform((x) => x ?? undefined),
});

const datum_timestamp_schema = z.object({
  t: z.literal("Timestamp"),
  c: z.coerce.date().nullish().transform((x) => x ?? undefined),
});

const datum_uuid_schema = z.object({
  t: z.literal("Uuid"),
  c: z.string().nullish().transform((x) => x ?? undefined),
});

export const datum_schema = z.union([
  datum_numeric_schema,
  datum_text_schema,
  datum_timestamp_schema,
  datum_uuid_schema,
]);

export type Datum = z.infer<typeof datum_schema>;

/**
 * Attempt to parse a Datum from a textual representation, for example when
 * pasting values from the clipboard. If unsuccessful, returns an empty Datum.
 */
export function parse_datum_from_text(
  presentation: Presentation,
  input: string,
): Datum {
  if (presentation.t === "Dropdown") {
    if (
      presentation.c.allow_custom ||
      presentation.c.options.some(({ value }) => value === input)
    ) {
      return { t: "Text", c: input };
    } else {
      return get_empty_datum_for(presentation);
    }
  } else if (presentation.t === "Numeric") {
    // For now, commas and spaces are accepted as thousands-separators, but full
    // stops are not. Only full stops are allowed as the decimal separator. This
    // is about as flexible as parsing can be with regards to international
    // standards from an English-centric perspective, without resorting to
    // troublesome heuristics to disambiguate between thousands-separators and
    // decimal separators.
    const input_cleaned = input.trim().replace(/^\+/, "").replace(
      /(?:,|\s+)([0-9]{3})(,|.|\s|$)/g,
      "$1$2",
    );
    console.log(NUMERIC_REGEX.test(input_cleaned));
    return {
      t: "Numeric",
      c: NUMERIC_REGEX.test(input_cleaned) ? input_cleaned : undefined,
    };
  } else if (presentation.t === "Text") {
    return { t: "Text", c: input };
  } else if (presentation.t === "Timestamp") {
    // TODO: implement
    console.warn("parsing timestamps from clipboard is not yet supported");
    return get_empty_datum_for(presentation);
  } else if (presentation.t === "Uuid") {
    try {
      return {
        t: "Uuid",
        c: uuid.stringify(uuid.parse(input)),
      };
    } catch {
      // uuid.parse() throws a TypeError if unsuccessful.
      return get_empty_datum_for(presentation);
    }
  }
  type _ = Assert<typeof presentation["t"] extends never ? true : false>;
  throw new Error("should be unreachable");
}
