import { Config, TokenSearchRules, TokenOptions } from './types'
import crypto from 'crypto'
import { MeiliSearchError } from './errors'
import { validateUuid4 } from './utils'

function encode64(data: any) {
  return Buffer.from(JSON.stringify(data)).toString('base64')
}

/**
 * Create the header of the token.
 *
 * @param apiKey - API key used to sign the token.
 * @param encodedHeader - Header of the token in base64.
 * @param encodedPayload - Payload of the token in base64.
 * @returns The signature of the token in base64.
 */
function sign(apiKey: string, encodedHeader: string, encodedPayload: string) {
  return crypto
    .createHmac('sha256', apiKey)
    .update(`${encodedHeader}.${encodedPayload}`)
    .digest('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '')
}

/**
 * Create the header of the token.
 *
 * @returns The header encoded in base64.
 */
function createHeader() {
  const header = {
    alg: 'HS256',
    typ: 'JWT',
  }

  return encode64(header).replace(/=/g, '')
}

/**
 * Validate the parameter used for the payload of the token.
 *
 * @param searchRules - Search rules that are applied to every search.
 * @param apiKey - Api key used as issuer of the token.
 * @param uid - The uid of the api key used as issuer of the token.
 * @param expiresAt - Date at which the token expires.
 */
function validateTokenParameters(tokenParams: {
  searchRules: TokenSearchRules
  uid: string
  apiKey: string
  expiresAt?: Date
}) {
  const { searchRules, uid, apiKey, expiresAt } = tokenParams

  if (expiresAt) {
    if (!(expiresAt instanceof Date)) {
      throw new MeiliSearchError(
        `Meilisearch: The expiredAt field must be an instance of Date.`
      )
    } else if (expiresAt.getTime() < Date.now()) {
      throw new MeiliSearchError(
        `Meilisearch: The expiresAt field must be a date in the future.`
      )
    }
  }

  if (searchRules) {
    if (!(typeof searchRules === 'object' || Array.isArray(searchRules))) {
      throw new MeiliSearchError(
        `Meilisearch: The search rules added in the token generation must be of type array or object.`
      )
    }
  }

  if (!apiKey || typeof apiKey !== 'string') {
    throw new MeiliSearchError(
      `Meilisearch: The API key used for the token generation must exist and be of type string.`
    )
  }

  if (!uid || typeof uid !== 'string') {
    throw new MeiliSearchError(
      `Meilisearch: The uid of the api key used for the token generation must exist, be of type string and comply to the uuid4 format.`
    )
  }

  if (!validateUuid4(uid)) {
    throw new MeiliSearchError(
      `Meilisearch: The uid of your key is not a valid uuid4. To find out the uid of your key use getKey().`
    )
  }
}

/**
 * Create the payload of the token.
 *
 * @param searchRules - Search rules that are applied to every search.
 * @param uid - The uid of the api key used as issuer of the token.
 * @param expiresAt - Date at which the token expires.
 * @returns The payload encoded in base64.
 */
function createPayload(payloadParams: {
  searchRules: TokenSearchRules
  uid: string
  expiresAt?: Date
}): string {
  const { searchRules, uid, expiresAt } = payloadParams

  const payload = {
    searchRules,
    apiKeyUid: uid,
    exp: expiresAt ? Math.floor(expiresAt.getTime() / 1000) : undefined,
  }

  return encode64(payload).replace(/=/g, '')
}

class Token {
  config: Config

  constructor(config: Config) {
    this.config = config
  }

  /**
   * Generate a tenant token
   *
   * @param apiKeyUid - The uid of the api key used as issuer of the token.
   * @param searchRules - Search rules that are applied to every search.
   * @param options - Token options to customize some aspect of the token.
   * @returns The token in JWT format.
   */
  generateTenantToken(
    apiKeyUid: string,
    searchRules: TokenSearchRules,
    options?: TokenOptions
  ): string {
    const apiKey = options?.apiKey || this.config.apiKey || ''
    const uid = apiKeyUid || ''
    const expiresAt = options?.expiresAt

    validateTokenParameters({ apiKey, uid, expiresAt, searchRules })

    const encodedHeader = createHeader()
    const encodedPayload = createPayload({ searchRules, uid, expiresAt })
    const signature = sign(apiKey, encodedHeader, encodedPayload)

    return `${encodedHeader}.${encodedPayload}.${signature}`
  }
}
export { Token }
