/**
 * Theme engine — load / save / apply pipeline.
 *
 * Public API:
 *   loadTheme()              read theme from localStorage (with legacy migration)
 *   saveTheme(theme)         persist + apply
 *   applyTheme(theme)        write CSS variables + data-* attrs to :root
 *   getTheme()               sync read of in-memory cache
 *   setThemePartial(patch)   merge + persist + apply
 *   setThemeFull(theme)      replace + persist + apply
 *   findActivePresetId(t)    match a Theme to a preset id (or 'custom')
 *   resetTheme()             reset to THEME_DEFAULT
 *   bootstrapTheme()         explicit start-up call (apply persisted theme +
 *                            install a MutationObserver for legacy data-theme
 *                            sync). Idempotent — safe to call multiple times.
 *
 * The engine emits a `cm:theme-change` CustomEvent on `document` on every
 * apply. Components subscribe to that event to refresh their state.
 *
 * SSR-safe: every DOM access is guarded by `typeof document !== 'undefined'`,
 * so importing this module in a server build does nothing observable.
 *
 * @countermeasure-platform/web-components/services/theme-engine
 */

import { setTheme as setUiTheme } from '../../ui'
import type { ResolvedTheme } from '../../ui'

import { ACCENT_PALETTES, COLOR_MODES } from './palettes'
import { FONT_STACKS, ensureGoogleFont } from './fonts'
import { ICON_SETS } from './icons'
import { THEME_PRESETS, defaultTheme } from './presets'
import type { ColorMode, Theme } from './types'

export const THEME_LS_KEY = 'cm-theme-v1'

const LEGACY_GLASS_KEY = 'preview:glass-level'
const LEGACY_DENSITY_KEY = 'preview:density'

// ---------------------------------------------------------------------------
// Persistence (with one-shot legacy migration)
// ---------------------------------------------------------------------------

function readLegacyMigration(base: Theme): Theme {
  let migrated = base
  try {
    const glassRaw = localStorage.getItem(LEGACY_GLASS_KEY)
    if (glassRaw !== null) {
      const n = Number(glassRaw)
      if (Number.isFinite(n)) {
        migrated = { ...migrated, transparency: Math.max(0, Math.min(0.2, n)) }
      }
    }
  } catch {
    /* ignore */
  }
  try {
    const density = localStorage.getItem(LEGACY_DENSITY_KEY)
    if (density === 'compact' || density === 'comfortable' || density === 'cozy') {
      migrated = { ...migrated, density }
    }
  } catch {
    /* ignore */
  }
  return migrated
}

export function loadTheme(): Theme {
  if (typeof localStorage === 'undefined') return defaultTheme()
  try {
    const raw = localStorage.getItem(THEME_LS_KEY)
    if (!raw) {
      // First-load migration: try to honor any legacy preview keys.
      return readLegacyMigration(defaultTheme())
    }
    const parsed = JSON.parse(raw) as Partial<Theme> | null
    return { ...defaultTheme(), ...(parsed ?? {}) }
  } catch {
    return defaultTheme()
  }
}

export function saveTheme(theme: Theme): void {
  try {
    if (typeof localStorage !== 'undefined') {
      localStorage.setItem(THEME_LS_KEY, JSON.stringify(theme))
    }
  } catch {
    /* ignore */
  }
  applyTheme(theme)
}

// ---------------------------------------------------------------------------
// Apply pipeline
// ---------------------------------------------------------------------------

function clamp(value: number, min: number, max: number): number {
  if (!Number.isFinite(value)) return min
  return Math.max(min, Math.min(max, value))
}

/**
 * Resolve a `Theme` into CSS variables + `data-*` attributes on `:root`.
 *
 * Returns silently in non-DOM environments (SSR-safe). Always emits a
 * `cm:theme-change` CustomEvent on `document` after applying.
 */
