import React from 'react'
import ReactDOM from 'react-dom/client'
import studio from '@theatre/studio'
import Extension from '@/components/Extension'
import PublishExtension from '@/components/PublishExtension'
import { ExtensionScriptManager, ScriptLoadingType } from '@/ExtensionScriptManager'
import { extensionEntry, createExtensionSDK, createSDK } from '@/scripts'
import { PlayCanvasTheatreEditor } from '@/theatre/PlayCanvasTheatreEditor'
import { Container, Button } from '@playcanvas/pcui'
import '@/global.css'
import '@/init'
import { pcEditor } from '@/global'
import SceneSettingsButton from '@/components/SceneSettingsButton'
import LoginButton from '@/components/LoginButton'
import useMainStore, { MainConfig } from '@/hooks/useMainStore'
import useQuestStore from '@/hooks/useQuestStore'
import { GLTFExporter2 } from './gltf-exporter'
import {
  seatHint,
  seatHintHover,
  seatHintFarAway,
  minimapArrow,
  minimapCone,
  minimapFrame,
  minimapMask,
} from '@/assets/preview-image'
import { cloneDeep } from 'lodash'
import { MAIN_CONFIG_PATH } from '@/constants'

// initialize the theatre studio at the first time is needed, but we choose to hide it by default
studio.initialize()
studio.ui.hide()

let currentUrl = window.location.href
let catchCopySource: any[] = []

const IMAGE_ASSET_CONFIGS = [
  { atlasName: 'seat-hint-atlas.png', spriteName: 'seat-hint-sprite', url: seatHint },
  {
    atlasName: 'seat-hover-hint-atlas.png',
    spriteName: 'seat-hover-hint-sprite',
    url: seatHintHover,
  },
  {
    atlasName: 'seat-hint-far-away-atlas.png',
    spriteName: 'seat-hint-far-away-sprite',
    url: seatHintFarAway,
  },
]

const IMAGE_ASSET_TEXTURE_CONFIGS = [
  { imgName: 'minimap-arrow.png', domId: 'minimap-arrow', url: minimapArrow },
  { imgName: 'minimap-cone.png', domId: 'minimap-cone', url: minimapCone },
  { imgName: 'minimap-frame.png', domId: 'minimap-frame', url: minimapFrame },
  { imgName: 'minimap-mask.png', domId: 'minimap-mask', url: minimapMask },
]

function resetExtensionEntity() {
  setTimeout(async () => {
    if (currentUrl !== window.location.href) {
      console.log('[Reset extension entity]:', { currentUrl, newUrl: window.location.href })
      currentUrl = window.location.href

      const { reloadExtensionConfigs } = useMainStore.getState()
      const { reloadQuestConfigs } = useQuestStore.getState()

      await checkAndCreateExtensionEntity()

      reloadExtensionConfigs()
      reloadQuestConfigs()
    }
  }, 1000)
}

function handleExtensionManager() {
  const extensionScriptManager = new ExtensionScriptManager()

  extensionScriptManager.initial().then(() => {
    extensionScriptManager.uploadScript('extension-entry@v2', extensionEntry)

    extensionScriptManager.uploadCreateSDKScript({
      filename: `create-extensions-sdk.mjs`, // ESM module
      text: createExtensionSDK,
      preload: false,
      data: {
        scripts: {},
        loading: false,
        loadingType: ScriptLoadingType.BeforeEngine,
      },
    })

    extensionScriptManager.uploadCreateSDKScript({
      filename: `create-sdk.mjs`, // ESM module
      text: createSDK,
      preload: true,
    })

    const createAtlasAndSpriteAsset = async (image: {
      atlasName: string
      spriteName: string
      url: string
    }): Promise<void> => {
      return extensionScriptManager.createTextureAtlas(image.atlasName, image.url).then((atlas) => {
        if (atlas) {
          extensionScriptManager.createSpriteAsset(image.spriteName, atlas)
        }
      })
    }

    const createTextureAsset = async (image: { imgName: string; url: string }): Promise<void> => {
      return extensionScriptManager.uploadImage(image.imgName, image.url).then((asset) => {
        if (asset) {
          console.log('[Successfully uploaded image asset]', { asset })
        }
      })
    }

    Promise.all(IMAGE_ASSET_CONFIGS.map((image) => createAtlasAndSpriteAsset(image)))
    Promise.all(IMAGE_ASSET_TEXTURE_CONFIGS.map((image) => createTextureAsset(image)))
  })
}

