/**
 * Organization Admin — Security Settings — /api/org-admin/security
 *
 * The same capabilities the Super Admin security page has, scoped to the
 * companies an org admin actually manages. Mirrors the four existing
 * org-admin routers: `authenticate` → `requireOrgAdmin` → scope every query
 * with the shared helpers, never with a role name written into this file.
 *
 * Two boundaries this router is responsible for:
 *
 *   1. **It only ever writes a company-scoped policy.** The scope comes from
 *      `req.orgContext`, never from the request body, so an admin cannot name
 *      `'global'` (or another company) and reach the platform policy. The
 *      global policy is unreachable from here by construction rather than by
 *      a check that could be forgotten.
 *   2. **Super Admin accounts are invisible.** Every user query goes through
 *      `orgUserFilter`, which already excludes `role: 'super-admin'`, so an
 *      admin can neither see nor reset that account.
 *
 * What an admin saves is a *request*. What their users get is that combined
 * with the platform floor by `combinePolicies` — an org may match or strengthen
 * the platform policy, never relax it. The GET returns the global policy and
 * the resolved effective policy alongside the org's own, so the page can show
 * exactly which of its controls are being overridden from above.
 */

import express, { Request, Response } from 'express';
import { body, param, query, validationResult } from 'express-validator';
import { authenticate } from '../middleware/auth';
import { orgScopeFilter, requireOrgAdmin, orgUserFilter } from '../middleware/orgAdmin';
import { getModels } from '../models';
import { logAudit, attachUserNames } from '../utils/auditLogger';
import {
  DEFAULT_TWO_FACTOR_SETTINGS,
  normaliseTwoFactorSettings,
  getTwoFactorSettings,
  permittedMethods,
  requiresEmailDelivery,
  isRoleExemptFromTwoFactor,
} from '../services/auth/twoFactorSettings';
import { sanitiseEnforceRoles, invalidateRoleDirectoryCache } from '../services/auth/roleDirectory';
import { combinePolicies, invalidatePolicyCache } from '../services/auth/policyResolution';
import { isTransactionalEmailReady } from '../services/email/transactionalMailer';
import { isSecretBoxConfigured, SECRET_BOX_KEY_MESSAGE } from '../services/auth/secretBox';
import { adminResetTwoFactor, adminUnlockTwoFactor, TWO_FACTOR_AUDIT_ACTIONS } from '../services/auth/twoFactorService';

const router = express.Router();
router.use(authenticate);
router.use(requireOrgAdmin);

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;
}

/**
 * The company this request may act on.
 *
 * Taken from the authenticated org context, never from the body or the query —
 * that is what stops an admin naming someone else's company or `'global'`.
 */
function scopeFor(req: Request): string {
  return String(req.orgContext!.companyId);
}

// ============================================================================
// COMPANY 2FA POLICY
// ============================================================================

/** GET /two-factor-settings — the org's policy, the platform floor, and the result. */
router.get('/two-factor-settings', async (req: Request, res: Response) => {
  try {
    const scope = scopeFor(req);
    const { TwoFactorPolicy } = getModels();

    const doc = await TwoFactorPolicy.findOne({ scope }).lean();
    const global = await getTwoFactorSettings();

    // An org that has never saved starts from the platform policy rather than
    // from the shipped defaults — otherwise the page would present controls
    // that silently disagree with what its users are actually experiencing.
    const org = doc ? normaliseTwoFactorSettings((doc as any).settings) : global;

    const keyConfigured = isSecretBoxConfigured();
    const smtpConfigured = await isTransactionalEmailReady();

    res.json({
      settings: org,
      // The floor. The page greys out anything weaker than this and explains why.
      globalPolicy: global,
      effective: combinePolicies(global, doc ? org : null),
      hasOwnPolicy: !!doc,
      defaults: DEFAULT_TWO_FACTOR_SETTINGS,
      scope: 'org',
      encryptionKeyConfigured: keyConfigured,
      encryptionKeyMessage: keyConfigured ? '' : SECRET_BOX_KEY_MESSAGE,
      smtpConfigured,
      smtpMessage: smtpConfigured
        ? ''
        : 'Email OTP needs an active email configuration. Ask your platform administrator to set one up.',
    });
  } catch (error: any) {
    console.error('[OrgSecurity] Failed to read the company 2FA policy:', error?.message);
    res.status(500).json({ error: 'Failed to load two-factor settings' });
  }
});

