/**
 * services/sessions.js
 * Active session management — list, describe, and revoke sessions
 * for a given user. Reads from the same SQLite store the
 * express-session middleware uses.
 *
 * The session store (`connect-sqlite3`) gives us a sessionStore
 * object with `.all(cb)` and `.destroy(sid, cb)`, but the `.all`
 * method drops the `sid` column when it parses the JSON. So we
 * open a direct read-only handle to the sessions DB and do our
 * own query — `json_extract(sess, '$.userId')` lets us filter at
 * the SQL level (SQLite 3.38+).
 */
'use strict';

const Database = require('better-sqlite3');
const path = require('path');

const SESSIONS_DB = process.env.SESSION_DB_DIR
  ? path.join(process.env.SESSION_DB_DIR, 'sessions.sqlite')
  : path.join(__dirname, '..', 'data', 'sessions.sqlite');

/**
 * Parse a User-Agent string into a short human label.
 * Examples:
 *   "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15
 *    (KHTML, like Gecko) Version/17.0 Safari/605.1.15"  → "Safari on macOS"
 *   "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)..."       → "Mobile Safari on iOS"
 *   "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/119.0..."          → "Chrome on Windows"
 */
function parseUserAgent(ua) {
  if (!ua) return { device: 'Unknown device', os: 'Unknown OS' };
  const u = String(ua);
  // Detect device
  let device = 'Desktop';
  if (/iPhone/.test(u))                       device = 'iPhone';
  else if (/iPad/.test(u))                    device = 'iPad';
  else if (/Android/.test(u)) {
    if (/Mobile/.test(u))                     device = 'Android phone';
    else                                      device = 'Android tablet';
  }
  else if (/Macintosh/.test(u))               device = 'Mac';
  else if (/Windows/.test(u))                 device = 'PC';
  else if (/Linux/.test(u))                   device = 'Linux';
  // Detect OS
  let os = '';
  if (/iPhone OS (\d+)[._]/.test(u))           os = 'iOS ' + RegExp.$1.replace(/_/g, '.');
  else if (/iPad.*OS (\d+)[._]/.test(u))       os = 'iPadOS ' + RegExp.$1.replace(/_/g, '.');
  else if (/Android (\d+)/.test(u))           os = 'Android ' + RegExp.$1;
  else if (/Mac OS X (\d+)[._]/.test(u))      os = 'macOS ' + RegExp.$1.replace(/_/g, '.');
  else if (/Windows NT (\d+\.\d+)/.test(u))   os = 'Windows ' + ({'10.0':'10/11','6.3':'8.1','6.2':'8','6.1':'7'}[RegExp.$1] || RegExp.$1);
  else if (/Linux/.test(u))                   os = 'Linux';
  // Detect browser
  let browser = '';
  if (/Edg\//.test(u))                       browser = 'Edge';
  else if (/OPR\/|Opera/.test(u))             browser = 'Opera';
  else if (/Firefox\//.test(u))               browser = 'Firefox';
  else if (/Chrome\//.test(u) && !/Chromium/.test(u)) browser = 'Chrome';
  else if (/Safari\//.test(u) && !/Chrome/.test(u)) browser = 'Safari';
  else if (/curl|wget|python/i.test(u))       browser = 'API client';
  const label = browser
    ? (os ? `${browser} on ${os}` : browser)
    : (os || device);
  return { device, os, browser, label };
}

/**
 * List active sessions for a user. Returns each session with:
 *   - sid:            the session ID (server-side, for revoke)
 *   - created_at:     ms timestamp when the session was first persisted
 *   - last_seen_at:   ms timestamp of the most recent activity (cookie.expires)
 *   - expires_at:     ms timestamp when the session will be reaped
 *   - ip:             IP recorded at login (if we tracked it — see below)
 *   - user_agent:     raw User-Agent string
 *   - device:         { device, os, browser, label } parsed label
 *   - is_current:     true if this sid matches the current request's session
 *   - password_at:    the password_changed_at stamped on this session
 *
 * To avoid a second backfill migration, `ip` is taken from the
 * session's known_ips if available; otherwise null. The most
 * reliable signal for "where is this session from" is the
 * IP+UA captured on login (already in user_known_ips keyed by
 * user_id, but not by session). For now we surface UA and the
 * cookie expiration — the IP is best-effort.
 */
function listForUser(userId, currentSid) {
  const db = new Database(SESSIONS_DB, { readonly: true });
  let rows;
  try {
    rows = db.prepare(`
      SELECT sid, expired,
             json_extract(sess, '$.userId')     AS uid,
             json_extract(sess, '$.cookie')     AS cookie,
             json_extract(sess, '$.created_ip') AS ip,
             json_extract(sess, '$.created_ua') AS ua,
             json_extract(sess, '$.created_at') AS created_at,
             json_extract(sess, '$.password_at') AS password_at,
             json_extract(sess, '$.created_geo') AS geo
        FROM sessions
       WHERE json_extract(sess, '$.userId') = ?
         AND (expired IS NULL OR expired > ?)
    `).all(userId, Date.now());
  } finally {
    db.close();
  }
  // Try to enrich with fresh geo data (the stored geo may be stale or
  // missing if a session predates the geo feature). The lookup is
  // cached, so this is cheap even for many sessions.
  const geo = require('./geo');
  return rows.map(r => {
    let cookie = null;
    try { cookie = r.cookie ? JSON.parse(r.cookie) : null; } catch (_) {}
    const ua = r.ua || '';
    const device = parseUserAgent(ua);
    let geoData = null;
    try { geoData = r.geo ? JSON.parse(r.geo) : null; } catch (_) { geoData = null; }
    if (!geoData && r.ip) geoData = geo.lookup(r.ip);  // backfill
    return {
      sid: r.sid,
      created_at:  r.created_at || null,
      last_seen_at: r.expired ? r.expired - (cookie && cookie.originalMaxAge || 0) : null,
      expires_at:   r.expired,
      ip:            r.ip || null,
      user_agent:    ua,
      device:        device,
      geo:           geoData,
      is_current:    r.sid === currentSid,
      password_at:   r.password_at || null,
    };
  });
}

/**
 * Revoke a specific session by sid. Verifies the session belongs to
 * the user first so a user can't revoke someone else's session.
 */
function revoke(userId, sid, cb) {
  const db = new Database(SESSIONS_DB, { readonly: true });
  let owner;
  try {
    owner = db.prepare(`
      SELECT json_extract(sess, '$.userId') AS uid
        FROM sessions WHERE sid = ?
    `).get(sid);
  } finally {
    db.close();
  }
  if (!owner || owner.uid !== userId) {
    return cb(new Error('SESSION_NOT_FOUND_OR_NOT_OWNED'));
  }
  // Delete directly — bypasses the store, but the store's `destroy` is
  // just a thin wrapper around the same SQL.
  const db2 = new Database(SESSIONS_DB);
  try {
    db2.prepare('DELETE FROM sessions WHERE sid = ?').run(sid);
    cb(null, { revoked: 1 });
  } catch (e) {
    cb(e);
  } finally {
    db2.close();
  }
}

/**
 * Revoke every session for the user EXCEPT the current one.
 * Returns the number of sessions revoked.
 */
function revokeOthers(userId, currentSid, cb) {
  const db = new Database(SESSIONS_DB);
  let n = 0;
  try {
    const info = db.prepare(`
      DELETE FROM sessions
       WHERE json_extract(sess, '$.userId') = ?
         AND sid != ?
    `).run(userId, currentSid);
    n = info.changes;
  } catch (e) {
    return cb(e);
  } finally {
    db.close();
  }
  cb(null, { revoked: n });
}

module.exports = { listForUser, revoke, revokeOthers, parseUserAgent };
