import _ from 'lodash'
import moment from 'moment'
import uuid from 'uuid'
import {Filter} from 'src/types/logs'
import {TimeRange, Namespace, QueryConfig} from 'src/types'
import {NULL_STRING} from 'src/shared/constants/queryFillOptions'
import {getDeep} from 'src/utils/wrappers'
import {
  quoteIfTimestamp,
  buildSelect,
  buildWhereClause,
  buildGroupBy,
  buildFill,
} from 'src/utils/influxql'

import {HistogramData} from 'src/types/histogram'
import {executeQueryAsync} from 'src/logs/api'
import {DEFAULT_TIME_FORMAT, FIELD_TIMESTAMP_NSINMS} from 'src/logs/constants'
import {TimeSeriesResponse} from 'src/types/series'

const BIN_COUNT = 30

const histogramFields = [
  {
    alias: '',
    args: [
      {
        alias: 'message',
        type: 'field',
        value: 'message',
      },
    ],
    type: 'func',
    value: 'count',
  },
]

const tableFields = [
  {
    alias: 'severity',
    type: 'field',
    value: 'severity',
  },
  {
    alias: 'timestamp',
    type: 'field',
    value: 'timestamp',
  },
  {
    alias: 'message',
    type: 'field',
    value: 'message',
  },
  {
    alias: 'facility',
    type: 'field',
    value: 'facility',
  },
  {
    alias: 'procid',
    type: 'field',
    value: 'procid',
  },
  {
    alias: 'appname',
    type: 'field',
    value: 'appname',
  },
  {
    alias: 'hostname',
    type: 'field',
    value: 'hostname',
  },
  {
    alias: 'host',
    type: 'field',
    value: 'host',
  },
]

const defaultQueryConfig = {
  areTagsAccepted: false,
  fill: '0',
  measurement: 'syslog',
  rawText: null,
  shifts: [],
  tags: {},
}

const keyMapping = (key: string): string => {
  switch (key) {
    case 'severity_1':
      return 'severity'
    default:
      return key
  }
}

const operatorMapping = (operator: string): string => {
  switch (operator) {
    case '==':
      return '='
    default:
      return operator
  }
}

const valueMapping = (operator: string, value): string => {
  if (operator === '=~' || operator === '!~') {
    return `${new RegExp(value)}`
  }
  return `'${value}'`
}

export const filtersClause = (filters: Filter[]): string => {
  return _.map(
    filters,
    (filter: Filter) =>
      `"${keyMapping(filter.key)}" ${operatorMapping(
        filter.operator
      )} ${valueMapping(filter.operator, filter.value)}`
  ).join(' AND ')
}

export function buildInfiniteScrollWhereClause({
  lower,
  upper,
  tags,
  areTagsAccepted,
}: QueryConfig): string {
  const timeClauses = []

  if (lower) {
    timeClauses.push(`time >= '${lower}'`)
  }

  if (upper) {
    timeClauses.push(`time < '${upper}'`)
  }

  const tagClauses = _.keys(tags).map(k => {
    const operator = areTagsAccepted ? '=' : '!='

    if (tags[k].length > 1) {
      const joinedOnOr = tags[k]
        .map(v => `"${k}"${operator}'${v}'`)
        .join(' OR ')
      return `(${joinedOnOr})`
    }

    return `"${k}"${operator}'${tags[k]}'`
  })

  const subClauses = timeClauses.concat(tagClauses)
  if (!subClauses.length) {
    return ''
  }

  return ` WHERE ${subClauses.join(' AND ')}`
}

function buildGeneralLogQuery(
  condition: string,
  config: QueryConfig,
  filters: Filter[],
  searchTerm: string | null = null
) {
  const {groupBy, fill = NULL_STRING} = config
  const select = buildSelect(config, '')
  const dimensions = buildGroupBy(groupBy)
  const fillClause = groupBy.time ? buildFill(fill) : ''

  if (!_.isEmpty(searchTerm)) {
    condition = `${condition} AND message =~ ${new RegExp(searchTerm)}`
  }

  if (!_.isEmpty(filters)) {
    condition = `${condition} AND ${filtersClause(filters)}`
  }

  return `${select}${condition}${dimensions}${fillClause}`
}

