const QUERY_EXTENSION_PREFIX = "~beignet-query:v1:";
const EMPTY_ARRAY_EXTENSION = `${QUERY_EXTENSION_PREFIX}{"kind":"empty-preserving","value":[]}`;
const EMPTY_OBJECT_EXTENSION = `${QUERY_EXTENSION_PREFIX}{"kind":"empty-preserving","value":{}}`;
const DATE_TIME_PATTERN =
  /^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?([Zz]|([+-])(\d{2}):(\d{2}))$/;
const NUMBER_PATTERN = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
const UNSAFE_QUERY_NAMES = new Set(["__proto__", "constructor", "prototype"]);

/** Behavior when a query collection is empty. */
export type QueryEmptyBehavior = "omit" | "preserve";

/** Scalar query parameter transport supported by OpenAPI form encoding. */
export type QueryScalarTransport =
  | { readonly kind: "string" }
  | { readonly kind: "number" }
  | { readonly kind: "integer" }
  | { readonly kind: "boolean" }
  | { readonly kind: "date-time" }
  | { readonly kind: "date" };

/** Repeated scalar query parameter transport. */
export type QueryArrayTransport<
  TItem extends QueryScalarTransport = QueryScalarTransport,
> = {
  readonly kind: "array";
  readonly item: TItem;
  readonly empty: QueryEmptyBehavior;
};

/** Fields supported inside one flat OpenAPI deepObject parameter. */
export type QueryDeepObjectFields = Readonly<
  Record<string, QueryScalarTransport>
>;

/** Flat OpenAPI deepObject query parameter transport. */
export type QueryDeepObjectTransport<
  TFields extends QueryDeepObjectFields = QueryDeepObjectFields,
> = {
  readonly kind: "deep-object";
  readonly fields: TFields;
  readonly empty: QueryEmptyBehavior;
};

/** Transport for one top-level query parameter. */
export type QueryFieldTransport =
  | QueryScalarTransport
  | QueryArrayTransport
  | QueryDeepObjectTransport;

/** Query field declarations keyed by their HTTP parameter names. */
export type QueryTransportFields = Readonly<
  Record<string, QueryFieldTransport>
>;

/**
 * Deterministic HTTP transport for a contract query schema.
 *
 * The same declaration drives server decoding, typed-client encoding, and
 * OpenAPI parameter serialization.
 */
export type QueryTransport<
  TFields extends QueryTransportFields = QueryTransportFields,
> = {
  readonly kind: "query-transport";
  readonly fields: TFields;
};

/** One deterministic query transport failure. */
export type QueryTransportIssue = {
  readonly message: string;
  readonly path: readonly (string | number)[];
};

/** Raised when an HTTP query cannot be encoded or decoded as declared. */
export class QueryTransportError extends Error {
  readonly issues: readonly QueryTransportIssue[];

  constructor(issue: QueryTransportIssue) {
    super(issue.message);
    this.name = "QueryTransportError";
    this.issues = [issue];
  }
}

/** Logical value encoded by one query field transport. */
export type QueryFieldTransportInput<TField extends QueryFieldTransport> =
  TField extends { readonly kind: "string" | "date-time" }
    ? string
    : TField extends { readonly kind: "number" | "integer" }
      ? number
      : TField extends { readonly kind: "boolean" }
        ? boolean
        : TField extends { readonly kind: "date" }
          ? Date
          : TField extends QueryArrayTransport<infer TItem>
            ? readonly QueryFieldTransportInput<TItem>[]
            : TField extends QueryDeepObjectTransport<infer TFields>
              ? {
                  [K in keyof TFields]?: QueryFieldTransportInput<TFields[K]>;
                }
              : never;

/** Logical values encoded by a query transport declaration. */
export type QueryTransportInput<TTransport extends QueryTransport> = {
  [K in keyof TTransport["fields"]]?: QueryFieldTransportInput<
    TTransport["fields"][K]
  >;
};

type QueryKeys<T> = Extract<keyof T, string>;
type DeclaredQueryTransportFields<TTransport extends QueryTransport> =
  TTransport extends QueryTransport<infer TFields> ? TFields : never;

