// The app registry — single source of truth for what shows up as a desktop
// icon and what opens in its window.
//
// Two layers:
//   - **Built-in apps** (always installed): the static `builtinApps` array
//     below. These ship with the build and can't be removed.
//   - **Installed apps** (operator-installed from the App Store): persisted
//     manifest ids in localStorage, re-resolved against the catalog at
//     load time. `installApp`/`uninstallApp` mutate this set.
//
// The public surface is reactive: `apps` is a derived store (built-in +
// installed) and `appById` is a derived Map. Consumers (Desktop.svelte,
// DockedLayer.svelte, Taskbar.svelte, icons.ts, windows.ts) subscribe or
// use `get()` for synchronous lookups. This is what lets an installed app
// appear on the desktop the moment it's registered, with no reload.
//
// App components are loaded lazily (`component: () => Promise<{ default:
// Component }>` — a dynamic-import loader). Desktop icons render from
// metadata alone; the chunk fetches on first window open, and Vite
// code-splits each app into its own chunk. See
// docs/mbse/components.md §9 for the full contract.
import type { Component } from 'svelte'
import { writable, derived, get, type Readable } from 'svelte/store'
import type { DashboardSummary } from '$lib/api'
import { openSignalCount } from '$lib/stores/context'
import {
  catalogById,
  type AppManifest,
  type AppPermission,
  type CatalogEntry
} from '$lib/app-store/catalog'
import ListTodoIcon from '@lucide/svelte/icons/list-todo'
import BoxesIcon from '@lucide/svelte/icons/boxes'
import ShieldCheckIcon from '@lucide/svelte/icons/shield-check'
import SirenIcon from '@lucide/svelte/icons/siren'
import SearchIcon from '@lucide/svelte/icons/search'
import TrendingUpIcon from '@lucide/svelte/icons/trending-up'
import SettingsIcon from '@lucide/svelte/icons/settings'
import EggIcon from '@lucide/svelte/icons/egg'
import StoreIcon from '@lucide/svelte/icons/store'
import Share2Icon from '@lucide/svelte/icons/share-2'
import ChartNetworkIcon from '@lucide/svelte/icons/chart-network'

export type { AppManifest, AppPermission }

// Two app kinds, picked by one flag:
//   - Windowed (default): renders in a wmkit floating window. Geometry
//     (width/height/min*) is required.
//   - Docked (docked: true): renders on the Docked Layer above the window
//     layer, with no window chrome and no taskbar button. Clicking its
//     desktop icon toggles visibility (see stores/docked.ts) rather than
//     opening a window. Geometry is forbidden — there is no window to size.
// Apps receive no props from the shell; they import the OS-service surface
// ($lib/stores/windows, $lib/stores/context, $lib/api, ...) directly. See
// docs/mbse/components.md §9 for the stable surface contract.
export interface AppDef {
  id: string
  title: string
  icon: Component
  // Dynamic-import loader. Invoked when an app window opens (windowed) or
  // when the Docked Layer first mounts the app (docked). Vite's module cache
  // makes the second open cheap (promise resolves from cache). The resolved
  // module is a standard Svelte module namespace — `mod.default` is the
  // component; LazyApp.svelte unwraps it.
  component: () => Promise<{ default: Component }>
  docked?: boolean
  noIcon?: boolean
  width?: number
  height?: number
  minWidth?: number
  minHeight?: number
  badge?: (summary: DashboardSummary | null) => number
  // Source — 'builtin' (always installed) or 'installed' (from the App
  // Store). Used by the App Store UI to distinguish uninstallable apps from
  // built-ins.
  source: 'builtin' | 'installed'
}