/** PUT /two-factor-settings — write this company's policy only. */
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('skip.enabled').optional().isBoolean(),
    body('skip.maxSkips').optional().isInt({ min: 0, max: 50 })
      .withMessage('Maximum skips must be between 0 and 50'),
    body('emailOtp.expiryMinutes').optional().isInt({ min: 1, max: 30 }),
    body('emailOtp.maxResends').optional().isInt({ min: 0, max: 10 }),
    body('emailOtp.resendCooldownSeconds').optional().isInt({ min: 15, max: 600 }),
    body('enforceRoles').optional().isArray(),
    // Role ids, checked against the roles this admin can actually see.
    body('enforceRoles.*').optional().isString().trim()
      .withMessage('Super Admin cannot be included in a company policy'),
    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('issuer').optional().isString().trim().isLength({ max: 60 }),
  ],
  async (req: Request, res: Response) => {
    if (rejectInvalid(req, res)) return;

    try {
      const scope = scopeFor(req);
      const { TwoFactorPolicy } = getModels();

      const existing = await TwoFactorPolicy.findOne({ scope }).lean();
      const global = await getTwoFactorSettings();
      const current = existing ? normaliseTwoFactorSettings((existing as any).settings) : global;

      // Scoped with the same filter the roles list uses, so an admin can only
      // target roles their organisation can see. Posting another org's role id
      // is refused rather than silently dropped.
      let enforceRoles = current.enforceRoles;
      if (Array.isArray(req.body.enforceRoles)) {
        // See the platform route — a just-created role must resolve here.
        invalidateRoleDirectoryCache();
        const sanitised = await sanitiseEnforceRoles(
          req.body.enforceRoles,
          orgScopeFilter(req.orgContext!),
          isRoleExemptFromTwoFactor,
        );
        if (sanitised.rejected.length > 0) {
          res.status(400).json({ error: `Unknown or unavailable role: ${sanitised.rejected.join(', ')}` });
          return;
        }
        enforceRoles = sanitised.roleIds;
      }

      const merged = normaliseTwoFactorSettings({
        ...current,
        ...req.body,
        enforceRoles,
        totp: { ...current.totp, ...(req.body.totp || {}) },
        skip: { ...current.skip, ...(req.body.skip || {}) },
        emailOtp: { ...current.emailOtp, ...(req.body.emailOtp || {}) },
        recoveryCodes: { ...current.recoveryCodes, ...(req.body.recoveryCodes || {}) },
        trustedDevices: { ...current.trustedDevices, ...(req.body.trustedDevices || {}) },
        verification: { ...current.verification, ...(req.body.verification || {}) },
      });

      // Refuse a policy that could not work, on the same grounds the platform
      // route does. Checked against the EFFECTIVE policy: the floor may already
      // require a factor this org did not ask for.
      const effective = combinePolicies(global, merged);

      if (effective.enabled && permittedMethods(effective).includes('totp') && !isSecretBoxConfigured()) {
        res.status(400).json({ error: SECRET_BOX_KEY_MESSAGE });
        return;
      }

      if (effective.enabled && requiresEmailDelivery(effective) && !(await isTransactionalEmailReady())) {
        res.status(400).json({
          error: 'Email OTP needs an active email configuration. Ask your platform administrator to '
            + 'set one up, or choose Authenticator App instead.',
        });
        return;
      }

      await TwoFactorPolicy.findOneAndUpdate(
        { scope },
        { $set: { scope, settings: merged, updatedByUserId: req.user!._id || req.user!.id } },
        { upsert: true, new: true },
      );

      invalidatePolicyCache(scope);

      void logAudit({
        userId: String(req.user!._id || req.user!.id),
        userEmail: req.user!.email,
        action: TWO_FACTOR_AUDIT_ACTIONS.settingsUpdated,
        resource: 'Auth',
        resourceId: scope,
        companyId: scope,
        details: { scope: 'company', before: existing ? current : null, after: merged },
        req,
      });

      res.json({ success: true, settings: merged, effective, globalPolicy: global, hasOwnPolicy: true });
    } catch (error: any) {
      console.error('[OrgSecurity] Failed to save the company 2FA policy:', error?.message);
      res.status(500).json({ error: 'Failed to save two-factor settings' });
    }
  },
);

