/**
 * OAuth / OpenID Connect routes for Redlight.
 *
 * Flow:
 * 1. Client calls GET /api/oauth/providers → returns enabled provider info
 * 2. Client calls GET /api/oauth/authorize → server generates PKCE + state, redirects to IdP
 * 3. IdP redirects to GET /api/oauth/callback with code + state
 * 4. Server exchanges code for tokens, fetches user info, creates/links account, returns JWT
 *
 * Security:
 * - PKCE (S256) everywhere
 * - Cryptographic state token (single-use, 10-min TTL)
 * - Client secret never leaves the server
 * - OAuth user info validated and sanitized before DB insertion
 * - Rate limited callback endpoint
 */

import { Router } from 'express';
import { rateLimit } from 'express-rate-limit';
import { v4 as uuidv4 } from 'uuid';
import { getDb } from '../config/database.js';
import { generateToken, getBaseUrl } from '../middleware/auth.js';
import { log } from '../config/logger.js';
import redis from '../config/redis.js';
import {
  getOAuthConfig,
  discoverOIDC,
  createOAuthState,
  consumeOAuthState,
  generateCodeVerifier,
  computeCodeChallenge,
  exchangeCode,
  fetchUserInfo,
} from '../config/oauth.js';

const router = Router();

// Rate limit the callback to prevent brute-force of authorization codes
const callbackLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 30,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many OAuth attempts. Please try again later.' },
});

// ── GET /api/oauth/providers — List available OAuth providers (public) ───────
router.get('/providers', async (req, res) => {
  try {
    const config = await getOAuthConfig();
    if (!config) {
      return res.json({ providers: [] });
    }

    // Never expose client secret or issuer details to the client
    res.json({
      providers: [
        {
          id: 'oidc',
          name: config.displayName || 'SSO',
        },
      ],
    });
  } catch (err) {
    log.auth.error(`OAuth providers error: ${err.message}`);
    res.json({ providers: [] });
  }
});

// ── GET /api/oauth/authorize — Start OAuth flow (redirects to IdP) ──────────
router.get('/authorize', async (req, res) => {
  try {
    const config = await getOAuthConfig();
    if (!config) {
      return res.status(400).json({ error: 'OAuth is not configured' });
    }

    // Discover OIDC endpoints
    const oidc = await discoverOIDC(config.issuer);

    // Generate PKCE pair
    const codeVerifier = generateCodeVerifier();
    const codeChallenge = computeCodeChallenge(codeVerifier);

    // Optional return_to from query (validate it's a relative path)
    let returnTo = req.query.return_to || null;
    if (returnTo && (typeof returnTo !== 'string' || !returnTo.startsWith('/') || returnTo.startsWith('//'))) {
      returnTo = null; // prevent open redirect
    }

    // Create server-side state
    const state = await createOAuthState('oidc', codeVerifier, returnTo);

    // Build callback URL
    const baseUrl = getBaseUrl(req);
    const redirectUri = `${baseUrl}/api/oauth/callback`;

    // Build authorization URL
    const params = new URLSearchParams({
      response_type: 'code',
      client_id: config.clientId,
      redirect_uri: redirectUri,
      scope: 'openid email profile',
      state,
      code_challenge: codeChallenge,
      code_challenge_method: 'S256',
    });

    const authUrl = `${oidc.authorization_endpoint}?${params.toString()}`;
    res.redirect(authUrl);
  } catch (err) {
    log.auth.error(`OAuth authorize error: ${err.message}`);
    res.status(500).json({ error: 'Could not start OAuth flow' });
  }
});