type QueryFieldMatches<TInput, TField extends QueryFieldTransport> =
  TField extends QueryArrayTransport<infer TItem>
    ? Exclude<TInput, undefined> extends readonly (infer TEntry)[]
      ? QueryFieldMatches<TEntry, TItem>
      : false
    : TField extends QueryDeepObjectTransport<infer TFields>
      ? Exclude<TInput, undefined> extends object
        ? Exclude<
            QueryKeys<Exclude<TInput, undefined>>,
            keyof TFields
          > extends never
          ? Exclude<
              keyof TFields,
              QueryKeys<Exclude<TInput, undefined>>
            > extends never
            ? false extends {
                [K in keyof TFields]: K extends keyof Exclude<TInput, undefined>
                  ? QueryFieldMatches<Exclude<TInput, undefined>[K], TFields[K]>
                  : false;
              }[keyof TFields]
              ? false
              : true
            : false
          : false
        : false
      : Exclude<TInput, undefined> extends QueryFieldTransportInput<TField>
        ? true
        : false;

type QueryTransportMatchesInput<
  TInput,
  TTransport extends QueryTransport,
> = unknown extends TInput
  ? true
  : TInput extends object
    ? keyof DeclaredQueryTransportFields<TTransport> extends never
      ? QueryKeys<TInput> extends never
        ? true
        : TInput[QueryKeys<TInput>] extends never
          ? true
          : false
      : Exclude<
            QueryKeys<TInput>,
            keyof DeclaredQueryTransportFields<TTransport>
          > extends never
        ? Exclude<
            keyof DeclaredQueryTransportFields<TTransport>,
            QueryKeys<TInput>
          > extends never
          ? false extends {
              [K in keyof DeclaredQueryTransportFields<TTransport>]: K extends keyof TInput
                ? QueryFieldMatches<
                    TInput[K],
                    DeclaredQueryTransportFields<TTransport>[K]
                  >
                : false;
            }[keyof DeclaredQueryTransportFields<TTransport>]
            ? false
            : true
          : false
        : false
    : false;

/**
 * Type-level contract used by `.query(...)` to keep schema input fields and
 * transport declarations aligned.
 */
export type QueryTransportCompatibility<
  TInput,
  TTransport extends QueryTransport,
> =
  QueryTransportMatchesInput<TInput, TTransport> extends true
    ? unknown
    : {
        readonly __beignetQueryTransportMismatch: "Query transport fields and value kinds must match the Standard Schema input type.";
      };

type EmptyOptions = {
  /**
   * Empty collections are omitted by default because standard OpenAPI query
   * serialization cannot distinguish them from absent parameters. `preserve`
   * opts into Beignet's versioned typed-client extension.
   */
  empty?: QueryEmptyBehavior;
};

/** Built-in, OpenAPI-inspectable query parameter transports. */
export const query = {
  string(): QueryScalarTransport & { readonly kind: "string" } {
    return Object.freeze({ kind: "string" });
  },
  number(): QueryScalarTransport & { readonly kind: "number" } {
    return Object.freeze({ kind: "number" });
  },
  integer(): QueryScalarTransport & { readonly kind: "integer" } {
    return Object.freeze({ kind: "integer" });
  },
  boolean(): QueryScalarTransport & { readonly kind: "boolean" } {
    return Object.freeze({ kind: "boolean" });
  },
  dateTime(): QueryScalarTransport & { readonly kind: "date-time" } {
    return Object.freeze({ kind: "date-time" });
  },
  date(): QueryScalarTransport & { readonly kind: "date" } {
    return Object.freeze({ kind: "date" });
  },
  array<const TItem extends QueryScalarTransport>(
    item: TItem,
    options: EmptyOptions = {},
  ): QueryArrayTransport<TItem> {
    return Object.freeze({
      kind: "array",
      item,
      empty: options.empty ?? "omit",
    });
  },
  deepObject<const TFields extends QueryDeepObjectFields>(
    fields: TFields,
    options: EmptyOptions = {},
  ): QueryDeepObjectTransport<TFields> {
    assertFieldNames(fields, "deepObject");
    return Object.freeze({
      kind: "deep-object",
      fields: copyFields(fields),
      empty: options.empty ?? "omit",
    });
  },
} as const;

/** Define a reusable deterministic query transport. */
export function defineQueryTransport<
  const TFields extends QueryTransportFields,
