/**
 * Copyright Zendesk, Inc.
 *
 * Use of this source code is governed under the Apache License, Version 2.0
 * found at http://www.apache.org/licenses/LICENSE-2.0.
 */

import type { DependencyList } from 'react'
import type { ActionLog } from './ActionLog'
import type { ActionLogCache } from './ActionLogCache'
import type { ACTION_TYPE, MARKER } from './constants'

export type ReportFnV1<Metadata extends Record<string, unknown>> = (
  reportArgs: ReportArguments<Metadata>,
) => void

export type ShouldResetOnDependencyChange = (
  oldDependencies: DependencyList,
  newDependencies: DependencyList,
) => boolean

export interface WithTimingId {
  /**
   * The identifier of this timing. Needs to be the same across all usages.
   * You may change it during the hook execution without restarting the measurement, however it's best to avoid that.
   * If you want an 'id' change to restart measurement, put it into hook's dependencies list.
   */
  id: string
}

export interface WithOnInternalError<
  CustomMetadata extends Record<string, unknown>,
> {
  onInternalError?: (
    error: Error,
    reportWithMetadata?: ReportArguments<CustomMetadata>,
  ) => void
}

export interface WithBeaconConfig<Placements extends string = string> {
  /**
   * Where is this beacon placed?
   * This value must be unique between all the instances of the hook, so they co-operate correctly.
   */
  placement: Placements
}

export interface WithActionLog<CustomMetadata extends Record<string, unknown>> {
  /**
   * An instance of ActionLog in a ref object (must be provided so it can be cached externally)
   */
  actionLog: ActionLog<CustomMetadata>
}

export interface WithShouldResetOnDependencyChangeFn {
  /**
   * An optional function that returns true if you want to restart measuring after dependencies have changed.
   * You may use this to prevent restarting in case you know a component re-renders with an invalid value.
   * Signature is: (oldDependencies, newDependencies): boolean
   * */
  shouldResetOnDependencyChangeFn?: ShouldResetOnDependencyChange
}

export interface WithMeasurementBeaconState
  extends WithShouldResetOnDependencyChangeFn {
  /**
   * Time different rendering stages by changing the value of this property.
   * Use one of the DEFAULT_STAGES or any string value for custom stages.
   * Must be used in combination with `finalStages`, or left undefined to disable capturing stages.
   */
  stage?: string

  /**
   * Setting this to anything other than undefined or null
   * will immediately set the stage to DEFAULT_STAGES.ERROR, and flush any report information
   */
  error?: Error | null

  /**
   * Setting this to `false` will immediately disable capturing any data.
   * If flushUponDeactivation is `true` and data is present, it will immediately flush any report information.
   * The equivalent of setting `stage` to INFORMATIVE_STAGES.INACTIVE.
   */
  isActive?: boolean
}

export interface StaticActionLogOptions<
  Placements extends string,
  CustomMetadata extends Record<string, unknown>,
> extends WithReportFn<CustomMetadata>,
    WithOnInternalError<CustomMetadata> {
  /** the number of milliseconds we will wait before another render, or sending the report */
  debounceMs?: number
  /** stop timing after this amount of milliseconds passes from the start of the render */
  timeoutMs?: number
  /**
   * When one of these stages is reached and debounced time passes, we will send the report.
   * If an empty array (default), the report will be sent immediately after rendering settles.
   * */
  finalStages?: readonly string[]
  /**
   * List all the stages that will be considered as "loading stages".
   * Used only for the reportFn. Does not affect other functionality.
   * */
  loadingStages?: readonly string[]
  /**
   * When one of these stages is reached, we will IMMEDIATELY send the report and reset.
   * This always includes the error stages.
   * */
  immediateSendReportStages?: readonly string[]
  /**
   * Will only activate the timing hook after *all* the placements listed in the array were mounted
   * Specifying this can be useful if you have conditional logic that means only some hooks will get mounted (e.g. timing of opening a menu).
   * */
  waitForBeaconActivation?: readonly Placements[]

  /**
   * Require a certain number of simultaneously mounted placements to send the timing report.
   */
  minimumExpectedSimultaneousBeacons?: number

  /**
   * If true, will flush the report immediately upon deactivation, without waiting for browser to settle.
   * Default is `false`.
   */
  flushUponDeactivation?: boolean
}

