const express = require('express');
const fs = require('fs');
const path = require('path');
const http = require('http');
const crypto = require('crypto');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

const app = express();
const HOST = process.env.HOST || '127.0.0.1';
const PORT = process.env.PORT || 3030;

// ─── Paths ───
const DATA_DIR = path.join(__dirname, 'data');
const USERS_FILE = path.join(DATA_DIR, 'users.json');
const SERVICES_CACHE = path.join(DATA_DIR, 'services-cache.json');
const TOKENS_FILE = path.join(DATA_DIR, 'tokens.json');
const LOCKOUT_FILE = path.join(DATA_DIR, 'lockouts.json');
const CONFIG_FILE = path.join(DATA_DIR, 'config.json');

// ─── Config ───
const INVENTORY_API = process.env.INVENTORY_API || 'http://127.0.0.1:3025';
const SESSION_MAX_AGE = 7 * 24 * 60 * 60 * 1000;       // 7 days
const MAGIC_LINK_EXPIRY = 15 * 60 * 1000;               // 15 min
const RESET_TOKEN_EXPIRY = 60 * 60 * 1000;              // 1 hour
const MAX_LOGIN_ATTEMPTS = 5;
const LOCKOUT_DURATION = 30 * 60 * 1000;                // 30 min
const SERVICE_SYNC_INTERVAL = 5 * 60 * 1000;            // 5 min

// ─── Bootstrap data files ───
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
const defaults = {
  [USERS_FILE]: '[]',
  [SERVICES_CACHE]: '{"services":{},"synced_at":null}',
  [TOKENS_FILE]: '{}',
  [LOCKOUT_FILE]: '{}',
  [CONFIG_FILE]: '{}',
};
for (const [f, d] of Object.entries(defaults)) {
  if (!fs.existsSync(f)) fs.writeFileSync(f, d);
}

// ─── JWT Secret (persistent across restarts) ───
function getJwtSecret() {
  const cfg = loadJSON(CONFIG_FILE);
  if (cfg.jwtSecret) return cfg.jwtSecret;
  const secret = crypto.randomBytes(64).toString('hex');
  cfg.jwtSecret = secret;
  saveJSON(CONFIG_FILE, cfg);
  return secret;
}
const JWT_SECRET = getJwtSecret();

// ─── Email config ───
let emailTransport = null;
function initEmail() {
  try {
    const nodemailer = require('nodemailer');
    const cfg = loadJSON(CONFIG_FILE);
    if (cfg.smtp) {
      emailTransport = nodemailer.createTransport(cfg.smtp);
      console.log('[email] SMTP transport configured');
    } else {
      console.log('[email] No SMTP config — magic links and resets will log to console');
    }
  } catch (e) {
    console.log('[email] nodemailer not installed — using console fallback');
  }
}

async function sendEmail(to, subject, html) {
  const cfg = loadJSON(CONFIG_FILE);
  const from = cfg.emailFrom || 'TARS <noreply@scottfelten.com>';
  if (emailTransport) {
    try {
      await emailTransport.sendMail({ from, to, subject, html });
      console.log(`[email] Sent to ${to}: ${subject}`);
      return true;
    } catch (e) {
      console.error(`[email] Failed to send to ${to}:`, e.message);
      return false;
    }
  }
  // Console fallback
  console.log(`\n══════ EMAIL (console fallback) ══════`);
  console.log(`To: ${to}`);
  console.log(`Subject: ${subject}`);
  console.log(`Body: ${html}`);
  console.log(`══════════════════════════════════════\n`);
  return true;
}

// ─── JSON helpers ───
function loadJSON(f) { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return {}; } }
function saveJSON(f, d) { fs.writeFileSync(f, JSON.stringify(d, null, 2)); }
function loadUsers() { try { return JSON.parse(fs.readFileSync(USERS_FILE, 'utf8')); } catch { return []; } }
function saveUsers(u) { fs.writeFileSync(USERS_FILE, JSON.stringify(u, null, 2)); }
function findUser(email) { return loadUsers().find(u => u.email.toLowerCase() === email.toLowerCase()); }