async function checkAndCreateExtensionEntity() {
  let extensionEntity = pcEditor.entities.root.findByName('ExtensionEntity')

  if (!extensionEntity) {
    extensionEntity = await pcEditor.entities.create({
      parent: pcEditor.entities.root,
      name: 'ExtensionEntity',
    })
  }

  window.extensionEntity = extensionEntity
  if (!window.extensionEntity.has('components.script.scripts.extensionEntry')) {
    addExtensionScript()
  }
}

function addExtensionScript() {
  const checkInterval = 500
  const timeoutLimit = 10000
  let elapsedTime = 0

  const scriptLoaded = () => pcEditor.call('assets:scripts:list').includes('extensionEntry')
  const addScript = async () => {
    try {
      await window.extensionEntity.addScript('extensionEntry')
      pcEditor.emit('extension-entity:added-script')
    } catch (error) {
      // this is caused by delete scene of current scene
      // so we need to reload the page to prevent the error
      console.warn('[Failed to add script to extension entity]', { error })
      location.reload()
    }
  }

  const asset = pcEditor.assets.findOne(
    (asset: any) => asset.get('name') === 'extension-entry@v2.js',
  )

  if (!asset) {
    console.warn('[Failed to find extension script asset]')
    return
  }

  const tryLoadScript = () => {
    if (scriptLoaded()) {
      addScript()
    } else if (elapsedTime < timeoutLimit) {
      elapsedTime += checkInterval
      setTimeout(tryLoadScript, checkInterval)
      pcEditor.call('scripts:parse', asset)
    } else {
      console.warn(
        '[Failed to load extension script] Timeout exceeded while waiting for the script to load.',
      )
    }
  }

  tryLoadScript()
}

function renderLoginButtonUI() {
  const app = document.createElement('div')
  app.id = 'login-button-root'

  const layoutAttributes = document.querySelector('#layout-toolbar')
  layoutAttributes?.append(app)

  console.log('[LOGIN BUTTON INJECTION]', { layoutAttributes })

  ReactDOM.createRoot(app).render(
    <React.StrictMode>
      <LoginButton />
    </React.StrictMode>,
  )
}

function renderSceneSettingButtonUI() {
  const app = document.createElement('div')
  app.id = 'scene-settings-button-root'

  const layoutAttributes = document.querySelector('#layout-toolbar')
  layoutAttributes?.append(app)

  console.log('[SCENE SETTING BUTTON INJECTION]', { layoutAttributes })

  ReactDOM.createRoot(app).render(
    <React.StrictMode>
      <SceneSettingsButton />
    </React.StrictMode>,
  )
}

function renderExtensionUI() {
  const app = document.createElement('div')
  app.id = 'extension-app-root'

  const layoutAttributes = document.querySelector('#layout-attributes')
  layoutAttributes?.append(app)

  console.log('[CUSTOM UI INJECTION]', { layoutAttributes })

  ReactDOM.createRoot(app).render(
    <React.StrictMode>
      <Extension />
    </React.StrictMode>,
  )
}

