// Setup Wizard backend.
//
// Three sync GETs (status / suggest-name / dns-check) plus one async POST
// (save) that hands off to the host task system. The lock file lives at
// /app/frontend/data/.setup_complete — under the existing frontend bind-mount
// so the container can read it and the host's setupApply can write it
// without us having to add a new bind-mount to docker-compose.yml.
//
// Sync endpoints intentionally do NOT round-trip through the task daemon —
// suggest-name and dns-check are pure read-only operations that we
// reimplement in JS, so they return in <50ms instead of waiting for the next
// cron tick.

const express = require('express');
const fs = require('fs');
const fsp = require('fs').promises;
const path = require('path');
const dns = require('dns').promises;
const https = require('https');
const { requireAuth } = require('../utils/middleware.js');
const { pokeFifo } = require('../utils/fifo.js');

const router = express.Router();

const TASKS_DIR = path.join(__dirname, '..', '..', 'frontend', 'data', 'tasks');
const FIFO_PATH = path.join(TASKS_DIR, '.queue.fifo');
const SETUP_LOCK_FILE = path.join(__dirname, '..', '..', 'frontend', 'data', '.setup_complete');

const ADJECTIVES = [
  'Quantum', 'Neutrino', 'Photon', 'Plasma', 'Quasar', 'Pulsar', 'Tachyon',
  'Boson', 'Fermion', 'Hadron', 'Gluon', 'Muon', 'Higgs', 'Entangled',
  'Singular', 'Warped', 'Tunneling', 'Coherent', 'Superposed', 'Spectral',
  'Orbital', 'Cosmic', 'Stellar', 'Nebular', 'Astral', 'Gravitic', 'Inertial',
  'Relativistic', 'Helical', 'Toroidal', 'Holographic', 'Cryogenic',
  'Crystalline', 'Resonant', 'Harmonic', 'Phasic', 'Drifting', 'Spinning',
  'Pulsing', 'Hyper'
];

const NOUNS = [
  'Frog', 'Fox', 'Otter', 'Raven', 'Wolf', 'Yak', 'Lynx', 'Owl', 'Hawk',
  'Crow', 'Newt', 'Wren', 'Eel', 'Crab', 'Squid', 'Octopus', 'Mantis',
  'Cobra', 'Viper', 'Ferret', 'Badger', 'Penguin', 'Panda', 'Lemur', 'Quark',
  'Nebula', 'Comet', 'Nova', 'Eclipse', 'Aurora', 'Vortex', 'Helix', 'Halo',
  'Phoenix', 'Hydra', 'Kraken', 'Sphinx', 'Specter', 'Phantom', 'Glyph'
];

function generateInstallName() {
  const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
  const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
  return `${adj}${noun}`;
}

// Install order is enforced server-side. Monitoring goes first so apps
// installing later detect a live Prometheus/Grafana and wire their metrics
// export at install time — Traefik, CrowdSec et al. are monitoring consumers.
// Grafana follows Prometheus because its datasource points at it.
const INSTALL_TIERS = [
  ['prometheus', 'grafana'],
  ['traefik', 'crowdsec']
];

function sortAppsByTier(apps) {
  const rank = new Map();
  let r = 0;
  for (const tier of INSTALL_TIERS) for (const slug of tier) rank.set(slug, r++);
  return [...apps].sort((a, b) => {
    const ra = rank.has(a) ? rank.get(a) : Infinity;
    const rb = rank.has(b) ? rank.get(b) : Infinity;
    if (ra !== rb) return ra - rb;
    return apps.indexOf(a) - apps.indexOf(b);
  });
}

function fetchPublicIp() {
  return new Promise((resolve) => {
    const req = https.get('https://api.ipify.org', { timeout: 3000 }, (res) => {
      let body = '';
      res.on('data', (chunk) => { body += chunk; });
      res.on('end', () => resolve(body.trim() || null));
    });
    req.on('error', () => resolve(null));
    req.on('timeout', () => { req.destroy(); resolve(null); });
  });
}

router.get('/status', requireAuth, async (req, res) => {
  const complete = fs.existsSync(SETUP_LOCK_FILE);
  res.json({ complete });
});

router.get('/suggest-name', requireAuth, (req, res) => {
  res.set('Cache-Control', 'no-store');
  res.json({ name: generateInstallName() });
});

router.get('/dns-check', requireAuth, async (req, res) => {
  const domain = String(req.query.domain || '').trim().toLowerCase();
  if (!domain || !/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(domain)) {
    return res.status(400).json({ matches: false, error: 'invalid domain' });
  }

  const [serverIp, domainIps] = await Promise.all([
    fetchPublicIp(),
    dns.resolve4(domain).catch(() => [])
  ]);

  const domainIp = domainIps[0] || null;
  const matches = !!(serverIp && domainIp && serverIp === domainIp);

  res.json({ matches, server_ip: serverIp, domain_ip: domainIp });
});