// ─── Lockout ───
function checkLockout(email) {
  const lockouts = loadJSON(LOCKOUT_FILE);
  const entry = lockouts[email.toLowerCase()];
  if (!entry) return { locked: false, attempts: 0 };
  if (entry.lockedUntil && Date.now() < entry.lockedUntil) {
    return { locked: true, attempts: entry.attempts, minutesLeft: Math.ceil((entry.lockedUntil - Date.now()) / 60000) };
  }
  if (entry.lockedUntil && Date.now() >= entry.lockedUntil) {
    // Lockout expired — reset
    delete lockouts[email.toLowerCase()];
    saveJSON(LOCKOUT_FILE, lockouts);
    return { locked: false, attempts: 0 };
  }
  return { locked: false, attempts: entry.attempts || 0 };
}

function recordFailedAttempt(email) {
  const lockouts = loadJSON(LOCKOUT_FILE);
  const key = email.toLowerCase();
  if (!lockouts[key]) lockouts[key] = { attempts: 0 };
  lockouts[key].attempts++;
  lockouts[key].lastAttempt = Date.now();
  if (lockouts[key].attempts >= MAX_LOGIN_ATTEMPTS) {
    lockouts[key].lockedUntil = Date.now() + LOCKOUT_DURATION;
    console.log(`[lockout] ${email} locked for ${LOCKOUT_DURATION / 60000} minutes`);
  }
  saveJSON(LOCKOUT_FILE, lockouts);
  return lockouts[key];
}

function clearLockout(email) {
  const lockouts = loadJSON(LOCKOUT_FILE);
  delete lockouts[email.toLowerCase()];
  saveJSON(LOCKOUT_FILE, lockouts);
}

// ─── Token store (magic links + password resets) ───
function createToken(email, type) {
  const tokens = loadJSON(TOKENS_FILE);
  const token = crypto.randomBytes(32).toString('hex');
  const expiry = type === 'magic' ? MAGIC_LINK_EXPIRY : RESET_TOKEN_EXPIRY;
  tokens[token] = { email: email.toLowerCase(), type, created: Date.now(), expires: Date.now() + expiry };
  // Clean expired tokens while we're here
  for (const [k, v] of Object.entries(tokens)) {
    if (v.expires < Date.now()) delete tokens[k];
  }
  saveJSON(TOKENS_FILE, tokens);
  return token;
}

function verifyToken(token, type) {
  const tokens = loadJSON(TOKENS_FILE);
  const entry = tokens[token];
  if (!entry) return null;
  if (entry.type !== type) return null;
  if (entry.expires < Date.now()) {
    delete tokens[token];
    saveJSON(TOKENS_FILE, tokens);
    return null;
  }
  // Consume the token (one-time use)
  delete tokens[token];
  saveJSON(TOKENS_FILE, tokens);
  return entry.email;
}

// ═══════════════════════════════════════
//  SERVICE DISCOVERY (from Inventory API)
// ═══════════════════════════════════════

let serviceMap = {};  // hostname → service-id (live in memory, backed by cache file)

function loadServiceCache() {
  const cache = loadJSON(SERVICES_CACHE);
  if (cache.services) serviceMap = cache.services;
  console.log(`[services] Loaded ${Object.keys(serviceMap).length} services from cache`);
}