export function applyTheme(theme: Theme): void {
  if (typeof document === 'undefined') return
  const root = document.documentElement
  const mode = COLOR_MODES[theme.mode] ?? COLOR_MODES.dark
  const accent = ACCENT_PALETTES[theme.accent] ?? ACCENT_PALETTES.orange
  const font = FONT_STACKS[theme.font] ?? FONT_STACKS.inter
  const set = ICON_SETS[theme.iconSet] ?? ICON_SETS.lucide

  // 1) data-theme — surface palette via tokens.css
  const previous = root.getAttribute('data-theme')
  const next = mode.id as ResolvedTheme
  if (previous !== next) {
    setUiTheme(next)
  } else {
    root.setAttribute('data-theme', next)
  }

  // 2) data-accent — emit the per-accent var block from tokens.css
  if (theme.accent === 'orange') {
    root.removeAttribute('data-accent')
  } else {
    root.setAttribute('data-accent', theme.accent)
  }

  // 3) data-density
  root.setAttribute('data-density', theme.density)

  // 4) Glass translucency derived from `transparency` (0..0.30)
  const t = clamp(Number(theme.transparency) || 0, 0, 0.3)
  const tint = mode.surfaceTint
  root.style.setProperty('--bg-glass', `rgba(${tint}, ${(t * 0.5).toFixed(4)})`)
  root.style.setProperty('--bg-glass-2', `rgba(${tint}, ${t.toFixed(4)})`)
  root.style.setProperty('--bg-glass-3', `rgba(${tint}, ${(t * 1.75).toFixed(4)})`)

  // 5) Brand / accent
  root.style.setProperty('--brand', accent.brand)
  root.style.setProperty('--brand-hot', accent.hot)
  root.style.setProperty('--brand-soft', accent.soft)
  root.style.setProperty('--accent-rgb', accent.rgb)

  // 6) Radius
  const r = clamp(Number(theme.radius) || 0, 0, 32)
  root.style.setProperty('--radius-sm', `${r * 0.5}px`)
  root.style.setProperty('--radius-md', `${r}px`)
  root.style.setProperty('--radius-lg', `${r * 1.5}px`)
  root.style.setProperty('--radius-xl', `${r * 2}px`)

  // 7) Density row height
  const rowHeight = theme.density === 'compact' ? 32 : theme.density === 'cozy' ? 42 : 36
  root.style.setProperty('--density-row', `${rowHeight}px`)

  // 8) Typography (lazy-load Google fonts, then write CSS vars)
  ensureGoogleFont(font.google)
  root.style.setProperty('--font-display', font.display)
  root.style.setProperty('--font-mono', font.mono)

  // 9) Icon set hooks
  root.setAttribute('data-icon-set', theme.iconSet)
  root.style.setProperty('--icon-stroke-width', String(set.strokeWidth))
  root.style.setProperty('--icon-linecap', set.linecap)
  root.style.setProperty('--icon-linejoin', set.linejoin)

  // Persist legacy keys for back-compat (existing components keep working).
  try {
    if (typeof localStorage !== 'undefined') {
      localStorage.setItem(LEGACY_GLASS_KEY, String(theme.transparency))
      localStorage.setItem(LEGACY_DENSITY_KEY, theme.density)
    }
  } catch {
    /* ignore */
  }

  // Broadcast
  document.dispatchEvent(new CustomEvent('cm:theme-change', { detail: theme }))
}

// ---------------------------------------------------------------------------
// In-memory current state cache
// ---------------------------------------------------------------------------

let cached: Theme | null = null

export function getTheme(): Theme {
  if (!cached) {
    cached = loadTheme()
  }
  return cached
}

export function setThemePartial(patch: Partial<Theme>): Theme {
  const next = { ...getTheme(), ...patch }
  cached = next
  saveTheme(next)
  return next
}

export function setThemeFull(theme: Theme): Theme {
  cached = theme
  saveTheme(theme)
  return theme
}

export function findActivePresetId(theme: Theme): string {
  for (const preset of THEME_PRESETS) {
    const a = { ...preset.theme, iconOverrides: {} }
    const b = { ...theme, iconOverrides: {} }
    if (JSON.stringify(a) === JSON.stringify(b)) return preset.id
  }
  return 'custom'
}

export function resetTheme(): Theme {
  return setThemeFull(defaultTheme())
}

// ---------------------------------------------------------------------------
// Bootstrap (explicit, idempotent)
// ---------------------------------------------------------------------------

let bootstrapped = false
let observer: MutationObserver | null = null

/**
 * Apply the persisted theme to `:root` and install a MutationObserver that
 * keeps the in-memory cache in sync if some other code path flips
 * `data-theme` directly (e.g. legacy `ThemeSwitcher` / URL handlers).
 *
 * Must be called explicitly by the host app — the engine no longer
 * auto-applies on import so it does not clobber a host's existing theme.
 *
 * Idempotent — calling it twice is a no-op.
 */
export function bootstrapTheme(): void {
  if (typeof document === 'undefined') return
  if (bootstrapped) return
  bootstrapped = true

  try {
    applyTheme(getTheme())
  } catch {
    /* ignore — likely test env */
  }

  try {
    observer = new MutationObserver(mutations => {
      for (const m of mutations) {
        if (m.type !== 'attributes') continue
        if (m.attributeName === 'data-theme') {
          const current = document.documentElement.getAttribute('data-theme')
          if (
            (current === 'dark' ||
              current === 'light' ||
              current === 'monokai' ||
              current === 'glass') &&
            cached &&
            cached.mode !== current
          ) {
            cached = { ...cached, mode: current as ColorMode }
            // Re-broadcast so listeners (preview tile, switchers) refresh
            document.dispatchEvent(new CustomEvent('cm:theme-change', { detail: cached }))
          }
        }
      }
    })
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['data-theme'],
    })
  } catch {
    /* ignore */
  }
}

/**
 * Tear down the bootstrap-installed observer. Mostly useful for tests.
 */
export function teardownTheme(): void {
  if (observer) {
    observer.disconnect()
    observer = null
  }
  bootstrapped = false
}

/** @internal Reset the in-memory cache. Mostly useful for tests. */
export function _resetCache(): void {
  cached = null
}
