// ============================================================
// OIDC ROUTES — OpenID Connect SSO authentication
// Compatible with: Azure AD, Okta, Keycloak, PocketID, Google
// ============================================================

var express = require('express');
var router = express.Router();
var crypto = require('crypto');
var jwt = require('jsonwebtoken');
var dns = require('dns').promises;
var net = require('net');
var db = require('../db/database');
var { JWT_SECRET, authMiddleware, adminMiddleware } = require('../middleware/auth');
var { hashToken, parseUserAgent, generateSessionId } = require('../utils/sessions');
var { isMobileClient } = require('../utils/platform');

// SSRF guard: reject issuer URLs that resolve to private / loopback / link-local IPs.
// Prevents an admin (or compromised admin account) from pointing OIDC at AWS metadata etc.
function isPrivateIp(ip) {
  if (!ip) return true;
  if (net.isIPv4(ip)) {
    var p = ip.split('.').map(Number);
    if (p[0] === 10) return true;
    if (p[0] === 127) return true;
    if (p[0] === 0) return true;
    if (p[0] === 169 && p[1] === 254) return true;
    if (p[0] === 172 && p[1] >= 16 && p[1] <= 31) return true;
    if (p[0] === 192 && p[1] === 168) return true;
    if (p[0] >= 224) return true;
    return false;
  }
  if (net.isIPv6(ip)) {
    var low = ip.toLowerCase();
    if (low === '::1' || low === '::' ) return true;
    if (low.startsWith('fe80:') || low.startsWith('fc') || low.startsWith('fd')) return true;
    return false;
  }
  return true;
}
async function assertSafeIssuer(urlStr) {
  var u = new URL(urlStr);
  if (u.protocol !== 'https:') throw new Error('OIDC issuer must use https://');
  var addrs = await dns.lookup(u.hostname, { all: true });
  for (var i = 0; i < addrs.length; i++) {
    if (isPrivateIp(addrs[i].address)) throw new Error('OIDC issuer resolves to a private IP (blocked)');
  }
}

// Signed, stateless OIDC state — survives restart / scales to multiple processes.
function signState(payload) {
  var body = Buffer.from(JSON.stringify(payload)).toString('base64url');
  var sig = crypto.createHmac('sha256', JWT_SECRET).update(body).digest('base64url');
  return body + '.' + sig;
}
function verifyState(token) {
  if (!token || typeof token !== 'string') return null;
  var parts = token.split('.');
  if (parts.length !== 2) return null;
  var expected = crypto.createHmac('sha256', JWT_SECRET).update(parts[0]).digest('base64url');
  if (!crypto.timingSafeEqual(Buffer.from(parts[1]), Buffer.from(expected))) return null;
  try {
    var payload = JSON.parse(Buffer.from(parts[0], 'base64url').toString('utf8'));
    if (!payload.expires || payload.expires < Date.now()) return null;
    return payload;
  } catch (e) { return null; }
}

function setAuthCookie(res, token) {
  var isProduction = process.env.NODE_ENV === 'production' || process.env.APP_URL;
  res.cookie('ped_auth', token, {
    httpOnly: true,
    secure: !!isProduction,
    sameSite: 'lax',
    maxAge: 30 * 24 * 60 * 60 * 1000,  // 30d safety net; middleware enforces 24h sliding idle
    path: '/'
  });
}

// ── GET OIDC status (public — frontend checks this to show SSO button) ──
router.get('/oidc-status', async function(req, res) {
  try {
    var enabled = await db.getSetting('oidc.enabled');
    var disableLocal = await db.getSetting('oidc.disable_local_auth');
    var buttonLabel = await db.getSetting('oidc.button_label');
    res.json({
      oidcEnabled: enabled === 'true',
      disableLocalAuth: disableLocal === 'true',
      buttonLabel: buttonLabel || 'Sign in with SSO'
    });
  } catch (e) {
    res.json({ oidcEnabled: false, disableLocalAuth: false });
  }
});

