/**
 * Super Admin — Security & Notification Settings — /api/super-admin/security
 *
 * Everything an administrator configures for the two features added here:
 * the two-factor policy, per-user 2FA status and reset, the 2FA audit trail,
 * and backup notification settings including a test send and a delivery log.
 *
 * Both configuration blobs live on `superAdmin.panelSettings`, the same store
 * `featureRequestNotifications` and `aiConfig` already use — so nothing is
 * hardcoded, nothing needs an environment variable, and a change takes effect
 * without a redeploy.
 *
 * A dedicated router rather than more endpoints on `superAdmin.ts` (already
 * 3,100 lines): these are cohesive, independently reviewable, and easy to
 * remove wholesale if the features are ever rolled back.
 */

import express, { Request, Response } from 'express';
import { body, param, query, validationResult } from 'express-validator';
import { authenticate, requireRole } from '../middleware/auth';
import { getModels } from '../models';
import { logAudit, attachUserNames } from '../utils/auditLogger';
import {
  DEFAULT_TWO_FACTOR_SETTINGS,
  normaliseTwoFactorSettings,
  invalidateTwoFactorSettingsCache,
  permittedMethods,
  requiresEmailDelivery,
  isRoleExemptFromTwoFactor,
} from '../services/auth/twoFactorSettings';
import { sanitiseEnforceRoles, invalidateRoleDirectoryCache } from '../services/auth/roleDirectory';
import { isTransactionalEmailReady } from '../services/email/transactionalMailer';
import {
  DEFAULT_BACKUP_NOTIFICATIONS,
  normaliseBackupNotificationSettings,
  invalidateBackupNotificationSettingsCache,
} from '../services/backup/backupNotificationSettings';
import { adminResetTwoFactor, adminUnlockTwoFactor, TWO_FACTOR_AUDIT_ACTIONS } from '../services/auth/twoFactorService';
import { sendTestBackupNotification } from '../services/backup/backupNotifications';
import { isSecretBoxConfigured, SECRET_BOX_KEY_MESSAGE } from '../services/auth/secretBox';

const router = express.Router();
router.use(authenticate, requireRole('super-admin'));

function rejectInvalid(req: Request, res: Response): boolean {
  const errors = validationResult(req);
  if (errors.isEmpty()) return false;
  res.status(400).json({ error: errors.array()[0]?.msg || 'Invalid request' });
  return true;
}

/**
 * Merge a settings blob into the super-admin's panelSettings.
 *
 * Reads the current object, merges the one key, and writes the whole thing back
 * — the same read/merge/save the main `PUT /super-admin/settings` route uses.
 *
 * A dotted `$set` (`panelSettings.twoFactor`) looks tidier and works against a
 * real MongoDB, but the in-memory mock model applies `$set` with a flat
 * `Object.assign`, which turns the dotted path into a *literal* key called
 * "panelSettings.twoFactor" and leaves `panelSettings` untouched. The save then
 * silently vanishes and the next read falls back to defaults. Merging in JS
 * behaves identically on both, so a mock-mode dev run is not quietly broken.
 *
 * Only the one key is replaced, so AI keys, maintenance mode and the
 * feature-request config sitting alongside it are preserved.
 */
async function savePanelSetting(key: string, value: any): Promise<boolean> {
  const { User } = getModels();
  const superAdmin = await User.findOne({ role: 'super-admin' });
  if (!superAdmin) return false;

  const current = (superAdmin as any).panelSettings || {};
  (superAdmin as any).panelSettings = { ...current, [key]: value };

  // Mixed fields need an explicit dirty flag under Mongoose; the mock document
  // has no such method, hence the guard.
  if (typeof (superAdmin as any).markModified === 'function') {
    (superAdmin as any).markModified('panelSettings');
  }
  await superAdmin.save();

  return true;
}

// ============================================================================
// TWO-FACTOR POLICY
// ============================================================================

