import { get } from "svelte/store"
import { apiBaseOverride } from "./lib/store"
import { apiBaseURL } from "./config"

const cache = new Map<string, { expire: number; data: unknown }>()

const cacheKey = (value: unknown) => JSON.stringify(value, Object.keys(value as object).sort())

const buildQuery = (options: ApiOptions) => {
    const params = new URLSearchParams()

    if (options.filter) params.set("filter", JSON.stringify(options.filter))
    if (options.sort) params.set("sort", options.sort)
    if (typeof options.limit === "number") params.set("limit", String(options.limit))
    if (typeof options.offset === "number") params.set("offset", String(options.offset))
    if (options.lookup) params.set("lookup", options.lookup)
    if (options.projection) params.set("projection", options.projection)

    if (options.params) {
        Object.entries(options.params).forEach(([key, value]) => {
            if (value !== undefined && value !== null) {
                params.set(key, value)
            }
        })
    }

    return params
}

const toAbsoluteEndpoint = (endpoint: string) => {
    const base = get(apiBaseOverride) ?? apiBaseURL
    if (endpoint.startsWith("/")) {
        return `${base}${endpoint.slice(1)}`
    }
    return `${base}${endpoint}`
}

const handleResponse = async <T>(response: Response): Promise<ApiResult<T>> => {
    const text = await response.text()
    const data = text ? JSON.parse(text) : null

    if (!response.ok) {
        const error = new Error(response.statusText)
        Object.assign(error, { status: response.status, data })
        throw error
    }

    const countHeader = response.headers.get("x-results-count")
    const count = countHeader ? parseInt(countHeader, 10) : Array.isArray(data) ? data.length : 0

    return {
        data,
        count,
    }
}

export async function api<T>(endpoint: string, options: ApiOptions = {}, body?: unknown): Promise<ApiResult<T>> {
    const method = (options.method || "GET").toUpperCase()
    const headers: Record<string, string> = {
        Accept: "application/json",
        ...options.headers,
    }

    const url = new URL(toAbsoluteEndpoint(endpoint), "http://dummy.base")
    const query = buildQuery(options)
    query.forEach((value, key) => {
        url.searchParams.set(key, value)
    })

    const init: RequestInit = {
        method,
        headers,
    }

    if (method === "POST" || method === "PUT" || method === "PATCH") {
        headers["Content-Type"] = "application/json"
        init.body = JSON.stringify(body ?? {})
    }

    if (method === "DELETE") {
        init.body = body ? JSON.stringify(body) : undefined
        if (init.body) headers["Content-Type"] = "application/json"
    }

    const response = await fetch(url.toString().replace("http://dummy.base", ""), init)
    return handleResponse<T>(response)
}

type EntryTypeSwitch<T> = T extends "medialib"
    ? MedialibEntry
    : T extends "content"
      ? ContentEntry
      : T extends "navigation"
        ? NavigationEntry
        : any

export async function getDBEntries<T extends CollectionName>(
    collectionName: T,
    filter?: { [key: string]: any },
    sort: string = "sort",
    limit?: number,
    offset?: number,
    projection?: string
): Promise<EntryTypeSwitch<T>[]> {
    const response = await api<EntryTypeSwitch<T>[]>(collectionName, { filter, sort, limit, offset, projection })
    return (response.data as EntryTypeSwitch<T>[]) ?? []
}

export async function getCachedEntries<T extends CollectionName>(
    collectionName: T,
    filter?: { [key: string]: any },
    sort: string = "sort",
    limit?: number,
    offset?: number,
    projection?: string
): Promise<EntryTypeSwitch<T>[]> {
    const key = cacheKey({ collectionName, filter, sort, limit, offset, projection })
    const cached = cache.get(key)
    if (cached && cached.expire > Date.now()) {
        return cached.data as EntryTypeSwitch<T>[]
    }

    const entries = await getDBEntries(collectionName, filter, sort, limit, offset, projection)
    cache.set(key, { data: entries, expire: Date.now() + 1000 * 60 * 5 })
    return entries
}

export async function getDBEntry<T extends CollectionName>(collectionName: T, filter: { [key: string]: any }) {
    const entries = await getDBEntries(collectionName, filter, "_id", 1)
    return entries?.[0]
}

export async function getCachedEntry<T extends CollectionName>(collectionName: T, filter: { [key: string]: any }) {
    const entries = await getCachedEntries(collectionName, filter, "_id", 1)
    return entries?.[0]
}

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

export async function putDBEntry<T extends CollectionName>(
    collectionName: T,
    entry: Partial<EntryTypeSwitch<T>> & { id: string }
) {
    const { id, ...payload } = entry
    return api<EntryTypeSwitch<T>>(`${collectionName}/${id}`, { method: "PUT" }, payload)
}

export async function deleteDBEntry<T extends CollectionName>(collectionName: T, id: string) {
    return api(`${collectionName}/${id}`, { method: "DELETE" })
}
