const QUERY_VALUE_PREFIX = "~beignet-query:";

type EncodedQueryValue =
  | { type: "null" }
  | { type: "string"; value: string }
  | { type: "number"; value: number }
  | { type: "boolean"; value: boolean }
  | { type: "date"; value: string }
  | { type: "array"; value: EncodedQueryValue[] }
  | { type: "object"; value: Record<string, EncodedQueryValue> };

function encodeStructuredValue(
  value: unknown,
  ancestors: Set<object>,
): EncodedQueryValue {
  if (value === null) return { type: "null" };
  if (typeof value === "string") return { type: "string", value };
  if (typeof value === "number" && Number.isFinite(value)) {
    return { type: "number", value };
  }
  if (typeof value === "boolean") return { type: "boolean", value };
  if (value instanceof Date && Number.isFinite(value.getTime())) {
    return { type: "date", value: value.toISOString() };
  }
  if (typeof value !== "object" || value === null) {
    throw new TypeError("Query value is not serializable.");
  }
  if (ancestors.has(value)) {
    throw new TypeError("Query value contains a circular reference.");
  }

  ancestors.add(value);
  try {
    if (Array.isArray(value)) {
      return {
        type: "array",
        value: value.map((entry) => encodeStructuredValue(entry, ancestors)),
      };
    }

    const prototype = Object.getPrototypeOf(value);
    if (prototype !== Object.prototype && prototype !== null) {
      throw new TypeError("Query value contains a non-plain object.");
    }
    return {
      type: "object",
      value: Object.fromEntries(
        Object.entries(value)
          .filter(([, entry]) => entry !== undefined)
          .map(([key, entry]) => [
            key,
            encodeStructuredValue(entry, ancestors),
          ]),
      ),
    };
  } finally {
    ancestors.delete(value);
  }
}

function decodeStructuredValue(value: unknown): unknown {
  if (typeof value !== "object" || value === null || !("type" in value)) {
    throw new TypeError("Invalid encoded query value.");
  }
  const encoded = value as { type: unknown; value?: unknown };

  switch (encoded.type) {
    case "null":
      return null;
    case "string":
      if (typeof encoded.value === "string") return encoded.value;
      break;
    case "number":
      if (typeof encoded.value === "number" && Number.isFinite(encoded.value)) {
        return encoded.value;
      }
      break;
    case "boolean":
      if (typeof encoded.value === "boolean") return encoded.value;
      break;
    case "date": {
      if (typeof encoded.value !== "string") break;
      const date = new Date(encoded.value);
      if (Number.isFinite(date.getTime())) return date;
      break;
    }
    case "array":
      if (Array.isArray(encoded.value)) {
        return encoded.value.map(decodeStructuredValue);
      }
      break;
    case "object":
      if (
        typeof encoded.value === "object" &&
        encoded.value !== null &&
        !Array.isArray(encoded.value)
      ) {
        return Object.fromEntries(
          Object.entries(encoded.value).map(([key, entry]) => [
            key,
            decodeStructuredValue(entry),
          ]),
        );
      }
      break;
  }

  throw new TypeError("Invalid encoded query value.");
}

export function encodeQueryValue(value: unknown): string {
  if (typeof value === "string" && !value.startsWith(QUERY_VALUE_PREFIX)) {
    return value;
  }
  return `${QUERY_VALUE_PREFIX}${JSON.stringify(
    encodeStructuredValue(value, new Set()),
  )}`;
}

export function decodeQueryValue(value: string): unknown {
  if (!value.startsWith(QUERY_VALUE_PREFIX)) return value;

  try {
    return decodeStructuredValue(
      JSON.parse(value.slice(QUERY_VALUE_PREFIX.length)) as unknown,
    );
  } catch {
    return value;
  }
}
