// A hierarchical state machine over a plain context object — the one place a game keeps "what am I
// doing right now": an enemy brain, a weapon (ready → fire → cooldown → reload), a game mode
// (warmup → round → over), a door. Declarative: the whole machine is ONE table you can read top
// to bottom, log, and later show in an inspector; the machine knows nothing about aspects, nodes
// or the network, so it runs wherever its owner ticks it (an `updateFixed` for anything that must
// match another machine's, `update` for presentation).
//
//   const Guard = defineStates<Bot>({
//     initial: 'patrol',
//     on: { hit: 'flinch' },                                    // from ANY state
//     to: { dead: (b) => b.hp <= 0 },                           // checked in every state, first
//     states: {
//       patrol: { update: (b, dt) => b.walk(dt), to: { combat: (b) => b.target !== null } },
//       combat: {
//         initial: 'engage',
//         to: { search: (b) => b.lostFor > 4 },                 // leaves whichever child is active
//         states: {
//           engage: { update: (b) => b.approach(), to: { shoot: (b) => b.visible && b.dist < 12 } },
//           shoot:  { enter: (b) => b.fire = true, exit: (b) => b.fire = false, to: { engage: (b) => !b.visible } },
//         },
//       },
//       search: { after: [6, 'patrol'] },                       // a timed transition
//       flinch: { after: [0.2, '$back'] },                      // '$back' = the state we came from
//       dead:   { enter: (b) => b.die() },
//     },
//   })
//   const fsm = new StateMachine(Guard, bot)
//   fsm.step(dt)                    // once per tick: guards (outer states first), then updates
//   fsm.send('hit')                 // an event: the innermost state that handles it wins
//   fsm.state                       // 'combat.shoot'   fsm.is('combat') → true
//   fsm.on('change', (to, from) => console.log(`[fsm] ${from} → ${to}`))
//
// Semantics, in order, per `step(dt)`:
//   1. every active state's clock advances (`t` = seconds in that state);
//   2. transitions are tested OUTERMOST first — the root's `to` (any-state guards), then each
//      active state's `to` guards (first true wins) and its `after` timer; a taken transition is
//      re-tested from the new state (a pass-through state chains), at most 8 hops per step;
//   3. `update` runs outermost → innermost; an update that RETURNS a state name transitions
//      there and ends the pass (the states it entered update next step).
// A transition exits states innermost-first up to the common ancestor and enters the new branch
// outermost-first, descending into `initial` children; the target state's own name (or an
// ancestor's) re-enters it. Names resolve relative to the declaring state (siblings, then each
// ancestor's siblings), or as a dotted path from the root; unknown names throw when the machine is
// built, so a typo is a startup error, not a silent no-op. `send` looks innermost → outermost
// (the root's `on` = any state) and returns whether someone handled it.

export type StateGuard<C> = (ctx: C, t: number) => boolean
export type StateHandler<C> = (ctx: C, ...args: any[]) => string | void

export interface StateDef<C> {
  /** The child entered by default when this (compound) state is entered. */
  initial?: string
  /** Child states — this becomes a compound state; only a leaf is ever "the" state. */
  states?: Record<string, StateDef<C>>
  /** Entered (from = the previous leaf's path, '' at start). */
  enter?: (ctx: C, from: string) => void
  /** Left (to = the next leaf's path). */
  exit?: (ctx: C, to: string) => void
  /** Every step while active, outer states first; `t` = seconds in this state. Return a state name
   *  to transition. */
  update?: (ctx: C, dt: number, t: number) => string | void
  /** Guarded transitions, tested every step before `update` — key = target, first true wins. */
  to?: Record<string, StateGuard<C>>
  /** Leave for `target` after `seconds` in this state. */
  after?: [seconds: number, target: string]
  /** Event handlers for `send(name, …args)`: a target name, or a function returning one (or nothing). */
  on?: Record<string, string | StateHandler<C>>
}

export interface StatesDef<C> {
  initial: string
  states: Record<string, StateDef<C>>
  /** Any-state guards — tested first, in every state. */
  to?: Record<string, StateGuard<C>>
  /** Any-state event handlers — the fallback when no active state handles the event. */
  on?: Record<string, string | StateHandler<C>>
  /** Any-state update, runs before the active states' own. */
  update?: (ctx: C, dt: number, t: number) => string | void
}

export type StateMachineEvents = {
  /** After a transition completed (both are leaf paths; `from` = '' for the initial entry). */
  change: (to: string, from: string) => void
}

/** Identity helper — names the context type once so every callback is typed. */
export const defineStates = <C>(def: StatesDef<C>): StatesDef<C> => def

const BACK = "$back"
const MAX_HOPS = 8

