import { useCallback, useEffect, useRef, useState } from 'react'
import type { SaveStatus } from '../../hooks/useAutoSave'
import {
  BEDTIME,
  BREAKS,
  ENCOURAGEMENTS,
  ERRORS,
  GREETING,
  MILESTONES,
  TIPS,
  WELCOME_BACK,
  milestoneLine,
  pick,
  type Line,
} from './tips'
import { analyzeProse } from './prose'
import { playPop, playSound, type SoundName } from '../../audio/sounds'
import { isBedtime } from '../../lib/night'

// The kitten's expression. Maps to a Lottie animation when assets are present,
// otherwise to an emoji placeholder (see PetalCompanion).
export type Mood = 'idle' | 'happy' | 'talking' | 'sleeping' | 'celebrate'

export type BubbleTone = 'cheer' | 'tip' | 'break' | 'error' | 'bedtime'
export interface Bubble extends Line {
  tone: BubbleTone
}

interface Signals {
  wordCount: number
  saveStatus: SaveStatus
  // True while the writing-assist LLM is unreachable (timeout / down). A
  // false→true flip earns a gentle "haiya".
  llmDown: boolean
  // Monotonic counters bumped by the app on each editor change / accepted
  // suggestion — lets the companion react without a full event bus.
  editTick: number
  acceptTick: number
  // The document's plain text, used by the rules-based prose checker to offer
  // context-aware writing notes instead of only generic tips.
  text: string
}

// Timing knobs (ms). Tuned to feel present but never naggy.
const IDLE_MS = 75_000 // no edits → kitten naps
const BREAK_MS = 25 * 60_000 // continuous writing → suggest a break
const TIP_MIN_GAP = 4 * 60_000 // at most one spontaneous tip per this window
const PROACTIVE_GAP = 40_000 // floor between any two unsolicited bubbles
const BEDTIME_GAP = 30 * 60_000 // at most one "go to bed" nudge per this window
// The late-night window itself (isBedtime) lives in ../../lib/night so the
// companion nag and the night-mode theme/starfall share one definition.
// How long a bubble lingers. These are *floors* — readBubbleMs extends them by
// how much there is to read, since she reads both the Mandarin and the English
// (and tips now quote a slice of her own sentence, so they run longer).
const BUBBLE_MS = 14_000 // baseline for a tip / break
const CHEER_MS = 9_000 // a short cheer still needs a beat in two languages
const READ_MS_PER_CHAR = 55 // ~18 chars/sec, generous for a bilingual ESL read
const MAX_BUBBLE_MS = 32_000 // cap so a long quote can't pin the bubble forever
const HOVER_GRACE_MS = 3_000 // lingers this long after she stops hovering
const now = () => Date.now()

// Reading time for a bubble: a base floor by tone, stretched by the combined
// length of the Mandarin + English lines so denser advice stays up long enough
// to actually finish reading.
function readBubbleMs(b: Bubble): number {
  const base = b.tone === 'cheer' ? CHEER_MS : b.tone === 'bedtime' ? BUBBLE_MS + 4_000 : BUBBLE_MS
  const chars = b.zh.length + b.en.length
  return Math.min(MAX_BUBBLE_MS, base + chars * READ_MS_PER_CHAR)
}

