'use strict';

/**
 * src/auth.js — аутентификация и авторизация.
 *
 * Два режима работы (определяются по наличию KC_CLIENT_ID + KC_CLIENT_SECRET):
 *
 *   OIDC-режим  — Authorization Code Flow через Keycloak.
 *                 Токен обменивается на /callback, пользователь сохраняется в сессии.
 *
 *   Mock-режим  — локальная RSA-пара, JWT выдаётся при логине.
 *                 Пользователь выбирается на /login из MOCK_USERS и тоже хранится в сессии.
 *
 * Dev-backdoor (DEV_MODE=true или DEV_SECRET задан):
 *   /dev-login — форма выбора произвольного пользователя, в обход OIDC/mock.
 *   DEV_MODE=true → доступен без пароля (локальная разработка).
 *   DEV_SECRET=xyz → доступен с ?key=xyz (staging/prod тестирование).
 *
 * Экспортируемые функции:
 *   initAuth()      → объект auth с middleware, OIDC-хелперами и флагами
 *   requireAdmin()  → middleware для admin-only маршрутов
 */

const jwt    = require('jsonwebtoken');
const crypto = require('crypto');
const https  = require('https');
const http   = require('http');

// ── Конфигурация из окружения ────────────────────────────────────────────────
const ISSUER           = process.env.JWT_ISSUER        || 'mock-auth-api';
const ADMIN_CLIENT_ID  = process.env.ADMIN_CLIENT_ID   || 'WZ01112';

const KC_BASE_URL      = (process.env.KC_BASE_URL || 'https://keycloak.nubes.ru/realms/cloud').replace(/\/$/, '');
const KC_CLIENT_ID     = process.env.KC_CLIENT_ID     || '';
const KC_CLIENT_SECRET = process.env.KC_CLIENT_SECRET || '';
const APP_URL          = (process.env.APP_URL || 'http://localhost:3000').replace(/\/$/, '');

// DEV_MODE и DEV_SECRET читаются внутри initAuth() — иначе при тестах
// значение фиксируется при первом require() и последующее присвоение process.env
// не влияет на уже загруженный модуль (кеш Node.js).

// OIDC endpoints (Keycloak realm cloud)
const OIDC = {
  auth:   KC_BASE_URL + '/protocol/openid-connect/auth',
  token:  KC_BASE_URL + '/protocol/openid-connect/token',
  certs:  KC_BASE_URL + '/protocol/openid-connect/certs',
  logout: KC_BASE_URL + '/protocol/openid-connect/logout',
};

// ── Кеши (инициализируются в initAuth) ─────────────────────────────────────
let mockKeyPair = null;
let mockJwks    = null;
let cachedJwks  = null; // OIDC public keys

// ── initAuth ────────────────────────────────────────────────────────────────
async function initAuth() {
  // Читаем здесь — не на уровне модуля, чтобы тесты могли менять process.env
  // до вызова initAuth() и видеть актуальные значения.
  const devMode   = process.env.DEV_MODE   === 'true';
  const devSecret = process.env.DEV_SECRET || '';

  // DEV_MODE=true недопустим в production — жёсткая защита от случайного деплоя.
  if (process.env.NODE_ENV === 'production' && devMode) {
    throw new Error('DEV_MODE=true запрещён в production (NODE_ENV=production)');
  }

  const isOidc = !!(KC_CLIENT_ID && KC_CLIENT_SECRET);

  if (isOidc) {
    console.log('[auth] OIDC — Keycloak', KC_BASE_URL);
    // Предзагрузка JWKS при старте — избегаем задержки на первом запросе.
    // При ошибке логируем предупреждение: JWKS будет загружен в exchangeCode().
    try {
      cachedJwks = JSON.parse(await httpGet(OIDC.certs));
      console.log('[auth] JWKS loaded:', cachedJwks.keys.length, 'ключей');
    } catch (e) {
      console.warn('[auth] JWKS preload failed:', e.message, '— будет загружен при первом запросе');
    }
  } else {
    console.log('[auth] MOCK — local RS256 key');
    mockKeyPair = crypto.generateKeyPairSync('rsa', {
      modulusLength: 2048,
      publicKeyEncoding:  { type: 'spki',  format: 'pem' },
      privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
    });
    const jwkPub = crypto.createPublicKey(mockKeyPair.publicKey).export({ format: 'jwk' });
    Object.assign(jwkPub, { alg: 'RS256', use: 'sig', kid: 'mock-1' });
    mockJwks = { keys: [jwkPub] };
  }

  if (devMode)   console.log('[auth] DEV_MODE — /dev-login доступен без пароля');
  if (devSecret) console.log('[auth] DEV_SECRET задан — /dev-login с ключом');

  return {
    isOidc,
    middleware:       createMiddleware(isOidc),
    bearerMiddleware:  createBearerMiddleware(isOidc),
    // OIDC-хелперы (используются в src/routes/auth.js)
    buildAuthUrl,
    exchangeCode,
    buildLogoutUrl,
    // Mock JWT (для API-клиентов если нужен bearer-токен)
    issueMockToken,
    // Флаги для routes/auth.js
    devLoginEnabled: devMode || !!devSecret,
    DEV_SECRET:      devSecret,
    // JWKS endpoint (только в mock-режиме)
    jwksHandler: !isOidc ? (req, res) => res.json(mockJwks) : null,
  };
}