/** GET /two-factor-settings */
router.get('/two-factor-settings', async (_req: Request, res: Response) => {
  try {
    const { User } = getModels();
    const superAdmin = await User.findOne({ role: 'super-admin' }).select('panelSettings').lean();

    const settings = normaliseTwoFactorSettings((superAdmin as any)?.panelSettings?.twoFactor);
    const keyConfigured = isSecretBoxConfigured();
    const smtpConfigured = await isTransactionalEmailReady();

    res.json({
      settings,
      defaults: DEFAULT_TWO_FACTOR_SETTINGS,
      // Lets the UI warn before an admin enables a policy that cannot work,
      // rather than surfacing it as a mystery failure at first enrolment.
      encryptionKeyConfigured: keyConfigured,
      encryptionKeyMessage: keyConfigured ? '' : SECRET_BOX_KEY_MESSAGE,
      // Email OTP cannot work without an active SMTP configuration; the UI
      // disables those options and explains why rather than letting an admin
      // save a policy that would lock every covered user out.
      smtpConfigured,
      smtpMessage: smtpConfigured
        ? ''
        : 'Email OTP needs an active email configuration. Set one up in Super Admin → Email Configuration first.',
    });
  } catch (error: any) {
    console.error('[Security] Failed to read 2FA settings:', error?.message);
    res.status(500).json({ error: 'Failed to load two-factor settings' });
  }
});

