import type { Constructable } from '@furystack/inject'
import type { EventHub } from '@furystack/utils'

export const NumberComparisonOperators = ['$gt', '$gte', '$lt', '$lte'] as const

export const StringComparisonOperators = ['$startsWith', '$endsWith', '$like', '$regex'] as const
export const SingleComparisonOperators = ['$eq', '$ne'] as const

export const ArrayComparisonOperators = ['$in', '$nin'] as const
export const LogicalOperators = ['$and', '$not', '$nor', '$or'] as const

export const allOperators = [
  ...SingleComparisonOperators,
  ...NumberComparisonOperators,
  ...ArrayComparisonOperators,
  ...LogicalOperators,
  ...StringComparisonOperators,
] as const

export type FilterType<T> = {
  [K in keyof T]?:
    | (T[K] extends string ? { [SCO in (typeof StringComparisonOperators)[number]]?: T[K] } : never)
    | (T[K] extends number ? { [SCO in (typeof NumberComparisonOperators)[number]]?: T[K] } : never)
    | { [SCO in (typeof SingleComparisonOperators)[number]]?: T[K] }
    | { [ACO in (typeof ArrayComparisonOperators)[number]]?: Array<T[K]> }
} & { [LO in (typeof LogicalOperators)[number]]?: Array<FilterType<T>> }

export const isLogicalOperator = (propertyString: string): propertyString is (typeof LogicalOperators)[number] =>
  LogicalOperators.includes(propertyString as (typeof LogicalOperators)[number])

export const isOperator = (propertyString: string): propertyString is (typeof allOperators)[number] =>
  allOperators.includes(propertyString as (typeof allOperators)[number])

export const t: FilterType<{ a: number; b: string; c: boolean }> = {
  a: { $eq: 3 },
  b: { $in: ['a', 'b', 'c'] },
  $and: [{ a: { $eq: 2 } }],
}

export interface CreateResult<T> {
  created: T[]
}

export type WithOptionalId<T, TPrimaryKey extends keyof T> = Omit<T, TPrimaryKey> & { [K in TPrimaryKey]?: T[K] }
/**
 * Type for default filtering model
 */
export interface FindOptions<T, TSelect extends Array<keyof T>> {
  /**
   * Limits the hits
   */
  top?: number

  /**
   * Skips the first N hit
   */
  skip?: number

  /**
   * Sets up an order by a field and a direction
   */
  order?: { [P in keyof T]?: 'ASC' | 'DESC' }

  /**
   * The result set will be limited to these fields
   */
  select?: TSelect

  /**
   * The fields should match this filter
   */
  filter?: FilterType<T>
}

export type PartialResult<T, TFields extends Array<keyof T>> = Pick<T, TFields[number]>

export const selectFields = <T extends object, TField extends Array<keyof T>>(entry: T, ...fields: TField) => {
  const returnValue = {} as PartialResult<T, TField>
  Object.keys(entry).map((key) => {
    const field: TField[number] = key as TField[number]
    if (fields.includes(field)) {
      returnValue[field] = entry[field]
    }
  })
  return returnValue
}

/**
 * Interface that defines a physical store implementation
 */
export interface PhysicalStore<T, TPrimaryKey extends keyof T, TWriteableData = WithOptionalId<T, TPrimaryKey>>
  extends EventHub<{
    onEntityAdded: { entity: T }
    onEntityUpdated: { id: T[TPrimaryKey]; change: Partial<T> }
    onEntityRemoved: { key: T[TPrimaryKey] }
  }> {
  /**
   * The Primary key field name
   */
  readonly primaryKey: TPrimaryKey

  /**
   * A constructable model
   */
  readonly model: Constructable<T>

  /**
   * Adds an entry to the store, returns a promise that will be resolved with the added data
   * @param entries The data to be added
   */
  add(...entries: TWriteableData[]): Promise<CreateResult<T>>

  /**
   * Updates an entry in the store, returns a promise that will be resolved once the update is done
   * @param id The primary key of the entry
   * @param data The data to be updated
   */
  update(id: T[TPrimaryKey], data: Partial<T>): Promise<void>

  /**
   * Returns a promise that will be resolved with the count of the elements
   */
  count(filter?: FilterType<T>): Promise<number>

  /**
   * Returns a promise that will be resolved with an array of elements that matches the filter
   * @param searchOptions An options object for the Search expression
   */
  find<TSelect extends Array<keyof T>>(findOptions: FindOptions<T, TSelect>): Promise<Array<PartialResult<T, TSelect>>>

  /**
   * Returns a promise that will be resolved with an entry with the defined primary key or undefined
   * @param key The primary key of the entry
   */
  get<TSelect extends Array<keyof T>>(
    key: T[TPrimaryKey],
    select?: TSelect,
  ): Promise<PartialResult<T, TSelect> | undefined>

  /**
   * Removes an entry with the defined primary key. Returns a promise that will be resolved once the operation is completed
   * @param key The primary key of the entry to remove
   */
  remove(...keys: Array<T[TPrimaryKey]>): Promise<void>
}
