/**
 * User store — DC-048.
 *
 * Tracks who is allowed to log in to a DashCaddy instance, and what role each
 * authenticated user has. Replaces the "one implicit operator" model that
 * DC-046/047 shipped with.
 *
 * TWO files (both live under platformPaths.dataDir):
 *
 *   data/users.json            — every user that has ever authenticated.
 *                                Shape: {
 *                                  users: {
 *                                    [userId]: {
 *                                      id, email, displayName, role,
 *                                      createdBy, createdAt,
 *                                      lastLoginAt, lastLoginIp, loginCount
 *                                    }
 *                                  },
 *                                  order: [userId, ...]
 *                                }
 *
 *   data/authorized-users.json — the ALLOWLIST. Emails on this list may log in.
 *                                The bootstrap user (first-ever login) is
 *                                implicitly authorized even if the file is
 *                                empty. Shape: { emails: ["a@x.com", ...] }
 *
 * Bootstrap rule: the FIRST email to ever successfully authenticate is
 * automatically granted role "admin" AND implicitly added to the allowlist.
 * This is recorded by writing a sentinel file `data/.bootstrapped` with the
 * admin email so we never bootstrap twice (e.g. after a restore from backup).
 *
 * Atomic writes: every persistence op writes to a .tmp file then renames.
 * process restart loses nothing in flight because rename is atomic on POSIX.
 *
 * Concurrency: a single in-process mutex serializes mutating ops. We don't
 * need cross-process locks because this API is single-instance by design.
 */

'use strict';

const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const platformPaths = require('../../platform-paths');

const ROLES = Object.freeze({
  ADMIN: 'admin',
  OPERATOR: 'operator',
  VIEWER: 'viewer',
});

// All roles recognized by the system. Used for validation only.
const VALID_ROLES = new Set(Object.values(ROLES));

// Email shape — same pragmatic regex as the email provider.
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

function _nowIso() { return new Date().toISOString(); }
function _isEmail(s) { return typeof s === 'string' && EMAIL_RE.test(s); }

function _defaultUsers() { return { users: {}, order: [] }; }
function _defaultAllowlist() { return { emails: [] }; }

// Coerce a candidate to a writable string dataDir; return null otherwise.
// Used by the factory's resolver to ignore test proxies / function-typed
// values from universal-deps that the `||` short-circuit can't filter.
function _resolveDataDir(opts) {
  const candidates = [
    opts.dataDir,
    opts.platformPaths && opts.platformPaths.dataDir,
    platformPaths && platformPaths.dataDir,
  ];
  for (const c of candidates) {
    if (typeof c === 'string' && c.length > 0) return c;
  }
  return require('os').tmpdir();
}

function _atomicWriteJSON(filePath, data) {
  const tmp = filePath + '.tmp.' + process.pid + '.' + Date.now();
  fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
  fs.renameSync(tmp, filePath);
}

function _readJSON(filePath, fallback) {
  try {
    const raw = fs.readFileSync(filePath, 'utf8');
    return JSON.parse(raw);
  } catch (err) {
    if (err && err.code === 'ENOENT') return fallback;
    // Corrupt file: log and return fallback so the API keeps serving.
    // The next mutation will rewrite the file cleanly.
    return fallback;
  }
}

/**
 * Factory. One user-store per process.
 *
 * @param {Object} [opts]
 * @param {string} [opts.dataDir] — override for tests
 * @param {Object} [opts.log]     — structured logger
 */
