import { EventEmitter } from 'events'
import type TypedEmitter from 'typed-emitter'
import { ConnectionState, Room, RoomConnectOptions, RoomOptions } from 'livekit-client'

export type LogMessage = {
  level: 'info' | 'warning' | 'error'
  message: any
}

export enum LogLevel {
  INFO = 'info',
  WARNING = 'warning',
  ERROR = 'error',
}

export enum CheckStatus {
  IDLE,
  RUNNING,
  SKIPPED,
  SUCCESS,
  FAILED,
}

export type CheckInfo = {
  name: string
  logs: Array<LogMessage>
  status: CheckStatus
  description: string
}

export interface CheckerOptions {
  errorsAsWarnings?: boolean
  roomOptions?: RoomOptions
  connectOptions?: RoomConnectOptions
}

export type PerformParams = any

export abstract class Checker extends (EventEmitter as new () => TypedEmitter<CheckerCallbacks>) {
  protected url: string

  protected token: string

  room: Room

  connectOptions?: RoomConnectOptions

  status: CheckStatus = CheckStatus.IDLE

  logs: Array<LogMessage> = []

  errorsAsWarnings: boolean = false

  name: string = ''

  constructor(url: string, token: string, options: CheckerOptions = {}) {
    super()
    this.url = url
    this.token = token
    this.room = new Room(options.roomOptions)
    this.connectOptions = options.connectOptions
    if (options.errorsAsWarnings) {
      this.errorsAsWarnings = options.errorsAsWarnings
    }
  }

  abstract get description(): string

  protected abstract perform(params: PerformParams): Promise<void>

  async run(performParams?: PerformParams, onComplete?: () => void) {
    if (this.status !== CheckStatus.IDLE) {
      throw Error('check is running already')
    }
    this.setStatus(CheckStatus.RUNNING)
    this.appendMessage(`${this.name} started.`)

    try {
      await this.perform(performParams)
    } catch (err) {
      if (err instanceof Error) {
        if (this.errorsAsWarnings) {
          this.appendWarning(err.message)
        } else {
          this.appendError(err.message)
        }
      }
    }

    await this.disconnect()

    // sleep for a bit to ensure disconnect
    await new Promise((resolve) => setTimeout(resolve, 500))

    if ((this.status as CheckStatus) !== CheckStatus.SKIPPED) {
      this.setStatus(this.isSuccess() ? CheckStatus.SUCCESS : CheckStatus.FAILED)
    }
    this.appendMessage(`${this.name} finished.`)

    if (onComplete) {
      onComplete()
    }
    return this.getInfo()
  }

  protected isSuccess(): boolean {
    return !this.logs.some((l) => l.level === 'error')
  }

  protected async connect(): Promise<Room> {
    if (this.room.state === ConnectionState.Connected) {
      return this.room
    }
    await this.room.connect(this.url, this.token, this.connectOptions)
    return this.room
  }

  protected async disconnect() {
    if (this.room && this.room.state !== ConnectionState.Disconnected) {
      await this.room.disconnect()
      // wait for it to go through
      await new Promise((resolve) => setTimeout(resolve, 500))
    }
  }

  protected skip() {
    this.setStatus(CheckStatus.SKIPPED)
  }

  protected appendMessage(message: any) {
    this.logs.push({ level: 'info', message })
    this.emit('update', this.getInfo())
  }

  protected appendWarning(message: string) {
    this.logs.push({ level: 'warning', message })
    this.emit('update', this.getInfo())
  }

  protected appendError(message: string) {
    this.logs.push({ level: 'error', message })
    this.emit('update', this.getInfo())
  }

  protected setStatus(status: CheckStatus) {
    this.status = status
    this.emit('update', this.getInfo())
  }

  getInfo(): CheckInfo {
    return {
      logs: this.logs,
      name: this.name,
      status: this.status,
      description: this.description,
    }
  }
}

export type InstantiableCheck<T extends Checker> = {
  new (url: string, token: string, options?: CheckerOptions): T
}

type CheckerCallbacks = {
  update: (info: CheckInfo) => void
}
