import type { SceneNode, Choice, EngineEvent, Hotspot, ChapterInfo } from '../types'
import { SceneManager } from './SceneManager'
import { VideoManager } from './VideoManager'
import { StateManager } from './StateManager'
import { ChoiceSystem } from '../systems/ChoiceSystem'
import { QTESystem } from '../systems/QTESystem'
import { AudioSystem } from '../systems/AudioSystem'
import { AchievementSystem } from '../systems/AchievementSystem'

type EventHandler = (...args: any[]) => void

export class Engine {
  sceneManager: SceneManager
  videoManager: VideoManager
  stateManager: StateManager
  choiceSystem: ChoiceSystem
  qteSystem: QTESystem
  audioSystem: AudioSystem
  achievementSystem: AchievementSystem

  private currentScene: SceneNode | null = null
  private events: Map<EngineEvent, Set<EventHandler>> = new Map()
  private ended = false
  private isInitialScene = true
  private qteTriggered = false
  private qteResolved = false
  private justCameFromImage = false
  private loopActive = false
  private onUnlockChapter: ((chapterId: string) => void) | null = null
  private onMarkWatched: ((sceneId: string) => void) | null = null
  private onMarkVisited: ((sceneId: string) => void) | null = null

  setChapterUnlockHandler(handler: (chapterId: string) => void) {
    this.onUnlockChapter = handler
  }

  setMarkWatchedHandler(handler: (sceneId: string) => void) {
    this.onMarkWatched = handler
  }

  setMarkVisitedHandler(handler: (sceneId: string) => void) {
    this.onMarkVisited = handler
  }

  constructor() {
    this.sceneManager = new SceneManager()
    this.videoManager = new VideoManager()
    this.stateManager = new StateManager()
    this.choiceSystem = new ChoiceSystem()
    this.qteSystem = new QTESystem()
    this.audioSystem = new AudioSystem()
    this.achievementSystem = new AchievementSystem()

    this.stateManager.onAfterApply = (vars) => {
      this.achievementSystem.check(vars)
    }

    this.videoManager.onTimeUpdate(this.onTimeUpdate)
  }

  on(event: EngineEvent, handler: EventHandler) {
    if (!this.events.has(event)) this.events.set(event, new Set())
    this.events.get(event)!.add(handler)
  }

  off(event: EngineEvent, handler: EventHandler) {
    this.events.get(event)?.delete(handler)
  }

  private emit(event: EngineEvent, ...args: any[]) {
    this.events.get(event)?.forEach((h) => h(...args))
  }

  start() {
    this.ended = false
    this.isInitialScene = true
    const startScene = this.sceneManager.getStartScene()
    this.goToScene(startScene)
  }

  private goToScene(scene: SceneNode) {
    this.currentScene = scene

    const chapter = this.sceneManager.getChapterBySceneId(scene.id)
    if (chapter) {
      this.onUnlockChapter?.(chapter.id)
      this.emit('chapterUnlock', chapter)
    }

    this.onMarkVisited?.(scene.id)

    this.qteTriggered = false
    this.qteResolved = false
    this.loopActive = false

    if (scene.type === 'image') {
      this.justCameFromImage = true
      this.isInitialScene = false
      this.emit('sceneChange', scene)
      const visible = this.getVisibleHotspots(scene)
      if (visible.length > 0) {
        this.emit('hotspotRequest', visible)
      }
      return
    }

    this.emit('sceneChange', scene)
    this.checkHotspotTime(scene, 0)

    const candidateIds = this.sceneManager.getCandidateSceneIds(
      scene,
      (conds) => conds ? this.stateManager.evaluate(conds) : true
    )
    const preloadUrls = candidateIds
      .map(id => this.sceneManager.getScene(id))
       .filter((s) => !!s && s.type !== 'image')
       .map(s => this.videoManager.resolveVideoUrl(s!, this.videoManager.streamingQuality))

    this.videoManager.onEnd(() => {
      if (!this.qteTriggered || this.qteResolved) {
        this.emit('videoEnd', scene)
        this.onVideoEnd(scene)
      }
    })

    const activeEl = this.videoManager.getActiveVideoElement()
    activeEl?.pause()

    if (this.justCameFromImage) {
      this.justCameFromImage = false
      if (activeEl) {
        activeEl.style.opacity = '0'
        activeEl.style.transition = 'none'
      }
    }

    if (this.isInitialScene) {
      this.isInitialScene = false
      this.videoManager.playInitial(this.videoManager.resolveVideoUrl(scene, this.videoManager.streamingQuality), preloadUrls)
    } else {
      this.videoManager.switchTo(this.videoManager.resolveVideoUrl(scene, this.videoManager.streamingQuality), preloadUrls)
    }
  }