function createUserStore(opts = {}) {
  // Resolve dataDir defensively — universal-deps test proxies can return
  // function-typed values for property access, which `||` won't filter.
  const dataDir = _resolveDataDir(opts);
  const log = opts.log || { info() {}, warn() {}, error() {} };

  const usersFile = path.join(dataDir, 'users.json');
  const allowlistFile = path.join(dataDir, 'authorized-users.json');
  const bootstrapSentinel = path.join(dataDir, '.bootstrapped');

  let _mutex = Promise.resolve();

  function _enqueue(fn) {
    const next = _mutex.then(fn, fn);
    // Swallow errors on the chain so one failure doesn't poison subsequent ops.
    _mutex = next.catch(() => {});
    return next;
  }

  function _loadUsers() {
    const data = _readJSON(usersFile, _defaultUsers());
    if (!data.users || typeof data.users !== 'object') data.users = {};
    if (!Array.isArray(data.order)) data.order = Object.keys(data.users);
    return data;
  }

  function _loadAllowlist() {
    const data = _readJSON(allowlistFile, _defaultAllowlist());
    if (!Array.isArray(data.emails)) data.emails = [];
    return data;
  }

  function _saveUsers(data) { _atomicWriteJSON(usersFile, data); }
  function _saveAllowlist(data) { _atomicWriteJSON(allowlistFile, data); }

  function _bootstrapDone() {
    try { return fs.existsSync(bootstrapSentinel); }
    catch { return false; }
  }

  function _writeBootstrapSentinel(adminEmail) {
    _atomicWriteJSON(bootstrapSentinel, {
      bootstrappedAt: _nowIso(),
      adminEmail: adminEmail.toLowerCase(),
    });
  }

  // ── Public API ─────────────────────────────────────────────────────────

  /**
   * Authenticate-or-create a user from an email. Implements the DC-048
   * bootstrap rule and the authorized-users allowlist.
   *
   * Returns one of:
   *   { ok: true,  user, role, isBootstrap }
   *   { ok: false, reason: 'not_authorized' }
   *
   * Reasons:
   *   'not_authorized' — email not in allowlist AND bootstrap already happened.
   *
   * If bootstrap hasn't happened yet (no users file, no .bootstrapped sentinel),
   * the first email that successfully passes shape validation becomes admin
   * AND gets added to the allowlist atomically.
   */
  function login({ email, ip, displayName, createdBy } = {}) {
    return _enqueue(() => {
      if (!_isEmail(email)) {
        return { ok: false, reason: 'invalid_email' };
      }
      const normalized = email.toLowerCase().trim();

      const users = _loadUsers();
      const allowlist = _loadAllowlist();

      // Existing user → just bump login counters.
      const existing = _findUserByEmail(users, normalized);
      if (existing) {
        existing.lastLoginAt = _nowIso();
        existing.lastLoginIp = ip || '';
        existing.loginCount = (existing.loginCount || 0) + 1;
        _saveUsers(users);
        log.info && log.info('user', 'login existing user', {
          userId: existing.id, email: normalized, role: existing.role,
        });
        return { ok: true, user: existing, role: existing.role, isBootstrap: false };
      }

      // New email. Allow if (a) bootstrap hasn't happened, or (b) allowlisted.
      const bootstrapPending = !_bootstrapDone() && users.order.length === 0;
      const onAllowlist = allowlist.emails.includes(normalized);

      if (!bootstrapPending && !onAllowlist) {
        log.info && log.info('user', 'login denied — not on allowlist', { email: normalized });
        return { ok: false, reason: 'not_authorized' };
      }

      // Bootstrap path: first-ever user becomes admin.
      const isBootstrap = bootstrapPending;
      const role = isBootstrap ? ROLES.ADMIN : ROLES.OPERATOR;

      const newUser = {
        id: crypto.randomUUID(),
        email: normalized,
        displayName: displayName || normalized.split('@')[0],
        role,
        createdBy: createdBy || (isBootstrap ? 'bootstrap' : 'invite'),
        createdAt: _nowIso(),
        lastLoginAt: _nowIso(),
        lastLoginIp: ip || '',
        loginCount: 1,
      };
      users.users[newUser.id] = newUser;
      users.order.unshift(newUser.id);

      // If bootstrap: implicitly allowlist + write sentinel.
      if (isBootstrap) {
        if (!allowlist.emails.includes(normalized)) {
          allowlist.emails.push(normalized);
        }
        _saveAllowlist(allowlist);
        _writeBootstrapSentinel(normalized);
      }

      _saveUsers(users);

      log.info && log.info('user', isBootstrap ? 'bootstrap admin created' : 'invited user created', {
        userId: newUser.id, email: normalized, role,
      });

      return { ok: true, user: newUser, role, isBootstrap };
    });
  }

  /**
   * Add an email to the allowlist WITHOUT creating a user record. Used when
   * admin pre-authorizes someone who hasn't logged in yet.
   *
   * Returns { ok: true, alreadyExisted: boolean }.
   */
  function addToAllowlist(email) {
    return _enqueue(() => {
      if (!_isEmail(email)) return { ok: false, reason: 'invalid_email' };
      const normalized = email.toLowerCase().trim();
      const allowlist = _loadAllowlist();
      if (allowlist.emails.includes(normalized)) {
        return { ok: true, alreadyExisted: true };
      }
      allowlist.emails.push(normalized);
      _saveAllowlist(allowlist);
      log.info && log.info('user', 'added to allowlist', { email: normalized });
      return { ok: true, alreadyExisted: false };
    });
  }

  /**
   * Remove an email from the allowlist. Does NOT delete the user record
   * (so the admin can read the login history) — but future logins by that
   * email will be rejected unless bootstrap re-runs (which it won't).
   */
  function removeFromAllowlist(email) {
    return _enqueue(() => {
      if (!_isEmail(email)) return { ok: false, reason: 'invalid_email' };
      const normalized = email.toLowerCase().trim();
      const allowlist = _loadAllowlist();
      const idx = allowlist.emails.indexOf(normalized);
      if (idx === -1) return { ok: true, alreadyRemoved: true };
      allowlist.emails.splice(idx, 1);
      _saveAllowlist(allowlist);
      log.info && log.info('user', 'removed from allowlist', { email: normalized });
      return { ok: true, alreadyRemoved: false };
    });
  }

  /**
   * Update an existing user's role. Role must be in VALID_ROLES.
   * Returns { ok: true } or { ok: false, reason }.
   */
  function setRole(userId, role) {
    return _enqueue(() => {
      if (!VALID_ROLES.has(role)) return { ok: false, reason: 'invalid_role' };
      const users = _loadUsers();
      const u = users.users[userId];
      if (!u) return { ok: false, reason: 'not_found' };
      u.role = role;
      _saveUsers(users);
      log.info && log.info('user', 'role updated', { userId, role });
      return { ok: true };
    });
  }

  /**
   * Delete a user record AND remove from allowlist. Cannot delete the last
   * admin (you'd lock yourself out). Returns { ok: true } or { ok: false, reason }.
   */
  function deleteUser(userId) {
    return _enqueue(() => {
      const users = _loadUsers();
      const u = users.users[userId];
      if (!u) return { ok: false, reason: 'not_found' };

      // Count remaining admins.
      const remainingAdmins = users.order
        .map(id => users.users[id])
        .filter(x => x && x.role === ROLES.ADMIN && x.id !== userId).length;
      if (u.role === ROLES.ADMIN && remainingAdmins === 0) {
        return { ok: false, reason: 'last_admin' };
      }

      delete users.users[userId];
      users.order = users.order.filter(id => id !== userId);

      // Also remove from allowlist so re-invite is a clean slate.
      const allowlist = _loadAllowlist();
      const idx = allowlist.emails.indexOf(u.email);
      if (idx !== -1) {
        allowlist.emails.splice(idx, 1);
        _saveAllowlist(allowlist);
      }

      _saveUsers(users);
      log.info && log.info('user', 'user deleted', { userId, email: u.email });
      return { ok: true };
    });
  }

  function listUsers() {
    return _enqueue(() => {
      const users = _loadUsers();
      return users.order
        .map(id => users.users[id])
        .filter(Boolean);
    });
  }

  /**
   * DC-052: count of users currently on this instance. Used by the
   * license-tier gate (Free = up to 3 users, Pro = unlimited). Counts
   * every user in users.json — including the TOTP-attributed system
   * record (`system@totp.local`) that DC-048 bootstraps on first
   * login. So a brand-new install always starts at count 1 (the host).
   */
  function countUsers() {
    return _enqueue(() => {
      const users = _loadUsers();
      return users.order.length;
    });
  }

  function listAllowlist() {
    return _enqueue(() => {
      const allowlist = _loadAllowlist();
      return [...allowlist.emails];
    });
  }

  function getUser(userId) {
    return _enqueue(() => {
      const users = _loadUsers();
      return users.users[userId] || null;
    });
  }

  function getUserByEmail(email) {
    return _enqueue(() => {
      if (!_isEmail(email)) return null;
      const users = _loadUsers();
      return _findUserByEmail(users, email.toLowerCase().trim()) || null;
    });
  }

  function isBootstrapComplete() {
    return _enqueue(() => _bootstrapDone());
  }

  /**
   * Helper for the auth system: given an email, return whether the user
   * is allowed to attempt login (allowlist OR bootstrap-pending). Used by
   * the email provider's `authorizedEmails()` dependency.
   */
  function isEmailAuthorized(email) {
    return _enqueue(() => {
      if (!_isEmail(email)) return false;
      const normalized = email.toLowerCase().trim();
      const allowlist = _loadAllowlist();
      if (allowlist.emails.includes(normalized)) return true;
      const users = _loadUsers();
      // Bootstrap path: if no users yet, the first login is implicitly allowed.
      return users.order.length === 0 && !_bootstrapDone();
    });
  }

  function _findUserByEmail(users, normalizedEmail) {
    for (const id of users.order) {
      const u = users.users[id];
      if (u && u.email === normalizedEmail) return u;
    }
    return null;
  }

  return {
    login,
    addToAllowlist,
    removeFromAllowlist,
    setRole,
    deleteUser,
    listUsers,
    countUsers,
    listAllowlist,
    getUser,
    getUserByEmail,
    isBootstrapComplete,
    isEmailAuthorized,
    // Constants for callers
    ROLES,
    VALID_ROLES,
  };
}

module.exports = { createUserStore, ROLES, VALID_ROLES };