export async function getQueryResultsCountForBounds(
  lower: string,
  upper: string,
  config: QueryConfig,
  filters: Filter[],
  searchTerm: string,
  proxyLink: string,
  namespace: Namespace
): Promise<number> {
  const {database, retentionPolicy, measurement} = config

  let rpSegment = ''
  if (retentionPolicy) {
    rpSegment = `"${retentionPolicy}"`
  }

  const fullyQualifiedMeasurement = `"${database}".${rpSegment}."${measurement}"`
  const select = `SELECT count(message) FROM ${fullyQualifiedMeasurement}`
  let condition = `WHERE time >= '${lower}' AND time <='${upper}'`

  if (!_.isEmpty(searchTerm)) {
    condition = `${condition} AND "message" =~ ${new RegExp(searchTerm)}`
  }

  if (!_.isEmpty(filters)) {
    condition = `${condition} AND ${filtersClause(filters)}`
  }

  const query = `${select} ${condition} FILL(0)`
  const result = await executeQueryAsync(proxyLink, namespace, query)
  return getDeep<number>(result, 'results.0.series.0.values.0.1', 0)
}

export function buildInfiniteScrollLogQuery(
  lower: string,
  upper: string,
  config: QueryConfig,
  filters: Filter[],
  searchTerm: string | null = null
) {
  const {tags, areTagsAccepted} = config

  const condition = buildInfiniteScrollWhereClause({
    lower,
    upper,
    tags,
    areTagsAccepted,
  })
  // add extra "_timestamp_ns" field to be able to know precise nanosecond time
  // with full precision that is lost during JSON deserialization to number
  config = {...config, fields: [...config.fields, FIELD_TIMESTAMP_NSINMS]}

  return buildGeneralLogQuery(condition, config, filters, searchTerm)
}

export function buildLogQuery(
  timeRange: TimeRange,
  config: QueryConfig,
  filters: Filter[],
  searchTerm: string | null = null
): string {
  const {groupBy, fill = NULL_STRING, tags, areTagsAccepted} = config
  const {upper, lower} = quoteIfTimestamp(timeRange)
  const select = buildSelect(config, '')
  const dimensions = buildGroupBy(groupBy)
  const fillClause = groupBy.time ? buildFill(fill) : ''

  let condition = buildWhereClause({lower, upper, tags, areTagsAccepted})
  if (!_.isEmpty(searchTerm)) {
    condition = `${condition} AND message =~ ${new RegExp(searchTerm)}`
  }

  if (!_.isEmpty(filters)) {
    condition = `${condition} AND ${filtersClause(filters)}`
  }

  return `${select}${condition}${dimensions}${fillClause}`
}

const computeSeconds = (range: TimeRange) => {
  const {upper, lower, seconds} = range

  if (seconds) {
    return seconds
  } else if (upper && upper.match(/now/) && lower) {
    return moment().unix() - moment(lower).unix()
  } else if (upper && lower) {
    return moment(upper).unix() - moment(lower).unix()
  }
  return 120
}

const createGroupBy = (range: TimeRange) => {
  const seconds = computeSeconds(range)
  const time = `${Math.max(Math.floor(seconds / BIN_COUNT), 1)}s`
  const tags = ['severity']

  return {time, tags}
}

export const buildHistogramQueryConfig = (
  namespace: Namespace,
  range: TimeRange
): QueryConfig => {
  const id = uuid.v4()
  const {database, retentionPolicy} = namespace

  return {
    ...defaultQueryConfig,
    id,
    range,
    database,
    retentionPolicy,
    groupBy: createGroupBy(range),
    fields: histogramFields,
  }
}

export const buildTableQueryConfig = (
  namespace: Namespace,
  range: TimeRange
): QueryConfig => {
  const id = uuid.v4()
  const {database, retentionPolicy} = namespace

  return {
    ...defaultQueryConfig,
    id,
    range,
    database,
    retentionPolicy,
    groupBy: {tags: []},
    fields: tableFields,
    fill: null,
  }
}

export const parseHistogramQueryResponse = (
  response: TimeSeriesResponse
): HistogramData => {
  const series = getDeep<any[]>(response, 'results.0.series', [])
  const data = series.reduce((acc, current) => {
    const group = getDeep<string>(current, 'tags.severity', '')

    if (!current.columns || !current.values) {
      return acc
    }

    const timeColIndex = current.columns.findIndex(v => v === 'time')
    const countColIndex = current.columns.findIndex(v => v === 'count')

    if (timeColIndex < 0 || countColIndex < 0) {
      return acc
    }

    const vs = current.values.map(v => {
      const time = v[timeColIndex]
      const value = v[countColIndex]

      return {
        key: `${group}-${value}-${time}`,
        time,
        value,
        group,
      }
    })

    return [...acc, ...vs]
  }, [])

  return data
}

export const formatTime = (time: number): string => {
  return moment(time).format(DEFAULT_TIME_FORMAT)
}

export const buildFindMeasurementQuery = (
  namespace: Namespace,
  measurement: string
) =>
  `SHOW MEASUREMENTS ON "${namespace.database}" WITH measurement = "${measurement}"`
