import {
  Bulk,
  connect,
  Redis,
  RedisConnectOptions,
  RedisValue,
} from "https://deno.land/x/redis@v0.31.0/mod.ts";
import { createLogger } from "@lib/log.ts";

const REDIS_HOST = Deno.env.get("REDIS_HOST");
const REDIS_PASS = Deno.env.get("REDIS_PASS") || "";
const REDIS_PORT = Deno.env.get("REDIS_PORT");

const log = createLogger("cache");

async function createCache<T>(): Promise<Redis> {
  if (REDIS_HOST) {
    const conf: RedisConnectOptions = {
      hostname: REDIS_HOST,
      port: REDIS_PORT || 6379,
      maxRetryCount: 2,
    };
    if (REDIS_PASS) {
      conf.password = REDIS_PASS;
    }
    try {
      const client = await connect(conf);
      log.info("redis connected");
      return client;
    } catch (_err) {
      log.info("cant connect to redis, falling back to mock");
    }
  }

  const mockRedis = new Map<string, RedisValue>();

  return {
    async keys() {
      return mockRedis.keys();
    },
    async delete(key: string) {
      mockRedis.delete(key);
      return key;
    },
    async set(key: string, value: RedisValue) {
      mockRedis.set(key, value);
      return value.toString();
    },
    async get(key: string) {
      return mockRedis.get(key) as Bulk;
    },
  };
}

const cache = await createCache();

export async function get<T>(id: string, binary = false) {
  if (binary && !(cache instanceof Map)) {
    const cacheHit = await cache.sendCommand("GET", [id], {
      returnUint8Arrays: true,
    }) as T;
    return cacheHit;
  }
  const cacheHit = await cache.get(id) as T;
  return cacheHit;
}
export function clearAll() {
  if ("flushall" in cache) {
    return cache.flushall();
  } else {
    for (const k of cache.keys()) {
      cache.delete(k);
    }
  }
}

export function expire(id: string, seconds: number) {
  if ("expire" in cache) {
    return cache.expire(id, seconds);
  }
}

type RedisOptions = {
  expires?: number;
  noLog?: boolean;
};

export function del(key: string) {
  return cache.del(key);
}

export function keys(prefix: string) {
  return cache.keys(prefix);
}

export function set<T extends RedisValue>(
  id: string,
  content: T,
  options?: RedisOptions,
) {
  if (options?.noLog !== true) log.debug("storing ", { id });
  return cache.set(id, content, { ex: options?.expires || undefined });
}

export const cacheFunction = async <T extends (() => Promise<unknown>)>(
  {
    fn,
    id,
    options = {},
  }: {
    fn: T;
    id: string;
    options?: RedisOptions;
  },
): Promise<Awaited<ReturnType<T>>> => {
  const cacheResult = await get(id) as string;

  if (cacheResult) {
    return JSON.parse(cacheResult) as Awaited<ReturnType<typeof fn>>;
  }

  const result = await fn();

  set(id, JSON.stringify(result), options);

  return result as Awaited<ReturnType<typeof fn>>;
};
