import { createBatchingExecutor } from '@graphql-tools/batch-execute'
import { buildHTTPExecutor } from '@graphql-tools/executor-http'
import type { AsyncExecutor } from '@graphql-tools/utils'
import { observableToAsyncIterable } from '@graphql-tools/utils'
import { httpGraphql, wsGraphql } from '@inject/shared/config'
import authenticatedFetch from '@inject/shared/utils/authenticatedFetch'
import { buildClientSchema, print } from 'graphql'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import { sessionid } from '../../connection/sessionid'
import remoteSchemaQL from '../../schema.remote.json'
import { netvar } from '../networkEventVar'
import type { SubschemaConfig } from './typing'

const ws: {
  current: SubscriptionClient | null
  sessionid: string | null
} = {
  current: null,
  sessionid: null,
}

const fetchCounted: typeof fetch = (url, init) =>
  new Promise((resolve, reject) => {
    netvar(1)
    authenticatedFetch(url, init)
      .then(resolve)
      .catch(reject)
      .finally(() => netvar(-1))
  })

const constructRemoteSchema = (): SubschemaConfig => {
  const httpExecutor = createBatchingExecutor(
    buildHTTPExecutor({
      endpoint: httpGraphql(window.VITE_HTTPS_HOST ?? 'localhost:8000'),
      fetch: fetchCounted,
    })
  )

  /*
   * Custom executor which executes the subscriptions in subscriptions-transport-ws commlayer
   * the layer is coded in such a way that it rationally keeps one subscription for all comms, welcome to 2024
   */
  const wsExecutor: AsyncExecutor = async ({
    document,
    variables,
    operationName,
  }) =>
    observableToAsyncIterable({
      subscribe: observer => {
        if (ws.current === null) {
          throw Error('WS socket unitialized')
        }
        const { unsubscribe } = ws.current
          .request({
            operationName,
            query: print(document),
            variables,
          })
          .subscribe({
            complete: () => observer.complete(),
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            next: data => observer.next(data as any),
            error(err) {
              if (!observer.error) return
              if (err instanceof Error) {
                return observer.error(err)
              }
            },
          })

        return { unsubscribe }
      },
    })

  return {
    //@ts-ignore
    schema: buildClientSchema(remoteSchemaQL),
    executor: async executorRequest => {
      if (executorRequest.operationType === 'subscription') {
        const sessionId = sessionid()
        if (ws.current === null && sessionId === null) {
          throw Error('No subscription to make without a sessionId')
        }
        if (
          ws.current === null ||
          (ws.current !== null && sessionId !== ws.sessionid) ||
          ws.current.status === WebSocket.CLOSED ||
          ws.current.status === WebSocket.CLOSING
        ) {
          if (ws.current !== null) {
            ws.current.close(true, false)
            ws.current = null
          }
          ws.current = new SubscriptionClient(
            wsGraphql(window.VITE_HTTPS_HOST ?? 'localhost:8000'),
            {
              reconnect: true,
              reconnectionAttempts: 5,
              inactivityTimeout: 30_000,
              connectionParams: () => ({
                sessionid: sessionId,
              }),
            }
          )
          ws.sessionid = sessionId
        }
        if (ws.current.status === WebSocket.OPEN) {
          return wsExecutor(executorRequest)
        } else {
          await new Promise<void>(resolve => {
            if (ws.current === null) {
              throw Error('WS not initialized anyway, wtf')
            }
            ws.current.on('connected', () => {
              resolve()
            })
          })
          return wsExecutor(executorRequest)
        }
      }
      return httpExecutor(executorRequest)
    },
  }
}

window.addEventListener('killWs', () => {
  if (ws.current !== null) {
    ws.current.close(true, true)
    ws.current = null
  }
})

export default constructRemoteSchema
