import { APPS_DIR, allApps, emit, registerApp, removeApp, restartApp, startApp } from '$apps'
import { computeHash, generateManifest } from '../sync'
import { loadGitignore } from '@gitignore'
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, unlinkSync, watch, writeFileSync } from 'fs'
import { dirname, join } from 'path'
import { Hype } from '@because/hype'

interface FileChangeEvent {
  hash?: string
  path: string
  type: 'change' | 'delete'
}

function safePath(base: string, ...parts: string[]): string | null {
  const canonicalBase = existsSync(base) ? realpathSync(base) : base
  const resolved = join(canonicalBase, ...parts)

  if (!resolved.startsWith(canonicalBase + '/') && resolved !== canonicalBase) {
    return null
  }

  return resolved
}

const router = Hype.router()

router.get('/apps', c => c.json(allApps().map(a => a.name)))

router.get('/apps/:app/manifest', c => {
  const appName = c.req.param('app')
  if (!appName) return c.json({ error: 'App not found' }, 404)

  const appPath = join(APPS_DIR, appName)

  const safeAppPath = safePath(APPS_DIR, appName)
  if (!safeAppPath) return c.json({ error: 'Invalid path' }, 400)
  if (!existsSync(join(appPath, 'package.json'))) return c.json({ error: 'App not found' }, 404)

  const manifest = generateManifest(appPath, appName)
  return c.json(manifest)
})

router.get('/apps/:app/files/:path{.+}', c => {
  const appName = c.req.param('app')
  const filePath = c.req.param('path')

  if (!appName || !filePath) return c.json({ error: 'Invalid path' }, 400)

  const basePath = join(APPS_DIR, appName)

  const fullPath = safePath(basePath, filePath)
  if (!fullPath) return c.json({ error: 'Invalid path' }, 400)
  if (!existsSync(fullPath)) return c.json({ error: 'File not found' }, 404)

  const content = readFileSync(fullPath)
  return new Response(content, {
    headers: { 'Content-Type': 'application/octet-stream' },
  })
})

router.put('/apps/:app/files/:path{.+}', async c => {
  const appName = c.req.param('app')
  const filePath = c.req.param('path')

  if (!appName || !filePath) return c.json({ error: 'Invalid path' }, 400)

  const basePath = join(APPS_DIR, appName)

  const fullPath = safePath(basePath, filePath)
  if (!fullPath) return c.json({ error: 'Invalid path' }, 400)

  const dir = dirname(fullPath)

  // Ensure directory exists
  if (!existsSync(dir)) {
    mkdirSync(dir, { recursive: true })
  }

  const body = await c.req.arrayBuffer()
  writeFileSync(fullPath, new Uint8Array(body))

  return c.json({ ok: true })
})

router.delete('/apps/:app', c => {
  const appName = c.req.param('app')
  if (!appName) return c.json({ error: 'App not found' }, 404)

  const appPath = safePath(APPS_DIR, appName)
  if (!appPath) return c.json({ error: 'Invalid path' }, 400)
  if (!existsSync(appPath)) return c.json({ error: 'App not found' }, 404)

  removeApp(appName)
  rmSync(appPath, { recursive: true })
  return c.json({ ok: true })
})

router.delete('/apps/:app/files/:path{.+}', c => {
  const appName = c.req.param('app')
  const filePath = c.req.param('path')

  if (!appName || !filePath) return c.json({ error: 'Invalid path' }, 400)

  const basePath = join(APPS_DIR, appName)

  const fullPath = safePath(basePath, filePath)
  if (!fullPath) return c.json({ error: 'Invalid path' }, 400)
  if (!existsSync(fullPath)) return c.json({ error: 'File not found' }, 404)

  unlinkSync(fullPath)
  return c.json({ ok: true })
})

router.post('/apps/:app/reload', async c => {
  const appName = c.req.param('app')
  if (!appName) return c.json({ error: 'App name required' }, 400)

  emit({ type: 'app:reload', app: appName })

  // Register new app or restart existing
  const app = allApps().find(a => a.name === appName)
  if (!app) {
    // New app - register it
    registerApp(appName)
  } else if (app.state === 'running') {
    // Existing app - restart it
    try {
      await restartApp(appName)
    } catch (e) {
      return c.json({ error: `Failed to restart app: ${e instanceof Error ? e.message : String(e)}` }, 500)
    }
  } else if (app.state === 'stopped' || app.state === 'invalid') {
    // App not running - try to start it
    startApp(appName)
  }

  return c.json({ ok: true })
})

router.sse('/apps/:app/watch', (send, c) => {
  const appName = c.req.param('app')

  const appPath = join(APPS_DIR, appName)

  const safeAppPath = safePath(APPS_DIR, appName)
  if (!safeAppPath || !existsSync(appPath)) return

  const gitignore = loadGitignore(appPath)
  let debounceTimer: Timer | null = null
  const pendingChanges = new Map<string, 'change' | 'delete'>()

  const watcher = watch(appPath, { recursive: true }, (_event, filename) => {
    if (!filename || gitignore.shouldExclude(filename)) return

    const fullPath = join(appPath, filename)
    const type = existsSync(fullPath) ? 'change' : 'delete'
    pendingChanges.set(filename, type)

    if (debounceTimer) clearTimeout(debounceTimer)
    debounceTimer = setTimeout(() => {
      for (const [path, changeType] of pendingChanges) {
        const evt: FileChangeEvent = { type: changeType, path }
        if (changeType === 'change') {
          try {
            const content = readFileSync(join(appPath, path))
            evt.hash = computeHash(content)
          } catch {
            continue // File was deleted between check and read
          }
        }
        send(evt)
      }
      pendingChanges.clear()
    }, 100)
  })

  return () => {
    if (debounceTimer) clearTimeout(debounceTimer)
    watcher.close()
  }
})

export default router