async function syncServices() {
  try {
    const resp = await fetch(`${INVENTORY_API}/api/github`);
    if (!resp.ok) throw new Error(`Inventory API returned ${resp.status}`);
    const data = await resp.json();

    // Build service map from inventory repos
    const newMap = {};
    const STATE_FILE = path.join(DATA_DIR, '../api/inventory-data.json');

    // Also try to get the inventory state which has service URLs
    let stateData = {};
    try {
      const stateResp = await fetch(`${INVENTORY_API}/api/state`);
      if (stateResp.ok) stateData = await stateResp.json();
    } catch {}

    // Known subdomain → repo mappings from inventory state
    // The inventory stores overrides with service URLs
    const SERVICE_URL_TO_REPO = {};

    // Build from repos — every repo with a known service URL gets mapped
    if (data.repos) {
      for (const repo of data.repos) {
        if (repo.archived) continue;
        // Check if there's state data with a service URL for this repo
        const state = stateData[repo.name];
        if (state && state.serviceUrl) {
          try {
            const url = new URL(state.serviceUrl);
            newMap[url.hostname] = repo.name;
            // Also map hostname:port if non-standard
            if (url.port) newMap[`${url.hostname}:${url.port}`] = repo.name;
          } catch {}
        }
      }
    }

    // Hardcoded fallbacks for services without inventory state
    // These cover *.scottfelten.com subdomains and external domains
    const FALLBACKS = {
      'docs.scottfelten.com': 'command-center',
      'ops.scottfelten.com': 'ops-dashboard',
      'models.scottfelten.com': 'models-embeddings',
      'goalstack.scottfelten.com': 'taos',
      'collab.scottfelten.com': 'ccgg-collab',
      'agents.scottfelten.com': 'agent-bios',
      'signal.scottfelten.com': 'signal-tower',
      'c360.scottfelten.com': 'customer-360',
      'research.scottfelten.com': 'research-queue',
      'files.scottfelten.com': 'workspace-file-server',
      'taos.scottfelten.com': 'taos',
      'inv.scottfelten.com': 'inventory',
      'paste.scottfelten.com': 'paste-to-md',
      'users.scottfelten.com': 'access-manager',
      'keys.scottfelten.com': 'api-keys-dashboard',
      'iag.scottfelten.com': 'iag-data-viewer',
      'intelligenceadvisorygroup.com': 'iag-website',
      'www.intelligenceadvisorygroup.com': 'iag-website',
      'intellicert.app': 'intellicert',
      'www.intellicert.app': 'intellicert',
      'app.usecasegen.app': 'usecasegen',
      'usecasegen.app': 'usecasegen',
      'www.usecasegen.app': 'usecasegen',
      'intelligenceagentpartners.com': 'intelligent-agent-partners',
      'www.intelligenceagentpartners.com': 'intelligent-agent-partners',
      'intelligentageintpartners.com': 'intelligent-agent-partners',
      'www.intelligentagentpartners.com': 'intelligent-agent-partners',
    };

    // Merge: inventory wins, fallbacks fill gaps
    for (const [host, svc] of Object.entries(FALLBACKS)) {
      if (!newMap[host]) newMap[host] = svc;
    }

    serviceMap = newMap;
    const cache = { services: newMap, synced_at: new Date().toISOString(), count: Object.keys(newMap).length };
    saveJSON(SERVICES_CACHE, cache);
    console.log(`[services] Synced ${Object.keys(newMap).length} service mappings from inventory`);
  } catch (e) {
    console.error(`[services] Sync failed: ${e.message} — using cached data`);
  }
}

// ═══════════════════════════════════════
//  SESSION MANAGEMENT (JWT cookie)
// ═══════════════════════════════════════

function createSession(user, method) {
  const payload = {
    email: user.email,
    name: user.name,
    role: user.role,
    services: user.services,
    method,  // 'google', 'magic-link', 'password'
  };
  return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' });
}

function verifySession(token) {
  try {
    return jwt.verify(token, JWT_SECRET);
  } catch {
    return null;
  }
}

function setSessionCookie(res, token) {
  res.cookie('_am_session', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    domain: '.scottfelten.com',
    maxAge: SESSION_MAX_AGE,
    path: '/',
  });
}

// ═══════════════════════════════════════
//  MIDDLEWARE
// ═══════════════════════════════════════

app.use(express.json());
app.use(require('cookie-parser')());

// Serve static files for public pages
app.use(express.static(path.join(__dirname, 'public')));

// ═══════════════════════════════════════
//  AUTH CHECK (nginx auth_request target)
// ═══════════════════════════════════════
//
// Called by every service's nginx config.
// Checks TWO auth methods:
//   1. Our own _am_session cookie (magic link / password users)
//   2. oauth2-proxy session (Google SSO users)
// Returns 200 (allowed), 401 (not authed), 403 (no access)

