/**
 * desktop/src/main.ts — the Electron entry point (glue only).
 *
 * Wires the desktop shell together: starts the embedded server (which hosts the
 * unchanged frontend), opens the window against http://127.0.0.1:<port>, installs
 * the app menu + tray + login item, registers the terminalapp:// deep link with a
 * single-instance lock, and runs a background poller that turns session-status
 * transitions into native notifications (the desktop app's core value over a
 * browser tab). All real logic lives in the pure modules consumed below; this
 * file owns only Electron lifecycle and dependency injection.
 *
 * __dirname is available because esbuild emits this as a CJS bundle (build.mjs).
 */
import { app, BrowserWindow, Menu, Notification, Tray } from 'electron'
import path from 'node:path'

import { createLogger } from './logger.js'
import { createSettingsStore } from './settings-store.js'
import { startEmbeddedServer } from './embedded-server.js'
import { computeNotifications } from './notifications.js'
import { mapLiveSessions } from './live-poll.js'
import { parseDeepLink, deepLinkToPath, DEEP_LINK_PROTOCOL } from './deep-link.js'
import { createMainWindow } from './window.js'
import { buildAppMenu } from './menu.js'
import { createTray } from './tray.js'
import type { NotifyOptions } from './notify-policy.js'
import type { EmbeddedServer, SessionStatusSnapshot } from './types.js'

/** Live-session poll cadence — cheap loopback GET, so a few seconds is plenty. */
const POLL_INTERVAL_MS = 4000
/** IPC channel used to hand a deep-link path to the (optional) preload bridge. */
const DEEP_LINK_CHANNEL = 'deep-link'
/** Preload bundle name, emitted next to this file by esbuild. */
const PRELOAD_FILE = 'preload.cjs'
/** Tray icon location relative to the build/ output dir (shipped under assets/). */
const TRAY_ICON_SUBPATH = path.join('..', 'assets', 'trayTemplate.png')

const logger = createLogger('main')
const settings = createSettingsStore(app.getPath('userData'), process.platform, logger)

let mainWindow: BrowserWindow | null = null
let tray: Tray | null = null
let server: EmbeddedServer | null = null
let pollTimer: NodeJS.Timeout | null = null
let isQuitting = false
let isShuttingDown = false
// Previous snapshot map, keyed by sessionId; replaced (never mutated) each tick.
let prevSnapshots: ReadonlyMap<string, SessionStatusSnapshot> = new Map()
// Deep link captured before window/server were ready (cold start); dispatched once
// onReady finishes. macOS 'open-url' can fire pre-ready; Win/Linux pass it in argv.
let pendingDeepLink: string | null = null
// The first successful poll seeds the baseline WITHOUT notifying, so launching with
// pre-existing waiting/stuck sessions (e.g. openAtLogin, window unfocused) does not
// burst-notify for transitions that happened before the app started.
let pollPrimed = false

function errorMessage(err: unknown): string {
  return err instanceof Error ? err.message : String(err)
}

/** Bring the window to the foreground (restoring/showing if hidden or minimized). */
function focusWindow(): void {
  if (!mainWindow) return
  if (mainWindow.isMinimized()) mainWindow.restore()
  mainWindow.show()
  mainWindow.focus()
}

/** Find the first terminalapp:// argument in a process argv list. */
function findDeepLinkArg(argv: readonly string[]): string | null {
  const prefix = `${DEEP_LINK_PROTOCOL}://`
  return argv.find((arg) => arg.startsWith(prefix)) ?? null
}

/**
 * Apply a deep link: validate it, then navigate the window same-origin to
 * /?join=<id> (the mechanism the unchanged frontend actually understands). The
 * IPC send is a best-effort cooperative hook for any future in-page handler.
 */
function dispatchDeepLink(url: string): void {
  const link = parseDeepLink(url)
  if (!link) {
    logger.warn(`ignoring malformed deep link: ${url}`)
    return
  }
  if (!mainWindow || !server) {
    // Cold start: window/server not up yet — buffer for onReady to dispatch.
    pendingDeepLink = url
    return
  }

  const targetPath = deepLinkToPath(link)
  focusWindow()
  mainWindow.webContents.send(DEEP_LINK_CHANNEL, targetPath)
  void mainWindow.loadURL(`http://127.0.0.1:${server.port}${targetPath}`)
}

/** Hide-to-tray on window close (keep the server alive) unless we're quitting. */
function installWindowCloseGuard(win: BrowserWindow): void {
  win.on('close', (event) => {
    if (isQuitting) return
    event.preventDefault()
    win.hide()
  })
  win.on('closed', () => {
    mainWindow = null
  })
}

