import { trace, type Tracer } from "@opentelemetry/api"

import { env } from "@/env/server"
import { createCounter } from "@/server/telemetry"

import { getCacheClient } from "@/services/dataCache"

import type { ServiceTokenResponse } from "@/types/tokens"

export async function getServiceToken() {
  const tracer = trace.getTracer("getServiceToken")

  return await tracer.startActiveSpan("getServiceToken", async () => {
    const scopes = ["profile", "hotel", "booking", "package", "availability"]

    const cacheKey = getServiceTokenCacheKey(scopes)
    const cacheClient = await getCacheClient()
    const token = await getOrSetServiceTokenFromCache(cacheKey, scopes, tracer)

    if (token.expiresAt < Date.now()) {
      await cacheClient.deleteKey(cacheKey)

      const newToken = await getOrSetServiceTokenFromCache(
        cacheKey,
        scopes,
        tracer
      )
      return newToken.jwt
    }

    return token.jwt
  })
}

async function getOrSetServiceTokenFromCache(
  cacheKey: string,
  scopes: string[],
  tracer: Tracer
) {
  const cacheClient = await getCacheClient()
  const token = await cacheClient.cacheOrGet(
    cacheKey,
    async () => {
      return await tracer.startActiveSpan("fetch new token", async () => {
        const newToken = await getJwt(scopes)
        return newToken
      })
    },
    "1h"
  )
  return token
}

async function getJwt(scopes: string[]) {
  const getJwtCounter = createCounter("tokenManager", "getJwt")
  const metricsGetJwt = getJwtCounter.init({
    scopes,
  })

  metricsGetJwt.start()

  const jwt = await fetchServiceToken(scopes)

  const expiresAt = Date.now() + jwt.expires_in * 1000

  metricsGetJwt.success()

  return { expiresAt, jwt }
}

async function fetchServiceToken(scopes: string[]) {
  const fetchServiceTokenCounter = createCounter(
    "tokenManager",
    "fetchServiceToken"
  )
  const metricsFetchServiceToken = fetchServiceTokenCounter.init({
    scopes,
  })

  metricsFetchServiceToken.start()

  const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Accept: "application/json",
    },
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_id: env.CURITY_CLIENT_ID_SERVICE,
      client_secret: env.CURITY_CLIENT_SECRET_SERVICE,
      scope: scopes.join(" "),
    }),
  })

  if (!response.ok) {
    await metricsFetchServiceToken.httpError(response)

    const text = await response.text()
    throw new Error(
      `[fetchServiceToken] Failed to obtain service token: ${JSON.stringify({
        status: response.status,
        statusText: response.statusText,
        text,
      })}`
    )
  }

  const result = response.json() as Promise<ServiceTokenResponse>

  metricsFetchServiceToken.success()

  return result
}

function getServiceTokenCacheKey(scopes: string[]): string {
  return `serviceToken:${scopes.join(",")}`
}
