// Preview mode — serves a composed deck over HTTP with SSE-based live reload.
import { createServer } from 'node:http'
import { readFileSync, existsSync, mkdirSync, rmSync, watch } from 'node:fs'
import { spawn } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { join, extname, resolve, relative, dirname } from 'node:path'
import { composeItem } from './item.js'
import { composeSchedule } from './schedule.js'
import { loadDeckMeta } from './config.js'
import { resolveSlug } from './resolve.js'

const MIME = {
  '.html':  'text/html; charset=utf-8',
  '.css':   'text/css',
  '.js':    'text/javascript',
  '.svg':   'image/svg+xml',
  '.png':   'image/png',
  '.jpg':   'image/jpeg',
  '.jpeg':  'image/jpeg',
  '.webp':  'image/webp',
  '.gif':   'image/gif',
  '.woff2': 'font/woff2',
}

const RELOAD_SCRIPT = `<script>(function(){` +
  `var es=new EventSource('/~events');` +
  `es.onmessage=function(){location.reload()};` +
  `})()</script>`

function injectReloadScript(html) {
  const idx = html.lastIndexOf('</body>')
  if (idx === -1) return html + RELOAD_SCRIPT
  return html.slice(0, idx) + RELOAD_SCRIPT + html.slice(idx)
}

function startServer(dir) {
  const clients = new Set()

  const server = createServer((req, res) => {
    const pathname = new URL(req.url, 'http://x').pathname

    if (pathname === '/~events') {
      res.writeHead(200, {
        'Content-Type':  'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection':    'keep-alive',
      })
      res.flushHeaders()
      clients.add(res)
      req.on('close', () => clients.delete(res))
      return
    }

    const file = join(dir, pathname === '/' ? 'index.html' : pathname)
    if (!existsSync(file)) {
      res.writeHead(404)
      return res.end('Not found')
    }

    const ext = extname(file)
    const body = readFileSync(file)
    const contentType = MIME[ext] ?? 'application/octet-stream'
    res.writeHead(200, { 'Content-Type': contentType })

    if (ext === '.html') {
      return res.end(injectReloadScript(body.toString('utf8')))
    }
    res.end(body)
  })

  return {
    server,
    notify: () => { for (const c of clients) c.write('data: reload\n\n') },
  }
}

const CHROME_PATHS = {
  darwin: [
    '/Applications/Vivaldi.app/Contents/MacOS/Vivaldi',
    '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
    '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
    '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
    '/Applications/Chromium.app/Contents/MacOS/Chromium',
  ],
  linux:  ['/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser'],
  win32:  [
    'C:/Program Files/Google/Chrome/Application/chrome.exe',
    'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
  ],
}

function openBrowser(url, { root, onExit = () => {} } = {}) {
  const chromeBin = (CHROME_PATHS[process.platform] ?? []).find(p => existsSync(p))

  if (!chromeBin) {
    const cmd = process.platform === 'darwin' ? 'open'
      : process.platform === 'win32' ? 'cmd' : 'xdg-open'
    const args = process.platform === 'win32' ? ['/c', 'start', url] : [url]
    spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref()
    return
  }

  mkdirSync(join(root, 'tmp'), { recursive: true })
  const profile = join(root, `tmp/preview-${randomUUID()}`)
  mkdirSync(profile)

  const proc = spawn(chromeBin, [
    '--no-first-run', '--no-default-browser-check',
    `--user-data-dir=${profile}`,
    `--app=${url}`,
  ], { stdio: 'ignore' })

  function cleanup() {
    try { proc.kill() } catch {}
    try { rmSync(profile, { recursive: true, force: true }) } catch {}
    onExit()
  }

  process.on('SIGINT',  () => { cleanup(); process.exit(0) })
  process.on('SIGTERM', () => { cleanup(); process.exit(0) })
  proc.on('close', cleanup)
}

export async function previewItem(slug, { root = process.cwd(), open = true } = {}) {
  const resolved = resolveSlug(slug, { root })
  const srcDir = dirname(resolved.path)
  const rel = relative(resolve(root, 'content'), srcDir)
  const destDir = resolve(root, 'decks', rel)

  const code = await composeItem(slug, [], { root })
  if (code !== 0) throw new Error(`initial compose failed (exit ${code})`)

  const { server, notify } = startServer(destDir)
  await new Promise(res => server.listen(0, res))
  const { port } = server.address()

  console.log(`\nPreview: http://localhost:${port}/\n`)

  let debounce
  const watcher = watch(resolved.path, () => {
    clearTimeout(debounce)
    debounce = setTimeout(async () => {
      try {
        await composeItem(slug, [], { root })
        notify()
      } catch (err) {
        console.error(`\nRecompose failed: ${err.message}\n`)
      }
    }, 150)
  })

  if (open) openBrowser(`http://localhost:${port}/`, { root, onExit: () => { watcher.close(); server.close() } })

  return {
    port,
    close: () => { watcher.close(); server.close() },
  }
}

export async function previewSchedule(slug, { root = process.cwd(), draft: isDraft = false, open = true } = {}) {
  const destDir = resolve(root, `decks/schedules/${slug}`)
  const scheduleDir = resolve(root, `content/schedules/${slug}`)
  const deckMeta = await loadDeckMeta(root)

  // Watchers are rebuilt on every recompose so that added/removed topics
  // are automatically picked up without restarting the process.
  let watchers = []
  function closeWatchers() { for (const w of watchers) w.close(); watchers = [] }

  function openWatchers(topicDirs, onChange) {
    const dirs = [scheduleDir, ...topicDirs]
    watchers = dirs.map(dir => watch(dir, { recursive: true }, onChange))
  }

  async function recompose() {
    closeWatchers()
    const { status, topicDirs } = await composeSchedule(slug, [], { root, draft: isDraft, deckMeta })
    if (status !== 0) { console.error(`\nRecompose failed (exit ${status})\n`); return }
    openWatchers(topicDirs, handler)
    notify()
  }

  let debounce
  function handler() {
    clearTimeout(debounce)
    debounce = setTimeout(async () => {
      try { await recompose() }
      catch (err) { console.error(`\nRecompose failed: ${err.message}\n`) }
    }, 150)
  }

  // Initial compose + watch setup
  const { status, topicDirs } = await composeSchedule(slug, [], { root, draft: isDraft, deckMeta })
  if (status !== 0) throw new Error(`initial compose failed (exit ${status})`)

  const { server, notify } = startServer(destDir)
  await new Promise(res => server.listen(0, res))
  const { port } = server.address()

  openWatchers(topicDirs, handler)

  console.log(`\nPreview: http://localhost:${port}/\n`)
  if (open) openBrowser(`http://localhost:${port}/`, { root, onExit: () => { closeWatchers(); server.close() } })

  return {
    port,
    close: () => { closeWatchers(); server.close() },
  }
}