// ── GET /api/auth/oidc — initiate OIDC login (redirects to IdP) ──────────
router.get('/oidc', async function(req, res) {
  try {
    var enabled = await db.getSetting('oidc.enabled');
    if (enabled !== 'true') {
      return res.status(400).json({ error: 'SSO is not enabled' });
    }

    var issuer = await db.getSetting('oidc.issuer');
    var clientId = await db.getSetting('oidc.client_id');

    if (!issuer || !clientId) {
      return res.status(500).json({ error: 'OIDC not fully configured' });
    }

    var oidc = require('openid-client');

    var appUrl = (process.env.APP_URL || 'http://localhost:3000').replace(/\/$/, '');
    var redirectUri = appUrl + '/api/auth/oidc/callback';

    await assertSafeIssuer(issuer);
    var config = await oidc.discovery(new URL(issuer), clientId);

    var nonce = crypto.randomBytes(24).toString('hex');
    var codeVerifier = oidc.randomPKCECodeVerifier();
    var codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);

    // Stateless signed state — survives restart, works across multiple processes.
    var state = signState({
      n: nonce,
      v: codeVerifier,
      expires: Date.now() + 5 * 60 * 1000
    });

    var authUrl = oidc.buildAuthorizationUrl(config, {
      redirect_uri: redirectUri,
      scope: 'openid email profile',
      state: state,
      nonce: nonce,
      code_challenge: codeChallenge,
      code_challenge_method: 'S256'
    });

    res.redirect(authUrl.href);
  } catch (err) {
    console.error('[OIDC] Auth initiation failed:', err.message);
    res.status(500).json({ error: 'SSO login failed: ' + err.message });
  }
});

// ── GET /api/auth/oidc/callback — handle IdP callback ────────────────────
router.get('/oidc/callback', async function(req, res) {
  var appUrl = (process.env.APP_URL || 'http://localhost:3000').replace(/\/$/, '');

  try {
    var state = req.query.state;
    var pending = verifyState(state);
    if (!pending) {
      return res.redirect(appUrl + '?error=invalid_state');
    }

    var issuer = await db.getSetting('oidc.issuer');
    var clientId = await db.getSetting('oidc.client_id');
    var clientSecret = await db.getSetting('oidc.client_secret');

    var oidc = require('openid-client');
    var redirectUri = appUrl + '/api/auth/oidc/callback';

    await assertSafeIssuer(issuer);
    var config = await oidc.discovery(new URL(issuer), clientId, clientSecret || undefined);

    var tokens = await oidc.authorizationCodeGrant(config, new URL(req.protocol + '://' + req.get('host') + req.originalUrl), {
      pkceCodeVerifier: pending.v,
      expectedNonce: pending.n,
      expectedState: state
    });

    var claims = tokens.claims();
    var sub = claims.sub;
    var email = claims.email;
    var emailVerified = claims.email_verified;
    var name = claims.name || claims.preferred_username || email;

    if (!email) {
      // Try userinfo endpoint
      var userinfo = await oidc.fetchUserInfo(config, tokens.access_token, sub);
      email = userinfo.email;
      if (typeof emailVerified === 'undefined') emailVerified = userinfo.email_verified;
      name = name || userinfo.name || userinfo.preferred_username || email;
    }

    if (!email) {
      return res.redirect(appUrl + '?error=no_email');
    }

    email = email.toLowerCase();

    // Find or create user
    var user = await db.get('SELECT * FROM users WHERE email = ?', [email]);

    if (user) {
      // Existing user. Two safe linking paths:
      //   1. Already linked (oidc_sub matches) → log in
      //   2. Not yet linked → auto-link ONLY if the IdP asserts the email
      //      is verified. Otherwise an attacker at the IdP could squat on
      //      an email they don't own and take over the local account.
      if (user.oidc_sub && user.oidc_sub !== sub) {
        console.warn('[OIDC] sub mismatch on existing user id=' + user.id + ' — IdP returned different sub than recorded. Refusing to auto-relink.');
        return res.redirect(appUrl + '?error=sub_mismatch');
      }
      if (!user.oidc_sub) {
        // Some IdPs (rare) serialize booleans as strings — accept both.
        var verified = emailVerified === true || emailVerified === 'true';
        if (!verified) {
          console.warn('[OIDC] refusing to auto-link existing user ' + user.id + ' — IdP did not assert email_verified=true');
          return res.redirect(appUrl + '?error=email_unverified');
        }
        await db.run('UPDATE users SET oidc_sub = ?, email_verified = true WHERE id = ?', [sub, user.id]);
        await db.run('INSERT INTO audit_log (user_id, action, category, details, ip_address) VALUES (?, ?, ?, ?, ?)',
          [user.id, 'oidc_linked', 'auth', 'Linked SSO identity ' + sub + ' via ' + issuer, req.ip]).catch(function(){});
      }
      if (user.disabled) {
        return res.redirect(appUrl + '?error=disabled');
      }
    } else {
      // Auto-create user from OIDC
      var userCount = await db.get('SELECT COUNT(*) as count FROM users', []);
      var role = (userCount && parseInt(userCount.count) === 0) ? 'admin' : 'user';
      var randomPw = crypto.randomBytes(32).toString('hex'); // Not used for OIDC login

      var result = await db.run(
        'INSERT INTO users (email, password, name, role, email_verified, oidc_sub) VALUES (?, ?, ?, ?, true, ?)',
        [email, randomPw, name, role, sub]
      );
      user = await db.get('SELECT * FROM users WHERE id = ?', [result.lastInsertRowid]);
    }

    // Issue JWT
    var token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: isMobileClient(req) ? '365d' : '30d' });
    setAuthCookie(res, token);

    // Create session record
    var ssoSessionId = generateSessionId();
    try {
      await db.run('INSERT INTO user_sessions (id, user_id, token_hash, ip_address, user_agent, device_label) VALUES (?, ?, ?, ?, ?, ?)',
        [ssoSessionId, user.id, hashToken(token), req.ip, req.headers['user-agent'] || '', parseUserAgent(req.headers['user-agent'])]);
    } catch (e) { /* table may not exist yet */ }

    await db.run('INSERT INTO audit_log (user_id, action, ip_address, details) VALUES (?, ?, ?, ?)',
      [user.id, 'login_oidc', req.ip, 'SSO via ' + issuer]);

    // Redirect to app — token is in httpOnly cookie, pass session ID
    res.redirect(appUrl + '?sso=ok&sid=' + ssoSessionId);
  } catch (err) {
    console.error('[OIDC] Callback error:', err.message);
    res.redirect(appUrl + '?error=sso_failed');
  }
});