>(fields: TFields): QueryTransport<TFields> {
  assertFieldNames(fields, "query");
  return Object.freeze({
    kind: "query-transport",
    fields: copyFields(fields),
  });
}

function copyFields<TFields extends QueryTransportFields>(
  fields: TFields,
): TFields {
  return Object.freeze({ ...fields }) as TFields;
}

function assertFieldNames(
  fields: Readonly<Record<string, unknown>>,
  location: string,
): void {
  for (const name of Object.keys(fields)) {
    if (
      name.length === 0 ||
      name.includes("[") ||
      name.includes("]") ||
      UNSAFE_QUERY_NAMES.has(name)
    ) {
      throw new Error(
        `Invalid ${location} transport field "${name}". Query transport field names must be non-empty, must not contain brackets, and must not use prototype-related names.`,
      );
    }
  }
}

function queryRecord(): Record<string, unknown> {
  return Object.create(null) as Record<string, unknown>;
}

function issue(path: readonly (string | number)[], message: string): never {
  throw new QueryTransportError({ path, message });
}

function isLeapYear(year: number): boolean {
  return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
}

function daysInMonth(year: number, month: number): number {
  if (month === 2) return isLeapYear(year) ? 29 : 28;
  return [4, 6, 9, 11].includes(month) ? 30 : 31;
}

function isDateTime(value: string): boolean {
  const match = DATE_TIME_PATTERN.exec(value);
  if (!match) return false;

  const year = Number(match[1]);
  const month = Number(match[2]);
  const day = Number(match[3]);
  const hour = Number(match[4]);
  const minute = Number(match[5]);
  const second = Number(match[6]);
  const offsetHour = match[9] === undefined ? 0 : Number(match[9]);
  const offsetMinute = match[10] === undefined ? 0 : Number(match[10]);
  const componentsAreValid =
    month >= 1 &&
    month <= 12 &&
    day >= 1 &&
    day <= daysInMonth(year, month) &&
    hour <= 23 &&
    minute <= 59 &&
    second <= 60 &&
    offsetHour <= 23 &&
    offsetMinute <= 59;
  if (!componentsAreValid) return false;

  if (second < 60) return Number.isFinite(Date.parse(value));

  const precedingSecond = value.replace(
    /^(\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:)60/,
    (_match, prefix: string) => `${prefix}59`,
  );
  const parsed = Date.parse(precedingSecond);
  if (!Number.isFinite(parsed)) return false;

  const utc = new Date(parsed);
  return (
    utc.getUTCHours() === 23 &&
    utc.getUTCMinutes() === 59 &&
    utc.getUTCSeconds() === 59 &&
    ((utc.getUTCMonth() === 5 && utc.getUTCDate() === 30) ||
      (utc.getUTCMonth() === 11 && utc.getUTCDate() === 31))
  );
}

function decodeScalar(
  transport: QueryScalarTransport,
  value: string,
  path: readonly (string | number)[],
): unknown {
  switch (transport.kind) {
    case "string":
      return value;
    case "number": {
      if (!NUMBER_PATTERN.test(value)) {
        return issue(path, "Expected a finite number query value.");
      }
      const decoded = Number(value);
      return Number.isFinite(decoded)
        ? decoded
        : issue(path, "Expected a finite number query value.");
    }
    case "integer": {
      if (!NUMBER_PATTERN.test(value)) {
        return issue(path, "Expected an integer query value.");
      }
      const decoded = Number(value);
      return Number.isSafeInteger(decoded)
        ? decoded
        : issue(path, "Expected a safe integer query value.");
    }
    case "boolean":
      if (value === "true") return true;
      if (value === "false") return false;
      return issue(path, 'Expected a boolean query value ("true" or "false").');
    case "date-time":
      return isDateTime(value)
        ? value
        : issue(path, "Expected an RFC 3339 date-time query value.");
    case "date": {
      if (!isDateTime(value)) {
        return issue(path, "Expected an RFC 3339 date-time query value.");
      }
      const decoded = new Date(value);
      return Number.isFinite(decoded.getTime())
        ? decoded
        : issue(
            path,
            "Expected an RFC 3339 date-time representable by JavaScript Date.",
          );
    }
  }
}

