import { CommandLog, EngineCommandManager } from 'lang/std/engineConnection'
import { WebrtcStats } from 'wasm-lib/kcl/bindings/WebrtcStats'
import { OsInfo } from 'wasm-lib/kcl/bindings/OsInfo'
import { isDesktop } from 'lib/isDesktop'
import { APP_VERSION } from 'routes/Settings'
import { UAParser } from 'ua-parser-js'
import screenshot from 'lib/screenshot'
import { VITE_KC_API_BASE_URL } from 'env'
import CodeManager from 'lang/codeManager'

/* eslint-disable suggest-no-throw/suggest-no-throw --
 * All the throws in CoreDumpManager are intentional and should be caught and handled properly
 * by the calling Promises with a catch block. The throws are essential to properly handling
 * when the app isn't ready enough or otherwise unable to produce a core dump. By throwing
 * instead of simply erroring, the code halts execution at the first point which it cannot
 * complete the core dump request.
 **/

/**
 * CoreDumpManager module
 * - for getting all the values from the JS world to pass to the Rust world for a core dump.
 * @module lib/coredump
 * @class
 */
// CoreDumpManager is instantiated in ModelingMachineProvider and passed to coreDump() in wasm.ts
// The async function coreDump() handles any errors thrown in its Promise catch method and rethrows
// them to so the toast handler in ModelingMachineProvider can show the user an error message toast
// TODO: Throw more
export class CoreDumpManager {
  engineCommandManager: EngineCommandManager
  codeManager: CodeManager
  token: string | undefined
  baseUrl: string = VITE_KC_API_BASE_URL

  constructor(
    engineCommandManager: EngineCommandManager,
    codeManager: CodeManager,
    token: string | undefined
  ) {
    this.engineCommandManager = engineCommandManager
    this.codeManager = codeManager
    this.token = token
  }

  // Get the token.
  authToken(): string {
    if (!this.token) {
      throw new Error('Token not set')
    }
    return this.token
  }

  // Get the base url.
  baseApiUrl(): string {
    return this.baseUrl
  }

  // Get the version of the app from the package.json.
  version(): string {
    return APP_VERSION
  }

  kclCode(): string {
    return this.codeManager.code
  }

  // Get the backend pool we've requested.
  pool(): string {
    return this.engineCommandManager.settings.pool || ''
  }

  // Get the os information.
  getOsInfo(): string {
    if (this.isDesktop()) {
      const osinfo: OsInfo = {
        platform: window.electron.platform ?? null,
        arch: window.electron.arch ?? null,
        browser: 'desktop',
        version: window.electron.version ?? null,
      }
      return JSON.stringify(osinfo)
    }

    const userAgent = window.navigator.userAgent || 'unknown browser'
    if (userAgent === 'unknown browser') {
      const osinfo: OsInfo = {
        platform: userAgent,
        arch: userAgent,
        version: userAgent,
        browser: userAgent,
      }
      return JSON.stringify(osinfo)
    }

    const parser = new UAParser(userAgent)
    const parserResults = parser.getResult()
    const osinfo: OsInfo = {
      platform: parserResults.os.name || userAgent,
      arch: parserResults.cpu.architecture || userAgent,
      version: parserResults.os.version || userAgent,
      browser: userAgent,
    }
    return JSON.stringify(osinfo)
  }

  isDesktop(): boolean {
    return isDesktop()
  }

  getWebrtcStats(): Promise<string> {
    if (!this.engineCommandManager.engineConnection) {
      // when the engine connection is not available, return an empty object.
      return Promise.resolve(JSON.stringify({}))
    }

    if (!this.engineCommandManager.engineConnection.webrtcStatsCollector) {
      // when the engine connection is not available, return an empty object.
      return Promise.resolve(JSON.stringify({}))
    }

    return this.engineCommandManager.engineConnection
      .webrtcStatsCollector()
      .catch((error: any) => {
        throw new Error(`Error getting webrtc stats: ${error}`)
      })
      .then((stats: any) => {
        const webrtcStats: WebrtcStats = {
          packets_lost: stats.rtc_packets_lost,
          frames_received: stats.rtc_frames_received,
          frame_width: stats.rtc_frame_width,
          frame_height: stats.rtc_frame_height,
          frame_rate: stats.rtc_frames_per_second,
          key_frames_decoded: stats.rtc_keyframes_decoded,
          frames_dropped: stats.rtc_frames_dropped,
          pause_count: stats.rtc_pause_count,
          total_pauses_duration: stats.rtc_total_pauses_duration_sec,
          freeze_count: stats.rtc_freeze_count,
          total_freezes_duration: stats.rtc_total_freezes_duration_sec,
          pli_count: stats.rtc_pli_count,
          jitter: stats.rtc_jitter_sec,
        }
        return JSON.stringify(webrtcStats)
      })
  }

