const Redis = require("ioredis");
const { LRUCache } = require("lru-cache");

const CACHE_PREFIX = "nextcache:";
const TAGS_PREFIX = "nextcache:tags:";
const LRU_MAX_SIZE = parseInt(process.env.LRU_CACHE_MAX_SIZE || "50", 10) * 1024 * 1024;

const memoryCache = new LRUCache({
  maxSize: LRU_MAX_SIZE,
  sizeCalculation: (entry) => entry.size,
});

let redis = null;
const pendingSets = new Map();

function getRedis() {
  if (!redis) {
    redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379", {
      maxRetriesPerRequest: 3,
      lazyConnect: true,
    });
    redis.on("error", (err) => {
      console.error("[redis] connection error:", err.message);
    });
  }
  return redis;
}

async function streamToBuffer(stream) {
  const reader = stream.getReader();
  const chunks = [];
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      chunks.push(value);
    }
  } finally {
    reader.releaseLock();
  }
  return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
}

function bufferToStream(buffer) {
  return new ReadableStream({
    start(controller) {
      controller.enqueue(buffer);
      controller.close();
    },
  });
}

async function areTagsExpired(tags, timestamp) {
  if (!tags?.length) return false;
  try {
    const values = await getRedis().mget(tags.map((t) => TAGS_PREFIX + t));
    return values.some((val) => val && parseInt(val, 10) > timestamp);
  } catch {
    return false;
  }
}

module.exports = {
  async get(cacheKey) {
    const pending = pendingSets.get(cacheKey);
    if (pending) await pending;

    const now = Date.now();

    // LRU
    const mem = memoryCache.get(cacheKey);
    if (mem) {
      const entry = mem.entry;
      if (now > entry.timestamp + entry.revalidate * 1000) {
        memoryCache.delete(cacheKey);
      } else if (await areTagsExpired(entry.tags, entry.timestamp)) {
        memoryCache.delete(cacheKey);
      } else {
        const [ret, keep] = entry.value.tee();
        entry.value = keep;
        return { ...entry, value: ret };
      }
    }

    // Redis
    try {
      const stored = await getRedis().get(CACHE_PREFIX + cacheKey);
      if (!stored) {
        return undefined;
      }

      const data = JSON.parse(stored);

      if (now > data.timestamp + data.revalidate * 1000) {
        await getRedis().del(CACHE_PREFIX + cacheKey);
        return undefined;
      }

      if (await areTagsExpired(data.tags, data.timestamp)) {
        await getRedis().del(CACHE_PREFIX + cacheKey);
        return undefined;
      }

      const buffer = Buffer.from(data.value, "base64");
      const entry = {
        value: bufferToStream(buffer),
        tags: data.tags,
        stale: data.stale,
        timestamp: data.timestamp,
        expire: data.expire,
        revalidate: data.revalidate,
      };

      const [ret, keep] = entry.value.tee();
      entry.value = keep;
      memoryCache.set(cacheKey, { entry, size: buffer.byteLength });

      return { ...entry, value: ret };
    } catch (err) {
      console.error("[redis] get error:", err.message);
      return undefined;
    }
  },

  async set(cacheKey, pendingEntry) {
    let resolve;
    const promise = new Promise((r) => (resolve = r));
    pendingSets.set(cacheKey, promise);

    try {
      const entry = await pendingEntry;
      const buffer = await streamToBuffer(entry.value);

      memoryCache.set(cacheKey, {
        entry: { ...entry, value: bufferToStream(buffer) },
        size: buffer.byteLength,
      });

      const ttl = entry.expire > 0 ? entry.expire : 86400;
      await getRedis().set(
        CACHE_PREFIX + cacheKey,
        JSON.stringify({
          value: buffer.toString("base64"),
          tags: entry.tags,
          stale: entry.stale,
          timestamp: entry.timestamp,
          expire: entry.expire,
          revalidate: entry.revalidate,
        }),
        "EX",
        ttl,
      );
    } catch (err) {
      console.error("[redis] set error:", err.message);
      memoryCache.delete(cacheKey);
    } finally {
      resolve();
      pendingSets.delete(cacheKey);
    }
  },

  async refreshTags() {},

  async getExpiration(tags) {
    if (!tags?.length) return 0;
    try {
      const values = await getRedis().mget(tags.map((t) => TAGS_PREFIX + t));
      return Math.max(0, ...values.map((v) => (v ? parseInt(v, 10) : 0)));
    } catch {
      return 0;
    }
  },

  async updateTags(tags) {
    if (!tags?.length) return;
    try {
      const now = Date.now();
      const pipeline = getRedis().pipeline();
      for (const tag of tags) {
        pipeline.set(TAGS_PREFIX + tag, now.toString(), "EX", 604800);
      }
      await pipeline.exec();

      for (const [key, cached] of memoryCache.entries()) {
        if (cached.entry.tags?.some((t) => tags.includes(t))) {
          memoryCache.delete(key);
        }
      }
    } catch (err) {
      console.error("[redis] updateTags error:", err.message);
    }
  },
};
