import { SIG_EXPIRATION_BUFFER_MS } from '../constants.js'
import type { RenegadeConfig } from '../createConfig.js'
import {
  SocketClosedError,
  WebSocketConnectionError,
  WebSocketRequestError,
} from '../errors/websocket.js'
import { addExpiringAuthToHeaders } from './http.js'

export enum AuthType {
  None = 'None',
  Wallet = 'Wallet',
  Admin = 'Admin',
}

export type RelayerWebsocketParams = {
  config: RenegadeConfig
  topic: string
  authType: AuthType
  onmessage: (this: WebSocket, ev: MessageEvent) => any
  onopenCallback?: (this: WebSocket, ev: Event) => any
  oncloseCallback?: (this: WebSocket, ev: CloseEvent) => any
  onerrorCallback?: (this: WebSocket, ev: Event) => any
}

type SubscriptionMessage = {
  headers?: Record<string, string>
  body: SubscriptionBody
}

type UnsubscriptionMessage = {
  body: UnsubscriptionBody
}

export type SubscriptionBody = {
  method: 'subscribe'
  topic: string
}

export type UnsubscriptionBody = {
  method: 'unsubscribe'
  topic: string
}

export class RelayerWebsocket {
  private config: RenegadeConfig
  private topic: string
  private authType: AuthType
  private onmessage: (this: WebSocket, ev: MessageEvent) => any
  private onopenCallback: ((this: WebSocket, ev: Event) => any) | null
  private oncloseCallback: ((this: WebSocket, ev: CloseEvent) => any) | null
  private onerrorCallback: ((this: WebSocket, ev: Event) => any) | null

  private ws: WebSocket | null = null

  private handleOpen = (event: Event) => {
    if (!this.ws) return
    const message = this.buildSubscriptionMessage()
    this.request(message)

    return this.onopenCallback?.call(this.ws, event)
  }

  private handleClose = (event: CloseEvent) => {
    this.cleanup()
    return this.oncloseCallback?.call(this.ws!, event)
  }

  private handleError = async (event: Event) => {
    this.cleanup()
    return this.onerrorCallback?.call(this.ws!, event)
  }

  constructor(params: RelayerWebsocketParams) {
    this.config = params.config
    this.topic = params.topic
    this.authType = params.authType
    this.onmessage = params.onmessage
    this.onopenCallback = params.onopenCallback ?? null
    this.oncloseCallback = params.oncloseCallback ?? null
    this.onerrorCallback = params.onerrorCallback ?? null
  }

  // --------------
  // | Public API |
  // --------------

  public async connect(): Promise<void> {
    if (this.ws) {
      throw new Error(
        'WebSocket connection attempt aborted: already connected.',
      )
    }

    const WebSocket = await import('isows').then((module) => module.WebSocket)
    const url = this.config.getWebsocketBaseUrl()
    this.ws = new WebSocket(url)

    this.ws.addEventListener('open', this.handleOpen)
    this.ws.addEventListener('message', this.onmessage)
    this.ws.addEventListener('close', this.handleClose)
    this.ws.addEventListener('error', this.handleError)

    // Wait for the socket to open.
    if (this.ws?.readyState === WebSocket.CONNECTING) {
      await new Promise((resolve, reject) => {
        if (!this.ws) return
        this.ws.onopen = (event) => resolve(event)
        this.ws.onerror = (error) =>
          reject(
            new WebSocketConnectionError({
              url,
              cause: error as unknown as Error,
            }),
          )
      })
    }
  }

  public close(): void {
    if (!this.ws) {
      throw new Error('WebSocket connection not open')
    }

    const message = this.buildUnsubscriptionMessage()
    this.request(message)

    this.ws.close()
  }

  // ---------------
  // | Private API |
  // ---------------

  private request(message: SubscriptionMessage | UnsubscriptionMessage): void {
    if (
      this.ws?.readyState === this.ws?.CLOSED ||
      this.ws?.readyState === this.ws?.CLOSING
    ) {
      throw new WebSocketRequestError({
        body: message,
        url: this.ws?.url || '',
        cause: new SocketClosedError({ url: this.ws?.url }),
      })
    }

    this.ws?.send(JSON.stringify(message))
  }

  private buildSubscriptionMessage(): SubscriptionMessage {
    const body = {
      method: 'subscribe' as const,
      topic: this.topic,
    }

    if (this.authType === AuthType.None) {
      return { body }
    }

    const headers = this.buildAuthHeaders(body)
    return {
      headers,
      body,
    }
  }

  private buildUnsubscriptionMessage(): UnsubscriptionMessage {
    return {
      body: {
        method: 'unsubscribe' as const,
        topic: this.topic,
      },
    }
  }

  private buildAuthHeaders(body: SubscriptionBody): Record<string, string> {
    const symmetricKey = this.config.getSymmetricKey(this.authType)

    return addExpiringAuthToHeaders(
      this.config,
      body.topic,
      {}, // Headers
      JSON.stringify(body),
      symmetricKey,
      SIG_EXPIRATION_BUFFER_MS,
    )
  }

  private cleanup(): void {
    // Remove all event listeners before nullifying the reference
    if (this.ws) {
      this.ws.removeEventListener('open', this.handleOpen)
      this.ws.removeEventListener('message', this.onmessage)
      this.ws.removeEventListener('close', this.handleClose)
      this.ws.removeEventListener('error', this.handleError)
    }

    // Nullify the WebSocket instance
    this.ws = null
  }
}