async function pollOnce(base: string): Promise<void> {
  // Read prefs live each tick so notify toggles take effect without a restart
  // (a settings UI wiring settings.set() lands in P2). NOTE: /live-sessions
  // carries status only, not approval state, so mapLiveSessions fixes
  // pendingApproval=false — the notifyOnApproval branch is driven by the hook
  // status bus (P2/D8), not this poll; status→'waiting' is the current proxy.
  const prefs = settings.get()
  try {
    const res = await fetch(`${base}/live-sessions`)
    if (!res.ok) {
      logger.warn(`live-sessions poll returned ${res.status}`)
      return
    }
    const json: unknown = await res.json()
    const snapshots = mapLiveSessions(json)
    const opts: NotifyOptions = {
      windowFocused: mainWindow?.isFocused() ?? false,
      notifyOnApproval: prefs.notifyOnApproval,
      notifyOnStatusChange: prefs.notifyOnStatusChange,
    }
    const { decisions, next } = computeNotifications(prevSnapshots, snapshots, opts)
    if (pollPrimed) {
      for (const decision of decisions) {
        new Notification({ title: decision.title, body: decision.body }).show()
      }
    } else {
      // Seed the baseline silently on the first successful poll (no burst).
      pollPrimed = true
    }
    prevSnapshots = next
  } catch (err: unknown) {
    // A poll failure (server warming up, transient fetch error) must never crash.
    logger.warn(`live-sessions poll failed: ${errorMessage(err)}`)
  }
}

function startPoller(base: string): void {
  pollTimer = setInterval(() => {
    void pollOnce(base)
  }, POLL_INTERVAL_MS)
}

async function onReady(): Promise<void> {
  const prefs = settings.get()
  server = await startEmbeddedServer({ prefs, logger })
  const base = `http://127.0.0.1:${server.port}`

  mainWindow = createMainWindow(`${base}/`, path.join(__dirname, PRELOAD_FILE))
  installWindowCloseGuard(mainWindow)

  Menu.setApplicationMenu(buildAppMenu())
  try {
    tray = createTray(path.join(__dirname, TRAY_ICON_SUBPATH), {
      show: () => focusWindow(),
      quit: () => {
        isQuitting = true
        app.quit()
      },
    })
  } catch (err: unknown) {
    // A missing/invalid tray icon (cosmetic) must not take down a working app.
    logger.warn(`tray unavailable (continuing without it): ${errorMessage(err)}`)
  }

  // Only register auto-launch when the user enabled it. Calling this on an
  // unsigned / non-standard-location build emits a native "Operation not permitted"
  // line, so skip it in the default (off) case to keep the log clean.
  if (prefs.openAtLogin) {
    try {
      app.setLoginItemSettings({ openAtLogin: true })
    } catch (err: unknown) {
      logger.warn(`could not set login item: ${errorMessage(err)}`)
    }
  }
  startPoller(base)

  // Cold-start deep link: macOS may have buffered an early 'open-url'; Windows/
  // Linux deliver it in argv. Dispatch now that the window + server exist.
  const coldLink = pendingDeepLink ?? findDeepLinkArg(process.argv)
  if (coldLink) {
    pendingDeepLink = null
    dispatchDeepLink(coldLink)
  }

  logger.info(`desktop ready on ${base} (bind ${server.bindHost})`)
}

/** Graceful teardown: stop the poller, close the server, then hard-exit. */
async function shutdown(): Promise<void> {
  if (pollTimer) {
    clearInterval(pollTimer)
    pollTimer = null
  }
  try {
    if (server) await server.close()
  } catch (err: unknown) {
    logger.error('error while closing embedded server', err)
  } finally {
    tray?.destroy()
    app.exit(0)
  }
}

if (!app.requestSingleInstanceLock()) {
  // Another instance owns the lock; it will receive our argv via 'second-instance'.
  app.quit()
} else {
  app.setAsDefaultProtocolClient(DEEP_LINK_PROTOCOL)

  app.on('second-instance', (_event, argv) => {
    focusWindow()
    const url = findDeepLinkArg(argv)
    if (url) dispatchDeepLink(url)
  })

  // macOS delivers deep links via 'open-url', not argv.
  app.on('open-url', (_event, url) => {
    dispatchDeepLink(url)
  })

  app.on('activate', () => {
    focusWindow()
  })

  // Tray keeps the app (and embedded server) alive after the last window closes,
  // so remote devices stay connected. Intentionally does not quit on any platform.
  app.on('window-all-closed', () => {})

  app.on('before-quit', (event) => {
    isQuitting = true
    if (isShuttingDown) return
    isShuttingDown = true
    // Take over quit so the async server.close() actually completes before exit.
    event.preventDefault()
    void shutdown()
  })

  app.whenReady().then(onReady).catch((err: unknown) => {
    logger.error('failed to start desktop app', err)
    app.quit()
  })
}
