/**
 * Lightweight Strapi REST fetch client.
 *
 * - No external SDK dependency (works with Strapi v4 & v5 REST API).
 * - Silently returns `null` when Strapi is disabled / unreachable so
 *   the site keeps building with its bundled default content.
 */

import { strapiConfig, isStrapiEnabled } from "./config"
import { resolveAssetUrl, resolveUploadUrl } from "@/lib/asset-url"
import type {
  StrapiResponse,
  StrapiSingleResponse,
  StrapiMedia,
} from "./types"

const UNAVAILABLE_RETRY_MS = 30_000
const shouldFailOnUnavailable =
  process.env.STRAPI_REQUIRE_CONTENT === "true"

let strapiUnavailableUntil = 0
let hasLoggedUnavailable = false
const loggedHttpFailures = new Set<string>()

function isTemporarilyUnavailable(): boolean {
  return Date.now() < strapiUnavailableUntil
}

function markTemporarilyUnavailable(): void {
  strapiUnavailableUntil = Date.now() + UNAVAILABLE_RETRY_MS
}

function describeFetchError(err: unknown): string {
  if (!(err instanceof Error)) return String(err)

  const cause = err.cause as { code?: unknown } | undefined
  const code = typeof cause?.code === "string" ? ` (${cause.code})` : ""
  return `${err.name}: ${err.message}${code}`
}

/** Build an absolute URL for a Strapi REST endpoint. */
function endpoint(path: string, qs?: Record<string, string | undefined>): string {
  const base = `${strapiConfig.url}${strapiConfig.apiPrefix}${path}`
  if (!qs) return base
  const params = new URLSearchParams()
  for (const [key, value] of Object.entries(qs)) {
    if (value !== undefined) params.set(key, value)
  }
  const query = params.toString()
  return query ? `${base}?${query}` : base
}

type StrapiMediaLike =
  | string
  | StrapiMedia
  | { data?: unknown; attributes?: unknown; url?: unknown; formats?: unknown }
  | unknown[]

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null
}

function firstString(...values: unknown[]): string {
  for (const value of values) {
    if (typeof value === "string" && value.trim() !== "") return value
  }
  return ""
}

function getFormatUrl(formats: unknown): string {
  if (!isRecord(formats)) return ""

  return firstString(
    isRecord(formats.large) ? formats.large.url : undefined,
    isRecord(formats.medium) ? formats.medium.url : undefined,
    isRecord(formats.small) ? formats.small.url : undefined,
    isRecord(formats.thumbnail) ? formats.thumbnail.url : undefined,
  )
}

function normalizeMediaPath(path: string): string {
  if (path.startsWith("/assets/")) return resolveAssetUrl(path)
  if (path.startsWith("/uploads/")) return resolveUploadUrl(path)

  if (path.startsWith("http")) {
    try {
      const url = new URL(path)
      const isConfiguredStrapi = url.origin === strapiConfig.url
      const isLocalStrapi =
        url.hostname === "localhost" ||
        url.hostname === "127.0.0.1" ||
        url.hostname === "0.0.0.0"

      if ((isLocalStrapi || isConfiguredStrapi) && url.pathname.startsWith("/uploads/")) {
        return resolveUploadUrl(url.pathname) + url.search
      }
    } catch {
      return path
    }

    return path
  }

  return path.startsWith("/") ? `${strapiConfig.url}${path}` : path
}

function unwrapMedia(media: StrapiMediaLike | null | undefined): unknown {
  if (!media) return null
  if (Array.isArray(media)) return unwrapMedia(media[0] as StrapiMediaLike)
  if (!isRecord(media)) return media

  const data = media.data
  if (Array.isArray(data)) return unwrapMedia(data[0] as StrapiMediaLike)
  if (isRecord(data)) return unwrapMedia(data as StrapiMediaLike)

  if (isRecord(media.attributes)) return unwrapMedia(media.attributes as StrapiMediaLike)

  return media
}

/** Resolve a Strapi media/file object into a usable URL. */
export function getMediaUrl(media: StrapiMediaLike | null | undefined): string {
  const resolved = unwrapMedia(media)
  if (!resolved) return ""
  if (typeof resolved === "string") return normalizeMediaPath(resolved)
  if (!isRecord(resolved)) return ""

  const path = firstString(resolved.url, getFormatUrl(resolved.formats))
  if (!path) return ""
  return normalizeMediaPath(path)
}

/**
 * Fetch a Strapi REST endpoint and return the parsed JSON, or `null`
 * on any error (network, non-2xx, disabled).
 */
export async function fetchStrapi<T>(
  path: string,
  qs?: Record<string, string | undefined>,
): Promise<T | null> {
  if (!isStrapiEnabled) return null
  if (isTemporarilyUnavailable()) return null

  const url = endpoint(path, qs)
  const headers: Record<string, string> = {
    "Content-Type": "application/json",
  }
  if (strapiConfig.readToken) {
    headers.Authorization = `Bearer ${strapiConfig.readToken}`
  }

  try {
    const res = await fetch(url, {
      headers,
      next: { revalidate: strapiConfig.revalidate },
    })
    if (!res.ok) {
      const failureKey = `${path}:${res.status}`
      const message = `[strapi] ${res.status} ${res.statusText} for ${path}.`
      if (shouldFailOnUnavailable) throw new Error(message)
      if (!loggedHttpFailures.has(failureKey)) {
        console.warn(`[strapi] ${res.status} ${res.statusText} for ${path}; using bundled defaults.`)
        loggedHttpFailures.add(failureKey)
      }
      return null
    }
    hasLoggedUnavailable = false
    loggedHttpFailures.delete(`${path}:${res.status}`)
    return (await res.json()) as T
  } catch (err) {
    markTemporarilyUnavailable()
    const message = `[strapi] CMS unavailable at ${strapiConfig.url}. ${describeFetchError(err)}`
    if (shouldFailOnUnavailable) throw new Error(message)
    if (!hasLoggedUnavailable) {
      console.warn(
        `[strapi] CMS unavailable at ${strapiConfig.url}; using bundled defaults. ${describeFetchError(err)}`,
      )
      hasLoggedUnavailable = true
    }
    return null
  }
}

/** Fetch a Collection Type (returns array wrapper). */
export async function fetchCollection<T>(
  pluralApiId: string,
  qs?: Record<string, string | undefined>,
): Promise<T[]> {
  const data = await fetchStrapi<StrapiResponse<T>>(`/${pluralApiId}`, qs)
  return data?.data ?? []
}

/** Fetch a Single Type (returns one document). */
export async function fetchSingle<T>(
  apiId: string,
  qs?: Record<string, string | undefined>,
): Promise<T | null> {
  const data = await fetchStrapi<StrapiSingleResponse<T>>(
    `/${apiId}`,
    qs,
  )
  return data?.data ?? null
}
