import { randomBytes } from 'node:crypto';
import got, { Got as GotInstance, Options as GotOptions } from 'got';
import { URL } from 'url';
import { PKG_VERSION } from './constants.js';
import { BaseRequester, PaginatableRequestParam, RequestParam, TokenResponse } from './types.js';

export interface Options {
  clientId?: string;
  clientSecret?: string;
  accessToken?: string;
  refreshToken?: string;
  gotOptions?: GotOptions;
  gotOAuthOptions?: GotOptions;
  autoRefreshAccessToken?: boolean;
}

export class MALClient implements BaseRequester {

  public clientId?: string;

  public clientSecret?: string;

  public accessToken?: string;

  public refreshToken?: string;

  public got: GotInstance;

  public gotOAuth: GotInstance;

  public PKCEChallangeGenerateSize = 32;

  public userAgent = '@chez14/mal-api-lite';

  public malApiBaseUrl = 'https://api.myanimelist.net/v2/';

  public malOAuthUrl = 'https://myanimelist.net/v1/oauth2/';

  /**
   * Create MAL API Client
   *
   * @param param0 Your trusty configuration
   */
  public constructor({ clientId, clientSecret, accessToken, refreshToken, gotOptions, gotOAuthOptions }: Options) {
    if ((!clientSecret || !clientId) && !(accessToken || refreshToken)) {
      // if either ( clientSecret or clientId ) not preset, AND accessToken or
      // refreshToken is provided...
      throw new Error(
        'You need to provide both (`clientSecret` and `clientId`) OR one of (`accessToken` or `refreshToken`)',
      );
    }

    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.accessToken = accessToken;
    this.refreshToken = refreshToken;

    this.userAgent += ` v${PKG_VERSION}`;

    this.got = got.extend({
      prefixUrl: this.malApiBaseUrl,
      responseType: 'json',
      headers: { 'user-agent': this.userAgent },
      hooks: {
        beforeRequest: [
          (options) => {
            if (this.accessToken) {
              options.headers.authorization = `Bearer ${this.accessToken}`;
            } else {
              options.headers['X-MAL-CLIENT-ID'] = this.clientId;
            }
          }
        ]
      },
      ...gotOptions,
    });
    this.gotOAuth = got.extend({
      prefixUrl: this.malOAuthUrl,
      headers: { 'user-agent': this.userAgent },
      responseType: 'json',
      ...gotOAuthOptions,
    });
  }



  /**
   * Get Access Token & Refresh Token from given Authorization Code.
   *
   * @param authCode Authorization code
   * @param codeVerifier PKCE Code Challenge
   * @param redirectUri Redirect url, specified on on previous step
   */
  public async resolveAuthCode(authCode: string, codeVerifier: string, redirectUri?: string): Promise<TokenResponse> {
    if (!this.clientSecret || !this.clientId) {
      throw new Error('clientSecret and clientId must be filled to use this function!');
    }

    const resp = await this.gotOAuth.post<TokenResponse>('token', {
      form: {
        client_id: this.clientId,
        client_secret: this.clientSecret,
        grant_type: 'authorization_code',
        code: authCode,
        redirect_uri: redirectUri,
        code_verifier: codeVerifier,
      },
    });
    const { access_token, refresh_token } = resp.body;
    this.accessToken = access_token;
    this.refreshToken = refresh_token;
    return resp.body;
  }

  /**
   * Generate OAuth URL to gain access to user account on MyAnimeList platform.
   * Will require clientId and clientSecret from custructor.
   *
   * @param codeChallenge PKCE Code Challenge
   * @param redirectUri If you have more than one Redirect URL, please specify
   * the url you use.
   * @param state Your app state
   * @param codeChallengeMethod Only accept "plain". Don't change unless you
   * know what you're doing!
   */
  public getOAuthURL(
    redirectUri?: string,
    codeChallenge?: string,
    state?: string,
  ): { url: string; codeChallenge: string; state?: string } {
    if (!this.clientId) {
      throw new Error('clientId must be filled to use this function!');
    }

    if (!codeChallenge) {
      codeChallenge = randomBytes(this.PKCEChallangeGenerateSize).toString('base64url');
    }

    const query: Record<string, string | undefined> = {
      response_type: 'code',
      client_id: this.clientId,
      state,
      redirect_uri: redirectUri,
      code_challenge: codeChallenge,
      code_challenge_method: 'plain',
    };

    const urlBuilder = new URL('authorize', this.gotOAuth.defaults.options.prefixUrl);
    Object.keys(query).forEach((key) => {
      if (query[key]) {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        urlBuilder.searchParams.append(key, query[key]!);
      }
    });

    return { url: urlBuilder.toString(), codeChallenge, state };
  }



  /**
   * Refresh your access token with refresh token.
   *
   * @param refreshToken Custom refresh token
   */
  public async resolveRefreshToken(refreshToken?: string): Promise<TokenResponse> {
    if (!refreshToken) {
      refreshToken = this.refreshToken;
    }

    if (!refreshToken) {
      throw Error('No refreshToken provided.');
    }

    const resp = await this.gotOAuth.post<TokenResponse>('token', {
      form: {
        client_id: this.clientId,
        client_secret: this.clientSecret,
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
      },
    });
    const { access_token, refresh_token } = resp.body;
    this.accessToken = access_token;
    this.refreshToken = refresh_token;
    return resp.body;
  }


  protected preprocessParam(param?: RequestParam): RequestParam | undefined {
    if (param?.fields && Array.isArray(param.fields)) {
      param.fields = param.fields.join(',');
    }

    return param;
  }


  /**
   * Do HTTP GET stuffs.
   *
   * @param resource Url to call
   * @param param Parameter body
   */
  public async get<T = any>(resource: string, param?: RequestParam | PaginatableRequestParam): Promise<T> { // eslint-disable-line @typescript-eslint/no-explicit-any
    param = this.preprocessParam(param);

    const response = await this.got.get<T>(resource, {
      searchParams: param,
    });

    return response.body;
  }

  /**
   * Do HTTP POST stuffs.
   *
   * @param resource Url to call
   * @param param Parameter body
   */
  public async post<T = any>(resource: string, param?: RequestParam): Promise<T> { // eslint-disable-line @typescript-eslint/no-explicit-any
    param = this.preprocessParam(param);

    const response = await this.got.post<T>(resource, {
      form: param,
    });
    return response.body;
  }



  /**
   * Do HTTP PATCH stuffs.
   *
   * @param resource Url to call
   * @param param Parameter body
   */
  public async patch<T = any>(resource: string, param?: RequestParam): Promise<T> { // eslint-disable-line @typescript-eslint/no-explicit-any
    param = this.preprocessParam(param);

    const response = await this.got.patch<T>(resource, {
      form: param,
    });
    return response.body;
  }

  /**
   * Do HTTP DELETE stuffs.
   *
   * @param resource Url to call
   * @param param Parameter body (discouraged)
   */
  public async delete<T = any>(resource: string): Promise<T> { // eslint-disable-line @typescript-eslint/no-explicit-any
    const response = await this.got.delete<T>(resource);
    return response.body;
  }
}