// ── Middleware — проверяет req.session.user или Bearer-токен ─────────────────
function createMiddleware(isOidc) {
  return function authMiddleware(req, res, next) {
    // 1. Сессия — основной источник пользователя (OIDC и mock)
    if (req.session && req.session.user) {
      req.user = req.session.user;
      return next();
    }

    // 2. Bearer-токен из заголовка (для API-клиентов без браузерной сессии)
    const bearer = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
    if (bearer) {
      try {
        const payload = isOidc ? verifyOidcToken(bearer) : verifyMockToken(bearer);
        req.user = userFromPayload(payload);
        return next();
      } catch (e) {
        return res.status(401).json({ error: 'Invalid token: ' + e.message });
      }
    }

    // 3. Устаревший jwt-cookie — плавная миграция: переносим в сессию
    if (!isOidc && req.cookies && req.cookies.jwt) {
      try {
        const payload = verifyMockToken(req.cookies.jwt);
        req.user = userFromPayload(payload);
        if (req.session) {
          req.session.user = req.user;
          res.clearCookie('jwt');
        }
        return next();
      } catch (_) {
        res.clearCookie('jwt');
      }
    }

    // 4. Не аутентифицирован → редирект (GET) или 401 (остальное)
    if (req.method === 'GET') {
      return res.redirect('/login?returnTo=' + encodeURIComponent(req.originalUrl));
    }
    return res.status(401).send('Unauthorized');
  };
}

// ── OIDC — redirect URL для Keycloak ────────────────────────────────────────
function buildAuthUrl(state) {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id:     KC_CLIENT_ID,
    redirect_uri:  APP_URL + '/callback',
    scope:         'openid profile email',
    state,
  });
  return OIDC.auth + '?' + params;
}

// ── OIDC — обмен authorization code → токены, возвращает { user, idToken, refreshToken }
async function exchangeCode(code) {
  const body = new URLSearchParams({
    grant_type:    'authorization_code',
    client_id:     KC_CLIENT_ID,
    client_secret: KC_CLIENT_SECRET,
    redirect_uri:  APP_URL + '/callback',
    code,
  }).toString();

  const raw  = await httpPost(OIDC.token, body, { 'Content-Type': 'application/x-www-form-urlencoded' });
  const data = JSON.parse(raw);
  if (!data.access_token) throw new Error('No access_token: ' + raw.slice(0, 200));

  // cachedJwks загружается в initAuth(); при провале preload — подгружаем здесь.
  if (!cachedJwks) cachedJwks = JSON.parse(await httpGet(OIDC.certs));
  const payload = verifyOidcToken(data.access_token);

  return {
    user:         userFromPayload(payload),
    idToken:      data.id_token      || null,
    refreshToken: data.refresh_token || null,
  };
}

// ── OIDC — URL для logout на Keycloak ───────────────────────────────────────
function buildLogoutUrl(idToken) {
  const params = new URLSearchParams({ post_logout_redirect_uri: APP_URL + '/login' });
  if (idToken) params.set('id_token_hint', idToken);
  return OIDC.logout + '?' + params;
}

// ── Mock — выпустить JWT (для API-клиентов) ──────────────────────────────────
function issueMockToken(claims) {
  if (!mockKeyPair) throw new Error('Mock keypair not initialized');
  return jwt.sign(
    { ClientID: claims.clientId, company_id: claims.companyId, company_name: claims.companyName, email: claims.email, login: claims.email },
    mockKeyPair.privateKey,
    { algorithm: 'RS256', issuer: ISSUER, subject: claims.companyId || 'mock', expiresIn: '24h', keyid: 'mock-1' }
  );
}

// ── Внутренние утилиты ───────────────────────────────────────────────────────
function verifyMockToken(token) {
  if (!mockKeyPair) throw new Error('Mock keypair not initialized');
  return jwt.verify(token, mockKeyPair.publicKey, { algorithms: ['RS256'], issuer: ISSUER });
}