app.get('/auth/check', (req, res) => {
  const host = req.headers['x-original-host'] || req.headers['host'] || '';
  const cookies = req.headers['cookie'] || '';

  console.log(`[auth/check] host=${host}`);

  // ── Method 1: Check our own session cookie ──
  const amSession = req.cookies?.['_am_session'];
  if (amSession) {
    const session = verifySession(amSession);
    if (session) {
      console.log(`[auth/check] Valid _am_session for ${session.email} (${session.method})`);
      return handleAccessCheck(res, session.email, host);
    }
  }

  // ── Method 2: Check oauth2-proxy ──
  const opts = {
    hostname: '127.0.0.1',
    port: 4180,
    path: '/oauth2/auth',
    method: 'GET',
    headers: {
      'Cookie': cookies,
      'Host': host,
      'X-Real-IP': req.headers['x-real-ip'] || '127.0.0.1',
      'X-Forwarded-For': req.headers['x-forwarded-for'] || '127.0.0.1',
      'X-Forwarded-Proto': 'https',
      'X-Forwarded-Host': host,
    },
  };

  const proxyReq = http.request(opts, (proxyRes) => {
    if (proxyRes.statusCode < 200 || proxyRes.statusCode >= 300) {
      return res.status(401).send('Not authenticated');
    }
    const email = proxyRes.headers['x-auth-request-email'] || '';
    if (!email) return res.status(401).send('No email from oauth2');

    res.setHeader('X-Auth-Request-User', proxyRes.headers['x-auth-request-user'] || '');
    res.setHeader('X-Auth-Request-Email', email);

    return handleAccessCheck(res, email, host);
  });

  proxyReq.on('error', (e) => {
    console.error('[auth/check] oauth2-proxy error:', e.message);
    res.status(500).send('Auth service error');
  });

  proxyReq.end();
});

function handleAccessCheck(res, email, host) {
  const user = findUser(email);
  if (!user) return res.status(403).send('User not registered');

  // Update last login (throttle)
  const users = loadUsers();
  const idx = users.findIndex(u => u.email.toLowerCase() === email.toLowerCase());
  if (idx >= 0) {
    const last = users[idx].lastLogin ? new Date(users[idx].lastLogin).getTime() : 0;
    if (Date.now() - last > 60000) {
      users[idx].lastLogin = new Date().toISOString();
      saveUsers(users);
    }
  }

  // Admin wildcard
  if (user.services.includes('*')) return res.status(200).send('OK');

  // Map host to service
  const serviceId = serviceMap[host];
  if (!serviceId) {
    console.log(`[auth/check] No service mapping for host: ${host}`);
    return res.status(403).send('Service not mapped');
  }

  if (user.services.includes(serviceId)) return res.status(200).send('OK');

  console.log(`[auth/check] ${email} denied access to ${serviceId} (${host})`);
  return res.status(403).send('Access denied');
}

// ═══════════════════════════════════════
//  MAGIC LINK AUTH
// ═══════════════════════════════════════

app.post('/auth/magic/request', async (req, res) => {
  const { email, redirect } = req.body;
  if (!email) return res.status(400).json({ error: 'Email required' });

  const user = findUser(email);
  if (!user) {
    // Don't reveal whether email exists — always return success
    console.log(`[magic] Request for unknown email: ${email}`);
    return res.json({ ok: true, message: 'If that email is registered, a login link has been sent.' });
  }

  const token = createToken(email, 'magic');
  const rd = redirect || 'https://users.scottfelten.com';
  const link = `https://users.scottfelten.com/auth/magic/verify?token=${token}&rd=${encodeURIComponent(rd)}`;

  await sendEmail(email, 'Your login link — scottfelten.com', `
    <h2>Login to scottfelten.com</h2>
    <p>Click the link below to sign in. This link expires in 15 minutes.</p>
    <p><a href="${link}" style="display:inline-block;padding:12px 24px;background:#3b82f6;color:white;border-radius:8px;text-decoration:none;font-weight:600;">Sign In</a></p>
    <p style="color:#666;font-size:12px;">Or copy this URL: ${link}</p>
    <p style="color:#999;font-size:11px;">If you didn't request this, ignore this email.</p>
  `);

  res.json({ ok: true, message: 'If that email is registered, a login link has been sent.' });
});