/** PUT /two-factor-settings */
router.put(
  '/two-factor-settings',
  [
    body('enabled').optional().isBoolean(),
    body('mode').optional().isIn(['optional', 'mandatory']),
    body('verificationMethod').optional().isIn(['totp', 'email', 'both'])
      .withMessage('Verification method must be Authenticator App, Email OTP, or Both'),
    body('emailOtp.expiryMinutes').optional().isInt({ min: 1, max: 30 })
      .withMessage('OTP expiry must be between 1 and 30 minutes'),
    body('emailOtp.maxResends').optional().isInt({ min: 0, max: 10 })
      .withMessage('Resend limit must be between 0 and 10'),
    body('emailOtp.resendCooldownSeconds').optional().isInt({ min: 15, max: 600 })
      .withMessage('Resend cooldown must be between 15 and 600 seconds'),
    body('enforceRoles').optional().isArray(),
    // Role ids, validated against the live Roles & Permissions data rather
    // than a fixed list — see sanitiseEnforceRoles below.
    body('enforceRoles.*').optional().isString().trim(),
    body('totp.algorithm').optional().isIn(['SHA1', 'SHA256', 'SHA512']),
    body('totp.digits').optional().isInt({ min: 6, max: 8 }),
    body('totp.period').optional().isInt({ min: 15, max: 120 }),
    body('totp.window').optional().isInt({ min: 0, max: 3 }),
    body('recoveryCodes.enabled').optional().isBoolean(),
    body('recoveryCodes.count').optional().isInt({ min: 1, max: 20 }),
    body('recoveryCodes.length').optional().isInt({ min: 8, max: 24 }),
    body('trustedDevices.enabled').optional().isBoolean(),
    body('trustedDevices.durationDays').optional().isInt({ min: 1, max: 365 }),
    body('trustedDevices.maxPerUser').optional().isInt({ min: 1, max: 50 }),
    body('challengeTtlMinutes').optional().isInt({ min: 1, max: 60 }),
    body('enrollmentTtlMinutes').optional().isInt({ min: 5, max: 120 }),
    body('verification.maxAttempts').optional().isInt({ min: 1, max: 20 }),
    body('verification.lockoutMinutes').optional().isInt({ min: 1, max: 1440 }),
    body('logoutAllOnReset').optional().isBoolean(),
    body('issuer').optional().isString().trim().isLength({ max: 60 }),
  ],
  async (req: Request, res: Response) => {
    if (rejectInvalid(req, res)) return;

    try {
      const { User } = getModels();
      const superAdmin = await User.findOne({ role: 'super-admin' }).select('panelSettings').lean();
      const current = normaliseTwoFactorSettings((superAdmin as any)?.panelSettings?.twoFactor);

      // A Super Admin sees every role, so the scope filter is empty. Names are
      // still accepted and converted, which is what carries an existing policy
      // across without an edit.
      let enforceRoles = current.enforceRoles;
      if (Array.isArray(req.body.enforceRoles)) {
        // A role created moments ago may not be in the directory cache yet, and
        // the ids arriving here are exactly the ones enforcement must resolve.
        invalidateRoleDirectoryCache();
        const sanitised = await sanitiseEnforceRoles(req.body.enforceRoles, {}, isRoleExemptFromTwoFactor);
        if (sanitised.rejected.length > 0) {
          res.status(400).json({ error: `Unknown or unavailable role: ${sanitised.rejected.join(', ')}` });
          return;
        }
        enforceRoles = sanitised.roleIds;
      }

      // Merge over the current values so a partial save cannot silently reset
      // untouched fields to their defaults.
      const merged = normaliseTwoFactorSettings({
        ...current,
        ...req.body,
        enforceRoles,
        totp: { ...current.totp, ...(req.body.totp || {}) },
        recoveryCodes: { ...current.recoveryCodes, ...(req.body.recoveryCodes || {}) },
        trustedDevices: { ...current.trustedDevices, ...(req.body.trustedDevices || {}) },
        verification: { ...current.verification, ...(req.body.verification || {}) },
        emailOtp: { ...current.emailOtp, ...(req.body.emailOtp || {}) },
      });

      // Refuse to enable a policy that cannot function.
      //
      // Two independent prerequisites, each only checked when the chosen method
      // actually needs it: TOTP needs an encryption key to seal its secrets,
      // and Email OTP needs a working mailer. Saving either without its
      // prerequisite would lock out every covered user at their next sign-in.
      if (merged.enabled && permittedMethods(merged).includes('totp') && !isSecretBoxConfigured()) {
        res.status(400).json({ error: SECRET_BOX_KEY_MESSAGE });
        return;
      }

      if (merged.enabled && requiresEmailDelivery(merged) && !(await isTransactionalEmailReady())) {
        res.status(400).json({
          error: 'Email OTP needs an active email configuration. Set one up in '
            + 'Super Admin → Email Configuration, or choose Authenticator App instead.',
        });
        return;
      }

      const saved = await savePanelSetting('twoFactor', merged);
      if (!saved) {
        res.status(404).json({ error: 'Super Admin not found' });
        return;
      }

      invalidateTwoFactorSettingsCache();

      void logAudit({
        userId: String(req.user!._id || req.user!.id),
        userEmail: req.user!.email,
        action: TWO_FACTOR_AUDIT_ACTIONS.settingsUpdated,
        resource: 'Auth',
        details: { before: current, after: merged },
        req,
      });

      res.json({ success: true, settings: merged });
    } catch (error: any) {
      console.error('[Security] Failed to save 2FA settings:', error?.message);
      res.status(500).json({ error: 'Failed to save two-factor settings' });
    }
  },
);

// ============================================================================
// PER-USER 2FA STATUS
// ============================================================================

