import { APPS_DIR, allApps, registerApp, removeApp, restartApp } from '$apps'
import { computeHash, generateManifest } from '../sync'
import { loadGitignore } from '@gitignore'
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, 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 {
  // Resolve base to canonical path (follows symlinks) if it exists
  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/versions', c => {
  const appName = c.req.param('app')
  if (!appName) return c.json({ error: 'App not found' }, 404)

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

  const currentLink = join(appDir, 'current')
  const currentVersion = existsSync(currentLink)
    ? realpathSync(currentLink).split('/').pop()
    : null

  const entries = readdirSync(appDir, { withFileTypes: true })
  const versions = entries
    .filter(e => e.isDirectory() && /^\d{8}-\d{6}$/.test(e.name))
    .map(e => e.name)
    .sort()
    .reverse() // Newest first

  return c.json({ versions, current: currentVersion })
})

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, 'current')

  const safeAppPath = safePath(APPS_DIR, appName)
  if (!safeAppPath) return c.json({ error: 'Invalid path' }, 400)
  if (!existsSync(appPath)) 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, 'current')

  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')
  const version = c.req.query('version') // Optional version parameter

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

  // Determine base path: specific version or current
  const basePath = version
    ? join(APPS_DIR, appName, version)
    : join(APPS_DIR, appName, 'current')

  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, 'current')

  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/deploy', c => {
  const appName = c.req.param('app')
  if (!appName) return c.json({ error: 'App name required' }, 400)

  const appDir = join(APPS_DIR, appName)

  // Generate timestamp: YYYYMMDD-HHMMSS format
  const now = new Date()
  const pad = (n: number) => String(n).padStart(2, '0')
  const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`

  const newVersion = join(appDir, timestamp)
  const currentLink = join(appDir, 'current')

  try {
    // 1. If current exists, copy it to new timestamp
    const currentReal = existsSync(currentLink) ? realpathSync(currentLink) : null

    if (currentReal) {
      cpSync(currentReal, newVersion, { recursive: true })
    } else {
      // First deployment - create directory
      mkdirSync(newVersion, { recursive: true })
    }

    return c.json({ ok: true, version: timestamp })
  } catch (e) {
    return c.json({ error: `Failed to create deployment: ${e instanceof Error ? e.message : String(e)}` }, 500)
  }
})

router.post('/apps/:app/activate', async c => {
  const appName = c.req.param('app')
  const version = c.req.query('version')

  if (!appName) return c.json({ error: 'App name required' }, 400)
  if (!version) return c.json({ error: 'Version required' }, 400)

  const appDir = join(APPS_DIR, appName)
  const versionDir = join(appDir, version)
  const currentLink = join(appDir, 'current')

  if (!existsSync(versionDir)) {
    return c.json({ error: 'Version not found' }, 404)
  }

  try {
    // Atomic symlink update
    const tempLink = join(appDir, '.current.tmp')

    // Remove temp link if it exists from previous failed attempt
    if (existsSync(tempLink)) {
      unlinkSync(tempLink)
    }

    // Create new symlink pointing to version directory (relative)
    symlinkSync(version, tempLink, 'dir')

    // Atomic rename over old symlink/directory
    renameSync(tempLink, currentLink)

    // Clean up old versions (keep 5 most recent)
    try {
      const entries = readdirSync(appDir, { withFileTypes: true })

      // Get all timestamp directories (exclude current, .current.tmp, etc)
      const versionDirs = entries
        .filter(e => e.isDirectory() && /^\d{8}-\d{6}$/.test(e.name))
        .map(e => e.name)
        .sort()
        .reverse() // Newest first

      // Keep 5 most recent, delete the rest
      const toDelete = versionDirs.slice(5)
      for (const dir of toDelete) {
        const dirPath = join(appDir, dir)
        rmSync(dirPath, { recursive: true, force: true })
        console.log(`Cleaned up old version: ${dir}`)
      }
    } catch (e) {
      // Log but don't fail activation if cleanup fails
      console.error(`Failed to clean up old versions: ${e}`)
    }

    // 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)
      }
    }

    return c.json({ ok: true })
  } catch (e) {
    return c.json({ error: `Failed to activate version: ${e instanceof Error ? e.message : String(e)}` }, 500)
  }
})

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

  const appPath = join(APPS_DIR, appName, 'current')

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

  // Resolve to canonical path for consistent watch events
  const canonicalPath = realpathSync(appPath)

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

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

    const fullPath = join(canonicalPath, 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(canonicalPath, 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
