import { computed, ref, watch } from 'vue'
import { eventBus } from '@/common/eventBus'
import { stepRegistry, type StepDef } from './steps'

export type AnchorSource = HTMLElement | (() => HTMLElement | null) | null

export interface ActiveStep {
  def: StepDef
  anchor: AnchorSource
}

export const tutorialEnabled = ref(true)
export const tutorialProgress = ref<Record<string, boolean>>({})
export const tutorialReady = ref(false)
export const activeStep = ref<ActiveStep | null>(null)
export const isTutorialActive = computed(() => activeStep.value !== null)
export const sessionSkipped = ref(false)
/** When a modal is open, this holds the step ID that should fire via the `?` button. */
export const modalTutorialStepId = ref<string | null>(null)
/** Hide the floating help button while modal dialogs are open. */
export const helpButtonHidden = ref(false)

export function setHelpButtonHidden(hidden: boolean) {
  helpButtonHidden.value = hidden
}

const queue: ActiveStep[] = []
let hydrated = false

/** Steps that were locally cleared for replay. Server hydration must not restore them. */
const locallyCleared = new Set<string>()

function applyDevOverrides() {
  if (typeof window === 'undefined') return
  if (!import.meta.env.DEV) return
  const params = new URLSearchParams(window.location.search)
  if (params.get('tutorial') === 'off') {
    tutorialEnabled.value = false
  }
  if (params.get('tutorial') === 'reset') {
    void resetAllProgress()
  }
}

export function hydrateFromProfile(profile: {
  tutorial_enabled?: boolean
  tutorial_progress?: Record<string, boolean>
}) {
  if (typeof profile.tutorial_enabled === 'boolean') {
    tutorialEnabled.value = profile.tutorial_enabled
  }
  if (profile.tutorial_progress && typeof profile.tutorial_progress === 'object') {
    // Merge server state with local state. Locally-cleared steps must NOT be
    // restored by the server (prevents replay chains from breaking mid-flight
    // when a PROFILE_UPDATED SSE arrives).
    const merged = { ...profile.tutorial_progress }
    for (const id of locallyCleared) {
      delete merged[id]
    }
    // Overlay any local optimistic seen flags
    for (const [key, val] of Object.entries(tutorialProgress.value)) {
      if (val) merged[key] = true
    }
    tutorialProgress.value = merged
  }
  tutorialReady.value = true
  if (!hydrated) {
    hydrated = true
    applyDevOverrides()
  }
  // Re-evaluate queue: any step now-seen should be dropped.
  for (let i = queue.length - 1; i >= 0; i--) {
    const entry = queue[i]
    if (entry && !shouldShowStep(entry.def.id)) queue.splice(i, 1)
  }
  if (activeStep.value && !shouldShowStep(activeStep.value.def.id)) {
    activeStep.value = null
    promote()
  }
}

export function shouldShowStep(id: string, ignoreEnabled = false): boolean {
  if (!ignoreEnabled && !tutorialEnabled.value && !locallyCleared.has(id)) return false
  if (sessionSkipped.value) return false
  if (tutorialProgress.value[id]) return false
  return true
}

function resolveAnchor(anchor: AnchorSource): HTMLElement | null {
  if (!anchor) return null
  if (typeof anchor === 'function') return anchor()
  return anchor
}

function promote() {
  if (activeStep.value) return
  while (queue.length > 0) {
    const next = queue.shift()!
    if (!shouldShowStep(next.def.id)) continue
    // If anchor is missing, try the registry fallback selector.
    if (next.anchor === null && next.def.anchorSelector) {
      const sel = next.def.anchorSelector
      next.anchor = () => document.querySelector(sel) as HTMLElement | null
    }
    if (next.anchor !== null) {
      const el = resolveAnchor(next.anchor)
      if (!el) continue
    }
    activeStep.value = next
    return
  }
}

/**
 * Queue a step to be shown. Idempotent: if already seen or session-skipped, no-op.
 * If anchor is provided but the element is missing at promotion time, the step is dropped.
 * If anchor is null, the step renders as a centered modal-style card.
 */