function encodeScalar(
  transport: QueryScalarTransport,
  value: unknown,
  path: readonly (string | number)[],
): string {
  switch (transport.kind) {
    case "string":
      return typeof value === "string"
        ? value
        : issue(path, "Expected a string query value.");
    case "number":
      return typeof value === "number" && Number.isFinite(value)
        ? String(value)
        : issue(path, "Expected a finite number query value.");
    case "integer":
      return typeof value === "number" && Number.isSafeInteger(value)
        ? String(value)
        : issue(path, "Expected a safe integer query value.");
    case "boolean":
      return typeof value === "boolean"
        ? String(value)
        : issue(path, "Expected a boolean query value.");
    case "date-time":
      return typeof value === "string" && isDateTime(value)
        ? value
        : issue(path, "Expected an RFC 3339 date-time query value.");
    case "date":
      return value instanceof Date && Number.isFinite(value.getTime())
        ? value.toISOString()
        : issue(path, "Expected a valid Date query value.");
  }
}

function oneValue(
  values: readonly string[],
  path: readonly (string | number)[],
): string {
  if (values.length !== 1) {
    return issue(path, "Expected exactly one query value.");
  }
  return values[0] as string;
}

function rawQuery(searchParams: URLSearchParams): Record<string, unknown> {
  const result = queryRecord();
  for (const name of new Set(searchParams.keys())) {
    const values = searchParams.getAll(name);
    result[name] = values.length === 1 ? values[0] : values;
  }
  return result;
}

function encodeArrayItem(
  field: QueryArrayTransport,
  value: unknown,
  path: readonly (string | number)[],
): string {
  const encoded = encodeScalar(field.item, value, path);
  if (
    field.empty === "preserve" &&
    field.item.kind === "string" &&
    encoded.startsWith(QUERY_EXTENSION_PREFIX)
  ) {
    return `${QUERY_EXTENSION_PREFIX}${JSON.stringify({
      kind: "literal",
      value: encoded,
    })}`;
  }
  return encoded;
}

function decodeArrayItem(
  field: QueryArrayTransport,
  value: string,
  path: readonly (string | number)[],
): unknown {
  if (
    field.empty === "preserve" &&
    field.item.kind === "string" &&
    value.startsWith(QUERY_EXTENSION_PREFIX)
  ) {
    try {
      const extension = JSON.parse(
        value.slice(QUERY_EXTENSION_PREFIX.length),
      ) as unknown;
      if (
        extension &&
        typeof extension === "object" &&
        "kind" in extension &&
        extension.kind === "literal" &&
        "value" in extension &&
        typeof extension.value === "string"
      ) {
        return extension.value;
      }
    } catch {
      // Unknown extension-looking strings remain ordinary string values.
    }
  }
  return decodeScalar(field.item, value, path);
}

/** Decode URL query values exactly once according to a contract transport. */
export function decodeQueryTransport(
  transport: QueryTransport,
  searchParams: URLSearchParams,
): Record<string, unknown> {
  const decoded = rawQuery(searchParams);

  for (const [name, field] of Object.entries(transport.fields)) {
    const values = searchParams.getAll(name);
    if (field.kind === "deep-object") {
      const direct = values.length > 0;
      const prefix = `${name}[`;
      const nestedNames = [...new Set(searchParams.keys())].filter((key) =>
        key.startsWith(prefix),
      );
      if (direct && nestedNames.length > 0) {
        issue([name], "Query parameter conflicts with its deepObject fields.");
      }
      if (direct) {
        if (
          field.empty === "preserve" &&
          values.length === 1 &&
          values[0] === EMPTY_OBJECT_EXTENSION
        ) {
          decoded[name] = {};
          continue;
        }
        issue([name], "Expected a deepObject query parameter.");
      }
      if (nestedNames.length === 0) continue;

      const object = queryRecord();
      for (const nestedName of nestedNames) {
        const suffix = nestedName.slice(prefix.length);
        if (!suffix.endsWith("]") || suffix.slice(0, -1).includes("[")) {
          issue([name], "Expected a flat deepObject query parameter.");
        }
        const childName = suffix.slice(0, -1);
        const child = field.fields[childName];
        if (!child) {
          issue(
            [name, childName],
            "Query transport does not declare this deepObject field.",
          );
        }
        object[childName] = decodeScalar(
          child,
          oneValue(searchParams.getAll(nestedName), [name, childName]),
          [name, childName],
        );
        delete decoded[nestedName];
      }
      decoded[name] = object;
      continue;
    }

    if (values.length === 0) continue;
    if (field.kind === "array") {
      if (
        field.empty === "preserve" &&
        values.length === 1 &&
        values[0] === EMPTY_ARRAY_EXTENSION
      ) {
        decoded[name] = [];
      } else {
        decoded[name] = values.map((value, index) =>
          decodeArrayItem(field, value, [name, index]),
        );
      }
      continue;
    }
    decoded[name] = decodeScalar(field, oneValue(values, [name]), [name]);
  }

  return Object.fromEntries(Object.entries(decoded));
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
    return false;
  }
  const prototype = Object.getPrototypeOf(value);
  return prototype === Object.prototype || prototype === null;
}

