/*
 *
 * @copyright Copyright (c) OX Software 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 { configMap } from './config_map.js'
import { getRedisKey, hash } from './util.js'
import logger from './logger.js'
import * as cache from './cache.js'
import * as redis from './redis.js'
import { Gauge } from 'prom-client'
import { warmCache } from './files.js'
import { getViteManifests } from './manifests.js'

export const versionInfo = {
  version: null,
  details: {}
}

export const versionUpdateGauge = new Gauge({
  name: 'version_update_event',
  help: 'Timestamp of a version update event',
  labelNames: ['version']
})

/**
 * Fetches latest version information from all the ui-containers
 * @returns {Promise<{version, details}>} Return a promise containing this information
 */
export async function fetchVersionInfo () {
  const versions = await Promise.all(configMap.urls.map(async baseUrl => {
    try {
      const response = await fetch(new URL('meta.json', baseUrl), { cache: 'no-store' })
      if (!response.ok) throw new Error()

      if (response.headers.get('version')) return response.headers.get('version')

      const meta = await response.json()
      const version = meta.commitSha || meta.buildDate || meta.version
      if (!version) throw new Error()
      return version
    } catch (err) {
      logger.warn(`[Version] UI container at ${baseUrl} does not have meta.json. Fall back to version hash based on manifest.`)
    }
    const response = await fetch(new URL('manifest.json', baseUrl), { cache: 'no-store' })
    if (!response.ok) throw new Error(`Cannot fetch manifest.json from ${baseUrl}`)
    const manifest = await response.json()
    return hash(manifest)
  }))
  const details = Object.fromEntries(configMap.urls.map((url, i) => [url, versions[i]]))
  const version = `${hash(Object.values(details))}${configMap.salt ? `-${configMap.salt}` : ''}`

  return { details, version }
}

/**
 * Gets version information from redis or the ui-containers, if not cached.
 * @returns {Promise<{version, details}>} Return a promise containing this information
 */
export async function getVersionInfo () {
  if (versionInfo.version) return versionInfo
  const redisVersionInfo = await redis.client.get(getRedisKey({ name: 'versionInfo' }))
  if (redisVersionInfo) {
    try {
      Object.assign(versionInfo, JSON.parse(redisVersionInfo))
      logger.info(`[Version] Got initial version from redis: '${versionInfo.version}'`)
      versionUpdateGauge.setToCurrentTime({ version: versionInfo.version })
    } catch (err) {
      logger.error('[Version] Error in getVersionInfo', err)
    }
  }

  return versionInfo
}

/**
 * Fetches latest version from all the ui-containers
 * @returns {Promise<number>} Return a promise containing this information
 */
export async function fetchLatestVersion () {
  const versionInfo = await fetchVersionInfo()
  return versionInfo.version
}

/**
 * Gets latest version from redis or the ui-containers, if not cached.
 * @returns {Promise<number>} Return a promise containing this information
 */
export async function getLatestVersion () {
  const versionInfo = await getVersionInfo()
  return versionInfo.version
}

export function registerLatestVersionListener (client) {
  const key = getRedisKey({ name: 'updateVersionInfo' })
  client.subscribe(key, (errs, count) => logger.info(`[Redis] Subscribed to ${key}.`))
  client.on('message', async (channel, stringifiedVersionInfo) => {
    if (channel !== key) return
    const updatedVersionInfo = JSON.parse(stringifiedVersionInfo)
    if (versionInfo.version === updatedVersionInfo.version) return logger.info(`[Version] Received 'updateVersionInfo' event but already contains that version '${updatedVersionInfo.version}'`)
    logger.info(`[Version] Received 'updateVersionInfo' event. Clearing cache. New version: '${updatedVersionInfo.version}'`)
    await configMap.load()
    versionUpdateGauge.setToCurrentTime({ version: updatedVersionInfo.version })
    cache.clear()
    warmCache({ version: updatedVersionInfo.version }).catch(err => logger.error(err))
    Object.assign(versionInfo, updatedVersionInfo)
  })
}

// only observe the version update event, no need to store the reference
// eslint-disable-next-line no-new
new Gauge({
  name: 'manifest_file_entries',
  help: 'Number of entries in merged vite manifest (number of all known files)',
  async collect () {
    const version = versionInfo.version
    this.set({ version }, Object.keys(await getViteManifests({ version })).length)
  },
  labelNames: ['version']
})

export async function fetchMergedMetadata () {
  const metadata = await Promise.all(configMap.urls.map(async url => {
    const { origin } = new URL(url)
    try {
      const response = await fetch(new URL('meta.json', origin), { cache: 'no-store' })
      if (!response.ok) return
      return response.json()
    } catch (e) {
      // unhandled
    }
  }))

  metadata.push({
    id: 'ui-middleware',
    name: 'UI Middleware',
    buildDate: process.env.BUILD_TIMESTAMP,
    commitSha: process.env.CI_COMMIT_SHA,
    version: process.env.APP_VERSION
  })

  // only return when contains data
  return metadata.filter(Boolean)
}

let prevProcessedVersion = null

export async function updateVersionProcessor (pubClient) {
  try {
    logger.debug('[Version] Check for new version')
    await configMap.load()

    const [storedVersion, fetchedVersionInfo] = await Promise.all([
      getLatestVersion(),
      fetchVersionInfo()
    ])

    if (prevProcessedVersion && storedVersion === fetchedVersionInfo.version) {
      // make sure to limit memory consumption and always check redis
      cache.clear()
      logger.debug(`[Version] No new version has been found. No update needed. Current version: ${storedVersion}`)
      return storedVersion
    }
    logger.info(`[Version] Found new source version. Current version: '${storedVersion}', new version: '${fetchedVersionInfo.version}'`)
    // that means, that between the previous update processing and this one, there was no version change
    if (!storedVersion || prevProcessedVersion === fetchedVersionInfo.version) {
      // update local version info
      Object.assign(versionInfo, fetchedVersionInfo)
      const stringifiedVersionInfo = JSON.stringify(versionInfo)
      cache.clear()

      await warmCache({ version: versionInfo.version, fetchFiles: true })
      await redis.client.set(getRedisKey({ name: 'versionInfo' }), stringifiedVersionInfo)
      await cache.get(getRedisKey({ version: versionInfo.version, name: 'mergedMetadata' }), async () => [await fetchMergedMetadata()])
      versionUpdateGauge.setToCurrentTime({ version: versionInfo.version })
      logger.info('[Version] publish update to other nodes.')
      pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), stringifiedVersionInfo)
    } else {
      logger.info(`[Version] do not execute update yet. Store version ${fetchedVersionInfo.version} as previous version.`)
      prevProcessedVersion = fetchedVersionInfo.version
    }
  } catch (err) {
    versionInfo.version = null
    logger.error(`[Version] comparing version is not possible. Error: ${err.message}`)
  }
}