/** GET /users — users with their 2FA enrolment state. */
router.get(
  '/users',
  [
    query('page').optional().isInt({ min: 1 }),
    query('limit').optional().isInt({ min: 1, max: 100 }),
    query('status').optional().isIn(['enabled', 'disabled', 'locked', 'all']),
  ],
  async (req: Request, res: Response) => {
    if (rejectInvalid(req, res)) return;

    try {
      const { User, UserTwoFactor } = getModels();

      const page = Math.max(1, Number(req.query.page) || 1);
      const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 20));
      const search = String(req.query.search || '').trim();
      const statusFilter = String(req.query.status || 'all');

      const userQuery: any = {};
      if (search) {
        const escaped = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        userQuery.$or = [
          { name: { $regex: escaped, $options: 'i' } },
          { email: { $regex: escaped, $options: 'i' } },
        ];
      }

      const users = await User.find(userQuery)
        .select('name email role status createdAt lastLoginAt')
        .sort({ createdAt: -1 })
        .lean();

      // One query for every enrolment, then joined in memory. The alternative —
      // a lookup per user — would be N+1 on a page of 100.
      const records = await UserTwoFactor.find({}).lean();
      const byUserId = new Map<string, any>();
      for (const r of records as any[]) byUserId.set(String(r.userId), r);

      const now = new Date();
      let rows = (users as any[]).map((u) => {
        const record = byUserId.get(String(u._id));
        const locked = !!(record?.lockedUntil && new Date(record.lockedUntil) > now);
        return {
          id: String(u._id),
          name: u.name,
          email: u.email,
          role: u.role,
          accountStatus: u.status,
          lastLoginAt: u.lastLoginAt,
          twoFactorStatus: record?.status || 'not_configured',
          twoFactorEnabled: record?.status === 'enabled',
          confirmedAt: record?.confirmedAt || null,
          lastVerifiedAt: record?.lastVerifiedAt || null,
          locked,
          lockedUntil: locked ? record.lockedUntil : null,
          resetAt: record?.resetAt || null,
        };
      });

      if (statusFilter === 'enabled') rows = rows.filter(r => r.twoFactorEnabled);
      else if (statusFilter === 'disabled') rows = rows.filter(r => !r.twoFactorEnabled);
      else if (statusFilter === 'locked') rows = rows.filter(r => r.locked);

      const total = rows.length;
      const start = (page - 1) * limit;

      res.json({
        users: rows.slice(start, start + limit),
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit) || 1,
        summary: {
          enrolled: rows.filter(r => r.twoFactorEnabled).length,
          locked: rows.filter(r => r.locked).length,
        },
      });
    } catch (error: any) {
      console.error('[Security] Failed to list user 2FA status:', error?.message);
      res.status(500).json({ error: 'Failed to load user two-factor status' });
    }
  },
);

/** POST /users/:id/reset-2fa — the lost-device escape hatch. */
router.post(
  '/users/:id/reset-2fa',
  [
    param('id').notEmpty().withMessage('User id is required'),
    body('reason').optional().isString().trim().isLength({ max: 500 }),
  ],
  async (req: Request, res: Response) => {
    if (rejectInvalid(req, res)) return;

    try {
      const result = await adminResetTwoFactor({
        targetUserId: req.params.id,
        actingUser: req.user!,
        reason: req.body.reason,
        req,
      });

      if (!result.success) {
        res.status(404).json({ error: result.error });
        return;
      }

      res.json({
        success: true,
        revokedDevices: result.revokedDevices ?? 0,
        message: 'Two-factor authentication has been reset. The user has been emailed and can set it up again.',
      });
    } catch (error: any) {
      console.error('[Security] Failed to reset user 2FA:', error?.message);
      res.status(500).json({ error: 'Failed to reset two-factor authentication' });
    }
  },
);

/** POST /users/:id/unlock-2fa — lift a lockout, leaving their factor intact. */
router.post(
  '/users/:id/unlock-2fa',
  [
    param('id').notEmpty().withMessage('User id is required'),
    body('reason').optional().isString().trim().isLength({ max: 500 }),
  ],
  async (req: Request, res: Response) => {
    if (rejectInvalid(req, res)) return;

    try {
      const result = await adminUnlockTwoFactor({
        targetUserId: req.params.id,
        actingUser: req.user!,
        reason: req.body.reason,
        req,
      });

      if (!result.success) {
        res.status(404).json({ error: result.error });
        return;
      }

      res.json({
        success: true,
        wasLocked: result.wasLocked ?? false,
        message: result.wasLocked
          ? 'The lockout has been lifted. The user can enter a code again straight away.'
          : 'This user was not locked out. Their failed-attempt counter has been cleared.',
      });
    } catch (error: any) {
      console.error('[Security] Failed to unlock user 2FA:', error?.message);
      res.status(500).json({ error: 'Failed to lift the lockout' });
    }
  },
);

