import * as Sentry from "@sentry/bun";

import { Elysia, t } from "elysia";
import { redis } from "@/services/redis";
import { ModelValidationError } from "@/errors/ModelValidationError";
import { loggerModule } from "@/utils/logger";
import { truncate } from "@/utils/truncate";
import { timeout } from "@/utils/timeout";

const MIN_LENGTH = 1;

const QUERY_TYPE = t.Object({ key: t.String({ minLength: MIN_LENGTH }) });
const cacheRouteLogger = loggerModule("cacheRoute");
export const cacheRoutes = new Elysia({ prefix: "/cache" })
    .get(
        "/",
        async ({ query: { key }, status }) => {
            key = validateKey(key);
            cacheRouteLogger.debug(`GET /cache ${key}`);

            const value = await redis.get(key);
            if (!value) {
                return status("Not Found", "Not Found");
            }

            try {
                const output = JSON.parse(value);
                return { data: output };
            } catch (e) {
                redis.del(key);
                cacheRouteLogger.error(`Invalid JSON in cache for '${key}'`, e);

                Sentry.captureException(e, {
                    tags: { cacheKey: key },
                    extra: { cacheKey: key, value: `${truncate(value, 100)}` },
                    fingerprint: ["get-route_invalid-json-in-cache"],
                });

                return status("Not Found", "Not Found");
            }
        },
        {
            query: QUERY_TYPE,
            response: { 200: t.Object({ data: t.Any() }), 404: t.String() },
        }
    )
    .put(
        "/",
        async ({ query: { key }, body, status, set }) => {
            key = validateKey(key);
            cacheRouteLogger.debug(`PUT /cache ${key}`);

            if (!body.ttl || body.ttl < 0) {
                cacheRouteLogger.warn(
                    `PUT /cache ${key} with ttl=${body.ttl}, will not cache the data`
                );
                return status("Bad Request", "ttl is required");
            }

            await redis.set(key, JSON.stringify(body.data), "EX", body.ttl);

            set.status = 204;
            return undefined;
        },
        {
            body: t.Object({ data: t.Any(), ttl: t.Number() }),
            query: QUERY_TYPE,
            response: { 204: t.Undefined(), 400: t.String() },
        }
    )
    .delete(
        "/",
        async ({ query: { key, fuzzy } }) => {
            key = validateKey(key);
            cacheRouteLogger.debug(
                `DELETE /cache ${key} ${fuzzy ? "fuzzy" : ""}`
            );
            const deletedKeys: number = fuzzy
                ? await deleteWithPattern(`*${key}*`)
                : await redis.del(key);

            cacheRouteLogger.info(`Deleted ${deletedKeys} keys for '${key}'`);
            return { deletedKeys };
        },
        {
            query: t.Object({
                ...QUERY_TYPE.properties,
                ...t.Object({ fuzzy: t.Optional(t.Boolean()) }).properties,
            }),
            response: {
                200: t.Object({ deletedKeys: t.Number() }),
                400: t.String(),
            },
        }
    );

function validateKey(key: string) {
    const parsedKey = decodeURIComponent(key);

    if (parsedKey.length < MIN_LENGTH) {
        throw new ModelValidationError(
            "Key has to be at least 1 character long"
        );
    }

    if (parsedKey.includes("*")) {
        throw new ModelValidationError("Key cannot contain wildcards");
    }

    return parsedKey;
}

async function deleteWithPattern(pattern: string) {
    let cursor = "0";
    const SCAN_SIZE = 500;

    let totalDeleteCount = 0;

    do {
        const [newCursor, foundKeys] = await redis.scan(
            cursor,
            "MATCH",
            pattern,
            "COUNT",
            SCAN_SIZE
        );

        cursor = newCursor;
        if (foundKeys.length === 0) {
            continue;
        }

        const deleteCount = await redis.del(foundKeys);

        cacheRouteLogger.info(`Deleted ${deleteCount} keys in this batch.`);
        totalDeleteCount += deleteCount;

        await timeout(10);
    } while (cursor !== "0");

    return totalDeleteCount;
}
