import request, { Header } from '../utils/request'
import { murmurHash3 } from '../utils/hash'
import { Logger, ErrorProperties } from '../utils/logger'

export type PerProductConfig = {
  content: Array<Source>
}

type Source = {
  source: string | null
  weight: number
}

export function isPerProductConfig(config: unknown, logger?: Logger, url?: string): config is PerProductConfig {
  const errProps: Pick<ErrorProperties, 'errorType' | 'errorContext'> = {
    errorType: 'validation',
    errorContext: 'per-product config',
  }
  if (typeof config !== 'object' || config === null) {
    logger?.log('error', { ...errProps, errorMessage: `${url} does not contain an object` })
    return false
  }
  if (!('content' in config)) {
    logger?.log('error', { ...errProps, errorMessage: `${url} does not contain a 'content' key` })
    return false
  }
  if (!Array.isArray(config.content)) {
    logger?.log('error', { ...errProps, errorMessage: `'content' in ${url} is not an array` })
    return false
  }
  if (config.content.length === 0) {
    logger?.log('error', { ...errProps, errorMessage: `'content' array in ${url} has length 0` })
    return false
  }
  if (!config.content.every(source => isSource(source, logger, url))) {
    return false
  }
  if (Math.abs(1 - config.content.reduce((sumOfWeights, source) => (sumOfWeights += source.weight), 0)) > 0.001) {
    logger?.log('error', { ...errProps, errorMessage: `sum of source weights in 'content' in ${url} does not equal 1` })
    return false
  }
  return true
}

function isSource(thing: unknown, logger?: Logger, url?: string): thing is Source {
  const errProps: Pick<ErrorProperties, 'errorType' | 'errorContext'> = {
    errorType: 'validation',
    errorContext: 'per-product config',
  }
  if (typeof thing !== 'object' || thing === null) {
    logger?.log('error', { ...errProps, errorMessage: `'content' in ${url} contains a source that is not an object` })
    return false
  }
  if (!('source' in thing)) {
    logger?.log('error', {
      ...errProps,
      errorMessage: `'content' in ${url} contains a source that is missing a 'source' key`,
    })
    return false
  }
  if (typeof thing.source !== 'string' && thing.source !== null) {
    logger?.log('error', {
      ...errProps,
      errorMessage: `'content' in ${url} contains a source with an invalid 'source' value`,
    })
    return false
  }
  if (!('weight' in thing)) {
    logger?.log('error', {
      ...errProps,
      errorMessage: `'content' in ${url} contains a source that is missing a 'weight' key`,
    })
    return false
  }
  if (typeof thing.weight !== 'number') {
    logger?.log('error', {
      ...errProps,
      errorMessage: `'content' in ${url} contains a source with an invalid 'weight' value`,
    })
    return false
  }
  return true
}

interface SelectSourceArgs {
  productId: string
  content: Array<Source>
  sessionId: string
}

export function selectSource(args: SelectSourceArgs): Source {
  const contentString = JSON.stringify(args.content)
  const hashString = `${args.productId}${contentString}${args.sessionId}`
  const hash = murmurHash3(hashString, 0)
  // divde by max 32 bit integer to scale to 0..1
  const scaledHash = hash / 0xffffffff
  // use scaled hash as a "random" number in weighted random selection
  let sum = 0
  const cumulativeWeights = args.content.map(source => (sum += source.weight))
  const index = cumulativeWeights.findIndex(cWeight => cWeight >= scaledHash)
  return args.content[index]
}

export class PerProductConfigCache {
  #cache = new Map<string, PerProductConfig | undefined>()
  #logger: Logger

  /** @internal */
  public constructor(logger: Logger) {
    this.#logger = logger
  }

  public async getConfig(cdnPath: string): Promise<PerProductConfig | undefined> {
    return this.#cache.has(cdnPath) ? this.#cache.get(cdnPath) : this.#fetchConfig(cdnPath)
  }

  async #fetchConfig(cdnPath: string): Promise<PerProductConfig | undefined> {
    const configUrl = `${cdnPath}/config.json`
    let response: Response
    try {
      response = await request.get(configUrl)
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : undefined
      this.#logger.log('error', {
        errorContext: 'per-product config',
        errorType: 'fetch',
        errorMessage: `Error fetching ${configUrl}: ${errorMessage}`,
      })
      return undefined
    }
    if (response.headers.get(Header.ContentLength) === '0') {
      return this.#cacheAndReturn(cdnPath, undefined)
    }
    let data: unknown
    try {
      data = await response.json()
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : undefined
      this.#logger.log('error', {
        errorContext: 'per-product config',
        errorType: 'parse',
        errorMessage: `Error parsing ${configUrl}: ${errorMessage}`,
      })
      return this.#cacheAndReturn(cdnPath, undefined)
    }
    if (isPerProductConfig(data, this.#logger, configUrl)) {
      return this.#cacheAndReturn(cdnPath, data)
    }
    return this.#cacheAndReturn(cdnPath, undefined)
  }

  #cacheAndReturn<T extends PerProductConfig | undefined>(cdnPath: string, config: T): T {
    this.#cache.set(cdnPath, config)
    return config
  }
}