/** GET /audit — the 2FA slice of the audit log. */
router.get(
  '/audit',
  [
    query('page').optional().isInt({ min: 1 }),
    query('limit').optional().isInt({ min: 1, max: 100 }),
  ],
  async (req: Request, res: Response) => {
    if (rejectInvalid(req, res)) return;

    try {
      const { AuditLog } = getModels();

      const page = Math.max(1, Number(req.query.page) || 1);
      const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 25));

      const filter: any = { resource: 'Auth' };

      if (req.query.action) {
        filter.action = String(req.query.action);
      } else {
        // Every 2FA action is namespaced `2fa.*`, so this scopes the view
        // without having to enumerate the action list here.
        filter.action = { $regex: '^2fa\\.' };
      }

      if (req.query.userId) filter.userId = String(req.query.userId);

      if (req.query.dateFrom || req.query.dateTo) {
        filter.createdAt = {};
        if (req.query.dateFrom) filter.createdAt.$gte = new Date(String(req.query.dateFrom));
        if (req.query.dateTo) filter.createdAt.$lte = new Date(String(req.query.dateTo));
      }

      const all = await AuditLog.find(filter).sort({ createdAt: -1 }).lean();
      const total = Array.isArray(all) ? all.length : 0;
      const start = (page - 1) * limit;
      const logs = (all as any[]).slice(start, start + limit);

      await attachUserNames(logs);

      res.json({
        logs,
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit) || 1,
        actions: Object.values(TWO_FACTOR_AUDIT_ACTIONS),
      });
    } catch (error: any) {
      console.error('[Security] Failed to load 2FA audit log:', error?.message);
      res.status(500).json({ error: 'Failed to load audit log' });
    }
  },
);

// ============================================================================
// BACKUP NOTIFICATIONS
// ============================================================================

/** GET /backup-notifications */
router.get('/backup-notifications', async (_req: Request, res: Response) => {
  try {
    const { User } = getModels();
    const superAdmin = await User.findOne({ role: 'super-admin' }).select('panelSettings').lean();

    res.json({
      settings: normaliseBackupNotificationSettings((superAdmin as any)?.panelSettings?.backupNotifications),
      defaults: DEFAULT_BACKUP_NOTIFICATIONS,
    });
  } catch (error: any) {
    console.error('[Security] Failed to read backup notification settings:', error?.message);
    res.status(500).json({ error: 'Failed to load backup notification settings' });
  }
});

/** PUT /backup-notifications */
router.put(
  '/backup-notifications',
  [
    body('enabled').optional().isBoolean(),
    body('notifyOnSuccess').optional().isBoolean(),
    body('notifyOnFailure').optional().isBoolean(),
    body('recipients').optional().isArray(),
    body('recipients.*').optional().isEmail().withMessage('Every recipient must be a valid email address'),
    body('includeCompanyRecipients').optional().isBoolean(),
    body('notifyBackupCreator').optional().isBoolean(),
    body('senderName').optional().isString().trim().isLength({ max: 120 }),
    body('senderEmail').optional({ values: 'falsy' }).isEmail().withMessage('Sender email must be a valid address'),
    body('subjectSuccess').optional().isString().trim().isLength({ min: 1, max: 300 }),
    body('subjectFailure').optional().isString().trim().isLength({ min: 1, max: 300 }),
    body('templateSuccess').optional().isString().trim().isLength({ max: 80 }),
    body('templateFailure').optional().isString().trim().isLength({ max: 80 }),
    body('footerHtml').optional().isString().isLength({ max: 5000 }),
    body('logoUrl').optional({ values: 'falsy' }).isURL().withMessage('Logo URL must be a valid URL'),
    body('includeServerInfo').optional().isBoolean(),
    body('retry.maxAttempts').optional().isInt({ min: 1, max: 10 }),
    body('retry.baseDelayMs').optional().isInt({ min: 0, max: 60000 }),
  ],
  async (req: Request, res: Response) => {
    if (rejectInvalid(req, res)) return;

    try {
      const { User } = getModels();
      const superAdmin = await User.findOne({ role: 'super-admin' }).select('panelSettings').lean();
      const current = normaliseBackupNotificationSettings((superAdmin as any)?.panelSettings?.backupNotifications);

      const merged = normaliseBackupNotificationSettings({
        ...current,
        ...req.body,
        retry: { ...current.retry, ...(req.body.retry || {}) },
      });

      const saved = await savePanelSetting('backupNotifications', merged);
      if (!saved) {
        res.status(404).json({ error: 'Super Admin not found' });
        return;
      }

      invalidateBackupNotificationSettingsCache();

      void logAudit({
        userId: String(req.user!._id || req.user!.id),
        userEmail: req.user!.email,
        action: 'backup.notification.settings.updated',
        resource: 'Backup',
        details: { before: current, after: merged },
        req,
      });

      res.json({ success: true, settings: merged });
    } catch (error: any) {
      console.error('[Security] Failed to save backup notification settings:', error?.message);
      res.status(500).json({ error: 'Failed to save backup notification settings' });
    }
  },
);

