/**
 * Minimal StartOS Package Registry Server
 *
 * Implements the Start9 Marketplace Protocol (Package API)
 * Reference: https://github.com/Start9Labs/registry/blob/master/marketplace_protocol.md
 *
 * Endpoints:
 *   GET /package/v0/info          - Registry metadata (name + categories)
 *   GET /package/v0/index         - Filtered package listing
 *   GET /package/v0/latest        - Latest version per package
 *   GET /package/v0/:s9pk         - Download .s9pk binary
 *   GET /package/v0/manifest/:id  - Package manifest JSON
 *   GET /package/v0/release-notes/:id
 *   GET /package/v0/icon/:id
 *   GET /package/v0/license/:id
 *   GET /package/v0/instructions/:id
 *   GET /package/v0/version/:id
 */

const express = require('express')
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')

const app = express()
const PORT = process.env.PORT || 3030

// ---------------------------------------------------------------------------
// Load configuration
// ---------------------------------------------------------------------------

const REGISTRY_CONFIG = JSON.parse(
  fs.readFileSync(path.join(__dirname, 'registry.json'), 'utf8'),
)

/**
 * Load all package configs from packages/<id>/package.json
 * Returns a Map of pkgId -> config
 */
function loadPackages() {
  const pkgDir = path.join(__dirname, 'packages')
  const packages = new Map()

  if (!fs.existsSync(pkgDir)) return packages

  for (const dir of fs.readdirSync(pkgDir)) {
    const configPath = path.join(pkgDir, dir, 'package.json')
    if (!fs.existsSync(configPath)) continue

    try {
      const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
      // Validate required fields
      if (!config.id || !config.version || !config.title) {
        console.warn(`[WARN] Skipping ${dir}: missing required fields (id, version, title)`)
        continue
      }
      // Resolve file paths relative to the package directory
      config._dir = path.join(pkgDir, dir)
      packages.set(config.id, config)
      console.log(`  Loaded: ${config.id} v${config.version}`)
    } catch (err) {
      console.warn(`[WARN] Failed to load ${configPath}: ${err.message}`)
    }
  }

  return packages
}

let PACKAGES = loadPackages()

// Reload packages on SIGHUP (allows updating without restart)
process.on('SIGHUP', () => {
  console.log('[INFO] Received SIGHUP, reloading packages...')
  PACKAGES = loadPackages()
  console.log(`[INFO] Loaded ${PACKAGES.size} package(s)`)
})

// ---------------------------------------------------------------------------
// Helper: Read a file from a package directory, with fallback
// ---------------------------------------------------------------------------

function readPackageFile(pkg, filename, fallback) {
  const filepath = path.join(pkg._dir, filename)
  if (fs.existsSync(filepath)) {
    return fs.readFileSync(filepath)
  }
  return fallback !== undefined ? Buffer.from(fallback) : null
}

function readPackageFileText(pkg, filename, fallback) {
  const buf = readPackageFile(pkg, filename, fallback)
  return buf ? buf.toString('utf8') : fallback || ''
}

/**
 * Get the icon as a data URL (base64-encoded PNG).
 * The /index endpoint requires this format.
 */
function getIconDataUrl(pkg) {
  const iconPath = path.join(pkg._dir, 'icon.png')
  if (!fs.existsSync(iconPath)) return null
  const data = fs.readFileSync(iconPath)
  return `data:image/png;base64,${data.toString('base64')}`
}

/**
 * Find the .s9pk file for a package. Supports multiple naming conventions:
 *   - <id>.s9pk
 *   - <id>_<arch>.s9pk
 *   - Any .s9pk file in the directory
 *
 * If spec/version-priority query params are provided, we ignore them for now
 * (single-version-per-package simplification).
 */
function findS9pk(pkg, preferredArch) {
  const dir = pkg._dir
  const candidates = fs.readdirSync(dir).filter((f) => f.endsWith('.s9pk'))

  if (candidates.length === 0) return null

  // Prefer arch-specific if requested
  if (preferredArch) {
    const archSpecific = candidates.find((f) => f.includes(`_${preferredArch}`))
    if (archSpecific) return path.join(dir, archSpecific)
  }

  // Prefer universal (no arch suffix), then first available
  const universal = candidates.find((f) => f === `${pkg.id}.s9pk`)
  if (universal) return path.join(dir, universal)

  return path.join(dir, candidates[0])
}

/**
 * Build the manifest object for a package.
 * This is what the StartOS client expects from /manifest/:id
 */