function renderPublishUI() {
  // The new PlayCanvas Editor renders the Builds panel with a `.builds-toolbar`
  // (Download + Publish buttons) and registers sub-views like `build-detail`
  // / `publish-new` via `picker:project:registerPanel`. We follow the same
  // pattern: register a `viverse-publish` panel hosting <PublishExtension />
  // and inject a "Publish to VIVERSE" button beside the native Publish button.

  // Use the editor's own `picker-publish-new` class so its `.back` button,
  // `> .form-footer` (sticky orange action bar), and panel padding rules all
  // apply natively — keeping us pixel-aligned with the new editor UI.
  const viversePanel = new Container({
    flex: true,
    class: ['picker-publish-new', 'picker-viverse-publish'],
  })

  const backButton = document.createElement('button')
  backButton.type = 'button'
  backButton.classList.add('back')
  backButton.textContent = 'Back to builds'
  backButton.addEventListener('click', () => {
    pcEditor.call('picker:builds-publish')
  })
  viversePanel.dom.appendChild(backButton)

  const reactRoot = document.createElement('div')
  reactRoot.id = 'custom-publish-root'
  viversePanel.dom.appendChild(reactRoot)

  // Real pcui Container for the footer — a direct child of `picker-publish-new`
  // so editor's `.picker-publish-new > .form-footer > .pcui-button` selectors
  // style our Publish/Preview buttons exactly like the native panel.
  // PublishExtension portals its action buttons into this node.
  const footerContainer = new Container({
    class: ['form-footer'],
  })
  viversePanel.append(footerContainer)

  // Expose the footer mount point so the React tree can portal into it.
  ;(
    window as unknown as { __viversePublishFooterMount?: HTMLElement }
  ).__viversePublishFooterMount = footerContainer.dom

  ReactDOM.createRoot(reactRoot).render(
    <React.StrictMode>
      <PublishExtension />
    </React.StrictMode>,
  )

  try {
    pcEditor.call('picker:project:registerPanel', 'viverse-publish', 'VIVERSE', viversePanel)
  } catch (error) {
    console.error('[PUBLISH UI INJECTION] Failed to register viverse-publish panel', error)
    return
  }

  const attachToolbarButton = (retries = 30) => {
    const toolbar = document.querySelector(
      '.picker-builds-publish > .primary-build-header > .builds-toolbar',
    )

    if (!toolbar) {
      if (retries > 0) {
        setTimeout(() => attachToolbarButton(retries - 1), 200)
      } else {
        console.warn('[PUBLISH UI INJECTION] `.builds-toolbar` not found')
      }
      return
    }

    if (toolbar.querySelector('.viverse-publish')) return

    const btnViverse = new Button({
      text: 'Publish to VIVERSE',
      icon: 'E226',
      class: 'viverse-publish',
    })
    btnViverse.on('click', () => {
      pcEditor.call('picker:project', 'viverse-publish')
    })
    toolbar.appendChild(btnViverse.dom)

    console.log('[PUBLISH UI INJECTION] "Publish to VIVERSE" button attached', { toolbar })
  }

  attachToolbarButton()
}

function overrideEditor() {
  // Override the source code of PlayCanvas Editor to add event.
  return new Promise<void>((resolve, reject) => {
    try {
      pcEditor.methodRemove('entities:duplicate')
      const projectUserSettings = pcEditor.call('settings:projectUser')
      /**
       * Duplicates the specified entities and adds them to the scene.
       *
       * @param {Observer[]} entities - The entities to duplicate
       */
      pcEditor.method('entities:duplicate', function (entities: any[]) {
        pcEditor.entities.duplicate(
          entities.map((entity) => entity.apiEntity),
          {
            select: true,
            history: true,
            rename: projectUserSettings.get('editor.renameDuplicatedEntities'),
          },
        )
        pcEditor.emit('entities:duplicate', entities)
      })

      pcEditor.methodRemove('entities:copy')
      /**
       * Copies the specified entities into localStorage
       *
       * @param {Observer[]} entities - The entities to copy
       */
      pcEditor.method('entities:copy', function (entities: any[]) {
        pcEditor.entities.copyToClipboard(entities.map((e) => e.apiEntity))
        pcEditor.emit('entities:copy', entities)
      })

      pcEditor.methodRemove('entities:paste')
      /**
       * Pastes entities in localStore under the specified parent
       *
       * @param {Observer} parent - The parent entity
       */
      pcEditor.method('entities:paste', function (parent: any) {
        pcEditor.entities.pasteFromClipboard(parent ? parent.apiEntity : null)
        pcEditor.emit('entities:paste', catchCopySource)
      })
      resolve()
    } catch (error) {
      reject(error)
    }
  })
}