/**
 * POST /backup-notifications/test
 *
 * Sends a sample using representative data, so an admin can validate SMTP,
 * subject and branding without waiting for a real backup to run.
 */
router.post(
  '/backup-notifications/test',
  [
    body('to').trim().isEmail().withMessage('A valid recipient email is required'),
    body('notificationType').optional().isIn(['success', 'failure']),
  ],
  async (req: Request, res: Response) => {
    if (rejectInvalid(req, res)) return;

    try {
      const result = await sendTestBackupNotification({
        to: String(req.body.to).trim(),
        notificationType: req.body.notificationType,
      });

      // 502 on a send failure: the request was valid, the upstream mail server
      // was the problem — matching how /api/smtp-config/send-test reports it.
      res.status(result.success ? 200 : 502).json(result);
    } catch (error: any) {
      console.error('[Security] Test backup notification failed:', error?.message);
      res.status(500).json({ success: false, message: error?.message || 'Failed to send the test email' });
    }
  },
);

/** GET /backup-notifications/logs — delivery history, including skips. */
router.get(
  '/backup-notifications/logs',
  [
    query('page').optional().isInt({ min: 1 }),
    query('limit').optional().isInt({ min: 1, max: 100 }),
    query('status').optional().isIn(['sent', 'failed', 'skipped', 'all']),
  ],
  async (req: Request, res: Response) => {
    if (rejectInvalid(req, res)) return;

    try {
      const { BackupNotificationLog } = getModels();

      const page = Math.max(1, Number(req.query.page) || 1);
      const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 25));
      const status = String(req.query.status || 'all');

      const filter: any = {};
      if (status === 'sent') filter.success = true;
      else if (status === 'failed') { filter.success = false; filter.skipped = false; }
      else if (status === 'skipped') filter.skipped = true;

      if (req.query.notificationType) filter.notificationType = String(req.query.notificationType);

      if (req.query.dateFrom || req.query.dateTo) {
        filter.createdAt = {};
        if (req.query.dateFrom) filter.createdAt.$gte = new Date(String(req.query.dateFrom));
        if (req.query.dateTo) filter.createdAt.$lte = new Date(String(req.query.dateTo));
      }

      const all = await BackupNotificationLog.find(filter).sort({ createdAt: -1 }).lean();
      const total = Array.isArray(all) ? all.length : 0;
      const start = (page - 1) * limit;

      res.json({
        logs: (all as any[]).slice(start, start + limit),
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit) || 1,
      });
    } catch (error: any) {
      console.error('[Security] Failed to load notification logs:', error?.message);
      res.status(500).json({ error: 'Failed to load notification logs' });
    }
  },
);

export default router;
