// Tamagotchi model: long-lived, persisted, slow-moving state (separate
// from the per-frame MascotRuntime in behavior.ts). Backed by a runes
// `$state` at module scope, mutators exported as functions, debounced
// localStorage persistence mirroring stores/windows.ts' 300ms cadence.
//
// Persistence schema lives at localStorage['oikos-mascot'] and is
// versioned via the `version` field; `migrate(raw)` is the stub where
// future schema changes go (v1 has no migrations to perform).
//
// Multi-tab races (two tabs both writing 'oikos-mascot') are
// last-writer-wins — accepted for v1, not solved. A future pass could
// listen to the `storage` event if it becomes a real problem.

import type { MascotStage } from './types'

const STORAGE_KEY = 'oikos-mascot'
const PERSIST_DEBOUNCE_MS = 300

export interface MascotModel {
  version: 1
  stage: MascotStage
  name: string | null
  /** Binary egg-hatch flag: 0 until first naming, 1 after. The egg → chick transition fires on naming, not on a timer. */
  hatchProgress: number
  /** 0..100, slow decay, boosted by pet/feed. */
  happiness: number
  /** Chick -> adult growth hook; reactions like `eureka` grant xp. */
  xp: number
  /** epoch ms when the egg hatched (chick/adult), null while still an egg. */
  hatchedAt: number | null
  /** Persisted rest x position (surface-relative) so the mascot doesn't reset to center on reload. */
  lastPos: { x: number } | null
  /** epoch ms of the last foreground tick — for capping passive decay. */
  lastSeen: number
}

/** XP required to graduate from chick to adult. */
export const ADULT_XP = 200

function defaultModel(): MascotModel {
  return {
    version: 1,
    stage: 'egg',
    name: null,
    hatchProgress: 0,
    happiness: 50,
    xp: 0,
    hatchedAt: null,
    lastPos: null,
    lastSeen: Date.now()
  }
}

// Module-scoped rune. Mutators below mutate this in place (Object.assign
// / direct property writes); Svelte's reactivity tracks deep property
// access in components that read it. `const` because the binding itself
// is never reassigned — only its properties are.
const model: MascotModel = $state(defaultModel())

// ─── load / migrate / persist ────────────────────────────────────────────

function migrate(raw: unknown): MascotModel {
  // v1 has no migrations to perform; this stub documents where future
  // version-gated schema changes go (switch on `raw.version`).
  if (raw && typeof raw === 'object') {
    const r = raw as Partial<MascotModel>
    if (r.version === 1) {
      return { ...defaultModel(), ...r, version: 1 } as MascotModel
    }
  }
  return defaultModel()
}

function load(): MascotModel {
  if (typeof localStorage === 'undefined') return defaultModel()
  const raw = localStorage.getItem(STORAGE_KEY)
  if (!raw) return defaultModel()
  try {
    return migrate(JSON.parse(raw))
  } catch {
    return defaultModel()
  }
}

let persistTimer: ReturnType<typeof setTimeout> | null = null

function schedulePersist(): void {
  if (typeof localStorage === 'undefined') return
  if (persistTimer) clearTimeout(persistTimer)
  persistTimer = setTimeout(() => {
    persistTimer = null
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(model))
    } catch {
      // quota / privacy mode — swallow; the model still lives in memory for this session
    }
  }, PERSIST_DEBOUNCE_MS)
}

function flushPersist(): void {
  if (persistTimer) {
    clearTimeout(persistTimer)
    persistTimer = null
  }
  if (typeof localStorage !== 'undefined') {
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(model))
    } catch {
      // ignore
    }
  }
}

/** Initialize the module state from localStorage. Idempotent. Call once on app boot (or first mascot mount). */
export function initMascotState(): void {
  if (typeof localStorage === 'undefined') return
  const loaded = load()
  // Mutate the existing $state object in place — reassigning `model` to
  // a new $state() isn't allowed outside the top level in runes mode.
  Object.assign(model, loaded)
  model.lastSeen = Date.now()
  if (typeof window !== 'undefined') {
    window.addEventListener('beforeunload', flushPersist)
  }
}

// ─── accessors / mutators ────────────────────────────────────────────────

export function getModel(): MascotModel {
  return model
}

export function setStage(stage: MascotStage): void {
  model.stage = stage
  if (stage !== 'egg' && model.hatchedAt === null) {
    model.hatchedAt = Date.now()
  }
  if (stage === 'adult') {
    model.xp = Math.max(model.xp, ADULT_XP)
  }
  schedulePersist()
}

export function setName(name: string): void {
  model.name = name.slice(0, 24)
  schedulePersist()
}

export function grantXp(n: number): void {
  if (n === 0) return
  model.xp = Math.max(0, model.xp + n)
  schedulePersist()
}

export function feed(): void {
  model.happiness = Math.min(100, model.happiness + 8)
  grantXp(2)
}

export function pet(): void {
  model.happiness = Math.min(100, model.happiness + 4)
  grantXp(1)
}

export function setLastPos(x: number): void {
  model.lastPos = { x }
  schedulePersist()
}

export function resetModel(): void {
  Object.assign(model, defaultModel())
  schedulePersist()
}

/**
 * Advance lifecycle state. Called ~1x/sec from Mascot.svelte's loop (NOT
 * every frame). The egg → chick transition is NOT timed here — it fires
 * once, on first naming (see MascotLayer's name-dialog submit handler,
 * which calls forceHatch() after setName). This tick only handles slow
 * passive happiness decay for hatched stages.
 */
export function tickLifecycle(dtMs: number): void {
  const dtSec = dtMs / 1000
  if (model.stage === 'chick') {
    // Slow passive happiness decay (1/sec) so the tamagotchi benefits from
    // being interacted with; only meaningful while the model is alive.
    model.happiness = Math.max(0, model.happiness - 0.05 * dtSec)
  }
  model.lastSeen = Date.now()
  advanceStageIfReady()
}

/** Promote egg -> chick when hatchProgress hits 1 (set by forceHatch on first naming), chick -> adult when xp hits ADULT_XP. */
export function advanceStageIfReady(): void {
  if (model.stage === 'egg' && model.hatchProgress >= 1) {
    setStage('chick')
  } else if (model.stage === 'chick' && model.xp >= ADULT_XP) {
    setStage('adult')
  }
}

/** Hatches the egg immediately. Called from MascotLayer's name-dialog submit handler after the first naming, and from the Debug radial-menu action. */
export function forceHatch(): void {
  if (model.stage === 'egg') {
    model.hatchProgress = 1
    setStage('chick')
  }
}
