/**
 * Admin + me routes — DC-048.
 *
 * Mounted at /api/v1/auth. All `/admin/*` routes require the session to
 * belong to a user with role 'admin'. `/me` requires any authenticated session.
 *
 * Endpoints:
 *   GET    /me                                  — current user (id, email, role, isAdmin)
 *   GET    /admin/users                         — list all users
 *   POST   /admin/users                         — pre-authorize an email (allowlist)
 *   PATCH  /admin/users/:id                     — change a user's role
 *   DELETE /admin/users/:id                     — delete user + remove from allowlist
 *   GET    /admin/allowlist                     — list authorized emails
 *   GET    /admin/invites                       — list outstanding invites
 *   POST   /admin/invites                       — issue a new invite (returns raw token ONCE)
 *   DELETE /admin/invites/:id                   — revoke an invite
 *
 *   POST   /invites/accept                      — PUBLIC — redeem an invite token,
 *                                                 create user, set session cookie
 *   GET    /invites/:token                      — PUBLIC — peek at an invite (email,
 *                                                 role, expires) without consuming it.
 */

'use strict';

const express = require('express');
const path = require('path');
const platformPaths = require('../../platform-paths');
const { createUserStore } = require('../../src/security/user-store');
const { createInviteStore } = require('../../src/security/invite-store');
const emailSender = require('../../src/auth/providers/email-sender');
const AuthProvider = require('../../src/auth/providers/base');
const { ValidationError, NotFoundError, ForbiddenError, PaymentRequiredError } = require('../../src/utilities/errors');
const { ok, successMessage } = require('../../src/utils/responses');

function _requireAdmin(req, _res, next) {
  if (!req.user || req.user.role !== 'admin') {
    return next(new ForbiddenError('Admin role required'));
  }
  next();
}

/**
 * DC-052: license-tier gate for user-creation endpoints.
 *
 * Free = up to 3 users total. Pro = unlimited. When the count would
 * exceed the cap and the host isn't Pro, throw a PaymentRequiredError
 * so the caller knows exactly what to do. The error message names the
 * tier name ("Pro") so the upsell is clear.
 *
 * NOTE: passes through when the userStore isn't mounted (single-user
 * installs without email auth — those don't even have /admin/*).
 */
async function _requireProIfUserLimitReached(req, _res, next) {
  try {
    const licenseManager = req.app.locals && req.app.locals.licenseManager;
    if (!licenseManager || typeof licenseManager.isPro !== 'function') return next();
    if (licenseManager.isPro()) return next();
    const userStore = req.app.locals && req.app.locals.userStore;
    if (!userStore || typeof userStore.countUsers !== 'function') return next();
    const count = await userStore.countUsers();
    if (count >= 3) {
      return next(new PaymentRequiredError(
        'Free tier supports up to 3 users. Upgrade to Pro for unlimited users.'
      ));
    }
    next();
  } catch (e) {
    next(e);
  }
}

function _buildEmailText({ acceptUrl, ttlHours, role }) {
  return [
    'Hi,',
    '',
    'You\'ve been invited to join a DashCaddy instance as a ' + role + '.',
    'Click the link below within ' + ttlHours + ' hours to accept:',
    '',
    acceptUrl,
    '',
    'This link is single-use. If you weren\'t expecting this invitation,',
    'you can safely ignore this email.',
    '',
    '— DashCaddy',
  ].join('\n');
}

function _buildEmailHtml({ acceptUrl, ttlHours, role }) {
  return [
    '<!doctype html><html><body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;">',
    '<h2 style="margin:0 0 12px">You\'re invited to DashCaddy</h2>',
    '<p>You\'ve been invited to join as <strong>' + role + '</strong>.</p>',
    '<p>Click the button below within ' + ttlHours + ' hours to accept:</p>',
    '<p style="margin:24px 0"><a href="' + acceptUrl + '" style="background:#1f2937;color:#fff;padding:10px 16px;border-radius:6px;text-decoration:none;display:inline-block">Accept invitation</a></p>',
    '<p style="color:#6b7280;font-size:12px">If the button doesn\'t work, paste this link into your browser:<br><span style="word-break:break-all">' + acceptUrl + '</span></p>',
    '<p style="color:#6b7280;font-size:12px">If you weren\'t expecting this, you can ignore this email.</p>',
    '</body></html>',
  ].join('\n');
}