// Built-in apps — always installed, can't be removed. All components use
// dynamic-import loaders so apps.ts stays out of the page module graph at
// import time (Phase 2 code-splitting: each page is its own chunk, the
// main bundle stays small). The mascot uses the same path — deferring its
// module graph also breaks what would otherwise be a static cycle through
// icons.ts back to APPS.
export const builtinApps: AppDef[] = [
  {
    id: 'tasks',
    title: 'Tasks',
    icon: ListTodoIcon,
    component: () => import('../pages/Overview.svelte'),
    width: 960,
    height: 680,
    minWidth: 480,
    minHeight: 420,
    source: 'builtin'
  },
  {
    // id stays 'kb' so persisted window geometry / desktop-icon position /
    // the 'oikos-kb-view' preference survive the rename to "Fleet".
    id: 'kb',
    title: 'Fleet',
    icon: BoxesIcon,
    component: () => import('../pages/KnowledgeBase.svelte'),
    width: 1000,
    height: 700,
    minWidth: 520,
    minHeight: 420,
    source: 'builtin'
  },
  {
    id: 'ops',
    title: 'Operations',
    icon: ShieldCheckIcon,
    component: () => import('../pages/Ops.svelte'),
    width: 860,
    height: 620,
    minWidth: 480,
    minHeight: 360,
    badge: (s) => s?.approvals_pending ?? 0,
    source: 'builtin'
  },
  {
    id: 'signals',
    title: 'Signals',
    icon: SirenIcon,
    component: () => import('../pages/Signals.svelte'),
    width: 860,
    height: 620,
    minWidth: 480,
    minHeight: 360,
    badge: (s) => openSignalCount(s),
    source: 'builtin'
  },
  {
    id: 'knowledge',
    title: 'Knowledge',
    icon: SearchIcon,
    component: () => import('../pages/Knowledge.svelte'),
    width: 800,
    height: 600,
    minWidth: 440,
    minHeight: 340,
    source: 'builtin'
  },
  {
    id: 'learning',
    title: 'Learning',
    icon: TrendingUpIcon,
    component: () => import('../pages/Learning.svelte'),
    width: 800,
    height: 600,
    minWidth: 440,
    minHeight: 340,
    source: 'builtin'
  },
  {
    id: 'settings',
    title: 'Settings',
    icon: SettingsIcon,
    component: () => import('../pages/Settings.svelte'),
    width: 640,
    height: 480,
    minWidth: 480,
    minHeight: 360,
    source: 'builtin'
  },
  {
    id: 'app-store',
    title: 'App Store',
    icon: StoreIcon,
    component: () => import('../pages/AppStore.svelte'),
    width: 720,
    height: 560,
    minWidth: 480,
    minHeight: 360,
    source: 'builtin'
  },
  {
    id: 'mascot',
    title: 'Cluck',
    icon: EggIcon,
    component: () => import('./mascot/MascotLayer.svelte'),
    docked: true,
    source: 'builtin'
  },
  {
    id: 'entity-graph',
    title: 'Entity Graph',
    icon: ChartNetworkIcon,
    component: () => import('../pages/EntityGraph.svelte'),
    width: 1100,
    height: 750,
    minWidth: 640,
    minHeight: 420,
    source: 'builtin'
  }
]

// --- Installed (operator-installed from the App Store) ---------------------

const INSTALLED_KEY = 'oikos-installed-apps'

function loadInstalled(): string[] {
  if (typeof localStorage === 'undefined') return []
  try {
    const raw = localStorage.getItem(INSTALLED_KEY)
    if (!raw) return []
    const ids = JSON.parse(raw) as string[]
    // Drop ids that no longer resolve to a catalog entry (the app was
    // removed from the catalog in a later build) so they don't linger as
    // phantom desktop icons.
    return ids.filter((id) => catalogById.has(id))
  } catch {
    return []
  }
}

// Persisted as the list of catalog manifest ids the operator has installed.
const installedIds = writable<string[]>(loadInstalled())

// Readable view for components (App Store UI) that need to re-render on
// install/uninstall. Mutations go through installApp/uninstallApp.
export const installedAppIds: Readable<string[]> = { subscribe: installedIds.subscribe }

function persist(ids: string[]): void {
  if (typeof localStorage === 'undefined') return
  localStorage.setItem(INSTALLED_KEY, JSON.stringify(ids))
}
installedIds.subscribe(persist)

function catalogEntryToAppDef(entry: CatalogEntry): AppDef {
  const m = entry.manifest
  return {
    id: m.id,
    title: m.title,
    icon: entry.icon,
    component: entry.load,
    docked: m.docked,
    noIcon: m.noIcon,
    width: m.width,
    height: m.height,
    minWidth: m.minWidth,
    minHeight: m.minHeight,
    source: 'installed'
  }
}

// The full app set: built-ins + installed catalog apps. Reactive so an
// install/uninstall is reflected on the desktop immediately, with no reload.
export const apps: Readable<AppDef[]> = derived(installedIds, (ids) => {
  const installed = ids
    .map((id) => catalogById.get(id))
    .filter((e): e is CatalogEntry => !!e)
    .map(catalogEntryToAppDef)
  return [...builtinApps, ...installed]
})

export const appById: Readable<Map<string, AppDef>> = derived(
  apps,
  (list) => new Map(list.map((a) => [a.id, a]))
)

// Install/uninstall. Idempotent — installing an already-installed app or
// uninstalling a not-installed one is a no-op. Uninstalling a built-in is
// refused (built-ins can't be removed).
export function installApp(manifestId: string): void {
  if (!catalogById.has(manifestId)) return
  installedIds.update((ids) => (ids.includes(manifestId) ? ids : [...ids, manifestId]))
}

export function uninstallApp(manifestId: string): void {
  installedIds.update((ids) => ids.filter((id) => id !== manifestId))
}

export function isInstalled(manifestId: string): boolean {
  return get(installedIds).includes(manifestId)
}

// --- Window-id helpers (unchanged from the static-registry era) -----------

// Window ids are namespaced so WindowLayer.svelte can tell at a glance which
// content branch owns an id: `app:<id>` for registry apps, `session:<id>`
// for task chat windows (see windows.ts), anything else is an entity slug.
const APP_PREFIX = 'app:'

export function appWindowId(id: string): string {
  return `${APP_PREFIX}${id}`
}

export function appIdFromWindowId(windowId: string): string | null {
  return windowId.startsWith(APP_PREFIX) ? windowId.slice(APP_PREFIX.length) : null
}
