import { fetchWithRefresh } from './refresh-on-401';

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

export type ApiEndpointSpec = {
  response: unknown;
  body?: unknown;
  params?: Record<string, string>;
  query?: Record<string, string | number | boolean | undefined>;
};

export type ApiContract = Record<string, Partial<Record<HttpMethod, ApiEndpointSpec>>>;

type RoutesForMethod<TContract extends ApiContract, TMethod extends HttpMethod> = {
  [TRoute in keyof TContract]: TMethod extends keyof TContract[TRoute] ? TRoute : never;
}[keyof TContract];

type EndpointFor<
  TContract extends ApiContract,
  TMethod extends HttpMethod,
  TRoute extends keyof TContract,
> = Extract<TContract[TRoute][TMethod], ApiEndpointSpec>;

type RequestOptionsForEndpoint<TEndpoint extends ApiEndpointSpec> = (TEndpoint extends {
  body: infer TBody;
}
  ? { body: TBody }
  : { body?: never }) &
  (TEndpoint extends { params: infer TParams } ? { params: TParams } : { params?: never }) &
  (TEndpoint extends { query: infer TQuery } ? { query?: TQuery } : { query?: never }) & {
    headers?: HeadersInit;
  };

type IfEmptyObject<T> = keyof T extends never ? true : false;
type HasRequiredKeys<T> =
  IfEmptyObject<T> extends true
    ? false
    : {
          [K in keyof T]-?: undefined extends T[K] ? never : K;
        }[keyof T] extends never
      ? false
      : true;

type RequestArgs<TEndpoint extends ApiEndpointSpec> =
  HasRequiredKeys<RequestOptionsForEndpoint<TEndpoint>> extends true
    ? [options: RequestOptionsForEndpoint<TEndpoint>]
    : [options?: RequestOptionsForEndpoint<TEndpoint>];

type ApiClient<TContract extends ApiContract> = {
  request<TMethod extends HttpMethod, TRoute extends RoutesForMethod<TContract, TMethod>>(
    method: TMethod,
    route: TRoute,
    ...args: RequestArgs<EndpointFor<TContract, TMethod, TRoute>>
  ): Promise<EndpointFor<TContract, TMethod, TRoute>['response']>;
};

export function buildApiPath<TParams extends Record<string, string>>(
  routeTemplate: string,
  params: TParams,
): string {
  let path = routeTemplate;
  for (const [key, value] of Object.entries(params)) {
    path = path.replaceAll(`:${key}`, encodeURIComponent(value));
  }
  return path;
}

function withQuery(
  path: string,
  query?: Record<string, string | number | boolean | undefined>,
): string {
  if (!query) return path;
  const url = new URL(path, 'https://multideal.local');
  for (const [key, value] of Object.entries(query)) {
    if (value !== undefined) {
      url.searchParams.set(key, String(value));
    }
  }
  return `${url.pathname}${url.search}`;
}

export function createApiClient<TContract extends ApiContract>(
  _contract: TContract,
  fetchImpl: typeof fetch = fetchWithRefresh,
): ApiClient<TContract> {
  return {
    async request(method, route, ...args) {
      const options = (args[0] ?? {}) as {
        body?: unknown;
        params?: Record<string, string>;
        query?: Record<string, string | number | boolean | undefined>;
        headers?: HeadersInit;
      };
      const path = options.params ? buildApiPath(String(route), options.params) : String(route);
      const url = withQuery(path, options.query);

      const response = await fetchImpl(url, {
        method,
        headers: {
          'Content-Type': 'application/json',
          ...(options.headers ?? {}),
        },
        body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
      });

      return (await response.json()) as EndpointFor<
        TContract,
        typeof method,
        typeof route
      >['response'];
    },
  };
}
