/**
 * API Layer
 *
 * Modern fetch-based API layer with:
 * - Request deduplication (identical concurrent GETs share one promise)
 * - Loading indicator integration (requestsStore)
 * - AbortSignal support
 * - Build-version checking (X-Build-Time header)
 * - Sentry span instrumentation
 * - In-memory cache for getCachedEntries / getCachedEntry
 */
import { get } from "svelte/store"
import { apiRequest, obj2str } from "../../../api/hooks/lib/ssr"
import * as sentry from "../sentry"
import { apiBaseOverride } from "./store"
import { incrementRequests, decrementRequests } from "./requestsStore"
import { setServerBuildTime } from "./serverBuildInfo"
import { checkBuildVersion } from "./versionCheck"
import { mockApiRequest } from "./mock"

// ---------------------------------------------------------------------------
// Request deduplication
// ---------------------------------------------------------------------------
const pendingRequests = new Map<string, Promise<ApiResult<unknown>>>()

/**
 * Generate a deterministic cache key for a request so identical parallel calls
 * collapse into a single network round-trip.
 */
const getRequestCacheKey = (endpoint: string, options?: ApiOptions, body?: unknown): string => {
    const deterministicStringify = (obj: unknown): string => {
        if (obj === null || obj === undefined) return String(obj)
        if (typeof obj !== "object") return JSON.stringify(obj)
        if (Array.isArray(obj)) return `[${obj.map(deterministicStringify).join(",")}]`
        const keys = Object.keys(obj).sort()
        const pairs = keys.map(
            (key) => `${JSON.stringify(key)}:${deterministicStringify((obj as Record<string, unknown>)[key])}`
        )
        return `{${pairs.join(",")}}`
    }
    return deterministicStringify({ endpoint, options, body })
}

// ---------------------------------------------------------------------------
// Core API function
// ---------------------------------------------------------------------------

/**
 * Execute an API request.
 *
 * - Identical concurrent GET requests are deduplicated automatically.
 * - Pass `signal` to opt-out of deduplication and enable cancellation.
 * - Drives the global loading indicator via `requestsStore`.
 * - Reads the `X-Build-Time` response header and triggers version check.
 *
 * @example
 * const { data, count } = await api<ContentEntry[]>("content", { filter: { lang: "de" } })
 */
export const api = async <T>(
    endpoint: string,
    options?: ApiOptions,
    body?: unknown,
    signal?: AbortSignal
): Promise<ApiResult<T>> => {
    const _apiBaseOverride = get(apiBaseOverride) || ""

    // Merge options – auth header injection can be added here later:
    // const authToken = getAccessToken()
    // ...(authToken ? { Authorization: `Bearer ${authToken}` } : {})
    const mergedHeaders = {
        ...(options?.headers || {}),
    }
    const mergedOptions: ApiOptions = { ...options, headers: mergedHeaders, signal }

    // Deduplication: skip when caller provides a signal (they want explicit control)
    const cacheKey = getRequestCacheKey(endpoint, options, body)
    if (!signal && pendingRequests.has(cacheKey)) {
        return pendingRequests.get(cacheKey) as Promise<ApiResult<T>>
    }

    const requestPromise = (async () => {
        try {
            if (signal?.aborted) {
                throw new DOMException("Aborted", "AbortError")
            }
            incrementRequests()

            // ── Mock interceptor (tree-shaken when __MOCK__ is false) ──
            if (__MOCK__) {
                const mockResult = mockApiRequest(endpoint, mergedOptions, body)
                if (mockResult) return mockResult as ApiResult<T>
                // No mock data for this endpoint → 404
                throw { response: { status: 404 }, data: { error: `[mock] No mock data for "${endpoint}"` } }
            }

            const data = await apiRequest(_apiBaseOverride + endpoint, mergedOptions, body, sentry)

            // Build-version check only on GETs (don't reload mid-write)
            const method = mergedOptions?.method?.toUpperCase() || "GET"
            if (method === "GET" && data?.buildTime) {
                setServerBuildTime(data.buildTime)
                checkBuildVersion(data.buildTime)
            }

            return data as ApiResult<T>
        } catch (err) {
            // Don't send abort errors to Sentry
            if (err instanceof DOMException && err.name === "AbortError") throw err

            // Auth: 401 handling placeholder
            // const status = (err as { response?: { status?: number } })?.response?.status
            // if (status === 401) { /* refresh token + retry */ }

            throw err
        } finally {
            if (!signal) {
                pendingRequests.delete(cacheKey)
            }
            decrementRequests()
        }
    })()

    if (!signal) {
        pendingRequests.set(cacheKey, requestPromise)
    }

    return requestPromise
}

// ---------------------------------------------------------------------------
// In-memory response cache (for getCachedEntries / getCachedEntry)
// ---------------------------------------------------------------------------
const cache: Record<string, { expire: number; data: unknown }> = {}
const CACHE_TTL = 1000 * 60 * 60 // 1 hour

// ---------------------------------------------------------------------------
// Generic collection helpers
// ---------------------------------------------------------------------------

type CollectionNameT = "medialib" | "content" | "navigation" | string

type EntryTypeSwitch<T extends string> = T extends "medialib"
    ? MedialibEntry
    : T extends "content"
      ? ContentEntry
      : T extends "navigation"
        ? NavigationEntry
        : Record<string, unknown>

export async function getDBEntries<T extends CollectionNameT>(
    collectionName: T,
    options?: ApiOptions
): Promise<EntryTypeSwitch<T>[]> {
    const c = await api<EntryTypeSwitch<T>[]>(collectionName, options)
    return c.data
}

export async function getCachedEntries<T extends CollectionNameT>(
    collectionName: T,
    options?: ApiOptions
): Promise<EntryTypeSwitch<T>[]> {
    const filterStr = obj2str({ collectionName, options })
    if (cache[filterStr] && cache[filterStr].expire >= Date.now()) {
        return cache[filterStr].data as EntryTypeSwitch<T>[]
    }
    const entries = await getDBEntries<T>(collectionName, options)
    cache[filterStr] = { expire: Date.now() + CACHE_TTL, data: entries }
    return entries
}

export async function getDBEntry<T extends CollectionNameT>(
    collectionName: T,
    options?: ApiOptions
): Promise<EntryTypeSwitch<T> | undefined> {
    return (await getDBEntries<T>(collectionName, { ...options, limit: 1 }))?.[0]
}

export async function getCachedEntry<T extends CollectionNameT>(
    collectionName: T,
    options?: ApiOptions
): Promise<EntryTypeSwitch<T> | undefined> {
    return (await getCachedEntries<T>(collectionName, { ...options, limit: 1 }))?.[0]
}

export async function postDBEntry<T extends CollectionNameT>(
    collectionName: T,
    entry: EntryTypeSwitch<T>
): Promise<ApiResult<EntryTypeSwitch<T>>> {
    return api<EntryTypeSwitch<T>>(collectionName, { method: "POST" }, entry)
}