function verifyOidcToken(token) {
  if (!cachedJwks) throw new Error('JWKS not loaded yet');

  // Декодируем заголовок токена для выбора ключа по kid.
  // base64url безопаснее base64: обрабатывает '-' и '_' без паддинга.
  const headerRaw = token.split('.')[0];
  let header;
  try {
    header = JSON.parse(Buffer.from(headerRaw, 'base64url').toString());
  } catch (e) {
    throw new Error('Malformed JWT header');
  }
  const kid = header.kid;

  // Ищем ключ по kid (точный match) или первый RSA-sig ключ (если kid не указан).
  const key = kid
    ? cachedJwks.keys.find(k => k.kid === kid && k.kty === 'RSA' && k.use === 'sig')
    : cachedJwks.keys.find(k => k.kty === 'RSA' && k.use === 'sig');
  if (!key) throw new Error('No matching RSA sig key in JWKS' + (kid ? ` (kid=${kid})` : ''));

  const pem = crypto.createPublicKey({ key, format: 'jwk' }).export({ type: 'spki', format: 'pem' });
  // Проверяем issuer и audience — защита от токенов других realm/клиентов (A02).
  return jwt.verify(token, pem, {
    algorithms: ['RS256'],
    issuer:     KC_BASE_URL,
    audience:   KC_CLIENT_ID,
  });
}

function userFromPayload(payload) {
  // clientid может быть "WZ01325" или "WZ01325, WZ54321" (ТЗ-плюс §6.1)
  const rawClientId = payload.ClientID || payload.client_id || payload.preferred_username || 'UNKNOWN';
  const allClientIds = rawClientId.split(',').map(s => s.trim()).filter(Boolean);
  const activeClientId = allClientIds[0] || 'UNKNOWN';

  return {
    email:          payload.email || payload.login || 'unknown',
    clientId:       activeClientId,                      // активная компания (первая в списке)
    allClientIds,                                         // все компании пользователя (массив)
    activeClientId,                                       // дубль для ясности
    companyId:      payload.company_id || payload.sub || null,
    companyName:    payload.company_name || activeClientId,
    isAdmin:        activeClientId === ADMIN_CLIENT_ID,
  };
}

// ── Bearer-only middleware (для /api/v1) ────────────────────────────────────
// Не смотрит на сессию — всегда проверяет Bearer JWT явно.
// Используется в src/api/index.js вместо auth.middleware.
function createBearerMiddleware(isOidc) {
  return function bearerAuthMiddleware(req, res, next) {
    const bearer = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
    if (!bearer) {
      return res.status(401).json({ error: 'Bearer token required' });
    }
    try {
      const payload = isOidc ? verifyOidcToken(bearer) : verifyMockToken(bearer);
      req.user = userFromPayload(payload);
      return next();
    } catch (e) {
      return res.status(401).json({ error: 'Invalid token: ' + e.message });
    }
  };
}

// ── requireAdmin middleware ──────────────────────────────────────────────────
/**
 * Middleware-защита для admin-only маршрутов.
 * Использовать после app.use(auth.middleware):
 *   router.get('/admin', requireAdmin, handler)
 */
function requireAdmin(req, res, next) {
  if (!req.user || !req.user.isAdmin) {
    return res.status(403).send('403 Forbidden: требуется роль администратора');
  }
  next();
}

// ── HTTP-утилиты (без внешних зависимостей) ─────────────────────────────────
function httpGet(url) {
  return new Promise((resolve, reject) => {
    const lib = url.startsWith('https') ? https : http;
    const req = lib.get(url, { timeout: 5000 }, (res) => {
      let data = '';
      res.on('data', c => { data += c; });
      res.on('end', () => res.statusCode === 200 ? resolve(data) : reject(new Error('HTTP ' + res.statusCode)));
    });
    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
  });
}

function httpPost(url, body, headers) {
  return new Promise((resolve, reject) => {
    const u   = new URL(url);
    const lib = u.protocol === 'https:' ? https : http;
    const opts = {
      hostname: u.hostname,
      port:     u.port || (u.protocol === 'https:' ? 443 : 80),
      path:     u.pathname + u.search,
      method:   'POST',
      headers:  { ...headers, 'Content-Length': Buffer.byteLength(body) },
      timeout:  5000,
    };
    const req = lib.request(opts, (res) => {
      let data = '';
      res.on('data', c => { data += c; });
      res.on('end', () => {
        if (res.statusCode >= 200 && res.statusCode < 300) resolve(data);
        else reject(new Error('HTTP ' + res.statusCode + ': ' + data.slice(0, 200)));
      });
    });
    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
    req.write(body);
    req.end();
  });
}

/**
 * Валидирует redirect-цель: разрешает только локальные пути.
 * Защита от open redirect: //evil.com и https://evil.com → '/'.
 */
function safeReturn(target) {
  if (typeof target === 'string' && target.startsWith('/') && !target.startsWith('//')) {
    return target;
  }
  return '/';
}

module.exports = { initAuth, requireAdmin, safeReturn };