// ── GET /api/oauth/callback — Handle IdP callback ──────────────────────────
router.get('/callback', callbackLimiter, async (req, res) => {
  try {
    const { code, state, error: oauthError, error_description } = req.query;

    // Build frontend error redirect helper
    const baseUrl = getBaseUrl(req);
    const errorRedirect = (msg) =>
      res.redirect(`${baseUrl}/oauth/callback?error=${encodeURIComponent(msg)}`);

    // Handle IdP errors
    if (oauthError) {
      log.auth.warn(`OAuth IdP error: ${oauthError} – ${error_description || ''}`);
      return errorRedirect(error_description || oauthError);
    }

    if (!code || !state) {
      return errorRedirect('Missing authorization code or state');
    }

    // Validate and consume state (CSRF + PKCE verifier retrieval)
    const stateData = await consumeOAuthState(state);
    if (!stateData) {
      return errorRedirect('Invalid or expired OAuth state. Please try again.');
    }

    // Load provider config
    const config = await getOAuthConfig();
    if (!config) {
      return errorRedirect('OAuth is not configured');
    }

    // Discover OIDC endpoints
    const oidc = await discoverOIDC(config.issuer);

    // Exchange code for tokens
    const redirectUri = `${baseUrl}/api/oauth/callback`;
    const tokenResponse = await exchangeCode(
      oidc, code, redirectUri, config.clientId, config.clientSecret, stateData.code_verifier,
    );

    if (!tokenResponse.access_token) {
      return errorRedirect('Token exchange failed: no access token received');
    }

    // Fetch user info from the IdP
    let userInfo;
    if (oidc.userinfo_endpoint) {
      userInfo = await fetchUserInfo(oidc.userinfo_endpoint, tokenResponse.access_token);
    } else {
      return errorRedirect('Provider does not support userinfo endpoint');
    }

    // Extract and validate user attributes
    const email = (userInfo.email || '').toLowerCase().trim();
    const sub = userInfo.sub || '';
    const name = userInfo.preferred_username || userInfo.name || email.split('@')[0] || '';
    const displayName = userInfo.name || userInfo.preferred_username || '';

    if (!email || !sub) {
      return errorRedirect('OAuth provider did not return an email or subject');
    }

    // Basic email validation
    const EMAIL_RE = /^[^\s@]{1,64}@[^\s@]{1,253}\.[^\s@]{2,}$/;
    if (!EMAIL_RE.test(email)) {
      return errorRedirect('OAuth provider returned an invalid email address');
    }

    const db = getDb();

    // ── Link or create user ──────────────────────────────────────────────
    // 1. Check if there's already a user linked with this OAuth provider + sub
    let user = await db.get(
      'SELECT id, name, display_name, email, role, email_verified FROM users WHERE oauth_provider = ? AND oauth_provider_id = ?',
      ['oidc', sub],
    );

    if (!user) {
      // 2. Check if a user with this email already exists (link accounts)
      user = await db.get(
        'SELECT id, name, display_name, email, role, email_verified, oauth_provider FROM users WHERE email = ?',
        [email],
      );

      if (user) {
        // Only auto-link to an existing local account if the IdP has actually
        // verified the email. Otherwise an attacker who registers at the IdP
        // with someone else's email could take over the local account.
        if (userInfo.email_verified !== true) {
          log.auth.warn(`OAuth account-linking blocked: provider did not assert email_verified=true for ${email}`);
          return errorRedirect('Your OAuth provider has not verified this email address. Please verify it with the provider before logging in here.');
        }
        await db.run(
          'UPDATE users SET oauth_provider = ?, oauth_provider_id = ?, email_verified = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
          ['oidc', sub, user.id],
        );
        log.auth.info(`Linked OAuth (oidc/${sub}) to existing user ${user.email}`);
      } else {
        // 3. Auto-register new user (if enabled)
        if (!config.autoRegister) {
          return errorRedirect('No account found for this email. Please register first or ask an admin.');
        }

        // Check registration mode
        const regModeSetting = await db.get("SELECT value FROM settings WHERE key = 'registration_mode'");
        const registrationMode = regModeSetting?.value || 'open';
        if (registrationMode === 'invite') {
          return errorRedirect('Registration is invite-only. An administrator must create your account first.');
        }

        // Sanitize username: only allow safe characters
        const safeUsername = name.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 30) || `user_${Date.now()}`;

        // Ensure username is unique
        let finalUsername = safeUsername;
        const existingUsername = await db.get('SELECT id FROM users WHERE LOWER(name) = LOWER(?)', [safeUsername]);
        if (existingUsername) {
          finalUsername = `${safeUsername}_${Date.now().toString(36)}`;
        }

        // No password needed for OAuth users — use a random hash that can't be guessed
        const randomPasswordHash = `oauth:${uuidv4()}`;

        const result = await db.run(
          `INSERT INTO users (name, display_name, email, password_hash, email_verified, oauth_provider, oauth_provider_id)
           VALUES (?, ?, ?, ?, 1, ?, ?)`,
          [finalUsername, displayName.slice(0, 100) || finalUsername, email, randomPasswordHash, 'oidc', sub],
        );

        user = await db.get(
          'SELECT id, name, display_name, email, role, email_verified FROM users WHERE id = ?',
          [result.lastInsertRowid],
        );
        log.auth.info(`Created new OAuth user: ${email} (oidc/${sub})`);
      }
    }

    // Generate JWT
    const token = generateToken(user.id);

    // Store id_token in Redis for RP-Initiated Logout (Keycloak SLO)
    if (tokenResponse.id_token) {
      try {
        await redis.setex(`oidc:id_token:${user.id}`, 7 * 24 * 3600, tokenResponse.id_token);
      } catch (redisErr) {
        log.auth.warn(`Failed to cache OIDC id_token: ${redisErr.message}`);
      }
    }

    // Redirect to frontend callback page with token.
    // Use a hash fragment so the token is never sent to the server (not logged, not in Referer headers).
    const returnTo = stateData.return_to || '/dashboard';
    res.redirect(`${baseUrl}/oauth/callback#token=${encodeURIComponent(token)}&return_to=${encodeURIComponent(returnTo)}`);
  } catch (err) {
    log.auth.error(`OAuth callback error: ${err.message}`);
    const baseUrl = getBaseUrl(req);
    res.redirect(`${baseUrl}/oauth/callback?error=${encodeURIComponent('OAuth authentication failed. Please try again.')}`);
  }
});

export default router;
