import { createWritable } from './files.js'
import { logger } from './logger.js'
import * as redis from './redis.js'
import { getRedisKey } from './util.js'
import { Gauge } from 'prom-client'

const cache = {}

export const fileCacheSizeGauge = new Gauge({
  name: 'file_cache_size',
  help: 'Number of entries in file cache'
})

function logAndIgnoreError (err) {
  logger.error(err)
}

export function set (key, value, timeout) {
  logger.debug(`[Cache] Set ${key}`)
  if (cache[key] === value) return
  cache[key] = value
  if (timeout) setTimeout(expire, timeout * 1000, key)
  if (redis.isEnabled()) {
    if (timeout) return redis.client.set(key, value, 'EX', timeout).catch(logAndIgnoreError)
    return redis.client.set(key, value).catch(logAndIgnoreError)
  }
}

export async function clear () {
  for (const prop of Object.getOwnPropertyNames(cache)) {
    delete cache[prop]
  }
  fileCacheSizeGauge.reset()
}

export function get (key, fallback) {
  if (cache[key]) {
    logger.debug(`[Cache] Resolve from memory: ${key}`)
    return cache[key]
  }

  const promise = (async () => {
    if (redis.isEnabled()) {
      let result = await redis.client.get(key).catch(err => logger.error(err))
      if (result) {
        logger.debug(`[Cache] Resolve from redis: ${key}`)
        result = JSON.parse(result)
        cache[key] = result
        redis.client.ttl(key).then(timeout => {
          if (timeout > 0) setTimeout(expire, timeout * 1000, key)
        })
        return result
      }
    }

    if (!fallback) return

    const [fallbackResult, timeout] = await fallback()
    if (fallbackResult) {
      logger.debug(`[Cache] Found a getter for: ${key}`)
      set(key, JSON.stringify(fallbackResult), timeout)
      // overwrite local cache again, as set() will store the stringified version
      cache[key] = fallbackResult
    }
    return fallbackResult
  })()

  cache[key] = promise

  return promise
}

export function getFile ({ name, version }, fallback) {
  const key = getRedisKey({ version, name })

  // try to get the file synchronously.
  const data = cache[key]
  if (data) {
    logger.debug(`[Cache] Resolve file from memory: ${key}`)
    return data
  }

  // if synchronously does not work, store the async promise for further requests
  const promise = (async () => {
    const bodyKey = getRedisKey({ version, name: `${name}:body` })
    const metaKey = getRedisKey({ version, name: `${name}:meta` })

    if (redis.isEnabled()) {
      const [body, meta = '{}'] = await Promise.all([
        redis.client.getBuffer(bodyKey),
        redis.client.get(metaKey)
      ]).catch((err) => {
        logger.error(`[Cache] could not access redis: ${err}`)
        return []
      })

      if (body) {
        logger.debug(`[Cache] Resolve file from redis: ${key}`)
        fileCacheSizeGauge.inc()
        return (cache[key] = { body, ...JSON.parse(meta) })
      }
    }

    const dataFromServer = await fallback({ version, name }).catch(err => {
      if (err.status !== 404) delete cache[key]
      throw err
    })

    if (redis.isEnabled()) {
      logger.debug(`[Cache] Store file in redis: ${key}`)
      const { body, ...rest } = dataFromServer
      redis.client.set(bodyKey, createWritable(body)).catch(err => logger.error(`[Cache] could not store ${bodyKey}: ${err}`))
      redis.client.set(metaKey, JSON.stringify(rest)).catch(err => logger.error(`[Cache] could not store ${metaKey}: ${err}`))
    }

    // overwrite cache with synchronous data
    logger.debug(`[Cache] Store file in memory: ${key}`)
    fileCacheSizeGauge.inc()
    return (cache[key] = dataFromServer)
  })()

  // temporary set to promise
  cache[key] = promise
  return promise
}

function expire (key) {
  logger.debug(`[Cache] Key ${key} has expired.`)
  delete cache[key]
}