import { ParsedQs } from 'qs';

/**
 * A list of routes and their parameters and bindings.
 *
 * Extended and filled by the route list generated with `php artisan ziggy:generate --types`.
 */
export interface RouteList {}

/**
 * Marker interface to configure Ziggy's type checking behavior.
 */
export interface TypeConfig {}

/**
 * A route name registered with Ziggy.
 */
type KnownRouteName = keyof RouteList;

/**
 * A route name, or any string.
 */
type RouteName = TypeConfig extends { strictRouteNames: true }
    ? KnownRouteName
    : KnownRouteName | (string & {});
// `(string & {})` prevents TypeScript from reducing this type to just `string`,
// which would prevent intellisense from autocompleting known route names.
// See https://stackoverflow.com/a/61048124/6484459.

/**
 * Information about a single route parameter.
 */
type ParameterInfo = { name: string; required: boolean; binding?: string };

/**
 * A primitive route parameter value, as it would appear in a URL.
 */
type RawParameterValue = string | number;
// TODO: Technically booleans work too, does it make sense to add them? Here? What would that look like?

/**
 * An object parameter value containing the 'default' binding key `id`, e.g. representing an Eloquent model.
 */
type DefaultRoutable = { id: RawParameterValue } & Record<keyof any, unknown>;

/**
 * A route parameter value.
 */
type ParameterValue = RawParameterValue | DefaultRoutable;

/**
 * A parseable route parameter, either plain or nested inside an object under its binding key.
 */
type Routable<I extends ParameterInfo> = I extends { binding: string }
    ? ({ [K in I['binding']]: RawParameterValue } & Record<keyof any, unknown>) | RawParameterValue
    : ParameterValue;

// Uncomment to test:
// type A = Routable<{ name: 'foo', required: true, binding: 'bar' }>;
// = RawParameterValue | { bar: RawParameterValue }
// type B = Routable<{ name: 'foo', required: true, }>;
// = RawParameterValue | DefaultRoutable

// Utility types for KnownRouteParamsObject
type RequiredParams<I extends readonly ParameterInfo[]> = Extract<I[number], { required: true }>;
type OptionalParams<I extends readonly ParameterInfo[]> = Extract<I[number], { required: false }>;

/**
 * An object containing a special '_query' key to target the query string of a URL.
 */
type HasQueryParam = { _query?: Record<string, unknown> };
/**
 * An object of parameters for an unspecified route.
 */
type GenericRouteParamsObject = Record<keyof any, unknown> & HasQueryParam;
// `keyof any` essentially makes it function as a plain `Record`
/**
 * An object of parameters for a specific named route.
 */
type KnownRouteParamsObject<I extends readonly ParameterInfo[]> = {
    [T in RequiredParams<I> as T['name']]: Routable<T>;
} & {
    [T in OptionalParams<I> as T['name']]?: Routable<T>;
} & GenericRouteParamsObject;
// `readonly` allows TypeScript to determine the actual values of all the
// parameter names inside the array, instead of just seeing `string`.
// See https://github.com/tighten/ziggy/pull/664#discussion_r1329978447.

// Uncomment to test:
// type A = KnownRouteParamsObject<[{ name: 'foo'; required: true }, { name: 'bar'; required: false }]>;
// = { foo: ... } & { bar?: ... }

/**
 * An object of route parameters.
 */
type RouteParamsObject<N extends RouteName> = N extends KnownRouteName
    ? KnownRouteParamsObject<RouteList[N]>
    : GenericRouteParamsObject;

/**
 * An array of parameters for an unspecified route.
 */
// TODO: this may be able to be more specific, like `Routable<ParameterInfo>[]`,
// depending how we want to handle nested objects inside parameter arrays
type GenericRouteParamsArray = unknown[];
/**
 * An array of parameters for a specific named route.
 */
type KnownRouteParamsArray<I extends readonly ParameterInfo[]> = [
    ...{ [K in keyof I]: Routable<I[K]> },
    ...unknown[],
];
// Because `K in keyof I` for a `readonly` array is always a number, even though this
// looks like `{ 0: T, 1: U, 2: V }` TypeScript generates `[T, U, V]`. The nested
// array destructing lets us type the first n items in the array, which are known
// route parameters, and then allow arbitrary additional items.
// See https://github.com/tighten/ziggy/pull/664#discussion_r1330002370.

// Uncomment to test:
// type B = KnownRouteParamsArray<[{ name: 'post'; required: true; binding: 'uuid' }]>;
// = [RawParameterValue | { uuid: RawParameterValue }, ...unknown[]]

/**
 * An array of route parameters.
 */
type RouteParamsArray<N extends RouteName> = N extends KnownRouteName
    ? KnownRouteParamsArray<RouteList[N]>
    : GenericRouteParamsArray;

/**
 * All possible parameter argument shapes for a route.
 */
type RouteParams<N extends RouteName> = RouteParamsObject<N> | RouteParamsArray<N>;

/**
 * A route.
 */
interface Route {
    uri: string;
    methods: ('GET' | 'HEAD' | 'POST' | 'PATCH' | 'PUT' | 'OPTIONS' | 'DELETE')[];
    domain?: string;
    parameters?: string[];
    bindings?: Record<string, string>;
    wheres?: Record<string, unknown>;
    middleware?: string[];
}

/**
 * Ziggy's config object.
 */
interface Config {
    url: string;
    port: number | null;
    defaults: Record<string, RawParameterValue>;
    routes: Record<string, Route>;
    location?: {
        host?: string;
        pathname?: string;
        search?: string;
    };
}

/**
 * Ziggy's Router class.
 */
interface Router {
    current(): RouteName | undefined;
    current<T extends RouteName>(name: T, params?: ParameterValue | RouteParams<T>): boolean;
    get params(): Record<string, string>;
    get routeParams(): Record<string, string>;
    get queryParams(): ParsedQs;
    has<T extends RouteName>(name: T): boolean;
}

/**
 * Ziggy's route helper.
 */
// Called with no arguments - returns a Router instance
export function route(): Router;

// Called with configuration arguments only - returns a configured Router instance
export function route(
    name: undefined,
    params: undefined,
    absolute?: boolean,
    config?: Config,
): Router;

// Called with a route name and optional additional arguments - returns a URL string
export function route<T extends RouteName>(
    name: T,
    params?: RouteParams<T> | undefined,
    absolute?: boolean,
    config?: Config,
): string;

export function route<T extends RouteName>(
    name: T,
    params?: ParameterValue | undefined,
    absolute?: boolean,
    config?: Config,
): string;

/**
 * Ziggy's Vue plugin.
 */
export const ZiggyVue: {
    install(app: any, options?: Config): void;
};

/**
 * Ziggy's React hook.
 */
export function useRoute(defaultConfig?: Config): typeof route;