import fetchRetry from "fetch-retry"
import { GraphQLClient } from "graphql-request"
import stringify from "json-stable-stringify-without-jsonify"
import { cache as reactCache } from "react"

import { env } from "@/env/server"
import { getPreviewHash, isPreviewByUid } from "@/lib/previewContext"

import { type CacheTime, getCacheClient } from "@/services/dataCache"

import { request as _request } from "./_request"
import { getOperationName } from "./getOperationName"

import type { DocumentNode } from "graphql"

import type { Data } from "@/types/request"

export async function request<T>(
  query: string | DocumentNode,
  variables?: Record<string, any>,
  cacheOptions?: {
    key: string | string[]
    ttl: CacheTime
  }
): Promise<Data<T>> {
  const shouldUsePreview = variables?.uid
    ? isPreviewByUid(variables.uid)
    : false

  const doCall = () => internalRequest<T>(query, shouldUsePreview, variables)

  if (!cacheOptions) {
    console.warn("[NO CACHE] for query", query)
    return doCall()
  }

  if (shouldUsePreview) {
    console.log("[NO CACHE] [PREVIEW] for query", query)
    return doCall()
  }

  const queryString = typeof query === "string" ? query : stringify(query)
  const variablesString = stringify(variables)

  const fullQuery = `${queryString}${variablesString}`
  const queryHash = await sha256(fullQuery)
  const operationName = getOperationName(query)

  const cacheKey: string = Array.isArray(cacheOptions.key)
    ? cacheOptions.key.join("_")
    : cacheOptions.key

  const extendedCacheKey = `${operationName}:${queryHash}:${cacheKey}`

  const _dataCache = await getCacheClient()
  return _dataCache.cacheOrGet(extendedCacheKey, doCall, cacheOptions.ttl, {
    includeGitHashInKey: false,
  })
}

function internalRequest<T>(
  query: string | DocumentNode,
  shouldUsePreview: boolean,
  variables?: Record<string, any>
): Promise<Data<T>> {
  const previewHash = getPreviewHash()
  const cmsUrl = shouldUsePreview ? env.CMS_PREVIEW_URL : env.CMS_URL

  // Creating a new client for each request to avoid conflicting parameters
  const client = new GraphQLClient(cmsUrl, {
    fetch: reactCache(async function (
      url: URL | RequestInfo,
      params?: RequestInit
    ) {
      const wrappedFetch = fetchRetry(fetch, {
        retries: 3,
        retryDelay: function (attempt) {
          return Math.pow(2, attempt) * 150 // 150, 300, 600
        },
      })
      return wrappedFetch(url, params)
    }),
  })

  const mergedParams =
    shouldUsePreview && previewHash
      ? {
          headers: {
            live_preview: previewHash,
            preview_token: env.CMS_PREVIEW_TOKEN,
          },
        }
      : {}

  return _request(client, query, variables, mergedParams)
}

async function sha256(input: string) {
  const encoder = new TextEncoder()
  const data = encoder.encode(input)
  const hashBuffer = await crypto.subtle.digest("SHA-256", data)
  const hashArray = Array.from(new Uint8Array(hashBuffer))
  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")

  return hashHex
}
