import { createSelector } from 'reselect'
import { traverse, TraversalCallbackContext } from 'object-traversal'
import { JsonValue } from 'type-fest'
import mapObject, { mapObjectSkip } from 'map-obj'

import { EventRecord, IdentifyEvent, SegmentEvent, TrackEvent, PageEvent } from '../..'

import { selectEventList } from './selectEventList'
import { selectEventsFilter } from './selectEventsFilter'


export type ObjectPropertyFragment = {
  type: 'property'
  value: string
}

export type ArrayIndexFragment = {
  type: 'index'
  value: string
}

export type QueryMatch = {
  keyPath: (ObjectPropertyFragment | ArrayIndexFragment)[]
  value: JsonValue
  matchingKeyRange: [number, number]
  matchingValueRange: [number, number]
}

export type QueryMatchedEventRecord = EventRecord & {
  queryMatch?: QueryMatch
}

const findQueryMatches = (query: string, event: SegmentEvent): QueryMatch => {
  let queryMatch: QueryMatch
  const includedInSearch: (keyof IdentifyEvent | keyof TrackEvent | keyof PageEvent)[] = [
    'traits',
    'properties',
    'event'
  ]
  const searchable = mapObject(event, (k, v) => (includedInSearch.includes(k) ? [k, v] : mapObjectSkip))
  const keyPath: (ObjectPropertyFragment | ArrayIndexFragment)[] = []
  const search = (haystack: string) => haystack.toLowerCase().indexOf(query.toLowerCase())
  const findMatch = (ctx: TraversalCallbackContext) => {
    // skip root level traversal
    if (!ctx.key) {
      return
    }

    const stepOutAmount = keyPath.length - ctx.meta.depth + 1
    keyPath.splice(keyPath.length - stepOutAmount, stepOutAmount, {
      type: Array.isArray(ctx.parent) ? 'index' : 'property',
      value: ctx.key
    })

    const keyMatchIdx = !Array.isArray(ctx.parent) ? search(ctx.key) : -1
    const valueMatchIdx = ['string', 'number', 'boolean'].includes(typeof ctx.value)
      ? search((ctx.value as { toString(): string }).toString())
      : -1

    if (keyMatchIdx > -1 || valueMatchIdx > -1) {
      queryMatch = {
        keyPath: [{ type: 'property', value: 'payload' }, ...keyPath],
        value: ctx.value as JsonValue,
        matchingKeyRange: keyMatchIdx > -1 ? [keyMatchIdx, keyMatchIdx + query.length] : [-1, -1],
        matchingValueRange: valueMatchIdx > -1 ? [valueMatchIdx, valueMatchIdx + query.length] : [-1, -1]
      }
      return true
    }
  }

  traverse(searchable, findMatch, {
    // stop the traversal once a query match is found
    haltOnTruthy: true,
    // disable the built-in nodePath resolver
    pathSeparator: null
  })

  return queryMatch
}

export const selectFilteredEventList = createSelector(
  selectEventList,
  selectEventsFilter,
  (eventsList, eventsFilter): (EventRecord | QueryMatchedEventRecord)[] => {
    const { query, eventTypes } = eventsFilter

    const filteredByType =
      eventTypes.length === 0
        ? eventsList
        : eventsList.filter((eventRecord) => eventTypes.includes(eventRecord.event.type))

    return !query
      ? filteredByType
      : filteredByType
          .map<QueryMatchedEventRecord>((eventRecord) => ({
            ...eventRecord,
            queryMatch: findQueryMatches(query, eventRecord.event)
          }))
          .filter(({ queryMatch }) => !!queryMatch)
  }
)