export interface WithReportFn<CustomMetadata extends Record<string, unknown>> {
  reportFn?: ReportFnV1<CustomMetadata>

  /**
   * Will fire any time an action is added to the action log.
   */
  onActionAddedCallback?: (action: ActionLog<CustomMetadata>) => void
}

export interface WithMetadata<Metadata extends Record<string, unknown>> {
  metadata?: Metadata
}

export interface ReportArguments<CustomMetadata extends Record<string, unknown>>
  extends Pick<
    StaticActionLogOptions<string, CustomMetadata>,
    'finalStages' | 'loadingStages' | 'immediateSendReportStages'
  > {
  readonly actions: readonly ActionWithStateMetadata[]
  readonly timingId?: string
  readonly isFirstLoad?: boolean
  readonly maximumActiveBeaconsCount?: number
  readonly minimumExpectedSimultaneousBeacons?: number
  readonly flushReason?: string
  readonly metadata?: CustomMetadata
  readonly measures: {
    tti?: PerformanceMeasure
    ttr?: PerformanceMeasure
  }
}

export interface DynamicActionLogOptions<
  CustomMetadata extends Record<string, unknown>,
> extends WithTimingId,
    WithShouldResetOnDependencyChangeFn,
    WithReportFn<CustomMetadata>,
    WithOnInternalError<CustomMetadata> {}

export interface WithActionLogCache<
  CustomMetadata extends Record<string, unknown>,
> {
  actionLogCache: ActionLogCache<CustomMetadata>
}

export interface ActionLogExternalApi<
  CustomMetadata extends Record<string, unknown> = Record<string, unknown>,
> {
  markRenderStart: (idSuffix: string) => void
  markRenderEnd: (idSuffix: string) => void
  dispose: (idSuffix: string) => void
  getActionLogForId: (idSuffix: string) => ActionLog<CustomMetadata>
  getActionLogForIdIfExists: (
    idSuffix: string,
  ) => ActionLog<CustomMetadata> | undefined
  setActive: (idSuffix: string, active: boolean) => void
  markStage: (
    idSuffix: string,
    stage: string,
    stageMetadata?: Record<string, unknown>,
  ) => void
  clear: (idSuffix: string) => void
  setMetadata: (idSuffix: string, metadata: CustomMetadata) => void
}

export interface WithGarbageCollectMs {
  garbageCollectMs: number
}

export interface UseActionLogCacheOptions<
  CustomMetadata extends Record<string, unknown>,
  Placements extends string = string,
> extends StaticActionLogOptions<Placements, CustomMetadata>,
    WithGarbageCollectMs {}

export interface UseActionLogOptions<
  CustomMetadata extends Record<string, unknown>,
  Placements extends string = string,
> extends WithTimingId,
    StaticActionLogOptions<Placements, CustomMetadata>,
    Partial<WithGarbageCollectMs>,
    WithActionLogCache<CustomMetadata> {}

export interface UseTimingMeasurementHookConfiguration<
  CustomMetadata extends Record<string, unknown>,
> extends DynamicActionLogOptions<CustomMetadata>,
    WithBeaconConfig,
    WithMeasurementBeaconState,
    WithActionLog<CustomMetadata> {}

export interface UseTimingHookConfiguration<
  CustomMetadata extends Record<string, unknown>,
> extends DynamicActionLogOptions<CustomMetadata>,
    WithBeaconConfig,
    WithMeasurementBeaconState,
    WithMetadata<CustomMetadata>,
    UseActionLogOptions<CustomMetadata> {}

export interface WithIdSuffix {
  idSuffix?: string
}

export interface UseTimingBeaconHookOptions<
  CustomMetadata extends Record<string, unknown>,
> extends Omit<DynamicActionLogOptions<CustomMetadata>, 'id'>,
    WithMeasurementBeaconState,
    WithBeaconConfig,
    WithMetadata<CustomMetadata>,
    WithIdSuffix {}

export interface GeneratedUseTimingBeaconHookConfiguration<
  CustomMetadata extends Record<string, unknown>,
> extends Omit<
    UseTimingBeaconHookOptions<CustomMetadata>,
    'placement' | 'type'
  > {}

export type GeneratedUseTimingBeaconHook = <
  CustomMetadata extends Record<string, unknown>,
