/**
 * services/trustedDevice.js
 *
 * "Remember this device" cookie policy. After a successful 2FA
 * verify, we issue a signed cookie containing a high-entropy random
 * token. The token's hash is stored in the DB; the plaintext only
 * ever lives in the browser cookie. Subsequent admin requests
 * check the cookie, look up the token hash, and if it's a live
 * trusted device we mark the session as 2FA-verified (one hour
 * rolling window) so the user doesn't have to re-enter the code.
 *
 * Why this matters for deploys:
 *   - data/sessions.sqlite is excluded from the deploy zip, so a
 *     deploy wipes the live session file. Without a trusted-device
 *     cookie, the operator has to log in + re-2FA after every push.
 *     With the cookie, the next admin request re-verifies them
 *     automatically (cookie is client-side, survives deploys).
 *
 * Security:
 *   - Token = 32 bytes from crypto.randomBytes(64) hex. Not brute-
 *     forceable.
 *   - Cookie is HttpOnly + SameSite=Strict (defence against CSRF
 *     and XSS) + Secure in production.
 *   - Token plaintext never persisted: only the sha256 hash.
 *   - DB leak doesn't give an attacker a usable token.
 *   - Logout + password change call removeAllForUser() so the
 *     token can't be used after credential rotation.
 *   - Default expiry 30 days. Operator can revoke individual
 *     devices from /admin (Trusted devices panel).
 */

'use strict';

const crypto = require('crypto');
const TrustedDevice = require('../models/trustedDevice');

const COOKIE_NAME = 'n2fa_trust';
const DEFAULT_TTL_DAYS = 30;
// Hard cap — even if someone passes 365, we never give a cookie
// longer than 365 days. Defends against a misconfigured caller
// accidentally creating a "remember forever" cookie.
const MAX_TTL_DAYS = 365;

function makeToken() {
  return crypto.randomBytes(32).toString('hex');
}

function expiryFromDays(days) {
  const safe = Math.max(1, Math.min(parseInt(days, 10) || DEFAULT_TTL_DAYS, MAX_TTL_DAYS));
  return Date.now() + safe * 24 * 60 * 60 * 1000;
}

/**
 * Issue a trusted-device cookie + DB row. Returns the token (so
 * the caller can set the cookie) and the row (for auditing).
 */
function issue(req, res, { user_id, ttlDays = DEFAULT_TTL_DAYS, remember = true } = {}) {
  if (!remember) return null;
  if (!user_id) throw new Error('trustedDevice.issue: user_id required');
  const token = makeToken();
  const expires_at = expiryFromDays(ttlDays);
  const row = TrustedDevice.create({
    user_id,
    token,
    user_agent: (req.get('user-agent') || '').slice(0, 256),
    ip_first_seen: req.ip,
    expires_at,
  });
  res.cookie(COOKIE_NAME, token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    expires: new Date(expires_at),
    path: '/',
  });
  return { row, token };
}

/**
 * Check the request's cookie against the DB. If valid, returns the
 * trusted-device row and touches last_used_at. Returns null if
 * missing, malformed, expired, or unknown.
 */
function check(req) {
  const token = req.cookies && req.cookies[COOKIE_NAME];
  if (!token) return null;
  // Token must look like 64 hex chars — prevents hitting the DB
  // with garbage from a malformed cookie.
  if (!/^[a-f0-9]{64}$/i.test(token)) {
    // Also clear the bad cookie so the browser stops sending it.
    if (req.res && typeof req.res.clearCookie === 'function') {
      req.res.clearCookie(COOKIE_NAME, { path: '/' });
    }
    return null;
  }
  const row = TrustedDevice.getByToken(token);
  if (!row) return null;
  if (row.expires_at < Date.now()) return null;
  // Fire-and-forget touch
  try { TrustedDevice.touch(row.id); } catch (_) {}
  return row;
}

/**
 * Clear the cookie on the response. Does NOT delete the DB row —
 * the caller does that explicitly via remove(). Use for "log out
 * of this device only" flows; for "log out everywhere" use
 * removeAllForUser() too.
 */
function clearCookie(res) {
  res.clearCookie(COOKIE_NAME, { path: '/' });
}

module.exports = {
  COOKIE_NAME,
  DEFAULT_TTL_DAYS,
  MAX_TTL_DAYS,
  issue,
  check,
  clearCookie,
  expiryFromDays,
};