interface SNode<C> {
  name: string
  path: string
  def: StateDef<C>
  parent: SNode<C> | null
  children: Map<string, SNode<C>>
  initial: SNode<C> | null
  depth: number
  /** seconds in this state (valid while active) */
  t: number
  /** resolved `to` targets in declaration order */
  guards: Array<[SNode<C>, StateGuard<C>]>
  after: [number, SNode<C> | typeof BACK] | null
  /** resolution cache for names returned at runtime */
  resolved: Map<string, SNode<C> | typeof BACK>
}

export class StateMachine<C> {
  readonly ctx: C
  private readonly root: SNode<C>
  private readonly byPath = new Map<string, SNode<C>>()
  /** the active branch, root's child (index 0) → leaf */
  private chain: SNode<C>[] = []
  private prev: SNode<C> | null = null
  private listeners: Array<(to: string, from: string) => void> = []
  private changed = 0

  constructor(def: StatesDef<C>, ctx: C) {
    this.ctx = ctx
    this.root = this.build("", "", def as StateDef<C>, null, 0)
    // second pass: resolve names (every state exists now)
    for (const n of this.byPath.values()) this.link(n)
    this.link(this.root)
    if (!this.root.initial) throw new Error("StateMachine: `initial` is required")
    this.enterBranch([], this.leafOf(this.root.initial), "")
  }

  /** The active leaf's path ('combat.shoot'). */
  get state(): string { return this.chain[this.chain.length - 1]?.path ?? "" }
  /** The leaf we came from ('' before the first transition). */
  get previous(): string { return this.prev?.path ?? "" }
  /** Seconds in the active leaf. */
  get time(): number { return this.chain[this.chain.length - 1]?.t ?? 0 }
  /** Every active state's path, outermost first. */
  get branch(): string[] { return this.chain.map((n) => n.path) }

  /** Is `name` (a state name or dotted path) active — as the leaf OR as one of its ancestors? */
  is(name: string): boolean {
    for (const n of this.chain) if (n.name === name || n.path === name) return true
    return false
  }
  /** Seconds in the named active state (0 when it is not active). */
  timeIn(name: string): number {
    for (const n of this.chain) if (n.name === name || n.path === name) return n.t
    return 0
  }

  /** Force a transition (a spawn, a reset from outside the table). */
  go(name: string): void {
    const leaf = this.chain[this.chain.length - 1] ?? this.root
    this.transition(this.resolve(leaf, name))
  }

  /** Deliver an event. Innermost active state first, the root's `on` last. True if handled. */
  send(event: string, ...args: unknown[]): boolean {
    for (let i = this.chain.length - 1; i >= 0; i--) {
      const n = this.chain[i]!
      const h = n.def.on?.[event]
      if (h === undefined) continue
      const target = typeof h === "string" ? h : h(this.ctx, ...args)
      if (typeof target === "string") this.transition(this.resolve(n, target))
      return true
    }
    const h = this.root.def.on?.[event]
    if (h === undefined) return false
    const target = typeof h === "string" ? h : h(this.ctx, ...args)
    if (typeof target === "string") this.transition(this.resolve(this.chain[this.chain.length - 1] ?? this.root, target))
    return true
  }

  /** One tick: clocks, guards (outer first, chained), then updates (outer first). */
  step(dt: number): void {
    for (const n of this.chain) n.t += dt
    this.root.t += dt
    for (let hop = 0; hop < MAX_HOPS; hop++) {
      const target = this.findTransition()
      if (target === null) break
      this.transition(target)
    }
    const mark = this.changed
    const r0 = this.root.def.update?.(this.ctx, dt, this.root.t)
    if (typeof r0 === "string") { this.transition(this.resolve(this.chain[this.chain.length - 1]!, r0)); return }
    if (this.changed !== mark) return
    const chain = this.chain.slice()
    for (const n of chain) {
      const r = n.def.update?.(this.ctx, dt, n.t)
      if (typeof r === "string") { this.transition(this.resolve(n, r)); return }
      if (this.changed !== mark) return   // an update transitioned through send()/go()
    }
  }

  /** Listen for transitions. */
  on(event: "change", fn: StateMachineEvents["change"]): this { this.listeners.push(fn); return this }
  off(event: "change", fn: StateMachineEvents["change"]): this {
    const i = this.listeners.indexOf(fn)
    if (i >= 0) this.listeners.splice(i, 1)
    return this
  }