// Each ticked app becomes its own `libreportal app install <name>` task —
// using the same task type the WebUI's app-install pipeline already
// understands, so the user sees individual progress per app instead of
// one opaque "setup apply" task. The first task writes the configs, the
// last marks the wizard complete; in between, the recommended apps run
// sequentially because the host daemon processes the FIFO in order.
async function enqueueTask(spec) {
  const id = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  const task = {
    id,
    command: spec.command,
    type: spec.type,
    app: spec.app || 'libreportal',
    config: spec.config || 'setup-wizard',
    status: 'queued',
    createdAt: new Date().toISOString(),
    startedAt: null,
    completedAt: null,
    heartbeatAt: null,
    exitCode: null,
    errorMessage: null,
    setupGroup: spec.setupGroup,
    setupRole: spec.setupRole // 'config' | 'app' | 'finalize'
  };
  const taskPath = path.join(TASKS_DIR, `${id}.json`);
  const tmp = `${taskPath}.tmp`;
  await fsp.writeFile(tmp, JSON.stringify(task, null, 2));
  await fsp.rename(tmp, taskPath);
  pokeFifo(FIFO_PATH, id);
  // Tiny stagger so each task gets a unique Date.now()-based id.
  await new Promise(r => setTimeout(r, 2));
  return id;
}

router.post('/save', requireAuth, async (req, res) => {
  const payload = req.body || {};

  if (!payload.install_name || !/^[a-zA-Z0-9-]+$/.test(payload.install_name)) {
    return res.status(400).json({ error: 'invalid install_name' });
  }
  if (!payload.timezone) {
    return res.status(400).json({ error: 'timezone required' });
  }

  // Domains are optional but each entry must be a valid hostname. Cap at
  // 9 because the config schema only has CFG_DOMAIN_1..CFG_DOMAIN_9.
  const domainRe = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i;
  payload.domains = Array.isArray(payload.domains)
    ? payload.domains.map(d => String(d).trim().toLowerCase()).filter(Boolean)
    : [];
  if (payload.domains.length > 9) payload.domains = payload.domains.slice(0, 9);
  for (const d of payload.domains) {
    if (!domainRe.test(d)) return res.status(400).json({ error: `invalid domain: ${d}` });
  }

  payload.apps = Array.isArray(payload.apps) ? payload.apps.filter(a => /^[a-z0-9_-]+$/i.test(a)) : [];
  payload.apps = sortAppsByTier(payload.apps);

  // Validate appOptions — shape: { <appSlug>: { <optId>: bool, ... } }
  const optsIn = (payload.appOptions && typeof payload.appOptions === 'object') ? payload.appOptions : {};
  const safeOpts = {};
  for (const [slug, opts] of Object.entries(optsIn)) {
    if (!/^[a-z0-9_-]+$/i.test(slug)) continue;
    if (!payload.apps.includes(slug)) continue;
    if (!opts || typeof opts !== 'object') continue;
    safeOpts[slug] = {};
    for (const [k, v] of Object.entries(opts)) {
      if (/^[a-z0-9_-]+$/i.test(k) && typeof v === 'boolean') safeOpts[slug][k] = v;
    }
  }
  payload.appOptions = safeOpts;

  const wantsTraefik = payload.apps.includes('traefik');
  if (wantsTraefik) {
    if (!payload.traefik_email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(payload.traefik_email)) {
      return res.status(400).json({ error: 'traefik_email required when installing Traefik' });
    }
  } else {
    delete payload.traefik_email;
  }

  const setupGroup = `setup_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
  const b64 = Buffer.from(JSON.stringify(payload)).toString('base64');

  try {
    await fsp.mkdir(TASKS_DIR, { recursive: true });
    const taskIds = [];

    taskIds.push(await enqueueTask({
      command: `libreportal setup config ${b64}`,
      type: 'setup-config',
      setupGroup,
      setupRole: 'config'
    }));

    for (const appName of payload.apps) {
      // Convert appOptions sub-flags into the framework's config_variables
      // arg. Convention: sub-option <opt> on app <slug> maps to
      // CFG_<SLUG>_<OPT>_ENABLED. dockerInstallApp parses these and writes
      // them into the template config before calling install<App>.
      let command = `libreportal app install ${appName}`;
      const opts = payload.appOptions[appName] || {};
      const cfgPairs = [];
      const slugUpper = appName.toUpperCase().replace(/-/g, '_');
      for (const [optId, value] of Object.entries(opts)) {
        if (typeof value !== 'boolean') continue;
        cfgPairs.push(`CFG_${slugUpper}_${optId.toUpperCase()}_ENABLED=${value}`);
      }
      if (cfgPairs.length) command += ` ${cfgPairs.join('|')}`;

      taskIds.push(await enqueueTask({
        command,
        type: 'app-install',
        app: appName,
        setupGroup,
        setupRole: 'app'
      }));
    }

    const finalizeId = await enqueueTask({
      command: `libreportal setup finalize`,
      type: 'setup-finalize',
      setupGroup,
      setupRole: 'finalize'
    });
    taskIds.push(finalizeId);

    res.status(201).json({
      setupGroup,
      taskIds,
      firstTaskId: taskIds[0],
      finalizeTaskId: finalizeId,
      installName: payload.install_name
    });
  } catch (err) {
    console.error('[setup] save failed:', err);
    res.status(500).json({ error: 'failed to enqueue setup tasks' });
  }
});

module.exports = router;