function buildManifest(pkg) {
  return {
    id: pkg.id,
    title: pkg.title,
    version: pkg.version,
    description: {
      short: pkg.descriptionShort || '',
      long: pkg.descriptionLong || '',
    },
    license: pkg.license || 'MIT',
    'release-notes': pkg.releaseNotes || {},
    'upstream-repo': pkg.upstreamRepo || '',
    'package-repo': pkg.packageRepo || '',
    'marketing-url': pkg.marketingUrl || '',
    'donation-url': pkg.donationUrl || null,
    images: pkg.images || {},
    dependencies: pkg.dependencies || {},
    alerts: pkg.alerts || {},
  }
}

// ---------------------------------------------------------------------------
// Routes: Package API v0
// ---------------------------------------------------------------------------

const router = express.Router()

/**
 * GET /info
 * Returns: { name: string, categories: string[] }
 */
router.get('/info', (req, res) => {
  res.json({
    name: REGISTRY_CONFIG.name,
    categories: REGISTRY_CONFIG.categories,
  })
})

/**
 * GET /index
 * Query params (all optional for our purposes):
 *   ids[]        - filter to specific package IDs
 *   category     - filter by category
 *   page         - pagination (default 1)
 *   per-page     - items per page (default 20)
 *   os.compat    - OS version compatibility
 *   hardware.*   - hardware filters
 *
 * Returns: Array of package objects
 */
router.get('/index', (req, res) => {
  let packages = Array.from(PACKAGES.values())

  // Filter by IDs if specified
  const ids = req.query['ids[]'] || req.query.ids
  if (ids) {
    const idList = Array.isArray(ids) ? ids : [ids]
    packages = packages.filter((p) => idList.includes(p.id))
  }

  // Filter by category if specified
  const category = req.query.category
  if (category) {
    packages = packages.filter(
      (p) => p.categories && p.categories.includes(category),
    )
  }

  // Pagination
  const page = parseInt(req.query.page) || 1
  const perPage = parseInt(req.query['per-page']) || 20
  const start = (page - 1) * perPage
  const paged = packages.slice(start, start + perPage)

  // Build response per marketplace protocol
  const result = paged.map((pkg) => ({
    icon: getIconDataUrl(pkg),
    license: readPackageFileText(pkg, 'LICENSE', pkg.license || 'MIT'),
    instructions: readPackageFileText(pkg, 'INSTRUCTIONS.md', ''),
    manifest: buildManifest(pkg),
    categories: pkg.categories || [],
    versions: [pkg.version],
    'dependency-metadata': pkg.dependencyMetadata || {},
  }))

  res.json(result)
})

/**
 * GET /latest
 * Query params:
 *   ids[] - package IDs to check
 *
 * Returns: { "<pkgId>": "<version>" | null }
 */
router.get('/latest', (req, res) => {
  const ids = req.query['ids[]'] || req.query.ids
  if (!ids) return res.json({})

  const idList = Array.isArray(ids) ? ids : [ids]
  const result = {}

  for (const id of idList) {
    const pkg = PACKAGES.get(id)
    result[id] = pkg ? pkg.version : null
  }

  res.json(result)
})

/**
 * GET /:s9pk
 * Downloads the .s9pk binary file.
 * The :s9pk param is the filename (e.g., "recap.s9pk")
 *
 * Optional query params:
 *   spec             - version range (ignored for single-version packages)
 *   version-priority - "min" or "max" (ignored for single-version packages)
 */
router.get('/:s9pk', (req, res, next) => {
  const filename = req.params.s9pk
  if (!filename.endsWith('.s9pk')) return next()

  // Extract package ID from filename (remove .s9pk and optional _arch suffix)
  const base = filename.replace('.s9pk', '')
  const pkgId = base.replace(/_(?:x86_64|aarch64|riscv64)$/, '')

  const pkg = PACKAGES.get(pkgId)
  if (!pkg) return res.status(404).json({ error: 'Package not found' })

  // Find the actual .s9pk file
  const arch = base.includes('_') ? base.split('_').pop() : null
  const s9pkPath = findS9pk(pkg, arch)
  if (!s9pkPath || !fs.existsSync(s9pkPath)) {
    return res.status(404).json({ error: 's9pk file not found' })
  }

  const stat = fs.statSync(s9pkPath)
  res.setHeader('Content-Type', 'application/octet-stream')
  res.setHeader('Content-Length', stat.size)
  res.setHeader(
    'Content-Disposition',
    `attachment; filename="${path.basename(s9pkPath)}"`,
  )

  const stream = fs.createReadStream(s9pkPath)
  stream.pipe(res)
})