export function maybeShow(
  id: string,
  anchor: AnchorSource = null,
  force = false,
) {
  if (!stepRegistry[id]) {
    if (import.meta.env.DEV) console.warn(`[tutorial] Unknown step id: ${id}`)
    return
  }
  if (!shouldShowStep(id, force)) return
  if (activeStep.value?.def.id === id) return
  if (queue.some((q) => q.def.id === id)) return
  queue.push({ def: stepRegistry[id], anchor })
  promote()
}

export function dismissActive(markSeen = true) {
  const current = activeStep.value
  if (!current) return
  activeStep.value = null
  if (markSeen) {
    // Steps cleared for manual replay (e.g. via the ? button) are ephemeral:
    // track them locally so the chain can continue, but don't persist to the
    // server. Forced intro / JIT auto-steps still hit the server.
    if (locallyCleared.has(current.def.id)) {
      tutorialProgress.value = { ...tutorialProgress.value, [current.def.id]: true }
    } else {
      void markStepSeen(current.def.id)
    }
    if (current.def.next) {
      const nextDef = stepRegistry[current.def.next]
      if (nextDef && shouldShowStep(nextDef.id)) {
        queue.unshift({ def: nextDef, anchor: null })
      }
    }
  }
  // Chain finished or dismissed: remove this step from the ephemeral guard
  // so future auto-triggers can persist normally.
  locallyCleared.delete(current.def.id)
  promote()
}

export function skipSession() {
  const current = activeStep.value
  sessionSkipped.value = true
  queue.length = 0
  activeStep.value = null
  if (current) {
    if (locallyCleared.has(current.def.id)) {
      tutorialProgress.value = { ...tutorialProgress.value, [current.def.id]: true }
      locallyCleared.delete(current.def.id)
    } else {
      void markStepSeen(current.def.id)
    }
  }
}

export async function markStepSeen(id: string): Promise<void> {
  // Optimistic — local first, then persist.
  if (tutorialProgress.value[id]) return
  tutorialProgress.value = { ...tutorialProgress.value, [id]: true }
  locallyCleared.delete(id)
  try {
    await fetch('/api/user/tutorial-progress', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ step_id: id, seen: true }),
    })
  } catch (e) {
    if (import.meta.env.DEV) console.warn('[tutorial] Failed to persist step', id, e)
  }
}

/** Clear local "seen" state for a chain of steps so it can be replayed.
 *  Marks each cleared step so server hydration won't restore it mid-replay. */
export function clearChainProgress(startId: string) {
  let id: string | undefined = startId
  while (id) {
    if (tutorialProgress.value[id]) {
      const progress = { ...tutorialProgress.value }
      delete progress[id]
      tutorialProgress.value = progress
    }
    locallyCleared.add(id)
    id = stepRegistry[id]?.next
  }
}

export async function resetAllProgress(): Promise<void> {
  tutorialProgress.value = {}
  sessionSkipped.value = false
  try {
    await fetch('/api/user/tutorial-progress', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ reset: true }),
    })
  } catch (e) {
    if (import.meta.env.DEV) console.warn('[tutorial] Failed to reset progress', e)
  }
}

export async function setTutorialEnabled(on: boolean): Promise<void> {
  tutorialEnabled.value = on
  if (!on) {
    queue.length = 0
    activeStep.value = null
  }
  try {
    await fetch('/api/user/tutorial-progress', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ enabled: on }),
    })
  } catch (e) {
    if (import.meta.env.DEV) console.warn('[tutorial] Failed to set enabled', e)
  }
}

let profileUpdatedBound = false
export function bindProfileUpdatedListener(refetch: () => Promise<void> | void) {
  if (profileUpdatedBound) return
  profileUpdatedBound = true
  eventBus.on('profile_updated', () => {
    void refetch()
  })
}

// When the active step is dismissed externally (e.g. anchor unmounts), drain the queue.
watch(activeStep, (val) => {
  if (val === null) promote()
})