function combineMaps(
  combinedMap: Map<string, string[]>,
  map1: Map<string, string>,
  map2: Map<string, string>,
) {
  // const combinedMap = new Map()

  // Helper function to add values to the map
  const addToMap = (map: Map<string, any[]>, key: string, value: any) => {
    if (map.has(key)) {
      map.get(key)?.push(value)
    } else {
      map.set(key, [value])
    }
  }

  // Add all entries from map1
  for (const [key, value] of map1) {
    addToMap(combinedMap, key, value)
  }

  // Add all entries from map2
  for (const [key, value] of map2) {
    addToMap(combinedMap, key, value)
  }

  return combinedMap
}

function copyEntityStructure(source: any) {
  const result = new Map<string, string>()
  result.set(source.get('name'), source.get('resource_id'))
  function recurse(currentObj: pc.Entity) {
    if (currentObj.children && currentObj.children.length > 0) {
      currentObj.children.forEach((child: any) => {
        if (child._guid) {
          result.set(`${currentObj.name}_${child.name}`, child._guid)
          recurse(child)
        }
      })
    }
  }

  recurse(source.entity || source.viewportEntity)
  return result
}

function hasEntityPicker(config: MainConfig) {
  const string = JSON.stringify(config)
  return string.includes('entityId')
}

function updateEntityId(combinedMap: Map<string, any[]>, index: number, copy: any) {
  // Recursive function to traverse and update entityId
  function traverseAndUpdate(obj: any) {
    for (const key in obj) {
      if (typeof obj[key] === 'object' && obj[key] !== null) {
        traverseAndUpdate(obj[key])
      } else if (key === 'entityId') {
        const searchValue = obj[key]
        for (const [_, value] of combinedMap) {
          // Check if the current value array includes the search value
          if (value.includes(searchValue)) {
            obj[key] = value[index]
          }
        }
      }
    }
  }

  // Start the traversal
  traverseAndUpdate(copy)
}

function copyExtensionConfigToNewEntity(sourceEntities: any[]) {
  catchCopySource = sourceEntities
  const config = JSON.parse(window.extensionEntity.get(MAIN_CONFIG_PATH) || '{}')

  const copyEntities = pcEditor.selection.items // A list of duplicated entities.
  sourceEntities.forEach((source: any) => {
    let combinedMap = new Map()
    const originMap = copyEntityStructure(source)

    const copyList = copyEntities.filter(
      (entity: any) => entity.viewportEntity.name === source.get('name'),
    )

    copyList.forEach((copy: any) => {
      const copyMap = copyEntityStructure(copy)
      combinedMap = combineMaps(combinedMap, originMap, copyMap)
    })

    for (const [_, value] of combinedMap.entries()) {
      const [originEntityId, ...duplicatedId] = value
      if (config[originEntityId]) {
        duplicatedId.forEach((guid: string, index: number) => {
          const deepCopy = cloneDeep(config[originEntityId])
          if (hasEntityPicker(deepCopy)) {
            updateEntityId(combinedMap, index + 1, deepCopy)
          }
          config[guid] = deepCopy
        })
      }
    }
  })

  window.extensionEntity.set(MAIN_CONFIG_PATH, JSON.stringify(config))

  const { reloadExtensionConfigs } = useMainStore.getState()
  reloadExtensionConfigs()
}

function initializeExtensionFeatures() {
  // Initial Theatre editor
  const initTheatreEditor = () => {
    const playCanvasTheatreEditor = new PlayCanvasTheatreEditor()
    console.log({ playCanvasTheatreEditor })
  }
  if (window.extensionEntity.has('components.script.scripts.extensionEntry.attributes.configs')) {
    initTheatreEditor()
    renderExtensionUI()
    renderSceneSettingButtonUI()
  } else {
    pcEditor.once('extension-entity:added-script', () => {
      // [Workaround]：
      // delay extension UI rendering to ensure "components.script.scripts.extensionEntry.attributes.configs"
      // is accessible after script attachment and initialization.
      setTimeout(() => {
        initTheatreEditor()
        renderExtensionUI()
        renderSceneSettingButtonUI()
      }, 1000)
    })
  }
}