>(
  options?: GeneratedUseTimingBeaconHookConfiguration<CustomMetadata>,
  restartWhenChanged?: DependencyList,
) => void

export interface GenerateUseTimingHooksConfiguration<
  Name extends string,
  Placements extends string,
  CustomMetadata extends Record<string, unknown>,
> extends Partial<UseActionLogOptions<CustomMetadata, Placements>>,
    WithIdPrefix {
  name: Name
}

export interface WithIdPrefix {
  idPrefix: string
}

export interface GetPrefixedUseTimingHooksConfiguration<
  Placements extends string,
  Metadata extends Record<string, unknown>,
> extends Omit<UseActionLogOptions<Metadata, Placements>, 'id'>,
    WithIdPrefix,
    WithBeaconConfig<Placements> {}

export interface GetExternalApiConfiguration<
  Placements extends string,
  Metadata extends Record<string, unknown>,
> extends WithActionLogCache<Metadata>,
    WithIdPrefix,
    WithBeaconConfig<Placements> {}

export type GeneratedTimingHooks<
  Name extends string,
  Placements extends readonly string[],
  Metadata extends Record<string, unknown> = Record<string, unknown>,
  GeneratedUseTimingBeacon = GeneratedUseTimingBeaconHook,
> = {
  [K in `use${Name}TimingIn${Placements[number]}`]: GeneratedUseTimingBeacon
} & {
  [K in `imperative${Placements[number]}TimingApi`]: ActionLogExternalApi<Metadata>
} & {
  actionLogCache: ActionLogCache<Metadata>
}

export type ActionType = (typeof ACTION_TYPE)[keyof typeof ACTION_TYPE]
export type Marker = (typeof MARKER)[keyof typeof MARKER]
export type SpanMarker = (typeof MARKER)['START' | 'END']
export type PointMarker = (typeof MARKER)['POINT']

export type CustomPerformanceEntry = PerformanceEntry & {
  startMark?: PerformanceEntry
  endMark?: PerformanceEntry
}

export interface BaseAction<NameT extends ActionType, MarkerT extends Marker> {
  timestamp: number
  entry: CustomPerformanceEntry
  type: NameT
  marker: MarkerT
  source: string
  metadata?: Record<string, unknown>
}

export type RenderActionType = (typeof ACTION_TYPE)['RENDER']
export type RenderAction = BaseAction<RenderActionType, SpanMarker>
export type UnresponsiveActionType = (typeof ACTION_TYPE)['UNRESPONSIVE']
export type UnresponsiveAction = BaseAction<UnresponsiveActionType, SpanMarker>
export type SpanAction = RenderAction | UnresponsiveAction
export type StageChangeActionType = (typeof ACTION_TYPE)['STAGE_CHANGE']

export interface StageChangeAction
  extends BaseAction<StageChangeActionType, PointMarker> {
  stage: string
  renderEntry?: CustomPerformanceEntry
}

export type DependencyChangeActionType =
  (typeof ACTION_TYPE)['DEPENDENCY_CHANGE']
export type DependencyChangeAction = BaseAction<
  DependencyChangeActionType,
  PointMarker
>
export type Action = SpanAction | StageChangeAction | DependencyChangeAction
export interface StateMeta {
  mountedPlacements: readonly string[]
  timingId: string
}
export type ActionWithStateMetadata = Action & StateMeta

export interface StageDescription extends StateMeta {
  type: ActionType
  source: string
  previousStage: string
  stage: string
  timeToStage: number
  /** relative timestamp of previous stage */
  previousStageTimestamp: number
  /** relative timestamp (ms since the beginning of the first action) */
  timestamp: number
  metadata?: Record<string, unknown>
  dependencyChanges: number
}

export interface TimingSpan {
  type: ActionType | 'ttr' | 'tti'
  description: string
  /** absolute timestamp of when render begun */
  startTime: number
  /** absolute timestamp of when render ended (ms since the beginning of the first action) */
  endTime: number
  /** relative endTime (ms since the beginning of the first action) */
  relativeEndTime: number

  entry?: PerformanceEntry
  data: StateMeta & {
    source?: string
    metadata?: Record<string, unknown>
    dependencyChanges?: number
    stage: string
    previousStage?: string
    timeToStage?: number
  }
}