// useCompanion is the behavior engine: it watches writing signals and decides
// when the kitten speaks, what mood it shows, and how to pace itself so the
// companion feels alive without interrupting. UI-agnostic — returns state only.
export function useCompanion({ wordCount, saveStatus, llmDown, editTick, acceptTick, text }: Signals) {
  const [mood, setMood] = useState<Mood>('idle')
  const [bubble, setBubble] = useState<Bubble | null>(null)

  const lastActivity = useRef(now())
  const sessionStart = useRef(now())
  const lastProactive = useRef(0)
  const lastTip = useRef(0)
  const lastBreak = useRef(0)
  const lastBedtime = useRef(0)
  const nextMilestone = useRef(0) // index into MILESTONES
  const sleeping = useRef(false)

  // Latest text for the prose checker, read lazily by the heartbeat (kept in a
  // ref so per-keystroke changes don't re-arm the interval).
  const textRef = useRef(text)
  textRef.current = text
  // Recently-surfaced hint ids, so the same untouched sentence isn't re-flagged
  // each cadence. A small ring — old ids age out and may resurface later.
  const recentHints = useRef<string[]>([])
  const lastRule = useRef<string>('')

  const bubbleTimer = useRef<ReturnType<typeof setTimeout>>(undefined)
  const moodTimer = useRef<ReturnType<typeof setTimeout>>(undefined)

  // Show a bubble + talking mood, then settle back. `proactive` messages respect
  // the spacing floor; user-triggered ones (cheers) always go through.
  const say = useCallback(
    (b: Bubble, opts?: { proactive?: boolean; celebrate?: boolean; sound?: SoundName }) => {
    const t = now()
    if (opts?.proactive) {
      if (bubble) return
      if (t - lastProactive.current < PROACTIVE_GAP) return
      lastProactive.current = t
    }
    sleeping.current = false
    // A sound to match the bubble: callers can name a specific one (the milestone
    // fanfare, the "haiya" on errors); everything else gets a rotating pop so the
    // same blip never repeats back to back.
    if (opts?.sound) playSound(opts.sound)
    else playPop()
    setBubble(b)
    setMood(opts?.celebrate ? 'celebrate' : 'talking')
    clearTimeout(bubbleTimer.current)
    clearTimeout(moodTimer.current)
    const dur = readBubbleMs(b)
    bubbleTimer.current = setTimeout(() => setBubble(null), dur)
    moodTimer.current = setTimeout(() => setMood('idle'), dur)
  }, [bubble])

  // Choose the next spontaneous tip. Prefer a real, context-aware note from the
  // rules-based prose checker (it's actionable and clearly about *her* writing);
  // fall back to a generic encouragement-style tip when the text reads clean or
  // every finding was shown recently. Avoids repeating a finding or the same
  // rule family back-to-back so the companion never feels like a broken record.
  const nextTip = useCallback((): Bubble => {
    const hints = analyzeProse(textRef.current)
    const fresh = hints.find(
      (h) => !recentHints.current.includes(h.id) && h.rule !== lastRule.current,
    )
    const hint = fresh ?? hints.find((h) => !recentHints.current.includes(h.id))
    if (hint) {
      lastRule.current = hint.rule
      recentHints.current = [hint.id, ...recentHints.current].slice(0, 8)
      return { zh: hint.zh, en: hint.en, tone: 'tip' }
    }
    return { ...pick(TIPS), tone: 'tip' }
  }, [])

  const dismiss = useCallback(() => {
    clearTimeout(bubbleTimer.current)
    clearTimeout(moodTimer.current)
    setBubble(null)
    setMood('idle')
  }, [])

  // Pause the auto-dismiss while she hovers a bubble — so a longer note never
  // vanishes mid-read. Releasing leaves it up for a short, fixed grace so it
  // doesn't linger forever once she looks away.
  const holdBubble = useCallback(() => {
    clearTimeout(bubbleTimer.current)
    clearTimeout(moodTimer.current)
  }, [])

  const releaseBubble = useCallback(() => {
    clearTimeout(bubbleTimer.current)
    clearTimeout(moodTimer.current)
    bubbleTimer.current = setTimeout(() => setBubble(null), HOVER_GRACE_MS)
    moodTimer.current = setTimeout(() => setMood('idle'), HOVER_GRACE_MS)
  }, [])

  // Opening hello (once), after a short beat so it doesn't race the first paint.
  useEffect(() => {
    const id = setTimeout(() => say({ ...GREETING, tone: 'tip' }), 1200)
    return () => clearTimeout(id)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  // Edits: record activity and wake from a nap with a warm welcome-back.
  const firstEdit = useRef(true)
  useEffect(() => {
    if (firstEdit.current) {
      firstEdit.current = false
      return
    }
    lastActivity.current = now()
    if (sleeping.current) {
      sleeping.current = false
      sessionStart.current = now() // a fresh stretch starts on return
      say({ ...WELCOME_BACK, tone: 'cheer' })
    } else if (mood === 'sleeping') {
      setMood('idle')
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [editTick])

  // Accepts: always cheer (it's a direct response to her action).
  const firstAccept = useRef(true)
  useEffect(() => {
    if (firstAccept.current) {
      firstAccept.current = false
      return
    }
    say({ ...pick(ENCOURAGEMENTS), tone: 'cheer' }, { celebrate: true })
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [acceptTick])

  // Word-count milestones: cheer the first time each threshold is crossed.
  useEffect(() => {
    while (
      nextMilestone.current < MILESTONES.length &&
      wordCount >= MILESTONES[nextMilestone.current]
    ) {
      const n = MILESTONES[nextMilestone.current]
      nextMilestone.current += 1
      // Skip silently if we're just loading a long doc (no edits yet).
      if (!firstEdit.current)
        say({ ...milestoneLine(n), tone: 'cheer' }, { celebrate: true, sound: 'milestone' })
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [wordCount])

  // Trouble — a save failed or the writing-assist LLM went unreachable. Play the
  // playful "haiya" and reassure her, but never spam: at most one per gap, and
  // only on the *transition* into trouble (not while it lingers).
  const lastError = useRef(0)
  const ERROR_GAP = 20_000
  const wasLlmDown = useRef(false)
  const haiya = useCallback(() => {
    const t = now()
    if (t - lastError.current < ERROR_GAP) {
      playSound('error') // still acknowledge it, just without a fresh bubble
      return
    }
    lastError.current = t
    say({ ...pick(ERRORS), tone: 'error' }, { sound: 'error' })
  }, [say])

  useEffect(() => {
    if (saveStatus === 'error') haiya()
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [saveStatus])

  useEffect(() => {
    if (llmDown && !wasLlmDown.current) haiya()
    wasLlmDown.current = llmDown
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [llmDown])

  // Occasional gentle cheer on a successful save (kept rare so it isn't noise).
  useEffect(() => {
    if (saveStatus === 'saved' && Math.random() < 0.18) {
      say({ ...pick(ENCOURAGEMENTS), tone: 'cheer' }, { proactive: true })
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [saveStatus])

  // Heartbeat: nap when idle, suggest breaks, and drop the odd writing tip.
  useEffect(() => {
    const id = setInterval(() => {
      const t = now()
      const idleFor = t - lastActivity.current

      if (idleFor > IDLE_MS) {
        sleeping.current = true
        if (!bubble) setMood('sleeping')
        return // resting — no nudges while she's away
      }

      // Active: time for a break?
      if (t - sessionStart.current > BREAK_MS && t - lastBreak.current > BREAK_MS) {
        lastBreak.current = t
        sessionStart.current = t
        say({ ...pick(BREAKS), tone: 'break' }, { proactive: true })
        return
      }

      // Burning the midnight oil? Gently suggest bed — caring, low-frequency,
      // and only while she's actually still at it (the idle branch above already
      // returned if she's away/napping).
      if (isBedtime() && t - lastBedtime.current > BEDTIME_GAP) {
        lastBedtime.current = t
        say({ ...pick(BEDTIME), tone: 'bedtime' }, { proactive: true })
        return
      }

      // Otherwise an occasional tip — context-aware when her text gives us
      // something concrete to gently point at, generic warmth otherwise.
      if (t - lastTip.current > TIP_MIN_GAP) {
        lastTip.current = t
        say(nextTip(), { proactive: true })
      }
    }, 10_000)
    return () => clearInterval(id)
  }, [bubble, say, nextTip])

  useEffect(
    () => () => {
      clearTimeout(bubbleTimer.current)
      clearTimeout(moodTimer.current)
    },
    [],
  )

  return { mood, bubble, dismiss, holdBubble, releaseBubble }
}
