/* eslint-disable no-console */
import { Headers } from '../../lib/headers/Headers.js'
import { Sdk } from './Sdk.js'
import { SdkError, IApiError } from './Errors.js'
import {
  ContextDeleteRecord,
  ContextListOptions,
  ContextListResult,
  IBulkOperationResult,
} from '../../events/BaseEvents.js'

export interface SdkOptions {
  /**
   * Uses the provided token for authentication.
   */
  token?: string
}

export interface IStoreRequestOptions extends SdkOptions {
  method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'
  headers?: Record<string, string>
  body?: string
  /**
   * If there's retry logic involved, this flag tells the engine
   * that the retry logic was triggered.
   */
  retry?: boolean
}

export interface IStoreResponse {
  status: number
  headers: Headers
  body?: string
}

export const E_INVALID_JSON = 'Invalid JSON response.'
export const E_RESPONSE_NO_VALUE = 'The response has no value.'
export const E_RESPONSE_STATUS = 'Invalid response status: '
export const E_RESPONSE_UNKNOWN = 'Unknown response from the server.'
export const E_RESPONSE_LOCATION = 'The response has no "location" header.'

export class SdkBase {
  constructor(public sdk: Sdk) {}

  protected logInvalidResponse(response: IStoreResponse): void {
    if (this.sdk.silent) {
      return
    }
    if (response.body) {
      try {
        const data = JSON.parse(response.body)
        if (data.message) {
          console.warn(`[Store message]: ${data.message}`)
        }
      } catch {
        // .
      }
    }
  }

  /**
   * Throws unified message for a common error status codes.
   * It handles 404, 403, and 401 status codes.
   */
  protected inspectCommonStatusCodes(response: IStoreResponse): void {
    const { status, body } = response
    if (status === 404) {
      let e = this.createGenericSdkError(body)
      if (!e) {
        e = new SdkError(`Not found.`, status)
        e.response = body
      }
      throw e
    }
    if (status === 403) {
      let e = this.createGenericSdkError(body)
      if (!e) {
        e = new SdkError(`You have no access to this resource.`, status)
        e.response = body
      }
      throw e
    }
    if (status === 401) {
      let e = this.createGenericSdkError(body)
      if (!e) {
        e = new SdkError(`Not authorized.`, status)
        e.response = body
      }
      throw e
    }
  }

  /**
   * Reads the response as ApiError
   * @param body The message returned by the store.
   * @returns The error schema or undefined when not an error;
   */
  protected readErrorResponse(body?: string): IApiError | undefined {
    if (!body) {
      return undefined
    }
    let data: IApiError
    try {
      data = JSON.parse(body)
    } catch {
      return undefined
    }
    if (data.error && data.message) {
      return data as IApiError
    }
    return undefined
  }

  protected createGenericSdkError(body?: string): SdkError | undefined {
    const info = this.readErrorResponse(body)
    if (!info) {
      return undefined
    }
    const e = new SdkError(info.message, info.code)
    e.detail = info.detail
    e.response = body
    return e
  }

  /**
   * A generic (common) method to batch-delete object from the store.
   *
   * @param url The URL object to make the request.
   * @param keys The list of keys to delete
   * @param request Optional request options.
   * @returns THe list of context delete records.
   */
  protected async genericBatchDelete(
    url: URL,
    keys: string[],
    request: SdkOptions = {}
  ): Promise<IBulkOperationResult<ContextDeleteRecord>> {
    const { token } = request
    const result = await this.sdk.http.post(url.toString(), { token, body: JSON.stringify(keys) })
    this.inspectCommonStatusCodes(result)
    const E_PREFIX = 'Unable to delete in batch. '
    if (result.status !== 200) {
      this.logInvalidResponse(result)
      let e = this.createGenericSdkError(result.body)
      if (!e) {
        e = new SdkError(`${E_PREFIX}${E_RESPONSE_STATUS}${result.status}`, result.status)
        e.response = result.body
      }
      throw e
    }
    if (!result.body) {
      throw new Error(`${E_PREFIX}${E_RESPONSE_NO_VALUE}`)
    }

    let data: IBulkOperationResult<ContextDeleteRecord>
    try {
      data = JSON.parse(result.body)
    } catch {
      throw new Error(`${E_PREFIX}${E_INVALID_JSON}.`)
    }
    if (!Array.isArray(data.items)) {
      throw new Error(`${E_PREFIX}${E_RESPONSE_UNKNOWN}.`)
    }
    return data
  }

  /**
   * Generic (common) function to delete an entity in the store.
   *
   * @param url The URL to make the DELETE request to.
   * @param request Optional request options.
   */
  protected async genericDelete(url: URL, request: SdkOptions = {}): Promise<void> {
    const { token } = request
    const result = await this.sdk.http.delete(url.toString(), { token })
    this.inspectCommonStatusCodes(result)
    const E_PREFIX = 'Unable to delete the object. '
    if (result.status !== 204) {
      this.logInvalidResponse(result)
      let e = this.createGenericSdkError(result.body)
      if (!e) {
        e = new SdkError(`${E_PREFIX}${E_RESPONSE_STATUS}${result.status}`, result.status)
        e.response = result.body
      }
      throw e
    }
  }

  protected async genericList(
    url: URL,
    options: ContextListOptions,
    request: SdkOptions = {}
  ): Promise<ContextListResult<unknown>> {
    const { token } = request
    this.sdk.appendListOptions(url, options)
    const result = await this.sdk.http.get(url.toString(), { token })
    this.inspectCommonStatusCodes(result)
    const E_PREFIX = 'Unable to list revisions for a file. '
    if (result.status !== 200) {
      this.logInvalidResponse(result)
      let e = this.createGenericSdkError(result.body)
      if (!e) {
        e = new SdkError(`${E_PREFIX}${E_RESPONSE_STATUS}${result.status}`, result.status)
        e.response = result.body
      }
      throw e
    }
    if (!result.body) {
      throw new Error(`${E_PREFIX}${E_RESPONSE_NO_VALUE}`)
    }
    let data: ContextListResult<unknown>
    try {
      data = JSON.parse(result.body)
    } catch {
      throw new Error(`${E_PREFIX}${E_INVALID_JSON}.`)
    }
    if (!Array.isArray(data.items)) {
      throw new Error(`${E_PREFIX}${E_RESPONSE_UNKNOWN}.`)
    }
    return data
  }

  protected readResponseBody(response: IStoreResponse, errorPrefix: string): string {
    if (response.status !== 200) {
      this.logInvalidResponse(response)
      let e = this.createGenericSdkError(response.body)
      if (!e) {
        e = new SdkError(`${errorPrefix}${E_RESPONSE_STATUS}${response.status}`, response.status)
        e.response = response.body
      }
      throw e
    }
    if (!response.body) {
      throw new Error(`${errorPrefix}${E_RESPONSE_NO_VALUE}`)
    }
    return response.body
  }

  protected readResponseJSON(response: IStoreResponse, errorPrefix: string): unknown {
    const body = this.readResponseBody(response, errorPrefix)
    let data: unknown
    try {
      data = JSON.parse(body)
    } catch {
      throw new Error(`${errorPrefix}${E_INVALID_JSON}.`)
    }
    return data
  }
}