module.exports = function({ asyncHandler, errorResponse, log, session, dataDir }) {
  const router = express.Router();
  // user-store / invite-store handle their own defensive dataDir resolution
  // (they ignore Proxy/function values from universal-deps test deps).
  const resolvedDataDir = dataDir || (platformPaths && platformPaths.dataDir);
  const userStore = createUserStore({ dataDir: resolvedDataDir, log });
  const inviteStore = createInviteStore({ dataDir: resolvedDataDir, log });

  // ── /me ───────────────────────────────────────────────────────────────

  router.get('/me', asyncHandler(async (req, res) => {
    if (!req.user || !req.user.id) {
      // Legacy session without user attribution. Return the bare role
      // (defaults to admin for backwards-compat) but signal via
      // `legacy: true` so the UI knows.
      return ok(res, {
        user: null,
        authenticated: session ? session.isSessionValid(req) : false,
        role: 'admin', // legacy: assume operator-level access
        legacy: true,
      });
    }
    const stored = await userStore.getUser(req.user.id);
    return ok(res, {
      user: stored
        ? {
            id: stored.id,
            email: stored.email,
            displayName: stored.displayName,
            role: stored.role,
            isAdmin: stored.role === 'admin',
            createdAt: stored.createdAt,
            lastLoginAt: stored.lastLoginAt,
            loginCount: stored.loginCount,
          }
        : null,
      authenticated: true,
      role: req.user.role,
      legacy: false,
    });
  }, 'auth-me'));

  // ── /admin/users ──────────────────────────────────────────────────────

  router.get('/admin/users', _requireAdmin, asyncHandler(async (_req, res) => {
    const users = await userStore.listUsers();
    return ok(res, { users });
  }, 'auth-admin-users-list'));

  router.post('/admin/users', _requireAdmin, _requireProIfUserLimitReached, asyncHandler(async (req, res) => {
    const { email, role } = req.body || {};
    if (!email) throw new ValidationError('email is required', 'email');
    if (role && !userStore.VALID_ROLES.has(role)) {
      throw new ValidationError('Invalid role', 'role');
    }
    const result = await userStore.addToAllowlist(email);
    if (!result.ok) throw new ValidationError(result.reason, 'email');
    // If a role was provided AND the user already exists, also set the role.
    if (role) {
      const existing = await userStore.getUserByEmail(email);
      if (existing) {
        await userStore.setRole(existing.id, role);
      }
    }
    return ok(res, {
      email: email.toLowerCase(),
      alreadyExisted: result.alreadyExisted,
    });
  }, 'auth-admin-users-create'));

  router.patch('/admin/users/:id', _requireAdmin, asyncHandler(async (req, res) => {
    const { role } = req.body || {};
    if (!role || !userStore.VALID_ROLES.has(role)) {
      throw new ValidationError('Invalid role', 'role');
    }
    const result = await userStore.setRole(req.params.id, role);
    if (!result.ok) {
      throw result.reason === 'not_found'
        ? new NotFoundError('User not found')
        : new ValidationError(result.reason, 'role');
    }
    return successMessage(res, 'Role updated');
  }, 'auth-admin-users-update'));

  router.delete('/admin/users/:id', _requireAdmin, asyncHandler(async (req, res) => {
    const result = await userStore.deleteUser(req.params.id);
    if (!result.ok) {
      if (result.reason === 'not_found') throw new NotFoundError('User not found');
      if (result.reason === 'last_admin') {
        throw new ValidationError('Cannot delete the last admin');
      }
      throw new ValidationError(result.reason);
    }
    return successMessage(res, 'User deleted');
  }, 'auth-admin-users-delete'));

  // ── /admin/allowlist ──────────────────────────────────────────────────

  router.get('/admin/allowlist', _requireAdmin, asyncHandler(async (_req, res) => {
    const emails = await userStore.listAllowlist();
    return ok(res, { emails });
  }, 'auth-admin-allowlist'));

  // ── /admin/invites ────────────────────────────────────────────────────

  router.get('/admin/invites', _requireAdmin, asyncHandler(async (_req, res) => {
    const invites = await inviteStore.listOutstanding();
    return ok(res, { invites });
  }, 'auth-admin-invites-list'));

  router.post('/admin/invites', _requireAdmin, _requireProIfUserLimitReached, asyncHandler(async (req, res) => {
    const { email, role, ttlHours, sendEmail } = req.body || {};
    if (!email) throw new ValidationError('email is required', 'email');
    const ttlMs = (typeof ttlHours === 'number' && ttlHours > 0 && ttlHours <= 168)
      ? ttlHours * 60 * 60 * 1000
      : inviteStore.DEFAULT_TTL_MS;
    const invitedBy = (req.user && req.user.email) || 'admin';
    const issued = await inviteStore.issue({
      email,
      role: (role && userStore.VALID_ROLES.has(role)) ? role : 'operator',
      ttlMs,
      invitedBy,
    });
    if (!issued.ok) throw new ValidationError(issued.reason, 'email');

    // Build the accept URL once — used both for the response and for email delivery.
    const baseUrl = (req.app.locals && req.app.locals.siteConfig && req.app.locals.siteConfig.publicBaseUrl
      ? req.app.locals.siteConfig.publicBaseUrl.replace(/\/+$/, '')
      : ((req.headers['x-forwarded-proto'] || req.protocol || 'https') + '://' +
         (req.headers['x-forwarded-host'] || req.headers.host || 'localhost:3001')));
    const acceptUrl = baseUrl + '/api/v1/auth/invites/' + encodeURIComponent(issued.token) + '/accept';

    // DC-085: link-first delivery. Default = no email, just hand the link back.
    // Operators opt INTO email by sending { sendEmail: true } (or the admin UI
    // checks the "Send email" checkbox). When SMTP is unconfigured AND the
    // operator did opt in, we surface the failure as `deliveredVia: 'failed'`
    // but NEVER leak the raw token into the server log — the link is already
    // in the response, so the operator has a UI-side fallback.
    let deliveredVia = 'manual';
    if (sendEmail === true) {
      const ttlHoursOut = Math.round(issued.ttlMs / (60 * 60 * 1000));
      const text = _buildEmailText({ acceptUrl, ttlHours: ttlHoursOut, role: issued.role });
      const html = _buildEmailHtml({ acceptUrl, ttlHours: ttlHoursOut, role: issued.role });
      try {
        const smtpConfig = req.app.locals && req.app.locals.emailConfig;
        if (smtpConfig && emailSender.isConfigured(smtpConfig)) {
          await emailSender.sendEmail(smtpConfig, issued.email, 'You\'re invited to DashCaddy', text, html);
          deliveredVia = 'email';
        } else {
          // Operator asked for email but SMTP isn't configured. Surface the
          // failure cleanly; the link is still in the response so the
          // operator can share it manually. Do NOT log the raw URL — it
          // would duplicate what's already in the response and pollute the
          // server log on every unconfigured-install invite.
          log.warn && log.warn('auth-invite-send',
            'invite send skipped: SMTP not configured (operator opted in)',
            { inviteId: issued.id, email: AuthProvider.maskEmail(issued.email) || '[unmaskable-email]' });
          deliveredVia = 'failed';
        }
      } catch (sendErr) {
        log.warn && log.warn('auth-invite-send',
          'invite send failed: ' + (sendErr.message || String(sendErr)),
          { inviteId: issued.id });
        deliveredVia = 'failed';
      }
    }

    const maskedEmail = email.replace(/(^.).+(@.*$)/, '$1***$2');
    const ttlHoursOut = Math.round(issued.ttlMs / (60 * 60 * 1000));
    const shareText =
      'Join my DashCaddy as ' + issued.role + ' — ' + acceptUrl +
      ' — expires in ' + ttlHoursOut + 'h.';

    return ok(res, {
      id: issued.id,
      email: issued.email,
      role: issued.role,
      expiresAt: issued.expiresAt,
      acceptUrl,
      shareText,
      deliveredVia,
      maskedEmail,
    });
  }, 'auth-admin-invites-create'));

  router.delete('/admin/invites/:id', _requireAdmin, asyncHandler(async (req, res) => {
    const result = await inviteStore.revoke(req.params.id);
    if (!result.ok) throw new NotFoundError('Invite not found');
    return successMessage(res, 'Invite revoked');
  }, 'auth-admin-invites-revoke'));

  // ── /invites (public) ──────────────────────────────────────────────────

  // PUBLIC: peek at an invite without consuming it.
  router.get('/invites/:token', asyncHandler(async (req, res) => {
    const peeked = await inviteStore.peek(req.params.token);
    if (!peeked) {
      // Same response as "not found" — don't leak token state.
      return ok(res, { valid: false });
    }
    return ok(res, {
      valid: true,
      email: peeked.email,
      role: peeked.role,
      expiresAt: peeked.expiresAt,
    });
  }, 'auth-invites-peek'));

  // PUBLIC: accept an invite token. Creates the user, sets the session.
  // DC-052: gated by Pro-or-room — if the user cap is hit and the host
  // isn't Pro, reject before the user is created. The invite token is
  // still marked used so a stale invite can't be replayed later when
  // room opens up.
  router.post('/invites/:token/accept', asyncHandler(async (req, res) => {
    const licenseManager = req.app.locals && req.app.locals.licenseManager;
    const localUserStore = req.app.locals && req.app.locals.userStore;
    if (licenseManager && typeof licenseManager.isPro === 'function' && !licenseManager.isPro()
        && localUserStore && typeof localUserStore.countUsers === 'function') {
      const count = await localUserStore.countUsers();
      if (count >= 3) {
        // Burn the invite — it can't be redeemed later under a paid tier
        // without the host first running `addToAllowlist` to re-add the
        // email. This prevents invite-leak spam from filling the user
        // table and being immortalized.
        await inviteStore.accept(req.params.token, { acceptedBy: null }).catch(() => {});
        throw new PaymentRequiredError(
          'Free tier supports up to 3 users. Upgrade to Pro to redeem this invitation.'
        );
      }
    }

    const result = await inviteStore.accept(req.params.token, {
      acceptedBy: req.user ? req.user.email : null,
    });
    if (!result.ok) {
      throw new ValidationError('Invitation is ' + result.reason.replace('_', ' '), 'token');
    }

    // Authorize the email + create the user record.
    const invite = result.invite;
    const userResult = await userStore.login({
      email: invite.email,
      ip: req.ip || '',
      displayName: invite.email.split('@')[0],
      createdBy: 'invite:' + invite.id,
    });
    if (!userResult.ok) {
      throw new ValidationError('Could not create user from invite: ' + userResult.reason);
    }

    // Create session (same shape as email verify path).
    if (session) {
      session.create(req, '24h');
      session.setCookie(res, '24h');
    }
    if (req.app.locals && req.app.locals.renewCSRFToken) {
      req.app.locals.renewCSRFToken(res, req.secure || req.protocol === 'https');
    }

    // Attach user to request for audit log.
    req.user = {
      id: userResult.user.id,
      email: userResult.user.email,
      role: userResult.user.role,
      isAdmin: userResult.user.role === 'admin',
      isBootstrap: false,
      viaProvider: 'invite',
    };

    log.info && log.info('auth', 'invite accepted, user created', {
      userId: userResult.user.id,
      email: AuthProvider.maskEmail(userResult.user.email) || '[unmaskable-email]',
      role: userResult.user.role,
      inviteId: invite.id,
    });

    return ok(res, {
      message: 'Invitation accepted',
      user: {
        id: userResult.user.id,
        email: userResult.user.email,
        role: userResult.user.role,
      },
      csrfToken: res.locals && res.locals.csrfToken,
    });
  }, 'auth-invites-accept'));

  return router;
};