// ── Admin: GET OIDC config ──────────────────────────────────────────────
router.get('/oidc/config', authMiddleware, adminMiddleware, async function(req, res) {
  try {
    var keys = ['oidc.enabled', 'oidc.issuer', 'oidc.client_id', 'oidc.client_secret', 'oidc.disable_local_auth', 'oidc.button_label', 'oidc.allowed_ips'];
    var config = {};
    for (var i = 0; i < keys.length; i++) {
      config[keys[i]] = await db.getSetting(keys[i]) || '';
    }
    // Mask client secret
    if (config['oidc.client_secret']) {
      config['oidc.client_secret'] = '••••••••' + config['oidc.client_secret'].slice(-4);
    }
    res.json({ success: true, config: config });
  } catch (e) { res.status(500).json({ error: 'Request failed' }); }
});

// ── Admin: PUT update OIDC config ───────────────────────────────────────
router.put('/oidc/config', authMiddleware, adminMiddleware, async function(req, res) {
  try {
    var allowed = ['oidc.enabled', 'oidc.issuer', 'oidc.client_id', 'oidc.client_secret', 'oidc.disable_local_auth', 'oidc.button_label', 'oidc.allowed_ips'];
    var updates = req.body;

    for (var i = 0; i < allowed.length; i++) {
      var key = allowed[i];
      if (updates[key] !== undefined) {
        // Don't overwrite secret with masked value
        if (key === 'oidc.client_secret' && updates[key].indexOf('••••') === 0) continue;
        await db.setSetting(key, updates[key]);
      }
    }

    res.json({ success: true });
  } catch (e) { res.status(500).json({ error: 'Request failed' }); }
});

module.exports = router;