function onReady() {
  if (!pcEditor) return

  console.log('[Successfully got window.editor]', { editor: pcEditor })

  pcEditor.on('extension-scripts:uploaded', addExtensionScript)
  pcEditor.once('assets:load', async () => {
    captureApiLogs()
    bindGLTFExportHotKey()
    await checkAndCreateExtensionEntity()
    initializeExtensionFeatures()
    renderLoginButtonUI()
    handleExtensionManager()
    renderPublishUI()

    // Re-check ExtensionEntity on subsequent `assets:load` events (e.g. after reconnect)
    pcEditor.on('assets:load', checkAndCreateExtensionEntity)
  })

  // Reset extension entity when switching scenes
  pcEditor.on('scene:unload', resetExtensionEntity)

  overrideEditor()
    .then(() => {
      pcEditor.on('entities:duplicate', copyExtensionConfigToNewEntity)
      pcEditor.on('entities:copy', copyExtensionConfigToNewEntity)
      pcEditor.on('entities:paste', copyExtensionConfigToNewEntity)
    })
    .catch((error) => {
      console.error(error)
      console.warn(
        "The Creator tools's copy/paste and duplicate functions currently do not work with the Extension config.",
      )
    })
}

if (document.readyState !== 'loading') {
  onReady() // Or setTimeout(onReady, 0); if you want it consistently async
} else {
  document.addEventListener('DOMContentLoaded', onReady)
}

const bindGLTFExportHotKey = () => {
  document.addEventListener('keypress', async (e) => {
    if (e.altKey && e.code === 'KeyD') {
      new GLTFExporter2().export()
    }
  })
}

// overwrite fetch to capture request and response data

function normalizeHeaders(headers: HeadersInit | undefined): Record<string, string> {
  const normalizedHeaders: Record<string, string> = {}

  if (headers instanceof Headers) {
    headers.forEach((value, key) => {
      normalizedHeaders[key] = value
    })
  } else if (Array.isArray(headers)) {
    headers.forEach(([key, value]) => {
      normalizedHeaders[key] = value
    })
  } else if (headers && typeof headers === 'object') {
    Object.assign(normalizedHeaders, headers)
  }

  return normalizedHeaders
}

interface RequestData {
  url: string
  method: string
  headers: HeadersInit | Record<string, string>
  body: any
}

interface ResponseData {
  url: string
  status: number
  headers: [string, string][]
  body: string
}

interface APILog {
  request: RequestData
  response: ResponseData
}

function captureApiLogs() {
  // need to remove the traceLogs from localStorage to avoid duplicate logs when the extension is reloaded
  localStorage.removeItem('viverse-logs')

  const originalFetch = window.fetch

  // List of allowed domains or URL patterns
  const allowedUrls = ['playcanvas.com/api/jobs', 'viverse.com/api']

  // Function to check if a URL matches any of the allowed patterns
  const urlFilter = (url: string): boolean => {
    return allowedUrls.some((allowedUrl) => url.includes(allowedUrl))
  }

  window.fetch = async function (...args: Parameters<typeof fetch>): Promise<Response> {
    const request = args[0]
    const requestInit = args[1]

    // Determine the request URL
    const url: string =
      request instanceof Request ? request.url : request instanceof URL ? request.href : request

    // Skip logging if the URL does not match any allowed pattern
    if (!urlFilter(url)) {
      return originalFetch.apply(this, args)
    }

    const method = requestInit?.method || 'GET'

    const requestData: RequestData = {
      url,
      method,
      headers: normalizeHeaders(requestInit?.headers),
      body: requestInit?.body || null,
    }

    const response = await originalFetch.apply(this, args)
    const clonedResponse = response.clone()
    const responseBody = await clonedResponse.text()

    const responseData: ResponseData = {
      url,
      status: response.status,
      headers: Array.from(clonedResponse.headers.entries()),
      body: responseBody,
    }

    // Save the log only if the URL matches the filter
    const logs: APILog[] = JSON.parse(localStorage.getItem('viverse-logs') || '[]')
    logs.push({ request: requestData, response: responseData })
    localStorage.setItem('viverse-logs', JSON.stringify(logs))

    return response
  }
}