app.get('/auth/magic/verify', (req, res) => {
  const { token, rd } = req.query;
  if (!token) return res.status(400).send('Missing token');

  const email = verifyToken(token, 'magic');
  if (!email) {
    return res.redirect('/login.html?error=expired');
  }

  const user = findUser(email);
  if (!user) return res.redirect('/login.html?error=not_registered');

  const sessionToken = createSession(user, 'magic-link');
  setSessionCookie(res, sessionToken);

  console.log(`[magic] Verified login for ${email}`);
  res.redirect(rd || '/');
});

// ═══════════════════════════════════════
//  EMAIL + PASSWORD AUTH
// ═══════════════════════════════════════

app.post('/auth/password/login', async (req, res) => {
  const { email, password } = req.body;
  if (!email || !password) return res.status(400).json({ error: 'Email and password required' });

  // Check lockout
  const lockout = checkLockout(email);
  if (lockout.locked) {
    return res.status(429).json({
      error: `Account locked. Try again in ${lockout.minutesLeft} minutes.`,
      locked: true,
      minutesLeft: lockout.minutesLeft,
    });
  }

  const user = findUser(email);
  if (!user || !user.passwordHash) {
    recordFailedAttempt(email);
    return res.status(401).json({ error: 'Invalid email or password' });
  }

  const valid = await bcrypt.compare(password, user.passwordHash);
  if (!valid) {
    const attempt = recordFailedAttempt(email);
    const remaining = MAX_LOGIN_ATTEMPTS - attempt.attempts;
    return res.status(401).json({
      error: 'Invalid email or password',
      attemptsRemaining: Math.max(0, remaining),
    });
  }

  // Success — clear lockout, create session
  clearLockout(email);
  const sessionToken = createSession(user, 'password');
  setSessionCookie(res, sessionToken);

  console.log(`[password] Login success for ${email}`);
  res.json({ ok: true, user: { email: user.email, name: user.name, role: user.role } });
});

// Admin sets password for a user
app.post('/auth/password/set', requireAdmin, async (req, res) => {
  const { email, password } = req.body;
  if (!email || !password) return res.status(400).json({ error: 'Email and password required' });
  if (password.length < 12) return res.status(400).json({ error: 'Password must be at least 12 characters' });

  const users = loadUsers();
  const idx = users.findIndex(u => u.email.toLowerCase() === email.toLowerCase());
  if (idx < 0) return res.status(404).json({ error: 'User not found' });

  users[idx].passwordHash = await bcrypt.hash(password, 12);
  users[idx].authMethods = [...new Set([...(users[idx].authMethods || ['google']), 'password'])];
  saveUsers(users);

  console.log(`[password] Password set for ${email} by admin`);
  res.json({ ok: true });
});

// Password reset request
app.post('/auth/password/reset-request', async (req, res) => {
  const { email } = req.body;
  if (!email) return res.status(400).json({ error: 'Email required' });

  const user = findUser(email);
  if (!user) {
    // Don't reveal existence
    return res.json({ ok: true, message: 'If that email is registered, a reset link has been sent.' });
  }

  const token = createToken(email, 'reset');
  const link = `https://users.scottfelten.com/login.html?reset=${token}`;

  await sendEmail(email, 'Password reset — scottfelten.com', `
    <h2>Reset your password</h2>
    <p>Click below to set a new password. This link expires in 1 hour.</p>
    <p><a href="${link}" style="display:inline-block;padding:12px 24px;background:#3b82f6;color:white;border-radius:8px;text-decoration:none;font-weight:600;">Reset Password</a></p>
    <p style="color:#666;font-size:12px;">Or copy: ${link}</p>
    <p style="color:#999;font-size:11px;">If you didn't request this, ignore this email.</p>
  `);

  res.json({ ok: true, message: 'If that email is registered, a reset link has been sent.' });
});