  // ---- building ----
  private build(name: string, path: string, def: StateDef<C>, parent: SNode<C> | null, depth: number): SNode<C> {
    const node: SNode<C> = { name, path, def, parent, children: new Map(), initial: null, depth, t: 0, guards: [], after: null, resolved: new Map() }
    if (path) this.byPath.set(path, node)
    if (def.states) {
      for (const [childName, childDef] of Object.entries(def.states)) {
        if (childName.includes(".") || childName.startsWith("$")) throw new Error(`StateMachine: bad state name '${childName}'`)
        node.children.set(childName, this.build(childName, path ? `${path}.${childName}` : childName, childDef, node, depth + 1))
      }
      const initial = def.initial ?? Object.keys(def.states)[0]!
      const child = node.children.get(initial)
      if (!child) throw new Error(`StateMachine: '${path || "root"}' has no child '${initial}' for initial`)
      node.initial = child
    } else if (def.initial) {
      throw new Error(`StateMachine: '${path}' names an initial child but has no states`)
    }
    return node
  }

  private link(n: SNode<C>): void {
    if (n.def.to) for (const [target, guard] of Object.entries(n.def.to)) n.guards.push([this.resolveStrict(n, target), guard])
    if (n.def.after) n.after = [n.def.after[0], this.resolve(n, n.def.after[1])]
    if (n.def.on) for (const h of Object.values(n.def.on)) if (typeof h === "string") this.resolve(n, h)
  }

  private resolveStrict(from: SNode<C>, name: string): SNode<C> {
    const r = this.resolve(from, name)
    if (r === BACK) throw new Error(`StateMachine: '${BACK}' is not allowed as a guard target of '${from.path}'`)
    return r
  }

  /** Relative first (siblings of `from`, then of each ancestor), then a dotted path from the root. */
  private resolve(from: SNode<C>, name: string): SNode<C> | typeof BACK {
    const cached = from.resolved.get(name)
    if (cached) return cached
    let found: SNode<C> | typeof BACK | undefined
    if (name === BACK) found = BACK
    else {
      for (let n: SNode<C> | null = from; n && !found; n = n.parent) {
        const scope = n.parent ?? this.root
        found = scope.children.get(name)
        if (!found && n === from) found = n.children.get(name)   // a compound state naming its own child
      }
      if (!found) found = this.byPath.get(name)
    }
    if (!found) throw new Error(`StateMachine: unknown state '${name}' (from '${from.path || "root"}')`)
    from.resolved.set(name, found)
    return found
  }

  // ---- running ----
  /** Guards never re-enter a state that is already active (a `dead` guard staying true does not
   *  re-enter `dead` every step) — only an explicit `go()` / event does that. */
  private findTransition(): SNode<C> | typeof BACK | null {
    const ctx = this.ctx
    for (const [target, guard] of this.root.guards) if (!this.active(target) && guard(ctx, this.root.t)) return target
    for (const n of this.chain) {
      for (const [target, guard] of n.guards) if (!this.active(target) && guard(ctx, n.t)) return target
      if (n.after && n.t >= n.after[0] && (n.after[1] === BACK || !this.active(n.after[1]))) return n.after[1]
    }
    return null
  }

  private active(n: SNode<C>): boolean { return this.chain[n.depth - 1] === n }

  private leafOf(n: SNode<C>): SNode<C> {
    while (n.initial) n = n.initial
    return n
  }

  private transition(target: SNode<C> | typeof BACK): void {
    const current = this.chain[this.chain.length - 1] ?? null
    let node: SNode<C>
    if (target === BACK) {
      if (!this.prev) return
      node = this.prev
    } else node = target
    const leaf = this.leafOf(node)
    // the new branch (root excluded)
    const next: SNode<C>[] = []
    for (let n: SNode<C> | null = leaf; n && n !== this.root; n = n.parent) next.unshift(n)
    // common prefix; a target that is the current leaf or one of its ancestors re-enters itself
    let common = 0
    while (common < this.chain.length && common < next.length && this.chain[common] === next[common]) common++
    if (node !== this.root && common >= node.depth && this.chain[node.depth - 1] === node) common = node.depth - 1
    const from = current?.path ?? ""
    const to = leaf.path
    for (let i = this.chain.length - 1; i >= common; i--) this.chain[i]!.def.exit?.(this.ctx, to)
    this.prev = current
    this.enterBranch(this.chain.slice(0, common), leaf, from, next)
  }

  private enterBranch(kept: SNode<C>[], leaf: SNode<C>, from: string, next?: SNode<C>[]): void {
    if (!next) {
      next = []
      for (let n: SNode<C> | null = leaf; n && n !== this.root; n = n.parent) next.unshift(n)
    }
    this.chain = kept.slice()
    this.changed++
    for (let i = kept.length; i < next.length; i++) {
      const n = next[i]!
      n.t = 0
      this.chain.push(n)
      n.def.enter?.(this.ctx, from)
    }
    for (const fn of this.listeners.slice()) fn(leaf.path, from)
  }
}
