export type HandlerHooks = {
  preRun?: () => Promise<void>
  postRun?: (events: number, handled: number) => Promise<void>
}

export type Event = { type: string }
export type Command = { type: string }
export type Aggregate = {}

export type StoreEvent<E = unknown, A = unknown> = EventMeta & { event: E & { __persisted?: A } }

export type ProviderBookmark = {
  readonly name: string
  getPosition(): Promise<any>
  setPosition(position: any): Promise<void>
}

export type DomainOptions<E extends Event, A extends Aggregate> = {
  aggregate: () => A
  stream: string
  fold: Fold<E, A>
  provider: Provider<E> | Promise<Provider<E>>
  useCache?: boolean
}

export type StreamsHandler<T extends { [key: string]: Event }> = <
  TStream extends keyof T,
  TType extends T[TStream]['type']
>(
  stream: TStream,
  type: TType,
  handler: (id: string, event: Ext<T[TStream], TType>, meta: EventMeta) => any
) => void

export type HandlerBookmark = string | ProviderBookmark

export type EventMeta = {
  stream: string
  position: any
  version: number
  timestamp: Date
  aggregateId: string
}

export type ErrorCallback = (
  err: any,
  stream: string,
  bookmark: string,
  event?: Event & { [key: string]: any }
) => any

type ID = { aggregateId: string }

export type BaseAggregate = { version: number; aggregateId: string; __pv?: string }

export type Fold<E extends Event, A extends Aggregate> = (
  ev: E,
  agg: A & BaseAggregate,
  meta: EventMeta
) => Partial<A>

export type Provider<Evt extends Event> = {
  driver: string
  onError: ErrorCallback
  getPosition(bookmark: string): Promise<any>
  setPosition(bookmark: string, position: any): Promise<void>
  getEventsFrom(
    stream: string | string[],
    position: any,
    limit?: number
  ): Promise<Array<StoreEvent<Evt>>>
  getEventsFor(
    stream: string,
    aggregateId: string,
    fromPosition?: any
  ): Promise<Array<StoreEvent<Evt>>>
  getLastEventFor(
    stream: string | string[],
    aggregateId?: string
  ): Promise<StoreEvent<Evt & { __persisted?: any }> | undefined>
  createEvents(
    stream: string,
    aggregateId: string,
    version: number,
    event: Evt[]
  ): Array<StoreEvent<Evt>>
  append(
    stream: string,
    aggregateId: string,
    version: number,
    event: StoreEvent<Evt>[]
  ): Promise<Array<StoreEvent<Evt>>>
  limit?: number
}

export type Handler<E extends Event> = {
  start(): void
  stop(): void
  reset(): void
  runOnce(): Promise<number>
  handle: <T extends E['type']>(
    type: T,
    cb: (aggregateId: string, event: Ext<E, T>, meta: EventMeta) => Promise<any>
  ) => void
  handlers: (body: HandlerBody<E>) => void
  name: string
}

export type Ext<E extends Event, T extends E['type']> = E extends {
  type: T
}
  ? E
  : never

export type CommandHandler<E extends Event, A extends Aggregate, C extends Command> = {
  [key in C['type']]: (cmd: OptCmd<C, key> & ID, agg: A & BaseAggregate) => Promise<E | E[] | void>
}

type OptCmd<C extends Command, T extends C['type']> = Omit<Ext<C, T>, 'type'> & { type: T }

export type DomainHandlerOpts = {
  hooks?: HandlerHooks
  /** Start handling events from the end of the stream */
  tailStream?: boolean

  /** Every time the handler starts, always start from the end of the stream */
  alwaysTailStream?: boolean

  /** When a handler throws, continue processing events */
  continueOnError?: boolean
}

export type Domain<E extends Event, A extends Aggregate, C extends Command> = {
  handler(bookmark: string, options?: DomainHandlerOpts): Handler<E>
  command: CmdBody<C, A>
  getAggregate(
    id: string
  ): Promise<ExecutableAggregate<C, A> & { aggregate: Readonly<A & BaseAggregate> }>
  retry?: boolean
}

export type CmdBody<C extends Command, A extends Aggregate> = {
  [cmd in C['type']]: (aggId: string, body: ExtCmd<C, cmd>) => Promise<A & BaseAggregate>
}

export type ExecutableAggregate<C extends Command, A extends Aggregate> = {
  [cmd in C['type']]: (
    body: ExtCmd<C, cmd>
  ) => Promise<ExecutableAggregate<C, A> & { aggregate: Readonly<A & BaseAggregate> }>
}

type ExtCmd<C extends Command, T extends C['type']> = Omit<Ext<C, T>, 'type'>

export type HandlerBody<E extends Event> = {
  [evt in E['type']]?: (id: string, evt: Ext<E, evt>, meta: EventMeta) => Promise<any>
}

export type StorableAggregate<
  E extends Event = any,
  A extends Aggregate = any,
  S extends string = string
> = {
  stream: S
  fold: Fold<E, A>
  aggregate: () => A
  version?: string
  persistAggregate?: boolean
}

export type AggregateStore = { [key: string]: StorableAggregate }

export type ProvidedAggregate<E extends Event, A extends Aggregate, S extends string = string> = {
  stream: S
  provider: Provider<E> | Promise<Provider<E>>
  getAggregate: (id: string) => Promise<A & BaseAggregate>
  toNextAggregate: (prev: A & BaseAggregate, event: StoreEvent<E>) => A & BaseAggregate

  version?: string
  persistAggregate?: boolean
}