/**
 * GET /manifest/:pkgId
 * Returns the package manifest as JSON.
 */
router.get('/manifest/:pkgId', (req, res) => {
  const pkg = PACKAGES.get(req.params.pkgId)
  if (!pkg) return res.status(404).json({ error: 'Package not found' })

  res.json(buildManifest(pkg))
})

/**
 * GET /release-notes/:pkgId
 * Returns: { "<version>": "<notes>" }
 */
router.get('/release-notes/:pkgId', (req, res) => {
  const pkg = PACKAGES.get(req.params.pkgId)
  if (!pkg) return res.status(404).json({ error: 'Package not found' })

  res.json(pkg.releaseNotes || {})
})

/**
 * GET /icon/:pkgId
 * Returns the package icon as a binary PNG.
 */
router.get('/icon/:pkgId', (req, res) => {
  const pkg = PACKAGES.get(req.params.pkgId)
  if (!pkg) return res.status(404).json({ error: 'Package not found' })

  const iconData = readPackageFile(pkg, 'icon.png')
  if (!iconData) return res.status(404).json({ error: 'Icon not found' })

  res.setHeader('Content-Type', 'image/png')
  res.setHeader('Content-Length', iconData.length)
  res.send(iconData)
})

/**
 * GET /license/:pkgId
 * Returns the license text.
 */
router.get('/license/:pkgId', (req, res) => {
  const pkg = PACKAGES.get(req.params.pkgId)
  if (!pkg) return res.status(404).json({ error: 'Package not found' })

  const text = readPackageFileText(pkg, 'LICENSE', pkg.license || 'MIT')
  res.setHeader('Content-Type', 'text/plain')
  res.setHeader('Content-Length', Buffer.byteLength(text))
  res.send(text)
})

/**
 * GET /instructions/:pkgId
 * Returns the instructions markdown.
 */
router.get('/instructions/:pkgId', (req, res) => {
  const pkg = PACKAGES.get(req.params.pkgId)
  if (!pkg) return res.status(404).json({ error: 'Package not found' })

  const text = readPackageFileText(pkg, 'INSTRUCTIONS.md', '')
  res.setHeader('Content-Type', 'text/plain')
  res.setHeader('Content-Length', Buffer.byteLength(text))
  res.send(text)
})

/**
 * GET /version/:pkgId
 * Returns: { version: "<version>" }
 */
router.get('/version/:pkgId', (req, res) => {
  const pkg = PACKAGES.get(req.params.pkgId)
  if (!pkg) return res.status(404).json({ error: 'Package not found' })

  res.json({ version: pkg.version })
})

// Mount at /package/v0 (the marketplace protocol path)
app.use('/package/v0', router)

// Also mount at /package/v1 for forward compatibility
// (StartOS 0.4.0 may try v1 first, then fall back to v0)
app.use('/package/v1', router)

// Health check
app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    packages: PACKAGES.size,
    uptime: process.uptime(),
  })
})

// Root - basic info
app.get('/', (req, res) => {
  res.json({
    name: REGISTRY_CONFIG.name,
    packages: PACKAGES.size,
    protocol: 'Start9 Marketplace Protocol',
    endpoints: {
      info: '/package/v0/info',
      index: '/package/v0/index',
      latest: '/package/v0/latest',
      health: '/health',
    },
  })
})

// ---------------------------------------------------------------------------
// Start server
// ---------------------------------------------------------------------------

app.listen(PORT, () => {
  console.log(`\n  StartOS Registry Server`)
  console.log(`  =======================`)
  console.log(`  Name:     ${REGISTRY_CONFIG.name}`)
  console.log(`  Port:     ${PORT}`)
  console.log(`  Packages: ${PACKAGES.size}`)
  PACKAGES.forEach((pkg) => {
    console.log(`    - ${pkg.id} v${pkg.version}`)
  })
  console.log(`\n  Endpoints:`)
  console.log(`    GET /package/v0/info`)
  console.log(`    GET /package/v0/index`)
  console.log(`    GET /package/v0/latest?ids[]=<id>`)
  console.log(`    GET /package/v0/<id>.s9pk`)
  console.log(`    GET /package/v0/manifest/<id>`)
  console.log(`    GET /package/v0/release-notes/<id>`)
  console.log(`    GET /package/v0/icon/<id>`)
  console.log(`    GET /package/v0/license/<id>`)
  console.log(`    GET /package/v0/instructions/<id>`)
  console.log(`    GET /package/v0/version/<id>`)
  console.log(`\n  Ready.\n`)
})