  // Currently just a placeholder to begin loading singleton and xstate data into
  getClientState(): Promise<string> {
    /**
     * Check if a function is private method
     */
    const isPrivateMethod = (key: string) => {
      return key.length && key[0] === '_'
    }

    // Turn off verbose logging by default
    const verboseLogging = false

    /**
     * Toggle verbose debug logging of step-by-step client state coredump data
     */
    const debugLog = verboseLogging ? console.log : () => {}

    console.warn('CoreDump: Gathering client state')

    // Initialize the clientState object
    let clientState = {
      // singletons
      engine_command_manager: {
        artifact_map: {},
        command_logs: [] as CommandLog[],
        engine_connection: { state: { type: '' } },
        default_planes: {},
        scene_command_artifacts: {},
      },
      kcl_manager: {
        ast: {},
        kcl_errors: [],
      },
      scene_infra: {},
      scene_entities_manager: {},
      editor_manager: {},
      // xstate
      auth_machine: {},
      command_bar_machine: {},
      file_machine: {},
      home_machine: {},
      modeling_machine: {},
      settings_machine: {},
    }
    debugLog('CoreDump: initialized clientState', clientState)
    debugLog('CoreDump: globalThis.window', globalThis.window)

    try {
      // Singletons

      // engine_command_manager
      debugLog('CoreDump: engineCommandManager', this.engineCommandManager)

      // artifact map - this.engineCommandManager.artifactGraph
      if (this.engineCommandManager?.artifactGraph) {
        debugLog(
          'CoreDump: Engine Command Manager artifact map',
          this.engineCommandManager.artifactGraph
        )
        clientState.engine_command_manager.artifact_map = structuredClone(
          this.engineCommandManager.artifactGraph
        )
      }

      // command logs - this.engineCommandManager.commandLogs
      if (this.engineCommandManager?.commandLogs) {
        debugLog(
          'CoreDump: Engine Command Manager command logs',
          this.engineCommandManager.commandLogs
        )
        clientState.engine_command_manager.command_logs = structuredClone(
          this.engineCommandManager.commandLogs
        )
      }

      // default planes - this.engineCommandManager.defaultPlanes
      if (this.engineCommandManager?.defaultPlanes) {
        debugLog(
          'CoreDump: Engine Command Manager default planes',
          this.engineCommandManager.defaultPlanes
        )
        clientState.engine_command_manager.default_planes = structuredClone(
          this.engineCommandManager.defaultPlanes
        )
      }

      // engine connection state
      if (this.engineCommandManager?.engineConnection?.state) {
        debugLog(
          'CoreDump: Engine Command Manager engine connection state',
          this.engineCommandManager.engineConnection.state
        )
        clientState.engine_command_manager.engine_connection.state =
          this.engineCommandManager.engineConnection.state
      }

      // in sequence - this.engineCommandManager.inSequence
      if (this.engineCommandManager?.inSequence) {
        debugLog(
          'CoreDump: Engine Command Manager in sequence',
          this.engineCommandManager.inSequence
        )
        ;(clientState.engine_command_manager as any).in_sequence =
          this.engineCommandManager.inSequence
      }

      // out sequence - this.engineCommandManager.outSequence
      if (this.engineCommandManager?.outSequence) {
        debugLog(
          'CoreDump: Engine Command Manager out sequence',
          this.engineCommandManager.outSequence
        )
        ;(clientState.engine_command_manager as any).out_sequence =
          this.engineCommandManager.outSequence
      }

      // KCL Manager - globalThis?.window?.kclManager
      const kclManager = (globalThis?.window as any)?.kclManager
      debugLog('CoreDump: kclManager', kclManager)

      if (kclManager) {
        // KCL Manager AST
        debugLog('CoreDump: KCL Manager AST', kclManager?.ast)
        if (kclManager?.ast) {
          clientState.kcl_manager.ast = structuredClone(kclManager.ast)
        }

        // KCL Errors
        debugLog('CoreDump: KCL Errors', kclManager?.kclErrors)
        if (kclManager?.kclErrors) {
          clientState.kcl_manager.kcl_errors = structuredClone(
            kclManager.kclErrors
          )
        }

        // KCL isExecuting
        debugLog('CoreDump: KCL isExecuting', kclManager?.isExecuting)
        if (kclManager?.isExecuting) {
          ;(clientState.kcl_manager as any).isExecuting = kclManager.isExecuting
        }

        // KCL logs
        debugLog('CoreDump: KCL logs', kclManager?.logs)
        if (kclManager?.logs) {
          ;(clientState.kcl_manager as any).logs = structuredClone(
            kclManager.logs
          )
        }

        // KCL programMemory
        debugLog('CoreDump: KCL programMemory', kclManager?.programMemory)
        if (kclManager?.programMemory) {
          ;(clientState.kcl_manager as any).programMemory = structuredClone(
            kclManager.programMemory
          )
        }

        // KCL wasmInitFailed
        debugLog('CoreDump: KCL wasmInitFailed', kclManager?.wasmInitFailed)
        if (kclManager?.wasmInitFailed) {
          ;(clientState.kcl_manager as any).wasmInitFailed =
            kclManager.wasmInitFailed
        }
      }

      // Scene Infra - globalThis?.window?.sceneInfra
      const sceneInfra = (globalThis?.window as any)?.sceneInfra
      debugLog('CoreDump: Scene Infra', sceneInfra)

      if (sceneInfra) {
        const sceneInfraSkipKeys = ['camControls']
        const sceneInfraKeys = Object.keys(sceneInfra)
          .sort()
          .filter((entry) => {
            return (
              typeof sceneInfra[entry] !== 'function' &&
              !sceneInfraSkipKeys.includes(entry)
            )
          })

        debugLog('CoreDump: Scene Infra keys', sceneInfraKeys)
        sceneInfraKeys.forEach((key: string) => {
          debugLog('CoreDump: Scene Infra', key, sceneInfra[key])
          try {
            ;(clientState.scene_infra as any)[key] = sceneInfra[key]
          } catch (error) {
            console.error(
              'CoreDump: unable to parse Scene Infra ' + key + ' data due to ',
              error
            )
          }
        })
      }

      // Scene Entities Manager - globalThis?.window?.sceneEntitiesManager
      const sceneEntitiesManager = (globalThis?.window as any)
        ?.sceneEntitiesManager
      debugLog('CoreDump: sceneEntitiesManager', sceneEntitiesManager)

      if (sceneEntitiesManager) {
        // Scene Entities Manager active segments
        debugLog(
          'CoreDump: Scene Entities Manager active segments',
          sceneEntitiesManager?.activeSegments
        )
        if (sceneEntitiesManager?.activeSegments) {
          ;(clientState.scene_entities_manager as any).activeSegments =
            structuredClone(sceneEntitiesManager.activeSegments)
        }
      }

      // Editor Manager - globalThis?.window?.editorManager
      const editorManager = (globalThis?.window as any)?.editorManager
      debugLog('CoreDump: editorManager', editorManager)

      if (editorManager) {
        const editorManagerSkipKeys = ['camControls']
        const editorManagerKeys = Object.keys(editorManager)
          .sort()
          .filter((entry) => {
            return (
              typeof editorManager[entry] !== 'function' &&
              !isPrivateMethod(entry) &&
              !editorManagerSkipKeys.includes(entry)
            )
          })

        debugLog('CoreDump: Editor Manager keys', editorManagerKeys)
        editorManagerKeys.forEach((key: string) => {
          debugLog('CoreDump: Editor Manager', key, editorManager[key])
          try {
            ;(clientState.editor_manager as any)[key] = structuredClone(
              editorManager[key]
            )
          } catch (error) {
            console.error(
              'CoreDump: unable to parse Editor Manager ' +
                key +
                ' data due to ',
              error
            )
          }
        })
      }

      // enableMousePositionLogs - Not coredumped
      // See https://github.com/KittyCAD/modeling-app/issues/2338#issuecomment-2136441998
      debugLog(
        'CoreDump: enableMousePositionLogs [not coredumped]',
        (globalThis?.window as any)?.enableMousePositionLogs
      )

      // XState Machines
      debugLog(
        'CoreDump: xstate services',
        (globalThis?.window as any)?.__xstate__?.services
      )

      debugLog('CoreDump: final clientState', clientState)

      const clientStateJson = JSON.stringify(clientState)
      debugLog('CoreDump: final clientState JSON', clientStateJson)

      return Promise.resolve(clientStateJson)
    } catch (error) {
      console.error('CoreDump: unable to return data due to ', error)
      return Promise.reject(JSON.stringify(error))
    }
  }

  // Return a data URL (png format) of the screenshot of the current page.
  screenshot(): Promise<string> {
    return (
      screenshot()
        .then((screenshotStr: string) => screenshotStr)
        // maybe rust should handle an error, but an empty string at least doesn't cause the core dump to fail entirely
        .catch((error: any) => ``)
    )
  }
}
