<script lang="ts">
  // MascotLayer: a pointer-events-none absolute inset-0 overlay at z-45
  // (above WindowLayer's z-40, below the desktop context menu's z-50
  // and the radial menu's z-[60]). Hosts the Mascot sprite, the
  // RadialMenu, the NameDialog, and the speech/name bubble. Owns the
  // MascotRuntime and the surface bounds; attaches the stimulus bus on
  // mount so the mascot reacts to chat/activity/events.
  //
  // Insertion point: rendered inside Desktop.svelte's surface <div>
  // (the `relative min-h-0 flex-1 overflow-hidden` element), after
  // <WindowLayer />, so its `absolute inset-0` shares the surface's
  // coordinate space and its ground line lands at the surface's bottom
  // edge (= the taskbar's top edge).

  import { onMount } from 'svelte'
  import Mascot from './Mascot.svelte'
  import MascotMenu from './MascotMenu.svelte'
  import NameDialog from './NameDialog.svelte'
  import * as ContextMenu from '$lib/components/ui/context-menu'
  import { MASCOT_ACTIONS } from './actions'
  import { attachStimuli } from './stimuli'
  import {
    initMascotState,
    getModel,
    setName as modelSetName,
    forceHatch as modelForceHatch,
    setStage,
    resetModel
  } from '$lib/mascot/state.svelte'
  import { forceBehavior } from '$lib/mascot/behavior'
  import type { MascotRuntime, AnimName, BehaviorId, MascotStage } from '$lib/mascot/types'

  let host = $state<HTMLDivElement | null>(null)

  // The runtime is created here (fresh per mount) and seeded from the
  // persisted model's lastPos + the live surface bounds. It's a `$state`
  // so mutations to runtime.x/y/behavior/etc. from the FSM are tracked
  // by the `$derived` position expressions in Mascot.svelte — a plain
  // `let` would update internally but never re-render the canvas.
  let runtime: MascotRuntime = $state({
    x: 100,
    y: 100,
    vx: 0,
    vy: 0,
    facing: 1,
    behavior: 'egg',
    behaviorUntil: Number.POSITIVE_INFINITY,
    anim: 'egg-idle',
    animStart: 0,
    reactAnim: null,
    bounds: { w: 800, h: 600 },
    groundY: 600,
    bubbleText: null,
    bubbleUntil: 0,
    blinkUntil: 0,
    fallPhaseAt: 0,
    flapCycleMs: 0,
    squashAt: 0,
    impactVy: 0,
    bounceCount: 0,
    investigateOnLand: false,
    busyTalking: false
  })

  let nameDialogOpen = $state(false)
  let nameDialogMode = $state<'hatch' | 'rename'>('hatch')

  const model = $derived(getModel())

  function requestRename() {
    nameDialogMode = 'rename'
    nameDialogOpen = true
  }

  function forceHatch() {
    const wasUnnamed = getModel().name === null
    modelForceHatch()
    if (runtime.behavior === 'egg') forceBehavior(runtime, 'idle')
    // If the egg was unnamed (e.g. debug force-hatch before the name
    // dialog was submitted), open the name dialog so the chick gets a
    // name — matching the normal hatch-on-naming flow.
    if (wasUnnamed) {
      nameDialogMode = 'hatch'
      nameDialogOpen = true
    }
  }

  function forceStageFn(s: MascotStage) {
    setStage(s)
    forceBehavior(runtime, 'idle')
  }

  function reset() {
    resetModel()
    forceBehavior(runtime, 'egg')
  }

  function refresh() {
    // No-op. The old manual menu needed a nudge to re-run its visibility
    // filter; the shadcn context-menu content is reactive off ctx.model
    // (a $state object), so action mutations (happiness, stage, ...) are
    // reflected live while the menu is open. Kept on actionCtx so leaf
    // action signatures stay stable.
  }

  function onPet() {
    // Plain-click pet: briefly show a happy bubble. The actual model.pet()
    // call already happened in Mascot.svelte.
    runtime.bubbleText = '❤️'
    runtime.bubbleUntil = performance.now() + 1500
  }

  function onNameSubmit(name: string) {
    modelSetName(name)
    if (nameDialogMode === 'hatch') {
      // Naming the egg is what hatches it — no timed incubation.
      forceHatch()
    }
    nameDialogOpen = false
  }

  // Stage transitions are driven by the name-dialog submit handler
  // (egg → chick on first naming) and the debug menu (force hatch /
  // force stage), not by observing model.stage here. No $effect needed.

  onMount(() => {
    initMascotState()
    // Seed runtime from persisted lastPos once we know the surface size.
    const rect = host?.getBoundingClientRect()
    if (rect) {
      runtime.bounds = { w: rect.width, h: rect.height }
      runtime.groundY = rect.height
      const lp = getModel().lastPos
      runtime.x = lp ? Math.max(24, Math.min(rect.width - 24, lp.x)) : rect.width / 2
      runtime.y = rect.height // ground
      runtime.anim = getModel().stage === 'egg' ? 'egg-idle' : 'idle'
      runtime.behavior = getModel().stage === 'egg' ? 'egg' : 'idle'
    }
    // First-run: a fresh egg with no name prompts for naming, which
    // hatches it. Returning users with a named mascot skip this.
    if (getModel().stage === 'egg' && getModel().name === null) {
      nameDialogMode = 'hatch'
      nameDialogOpen = true
    }
    // Attach the stimulus bus (chat/activity/events -> reactions + the
    // continuous "busy" state). Both are gated to non-egg stages: the egg
    // isn't "alive" yet (no name, no hatched chick to react), so stimulus
    // events are silently dropped until the egg hatches. This keeps the
    // egg calm during the naming dialog rather than playing alarm
    // animations behind it.
    const detach = attachStimuli(
      (reaction, bubbleOverride) => {
        if (getModel().stage === 'egg') return
        // Dragging always wins — never let a reaction interrupt an active
        // drag (previously it could visually flash a reaction animation
        // mid-drag, even though position tracking stayed correct; see the
        // physics audit's Finding 5). Sleep is only interrupted by
        // reactions that explicitly opt in via interruptsSleep.
        if (runtime.behavior === 'dragged') return
        if (runtime.behavior === 'sleep' && !reaction.interruptsSleep) return
        // Reaction dispatch: respect priority + cooldown (handled in stimuli.ts);
        // here we just force the behavior.
        const anim = reaction.anim as AnimName
        const id: BehaviorId = 'react'
        runtime.reactAnim = anim
        forceBehavior(runtime, id, { anim, durationMs: reaction.durationMs })
        const bubble = bubbleOverride ?? reaction.bubble
        if (bubble) {
          runtime.bubbleText = bubble
          runtime.bubbleUntil = performance.now() + reaction.durationMs
        }
        if (reaction.effect) reaction.effect()
      },
      (phase) => {
        // Continuous engagement with the focused task's active turn — see
        // stimuli.ts. Not a timed pulse: stays in `busy` until stimuli.ts
        // reports the turn ended (phase === null), same drag/sleep gating
        // as reactions above. Also never preempts an in-flight reaction
        // pulse (eureka/alarmed/happy) — those are short and should play
        // out; the next chat update re-affirms busy shortly after (the
        // underlying store keeps emitting throughout an active turn), so
        // this self-heals within a token or two rather than needing an
        // explicit "resume busy after this pulse" handoff.
        if (getModel().stage === 'egg') return
        if (runtime.behavior === 'dragged' || runtime.behavior === 'react') return
        if (phase === null) {
          if (runtime.behavior === 'busy') forceBehavior(runtime, 'idle')
          return
        }
        if (runtime.behavior === 'sleep') return
        runtime.busyTalking = phase === 'talking'
        if (runtime.behavior !== 'busy') forceBehavior(runtime, 'busy')
      }
    )
    return () => detach()
  })

  // Action context handed to RadialMenu leaf handlers.
  const actionCtx = $derived({
    model,
    runtime,
    force: (id: BehaviorId, opts?: { anim?: AnimName; durationMs?: number }) =>
      forceBehavior(runtime, id, opts),
    requestRename,
    forceHatch,
    forceStage: forceStageFn,
    reset,
    refresh
  })
</script>

<div bind:this={host} class="pointer-events-none absolute inset-0 z-[45]">
  <ContextMenu.Root>
    <!-- The trigger wraps the mascot sprite; right-click on the canvas
         bubbles up to it and bits-ui opens the menu content (portaled to
         <body>, so it's fully interactive despite this layer's
         pointer-events-none root — the old RadialMenu was non-interactive
         because it rendered inside this root without re-enabling
         pointer-events). Left-click drag/pet passes through untouched. -->
    <ContextMenu.Trigger>
      <Mascot
        bind:runtime
        {onPet}
        onRequestName={() => {
          nameDialogMode = 'hatch'
          nameDialogOpen = true
        }}
      />
    </ContextMenu.Trigger>
    <ContextMenu.Content class="min-w-56 max-w-72">
      <MascotMenu actions={MASCOT_ACTIONS} ctx={actionCtx} />
    </ContextMenu.Content>
  </ContextMenu.Root>
</div>

{#if nameDialogOpen}
  <NameDialog
    mode={nameDialogMode}
    initial={model.name ?? ''}
    onSubmit={onNameSubmit}
    onCancel={() => (nameDialogOpen = false)}
  />
{/if}