// Password reset execution
app.post('/auth/password/reset', async (req, res) => {
  const { token, password } = req.body;
  if (!token || !password) return res.status(400).json({ error: 'Token and password required' });
  if (password.length < 12) return res.status(400).json({ error: 'Password must be at least 12 characters' });

  const email = verifyToken(token, 'reset');
  if (!email) return res.status(400).json({ error: 'Invalid or expired reset token' });

  const users = loadUsers();
  const idx = users.findIndex(u => u.email.toLowerCase() === email.toLowerCase());
  if (idx < 0) return res.status(404).json({ error: 'User not found' });

  users[idx].passwordHash = await bcrypt.hash(password, 12);
  users[idx].authMethods = [...new Set([...(users[idx].authMethods || ['google']), 'password'])];
  saveUsers(users);

  clearLockout(email);
  console.log(`[password] Password reset complete for ${email}`);
  res.json({ ok: true });
});

// ═══════════════════════════════════════
//  SESSION INFO
// ═══════════════════════════════════════

app.get('/auth/session', (req, res) => {
  const amSession = req.cookies?.['_am_session'];
  if (amSession) {
    const session = verifySession(amSession);
    if (session) return res.json({ authenticated: true, ...session });
  }
  res.json({ authenticated: false });
});

app.post('/auth/logout', (req, res) => {
  res.clearCookie('_am_session', { domain: '.scottfelten.com', path: '/' });
  res.json({ ok: true });
});

// ═══════════════════════════════════════
//  ADMIN MIDDLEWARE
// ═══════════════════════════════════════

function requireAdmin(req, res, next) {
  // Check our session cookie first
  const amSession = req.cookies?.['_am_session'];
  if (amSession) {
    const session = verifySession(amSession);
    if (session) {
      const user = findUser(session.email);
      if (user && user.role === 'admin') return next();
    }
  }
  // Fall back to nginx-forwarded headers (oauth2-proxy)
  const email = req.headers['x-email'] || req.headers['x-forwarded-email'] || '';
  if (email) {
    const user = findUser(email);
    if (user && user.role === 'admin') return next();
  }
  return res.status(403).json({ error: 'Admin access required' });
}

// ═══════════════════════════════════════
//  ADMIN API
// ═══════════════════════════════════════

// List users
app.get('/api/users', requireAdmin, (req, res) => {
  const users = loadUsers().map(u => {
    const { passwordHash, ...safe } = u;
    return { ...safe, hasPassword: !!passwordHash };
  });
  res.json(users);
});

// Get single user
app.get('/api/users/:email', requireAdmin, (req, res) => {
  const user = findUser(req.params.email);
  if (!user) return res.status(404).json({ error: 'User not found' });
  const { passwordHash, ...safe } = user;
  res.json({ ...safe, hasPassword: !!passwordHash });
});

// Create user
app.post('/api/users', requireAdmin, async (req, res) => {
  const { email, name, role, services, metadata, tags, password, authMethods } = req.body;
  if (!email || !name) return res.status(400).json({ error: 'email and name required' });

  const users = loadUsers();
  if (users.find(u => u.email.toLowerCase() === email.toLowerCase())) {
    return res.status(409).json({ error: 'User already exists' });
  }

  const adminEmail = req.headers['x-email'] || req.headers['x-forwarded-email'] || 'api';

  const newUser = {
    email: email.toLowerCase(),
    name,
    role: role || 'client',
    services: services || [],
    metadata: metadata || {},
    tags: tags || [],
    authMethods: authMethods || ['google'],
    passwordHash: password ? await bcrypt.hash(password, 12) : null,
    createdAt: new Date().toISOString().split('T')[0],
    createdBy: adminEmail,
    lastLogin: null,
  };

  users.push(newUser);
  saveUsers(users);
  syncAllowlist(users);

  const { passwordHash, ...safe } = newUser;
  res.status(201).json({ ...safe, hasPassword: !!passwordHash });
});

