import type { HttpResponse } from '@angular/common/http';
import { HttpClient, HttpErrorResponse, HttpEventType, HttpRequest } from '@angular/common/http';
import {
  assertInInjectionContext,
  inject,
  provideAppInitializer,
  runInInjectionContext,
} from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { filter } from 'rxjs/operators';

import { createSseClient } from '../core/serverSentEvents';
import type { HttpMethod } from '../core/types';
import { getValidRequestBody } from '../core/utils';
import type {
  Client,
  Config,
  RequestOptions,
  ResolvedRequestOptions,
  ResponseStyle,
} from './types';
import {
  buildUrl,
  createConfig,
  createInterceptors,
  mergeConfigs,
  mergeHeaders,
  setAuthParams,
} from './utils';

export function provideHeyApiClient(client: Client) {
  return provideAppInitializer(() => {
    const httpClient = inject(HttpClient);
    client.setConfig({ httpClient });
  });
}

export const createClient = (config: Config = {}): Client => {
  let _config = mergeConfigs(createConfig(), config);

  const getConfig = (): Config => ({ ..._config });

  const setConfig = (config: Config): Config => {
    _config = mergeConfigs(_config, config);
    return getConfig();
  };

  const interceptors = createInterceptors<
    HttpRequest<unknown>,
    HttpResponse<unknown>,
    unknown,
    ResolvedRequestOptions
  >();

  const requestOptions = <
    TData = unknown,
    ThrowOnError extends boolean = false,
    TResponseStyle extends ResponseStyle = 'fields',
  >(
    options: RequestOptions<TData, TResponseStyle, ThrowOnError>,
  ) => {
    const opts = {
      ..._config,
      ...options,
      headers: mergeHeaders(_config.headers, options.headers),
      httpClient: options.httpClient ?? _config.httpClient,
      serializedBody: undefined as string | undefined,
    };

    if (!opts.httpClient) {
      if (opts.injector) {
        opts.httpClient = runInInjectionContext(opts.injector, () => inject(HttpClient));
      } else {
        assertInInjectionContext(requestOptions);
        opts.httpClient = inject(HttpClient);
      }
    }

    if (opts.body !== undefined && opts.bodySerializer) {
      opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined;
    }

    // remove Content-Type header if body is empty to avoid sending invalid requests
    if (opts.body === undefined || opts.serializedBody === '') {
      opts.headers.delete('Content-Type');
    }

    const url = buildUrl(opts as Config & RequestOptions);

    const req = new HttpRequest<unknown>(opts.method ?? 'GET', url, getValidRequestBody(opts), {
      redirect: 'follow',
      ...opts,
    });

    return { opts, req, url };
  };

  const beforeRequest = async <
    TData = unknown,
    TResponseStyle extends ResponseStyle = 'fields',
    ThrowOnError extends boolean = boolean,
    Url extends string = string,
  >(
    options: RequestOptions<TData, TResponseStyle, ThrowOnError, Url>,
  ) => {
    const { opts, req, url } = requestOptions(options);

    if (opts.security) {
      await setAuthParams(opts);
    }

    if (opts.requestValidator) {
      await opts.requestValidator(opts);
    }

    return { opts, req, url };
  };

  const request: Client['request'] = async (options) => {
    const throwOnError = options.throwOnError ?? _config.throwOnError;
    const responseStyle = options.responseStyle ?? _config.responseStyle;

    const result: {
      request?: HttpRequest<unknown>;
      response?: any;
    } = {
      request: undefined,
      response: undefined,
    };

    try {
      const { opts, req: initialReq } = await beforeRequest(options);

      let req = initialReq;
      result.request = req;

      for (const fn of interceptors.request.fns) {
        if (fn) {
          req = await fn(req, opts as ResolvedRequestOptions);
          result.request = req;
        }
      }

      result.response = await firstValueFrom(
        opts
          .httpClient!.request(req)
          .pipe(filter((event) => event.type === HttpEventType.Response)),
      );

      for (const fn of interceptors.response.fns) {
        if (fn) {
          result.response = await fn(result.response, req, opts as ResolvedRequestOptions);
        }
      }

      let bodyResponse = result.response.body;

      if (opts.responseValidator) {
        await opts.responseValidator(bodyResponse);
      }

      if (opts.responseTransformer) {
        bodyResponse = await opts.responseTransformer(bodyResponse);
      }

      return opts.responseStyle === 'data' ? bodyResponse : { data: bodyResponse, ...result };
    } catch (error) {
      if (error instanceof HttpErrorResponse) {
        result.response = error;
      }

      let finalError = error instanceof HttpErrorResponse ? error.error : error;

      for (const fn of interceptors.error.fns) {
        if (fn) {
          finalError = await fn(
            finalError,
            result.response,
            result.request,
            options as ResolvedRequestOptions,
          );
        }
      }

      if (throwOnError) {
        throw finalError;
      }

      return responseStyle === 'data'
        ? undefined
        : {
            error: finalError,
            ...result,
          };
    }
  };

  const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
    request({ ...options, method });

  const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
    const { opts, url } = await beforeRequest(options);
    return createSseClient({
      ...opts,
      body: opts.body as BodyInit | null | undefined,
      headers: opts.headers as unknown as Record<string, string>,
      method,
      serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined,
      url,
    });
  };

  const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options });

  return {
    buildUrl: _buildUrl,
    connect: makeMethodFn('CONNECT'),
    delete: makeMethodFn('DELETE'),
    get: makeMethodFn('GET'),
    getConfig,
    head: makeMethodFn('HEAD'),
    interceptors,
    options: makeMethodFn('OPTIONS'),
    patch: makeMethodFn('PATCH'),
    post: makeMethodFn('POST'),
    put: makeMethodFn('PUT'),
    request,
    requestOptions: (options) => {
      if (options.security) {
        throw new Error('Security is not supported in requestOptions');
      }

      if (options.requestValidator) {
        throw new Error('Request validation is not supported in requestOptions');
      }

      return requestOptions(options).req;
    },
    setConfig,
    sse: {
      connect: makeSseFn('CONNECT'),
      delete: makeSseFn('DELETE'),
      get: makeSseFn('GET'),
      head: makeSseFn('HEAD'),
      options: makeSseFn('OPTIONS'),
      patch: makeSseFn('PATCH'),
      post: makeSseFn('POST'),
      put: makeSseFn('PUT'),
      trace: makeSseFn('TRACE'),
    },
    trace: makeMethodFn('TRACE'),
  } as Client;
};