/** Encode typed query input according to the same contract transport. */
export function encodeQueryTransport(
  transport: QueryTransport,
  value: unknown,
): URLSearchParams {
  if (!isPlainObject(value)) {
    issue([], "Expected query input to be an object.");
  }

  const params = new URLSearchParams();
  for (const name of Object.keys(value)) {
    if (value[name] !== undefined && !transport.fields[name]) {
      issue([name], "Query transport does not declare this field.");
    }
  }

  for (const [name, field] of Object.entries(transport.fields)) {
    const fieldValue = value[name];
    if (fieldValue === undefined) continue;
    if (fieldValue === null) {
      issue(
        [name],
        "Null query values are not representable; omit the parameter instead.",
      );
    }

    if (field.kind === "array") {
      if (!Array.isArray(fieldValue)) {
        issue([name], "Expected an array query value.");
      }
      if (fieldValue.length === 0) {
        if (field.empty === "preserve") {
          params.append(name, EMPTY_ARRAY_EXTENSION);
        }
        continue;
      }
      fieldValue.forEach((entry, index) => {
        params.append(name, encodeArrayItem(field, entry, [name, index]));
      });
      continue;
    }

    if (field.kind === "deep-object") {
      if (!isPlainObject(fieldValue)) {
        issue([name], "Expected an object query value.");
      }
      for (const childName of Object.keys(fieldValue)) {
        if (fieldValue[childName] !== undefined && !field.fields[childName]) {
          issue(
            [name, childName],
            "Query transport does not declare this deepObject field.",
          );
        }
      }
      const entries = Object.entries(fieldValue).filter(
        ([, entry]) => entry !== undefined,
      );
      if (entries.length === 0) {
        if (field.empty === "preserve") {
          params.append(name, EMPTY_OBJECT_EXTENSION);
        }
        continue;
      }
      for (const [childName, entry] of entries) {
        const child = field.fields[childName];
        if (!child) continue;
        params.append(
          `${name}[${childName}]`,
          encodeScalar(child, entry, [name, childName]),
        );
      }
      continue;
    }

    params.append(name, encodeScalar(field, fieldValue, [name]));
  }

  return params;
}

/** OpenAPI schema keywords implied by a query field transport. */
export function queryTransportSchema(
  field: QueryFieldTransport,
): Record<string, unknown> {
  switch (field.kind) {
    case "string":
      return { type: "string" };
    case "number":
      return { type: "number" };
    case "integer":
      return {
        type: "integer",
        minimum: Number.MIN_SAFE_INTEGER,
        maximum: Number.MAX_SAFE_INTEGER,
      };
    case "boolean":
      return { type: "boolean" };
    case "date-time":
    case "date":
      return { type: "string", format: "date-time" };
    case "array":
      return {
        type: "array",
        items: queryTransportSchema(field.item),
        ...(field.empty === "preserve"
          ? { "x-beignet-empty-query": "v1" }
          : {}),
      };
    case "deep-object":
      return {
        type: "object",
        properties: Object.fromEntries(
          Object.entries(field.fields).map(([name, child]) => [
            name,
            queryTransportSchema(child),
          ]),
        ),
        additionalProperties: false,
        ...(field.empty === "preserve"
          ? { "x-beignet-empty-query": "v1" }
          : {}),
      };
  }
}
