/**
 * @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
 * @license AGPL-3.0
 *
 * This code is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
 *
 * Any use of the work other than as authorized under this license or copyright law is prohibited.
 */

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

const cache = {}

function createWritable (body) {
  return (typeof body !== 'string' && !(body instanceof Buffer)) ? JSON.stringify(body) : body
}

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

export const versionCountGauge = new Gauge({
  name: 'redis_version_count_gauge',
  help: 'Number of cache versions stored in Redis',
  async collect () {
    const pattern = `${process.env.REDIS_PREFIX}:*:mergedMetadata`
    const results = await redis.client.keys(pattern)
    this.set(results.length)
  }
})

export const usedMemoryGauge = new Gauge({
  name: 'redis_used_ram',
  help: 'Total used memory in human readable units and as a percentage (RSS / total memory)',
  labelNames: ['memory_type'],
  async collect () {
    redis.client.info('memory', (err, result) => {
      if (err) {
        throw err
      }

      // Parse the result
      const infoLines = result.split('\r\n')
      const memoryInfo = {}

      for (const line of infoLines) {
        const [key, value] = line.split(':')
        if (key && value) {
          memoryInfo[key] = value
        }
      }
      const usedMemoryRSS = parseFloat(memoryInfo.used_memory_rss)
      const memoryTotal = parseFloat(memoryInfo.total_system_memory)

      this.set({ memory_type: 'usedMemoryRSSHuman' }, usedMemoryRSS)
      this.set({ memory_type: 'totalUsedMemoryPercentage' }, usedMemoryRSS / memoryTotal)
    })
  }
})

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 (timeout) return redis.client.set(key, value, 'EX', timeout).catch(err => logger.error(err))
  return redis.client.set(key, value).catch(err => logger.error(err))
}

export async function clear () {
  for (const prop of Object.getOwnPropertyNames(cache)) {
    delete cache[prop]
  }
  fileCacheSizeGauge.reset()
}
export async function get (key, fallback) {
  if (cache[key]) {
    logger.debug(`[Cache] Resolve from memory: ${key}`)
    return cache[key]
  }

  const promise = (async () => {
    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` })

    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) })
    }

    if (!fallback) {
      delete cache[key]
      throw new NotFoundError(`[Cache] Not found: ${key}`)
    }
    const dataFromServer = await fallback({ version, name }).catch(err => {
      if (err.status !== 404) delete cache[key]
      throw err
    })

    {
      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.`)
  fileCacheSizeGauge.dec()
  delete cache[key]
}