  private onTimeUpdate = (time: number) => {
    const scene = this.currentScene
    if (!scene) return

    this.checkHotspotTime(scene, time)
    this.checkLoop(time)

    // QTE check after loop check, so loop doesn't interfere with QTE
    if (!scene.qte || this.qteTriggered) return
    if (time >= scene.qte.triggerTime) {
      this.qteTriggered = true
      const qte = scene.qte

      this.emit('qteTrigger', qte)

      this.audioSystem.duckOn('qte')

      this.qteSystem.trigger(
        qte,
        (remaining, total) => {
          this.emit('qteTimer', { remaining, total })
        },
        (success) => {
          this.qteResolved = true
          this.audioSystem.duckOff('qte')
          if (success) {
            if (qte.effects?.success) {
              this.stateManager.apply(qte.effects.success)
            }
            this.emit('qteResult', { success: true })
            const targetScene = this.sceneManager.getScene(qte.successScene)
            if (targetScene) {
              this.goToScene(targetScene)
            } else {
              this.endGame()
            }
          } else {
            if (qte.effects?.fail) {
              this.stateManager.apply(qte.effects.fail)
            }
            this.emit('qteResult', { success: false })
            const targetScene = this.sceneManager.getScene(qte.failScene)
            if (targetScene) {
              this.goToScene(targetScene)
            } else {
              this.endGame()
            }
          }
        }
      )
    }
  }

  private checkLoop(time: number) {
    const scene = this.currentScene
    if (!scene?.loopStart) return

    if (!this.loopActive && time >= scene.loopStart) {
      this.loopActive = true
      const validChoices = this.getValidChoices(scene)
      if (validChoices.length > 0) {
        this.emit('choiceRequest', validChoices)
        this.audioSystem.duckOn('choice')
        this.choiceSystem.start(
          validChoices,
          (timerState) => {
            this.emit('choiceTimer', timerState)
          },
          (defaultChoice) => {
            this.emit('choiceTimeout', defaultChoice)
            this.makeChoice(defaultChoice)
          }
        )
      }
    }

    if (this.loopActive && scene.loopEnd && time >= scene.loopEnd) {
      this.videoManager.seekTo(scene.loopStart)
    }
  }

  private checkHotspotTime(scene: SceneNode, time: number) {
    if (!scene.hotspots || scene.hotspots.length === 0) return

    const visible = scene.hotspots.filter((hs) => {
      if (hs.conditions && !this.stateManager.evaluate(hs.conditions)) return false
      if (hs.showAt !== undefined && time < hs.showAt) return false
      if (hs.hideAt !== undefined && time >= hs.hideAt) return false
      return true
    })

    this.emit('hotspotUpdate', visible)

    if (visible.length > 0) {
      this.audioSystem.duckOn('hotspot')
    } else {
      this.audioSystem.duckOff('hotspot')
    }
  }

  getVisibleHotspots(scene: SceneNode): Hotspot[] {
    if (!scene.hotspots) return []
    return scene.hotspots.filter((hs) => {
      if (hs.conditions && !this.stateManager.evaluate(hs.conditions)) return false
      return true
    })
  }

  clickHotspot(hotspot: Hotspot) {
    if (!this.currentScene) return

    this.audioSystem.duckOff('hotspot')

    if (hotspot.effects) {
      this.stateManager.apply(hotspot.effects)
    }

    this.stateManager.recordChoice({
      sceneId: this.currentScene.id,
      choiceIndex: -1,
      choiceText: hotspot.label,
    })

    const next = this.sceneManager.getScene(hotspot.targetScene)
    if (next) {
      this.goToScene(next)
    } else {
      this.endGame()
    }
  }