// ============================================================================
// USERS IN THIS ORG
// ============================================================================

/** GET /users — 2FA status for the admin's users. Super Admins never appear. */
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');

      // The scope filter is the security boundary — it restricts to the admin's
      // companies AND excludes super-admin accounts.
      const filter: any = { ...orgUserFilter(req.orgContext!) };
      if (search) {
        const escaped = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        filter.$or = [
          { name: { $regex: escaped, $options: 'i' } },
          { email: { $regex: escaped, $options: 'i' } },
        ];
      }

      const users = await User.find(filter)
        .select('name email role status createdAt lastLoginAt')
        .sort({ createdAt: -1 })
        .lean();

      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',
          method: record?.method || null,
          skipCount: record?.skipCount ?? 0,
          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('[OrgSecurity] Failed to list users:', error?.message);
      res.status(500).json({ error: 'Failed to load user two-factor status' });
    }
  },
);

/** POST /users/:id/reset-2fa — only for a user inside the admin's scope. */
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 { User } = getModels();

      // Confirm membership through the same filter that lists them, so the
      // reset can never reach a user the admin cannot see — including a
      // super-admin, whom `orgUserFilter` excludes.
      const target = await User.findOne({
        _id: req.params.id,
        ...orgUserFilter(req.orgContext!),
      }).lean();

      if (!target) {
        // 404 rather than 403: whether that id exists at all is not this
        // admin's business.
        res.status(404).json({ error: 'User not found' });
        return;
      }

      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('[OrgSecurity] Failed to reset a user 2FA:', error?.message);
      res.status(500).json({ error: 'Failed to reset two-factor authentication' });
    }
  },
);

/** POST /users/:id/unlock-2fa — only for a user inside the admin's scope. */
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 { User } = getModels();

      // Same membership check the reset does — `orgUserFilter` also excludes
      // super-admins, so this can never reach that account.
      const target = await User.findOne({
        _id: req.params.id,
        ...orgUserFilter(req.orgContext!),
      }).lean();

      if (!target) {
        res.status(404).json({ error: 'User not found' });
        return;
      }

      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('[OrgSecurity] Failed to unlock a user 2FA:', error?.message);
      res.status(500).json({ error: 'Failed to lift the lockout' });
    }
  },
);

/** GET /audit — 2FA events for this org's users only. */
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, User } = getModels();

      const page = Math.max(1, Number(req.query.page) || 1);
      const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 25));

      // Scope by actor: only events belonging to users this admin manages. A
      // company id on the entry is not enough, because most 2FA events are
      // recorded against the user rather than an organisation.
      const visible = await User.find(orgUserFilter(req.orgContext!)).select('_id').lean();
      const visibleIds = (visible as any[]).map(u => String(u._id));

      const filter: any = {
        resource: 'Auth',
        userId: { $in: visibleIds },
        action: req.query.action ? String(req.query.action) : { $regex: '^2fa\\.' },
      };

      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('[OrgSecurity] Failed to load the audit log:', error?.message);
      res.status(500).json({ error: 'Failed to load audit log' });
    }
  },
);

export default router;