// Update user
app.put('/api/users/:email', requireAdmin, (req, res) => {
  const users = loadUsers();
  const idx = users.findIndex(u => u.email.toLowerCase() === req.params.email.toLowerCase());
  if (idx < 0) return res.status(404).json({ error: 'User not found' });

  const { name, role, services, metadata, tags, authMethods } = req.body;
  if (name !== undefined) users[idx].name = name;
  if (role !== undefined) users[idx].role = role;
  if (services !== undefined) users[idx].services = services;
  if (metadata !== undefined) users[idx].metadata = { ...users[idx].metadata, ...metadata };
  if (tags !== undefined) users[idx].tags = tags;
  if (authMethods !== undefined) users[idx].authMethods = authMethods;

  saveUsers(users);
  syncAllowlist(users);

  const { passwordHash, ...safe } = users[idx];
  res.json({ ...safe, hasPassword: !!passwordHash });
});

// Delete user
app.delete('/api/users/:email', requireAdmin, (req, res) => {
  const users = loadUsers();
  const idx = users.findIndex(u => u.email.toLowerCase() === req.params.email.toLowerCase());
  if (idx < 0) return res.status(404).json({ error: 'User not found' });

  const adminEmail = (req.headers['x-email'] || '').toLowerCase();
  if (users[idx].email.toLowerCase() === adminEmail) {
    return res.status(400).json({ error: 'Cannot delete yourself' });
  }

  users.splice(idx, 1);
  saveUsers(users);
  syncAllowlist(users);
  res.json({ deleted: true });
});

// ═══════════════════════════════════════
//  SERVICES API (dynamic from inventory)
// ═══════════════════════════════════════

// List available services (for UI dropdown)
app.get('/api/services', requireAdmin, (req, res) => {
  // Deduplicate: return unique service IDs
  const serviceIds = [...new Set(Object.values(serviceMap))].sort();
  res.json(serviceIds);
});

// Full service map (hostname → service-id)
app.get('/api/services/map', requireAdmin, (req, res) => {
  res.json(serviceMap);
});

// Force sync from inventory
app.post('/api/services/sync', requireAdmin, async (req, res) => {
  await syncServices();
  res.json({
    services: [...new Set(Object.values(serviceMap))].sort(),
    count: Object.keys(serviceMap).length,
    synced_at: new Date().toISOString(),
  });
});

// ─── Sync oauth2-proxy allowlist ───
function syncAllowlist(users) {
  // Only include users with google as an auth method (or admin/legacy users)
  const emails = users
    .filter(u => !u.authMethods || u.authMethods.includes('google') || u.role === 'admin')
    .map(u => u.email)
    .join('\n') + '\n';
  try {
    fs.writeFileSync('/etc/oauth2-proxy-users.txt', emails);
  } catch (e) {
    console.error('Failed to sync allowlist:', e.message);
  }
}

// ─── Email config management ───
app.get('/api/config/email', requireAdmin, (req, res) => {
  const cfg = loadJSON(CONFIG_FILE);
  const smtpConfigured = !!cfg.smtp;
  res.json({
    configured: smtpConfigured,
    from: cfg.emailFrom || 'noreply@scottfelten.com',
    host: cfg.smtp?.host || null,
  });
});

app.put('/api/config/email', requireAdmin, (req, res) => {
  const { host, port, user, pass, from } = req.body;
  const cfg = loadJSON(CONFIG_FILE);
  if (host && user && pass) {
    cfg.smtp = { host, port: port || 587, secure: false, auth: { user, pass } };
    cfg.emailFrom = from || `TARS <${user}>`;
    saveJSON(CONFIG_FILE, cfg);
    initEmail();  // Re-initialize transport
    res.json({ ok: true, configured: true });
  } else {
    res.status(400).json({ error: 'host, user, and pass required' });
  }
});

// ─── Health ───
app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    users: loadUsers().length,
    services: Object.keys(serviceMap).length,
    emailConfigured: !!emailTransport,
  });
});

// ═══════════════════════════════════════
//  START
// ═══════════════════════════════════════

app.listen(PORT, HOST, async () => {
  console.log(`access-manager v2 listening on ${HOST}:${PORT}`);
  console.log(`Users: ${loadUsers().length}`);

  // Load cached services, then sync
  loadServiceCache();
  await syncServices();

  // Initialize email
  initEmail();

  // Periodic service sync
  setInterval(syncServices, SERVICE_SYNC_INTERVAL);
});