  private onVideoEnd(scene: SceneNode) {
    this.onMarkWatched?.(scene.id)

    if (this.loopActive) return

    const validChoices = this.getValidChoices(scene)

    if (validChoices.length > 0) {
      this.emit('choiceRequest', validChoices)
      this.audioSystem.duckOn('choice')

      this.choiceSystem.start(
        validChoices,
        (timerState) => {
          this.emit('choiceTimer', timerState)
        },
        (defaultChoice) => {
          this.audioSystem.duckOff('choice')
          this.emit('choiceTimeout', defaultChoice)
          this.makeChoice(defaultChoice)
        }
      )
    } else if (scene.nextScene) {
      if (Array.isArray(scene.nextScene)) {
        for (const route of scene.nextScene) {
          if (!route.conditions || this.stateManager.evaluate(route.conditions)) {
            const next = this.sceneManager.getScene(route.targetScene)
            if (next) {
              this.goToScene(next)
            } else {
              this.endGame()
            }
            return
          }
        }
        this.endGame()
      } else {
        const next = this.sceneManager.getScene(scene.nextScene)
        if (next) {
          this.goToScene(next)
        } else {
          this.endGame()
        }
      }
    } else if (scene.hotspots?.length) {
      return
    } else {
      this.endGame()
    }
  }

  getValidChoices(scene: SceneNode): Choice[] {
    if (!scene.choices) return []
    return scene.choices.filter((c) =>
      !c.conditions || this.stateManager.evaluate(c.conditions)
    )
  }

  makeChoice(choice: Choice) {
    if (!this.currentScene) return

    this.audioSystem.duckOff('choice')

    if (choice.effects) {
      this.stateManager.apply(choice.effects)
    }

    this.stateManager.recordChoice({
      sceneId: this.currentScene.id,
      choiceIndex: this.currentScene.choices?.indexOf(choice) ?? -1,
      choiceText: choice.text,
    })

    const next = this.sceneManager.getScene(choice.targetScene)
    if (next) {
      this.goToScene(next)
    } else {
      this.endGame()
    }
  }

  endGame() {
    this.ended = true
    this.loopActive = false
    this.qteSystem.cancel()
    this.audioSystem.stop(2.0)
    this.emit('gameEnd')
  }

  skipCurrentScene() {
    const scene = this.currentScene
    if (!scene) return
    this.videoManager.getActiveVideoElement()?.pause()
    this.onVideoEnd(scene)
  }

  startChapter(chapterId: string) {
    const chapter = this.sceneManager.getChapter(chapterId)
    if (!chapter) return

    const scene = this.sceneManager.getScene(chapter.startScene)
    if (!scene) return

    this.initChapterState(chapter)
    this.ended = false
    this.isInitialScene = false
    this.goToScene(scene)
  }

  private initChapterState(chapter: { defaultVariables?: Record<string, number> }) {
    if (chapter.defaultVariables) {
      for (const [key, val] of Object.entries(chapter.defaultVariables)) {
        this.stateManager.variables[key] = val
      }
    }
    this.stateManager.flags = new Set()
    this.stateManager.history = []
  }

  startAtScene(chapterId: string, sceneId: string) {
    const chapter = this.sceneManager.getChapter(chapterId)
    if (!chapter) return

    const scene = this.sceneManager.getScene(sceneId)
    if (!scene) return

    this.initChapterState(chapter)
    this.ended = false
    this.isInitialScene = false
    this.goToScene(scene)
  }

  resumeScene(sceneId: string, savedState: { variables: Record<string, number>; flags: string[]; history: any[] }) {
    this.stateManager.variables = { ...savedState.variables }
    this.stateManager.flags = new Set(savedState.flags)
    this.stateManager.history = [...savedState.history]

    const scene = this.sceneManager.getScene(sceneId)
    if (!scene) {
      this.endGame()
      return
    }

    this.ended = false
    this.isInitialScene = false
    this.goToScene(scene)
  }

  destroy() {
    this.qteSystem.destroy()
    this.audioSystem.destroy()
    this.videoManager.detach()
    this.events.clear()
  }
}
