import type {
  LocationQuery,
  LocationQueryRaw,
  RouteLocationNormalizedLoaded,
  Router,
} from 'vue-router';

import { watch } from 'vue';

type ListRoutePrimitive = number | string;

interface ListRouteStateField<Value extends ListRoutePrimitive> {
  defaultValue: Value;
  parse: (value: string) => Value;
  queryKey?: string;
  serialize: (value: Value) => string | undefined;
}

type ListRouteStateSchema<State extends object> = {
  fields: {
    [Key in keyof State]: ListRouteStateField<
      Extract<State[Key], ListRoutePrimitive>
    >;
  };
  path: string;
};

interface StringListRouteFieldOptions {
  defaultValue?: string;
  queryKey?: string;
  trim?: boolean;
}

interface PositiveIntegerListRouteFieldOptions {
  allowedValues?: readonly number[];
  defaultValue: number;
  queryKey?: string;
}

function readListRouteQueryValue(query: LocationQuery, key: string): string {
  const value = query[key];
  const normalized = Array.isArray(value) ? value[0] : value;
  return normalized === null || normalized === undefined
    ? ''
    : String(normalized);
}

function stringListRouteField(
  options: StringListRouteFieldOptions = {},
): ListRouteStateField<string> {
  const defaultValue = options.defaultValue ?? '';
  const normalize = (value: string) =>
    options.trim ? value.trim() : String(value || '');
  return {
    defaultValue,
    parse(value) {
      return normalize(value) || defaultValue;
    },
    queryKey: options.queryKey,
    serialize(value) {
      const normalized = normalize(value) || defaultValue;
      return normalized === defaultValue ? undefined : normalized;
    },
  };
}

function enumListRouteField<
  const AllowedValues extends readonly string[],
  const DefaultValue extends string,
>(
  allowedValues: AllowedValues,
  defaultValue: DefaultValue,
  queryKey?: string,
): ListRouteStateField<AllowedValues[number] | DefaultValue> {
  const allowed = new Set<string>(allowedValues);
  return {
    defaultValue,
    parse(value) {
      return allowed.has(value)
        ? (value as AllowedValues[number])
        : defaultValue;
    },
    queryKey,
    serialize(value) {
      return value === defaultValue || !allowed.has(value) ? undefined : value;
    },
  };
}

function positiveIntegerListRouteField(
  options: PositiveIntegerListRouteFieldOptions,
): ListRouteStateField<number> {
  const allowedValues = options.allowedValues
    ? new Set(options.allowedValues)
    : undefined;
  const isValid = (value: number) =>
    Number.isSafeInteger(value) &&
    value > 0 &&
    (!allowedValues || allowedValues.has(value));
  return {
    defaultValue: options.defaultValue,
    parse(value) {
      if (!/^\d+$/.test(value)) return options.defaultValue;
      const parsed = Number(value);
      return isValid(parsed) ? parsed : options.defaultValue;
    },
    queryKey: options.queryKey,
    serialize(value) {
      return value === options.defaultValue || !isValid(value)
        ? undefined
        : String(value);
    },
  };
}

function defineListRouteStateSchema<State extends object>(
  schema: ListRouteStateSchema<State>,
): ListRouteStateSchema<State> {
  return schema;
}

function listRouteFieldEntries<State extends object>(
  schema: ListRouteStateSchema<State>,
) {
  return (Object.keys(schema.fields) as Array<keyof State>).map((key) => ({
    field: schema.fields[key],
    key,
    queryKey: schema.fields[key].queryKey || String(key),
  }));
}

function parseListRouteState<State extends object>(
  query: LocationQuery,
  schema: ListRouteStateSchema<State>,
): State {
  const state = {} as State;
  for (const { field, key, queryKey } of listRouteFieldEntries(schema)) {
    state[key] = field.parse(
      readListRouteQueryValue(query, queryKey),
    ) as State[typeof key];
  }
  return state;
}

function buildListRouteStateQuery<State extends object>(
  state: State,
  schema: ListRouteStateSchema<State>,
): LocationQueryRaw {
  const query: LocationQueryRaw = {};
  for (const { field, key, queryKey } of listRouteFieldEntries(schema)) {
    const value = field.serialize(
      state[key] as Extract<State[typeof key], ListRoutePrimitive>,
    );
    if (value !== undefined) query[queryKey] = value;
  }
  return query;
}

function mergeListRouteStateQuery<State extends object>(
  query: LocationQuery,
  state: State,
  schema: ListRouteStateSchema<State>,
): LocationQueryRaw {
  const ownedKeys = new Set(
    listRouteFieldEntries(schema).map(({ queryKey }) => queryKey),
  );
  return {
    ...Object.fromEntries(
      Object.entries(query).filter(([key]) => !ownedKeys.has(key)),
    ),
    ...buildListRouteStateQuery(state, schema),
  };
}

function buildListRouteFullPath<State extends object>(
  router: Router,
  query: LocationQuery,
  state: State,
  schema: ListRouteStateSchema<State>,
): string {
  return router.resolve({
    path: schema.path,
    query: mergeListRouteStateQuery(query, state, schema),
  }).fullPath;
}

function watchListRouteState<State extends object>(
  route: RouteLocationNormalizedLoaded,
  schema: ListRouteStateSchema<State>,
  onChange: (state: State) => void,
) {
  return watch(
    () => route.fullPath,
    () => {
      if (route.path !== schema.path) return;
      onChange(parseListRouteState(route.query, schema));
    },
    { flush: 'post' },
  );
}

export {
  buildListRouteFullPath,
  buildListRouteStateQuery,
  defineListRouteStateSchema,
  enumListRouteField,
  mergeListRouteStateQuery,
  parseListRouteState,
  positiveIntegerListRouteField,
  readListRouteQueryValue,
  stringListRouteField,
  watchListRouteState,
};
export type { ListRouteStateField, ListRouteStateSchema };
