import { ApiError } from "../errors/api";
import { AuthError } from "../errors/auth";
import { HttpError } from "../errors/http";
import axios from 'axios';
import type { ApiResponse } from "../types/api";
import type { InitOptions, RequestInit, RequestMethod } from "../types/lib";

export class RestClient {
  private _token?: ApiResponse.OAuth;
  private _token_expire?: number;
  private account: string;
  private password: string;
  private grant_type: string;
  private url_base: string;
  private on_error?: (error: Error | ApiError | AuthError | HttpError) => void | Promise<void>;

  constructor(options: InitOptions) {
    this.account = options.account;
    this.password = options.password;
    this.grant_type = options.grant_type ?? "client_credentials";
    this.url_base = options.url_base ?? "https://api.cdek.ru/v2";
    this.on_error = options.on_error;
  }

  get token() {
    return this._token;
  }

  get token_expire() {
    return this._token_expire;
  }

  async auth(): Promise<void> {
    try {
      const res = await axios.post<any>(`${this.url_base}/oauth/token`, null, {
        params: {
          grant_type: 'client_credentials',
          client_id: this.account,
          client_secret: this.password,
        },
        headers: {
          'Accept': 'application/json',
          'X-App-Name': 'shildikOnline',
        },
      });

      if (res.status !== 200) {
        throw new AuthError('Произошла ошибка');
      }

      this._token = res.data;
      this._token_expire = Date.now() + (this.token?.expires_in ?? 3600) * 1000;
    } catch (err: any) {
      throw new AuthError('Authentication failed');
    }
  }

  private async request<T>(init: RequestInit & { method: RequestMethod }): Promise<{ data: T; headers: any }> {
    try {
      if (!this.token || this.token_expire === undefined || Date.now() > this.token_expire) {
        await this.auth();
      }

      const url = `${this.url_base}${init.url}${init.query ? "?" + this.params(init.query) : ""}`;

      const headers = {
        "Authorization": `Bearer ${this.token?.access_token}`,
        "Content-Type": "application/json",
      };

      const res = await axios({
        url,
        method: init.method,
        headers,
        params: init.query,
        data: init.payload ? JSON.stringify(init.payload) : undefined,
      });

      return { data: res.data as T, headers: res.headers };
    } catch (err: any) {
      if (this.on_error) {
        await this.on_error(err);
        return { data: null as T, headers: {} };
      } else {
        throw err;
      }
    }
  }

  get<T>(init: RequestInit): Promise<{ data: T; headers: any }> {
    return this.request<T>({ ...init, method: "GET" });
  }

  post<T>(init: RequestInit): Promise<{ data: T; headers: any }> {
    return this.request<T>({ ...init, method: "POST" });
  }

  put<T>(init: RequestInit): Promise<{ data: T; headers: any }> {
    return this.request<T>({ ...init, method: "PUT" });
  }

  patch<T>(init: RequestInit): Promise<{ data: T; headers: any }> {
    return this.request<T>({ ...init, method: "PATCH" });
  }

  delete<T>(init: RequestInit): Promise<{ data: T; headers: any }> {
    return this.request<T>({ ...init, method: "DELETE" });
  }

  private params(query: Record<string, any>): URLSearchParams {
    return new URLSearchParams(
      Object.entries(query).map<string[]>((item) => [item[0], item[1].toString()])
    );
  }
}
