/**
 * Authentication Routes
 */

import crypto from 'crypto';
import express, { Request, Response } from 'express';
import { body, validationResult } from 'express-validator';
import { generateToken, authenticate } from '../middleware/auth';
import { authenticateJwtOrApiToken } from '../middleware/dualAuth';
import { authRateLimiter, registerRateLimiter, passwordResetRateLimiter } from '../middleware/rateLimiter';
import { getModels } from '../models';
import { seedOrgRoles } from '../scripts/seedOrgRoles';
import { buildPermissionMap } from '../middleware/permissions';
import { sendPasswordResetEmail, sendPasswordChangedEmail, sendWelcomeEmail } from '../services/email';
import { notificationService } from '../services/notificationService';
import { getRequestAppBaseUrl } from '../config/appUrls';
import { recordReferralOnRegistration } from '../services/referralCode';
import {
  evaluateLogin,
  createChallenge,
  sendChallengeOtp,
  type SendOtpOutcome,
} from '../services/auth/twoFactorService';
import { buildLoginResponse } from '../services/auth/loginResponse';

const router = express.Router();

/** Escape a value before it is used inside a RegExp. */
const escapeRegex = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

/**
 * Look up an account by email the way login does: exact match first, then a
 * case-insensitive match, then the Gmail dot-equivalence fallback for addresses
 * written before the old normalizeEmail() sanitizer was removed (it stripped
 * dots from the local part). Shared so login, check-email and the password
 * reset flow can never disagree about which account an address belongs to.
 */
async function findUserByEmail(User: any, rawEmail: string): Promise<any | null> {
  const email = (rawEmail || '').toLowerCase().trim();
  if (!email) return null;

  let user = await User.findOne({ email });
  if (user) return user;

  user = await User.findOne({ email: { $regex: new RegExp(`^${escapeRegex(email)}$`, 'i') } });
  if (user) return user;

  const gmailMatch = email.match(/^([^@]+)@gmail\.com$/i);
  if (gmailMatch) {
    const normalizedEmail = `${gmailMatch[1].replace(/\./g, '')}@gmail.com`;
    user = await User.findOne({ email: { $regex: new RegExp(`^${escapeRegex(normalizedEmail)}$`, 'i') } });
    if (user) return user;
  }

  return null;
}

/** Maximum length for a Company Name — mirrors the frontend's COMPANY_NAME_MAX_LENGTH
 *  and the Company schema's maxlength. */
const COMPANY_NAME_MAX_LENGTH = 256;

/**
 * Sign-in failure messages, split by cause.
 *
 * These deliberately distinguish "no such account" from "wrong password", which
 * makes /login an account-existence oracle: an attacker can learn which
 * addresses hold accounts without ever guessing a password. That is a product
 * decision, requested explicitly, and it mirrors the same change made to
 * /forgot-password. `authRateLimiter` still runs before both branches, so bulk
 * harvesting is limited rather than impossible.
 *
 * Nothing else about the flow changes: the password is still compared with
 * bcrypt for a known account, both failures still return 401, and both are still
 * logged.
 */
// Named for the login flow specifically: /forgot-password has its own, shorter
// EMAIL_NOT_REGISTERED_MESSAGE further down, and the two screens word it
// differently.
const LOGIN_EMAIL_NOT_REGISTERED_MESSAGE = 'This email address is not registered.';
const INCORRECT_PASSWORD_MESSAGE = 'Incorrect password. Please try again.';

/** Fields whose submitted value must never be echoed back to the client. */
const SENSITIVE_FIELDS = new Set(['password', 'newPassword', 'currentPassword', 'token']);

/**
 * express-validator's `errors.array()` includes the offending `value`, which for a
 * failed password check means the plaintext password travels back in the 400 body
 * (and into any response logging or error reporting downstream). Strip it for
 * sensitive fields; every other field keeps `value`, which clients rely on.
 */
function safeValidationErrors(errors: ReturnType<typeof validationResult>) {
  return errors.array().map((err: any) =>
    err.type === 'field' && SENSITIVE_FIELDS.has(err.path)
      ? { ...err, value: undefined }
      : err
  );
}

/**
 * The individual password complexity rules, in policy order. Broken out so a failure
 * can name exactly which requirement is missing rather than restating the whole policy.
 * Mirrors PASSWORD_COMPLEXITY_RULES in the frontend's fieldValidators.
 */
const PASSWORD_COMPLEXITY_RULES: ReadonlyArray<{ test: RegExp; message: string }> = [
  { test: /[A-Z]/, message: 'Password must contain at least one uppercase letter.' },
  { test: /[a-z]/, message: 'Password must contain at least one lowercase letter.' },
  { test: /\d/, message: 'Password must contain at least one number.' },
  { test: /[^a-zA-Z\d\s]/, message: 'Password must contain at least one special character.' },
];

/**
 * Self-service password policy — the single backend source of truth, mirroring
 * the frontend's `validatePassword()`. Used by registration and password reset.
 * On a complexity failure it reports only the unmet requirement(s), one bullet per
 * line, matching the message the frontend renders character for character.
 */
const passwordPolicyValidator = () =>
  body('password')
    .isLength({ min: 8 }).withMessage('Password must be at least 8 characters')
    .custom((value: unknown) => {
      const strValue = String(value ?? '').trim();
      const missing = PASSWORD_COMPLEXITY_RULES
        .filter(rule => !rule.test.test(strValue))
        .map(rule => rule.message);

      if (missing.length === 0) return true;
      throw new Error(missing.map(message => `• ${message}`).join('\n'));
    });

// Register
router.post(
  '/register',
  authRateLimiter,
  // Layered deliberately: authRateLimiter absorbs bursts across all auth
  // endpoints, registerRateLimiter caps sustained account creation per IP.
  registerRateLimiter,
  [
    body('email').isEmail().withMessage('Valid email required'),
    passwordPolicyValidator(),
    body('name').trim().notEmpty().withMessage('Name is required')
      .isLength({ min: 2 }).withMessage('Name must be at least 2 characters')
      .matches(/^[a-zA-Z\s'\-]+$/).withMessage('Name must contain only letters, spaces, hyphens, or apostrophes'),
    body('companyName').trim().notEmpty().withMessage('Company name is required')
      .isLength({ min: 2 }).withMessage('Company name must be at least 2 characters')
      .isLength({ max: COMPANY_NAME_MAX_LENGTH })
      .withMessage(`Company name cannot exceed ${COMPANY_NAME_MAX_LENGTH} characters`),
  ],
  async (req: Request, res: Response) => {
    try {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        res.status(400).json({ errors: safeValidationErrors(errors) });
        return;
      }

      const { User, Company } = getModels();
      // `referralCode` is optional and carried from `/register?ref=CODE`. It is
      // deliberately NOT validated by the express-validator chain above: a bad
      // or missing code must never block a registration, so it is resolved
      // after the account exists and simply ignored when it does not match.
      const { email, password, name, companyName, referralCode } = req.body;

      // Check if user exists (case-insensitive to prevent duplicates with different casing)
      const existingUser = await User.findOne({
        email: { $regex: new RegExp(`^${email.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i') }
      });
      if (existingUser) {
        res.status(400).json({ error: 'User already exists' });
        return;
      }

      // Create user
      const user = new User({
        email,
        passwordHash: password,
        name,
        role: 'admin',
        isOrgAdmin: true, // Company creator gets org admin access by default
        companyIds: [],
      });

      await user.save();

      // Create default company
      const company = new Company({
        name: companyName?.trim() || `${name}'s Company`,
        userIds: [user.id],
        isActive: true,
      });

      await company.save();

      // Seed org-scoped default roles for this company
      const orgAdminRoleId = await seedOrgRoles(company.id);

      // Update user with company and role assignment
      user.companyIds = [company.id];
      user.activeCompanyId = company.id;
      if (orgAdminRoleId) {
        user.roleId = orgAdminRoleId;
      }
      await user.save();

      // ── Referral link attribution ─────────────────────────────────────────
      // The account and its company are now persisted, which is the only point
      // at which a referral may be recorded — an abandoned or failed
      // registration must never produce one. recordReferralOnRegistration never
      // throws and returns null for a missing, invalid, self- or duplicate
      // referral, so registration behaviour is identical with or without a code.
      if (referralCode) {
        await recordReferralOnRegistration({
          referralCode,
          newUser: { id: user.id, name: user.name, email: user.email },
        });
      }

      // Expose the newly-registered actor to the activity-capture middleware
      // (register is a public route with no `req.user`). Additive only.
      res.locals.activityActor = {
        id: user.id,
        email: user.email,
        name: user.name,
        organizationId: user.activeCompanyId || (user.companyIds?.[0] ?? null),
      };

      // ── Registration success email ─────────────────────────────────────────
      // Placed before the second-factor branch so it is sent on BOTH response
      // paths (challenge and proceed), and awaited rather than fire-and-forget:
      // an un-awaited send can be cut short when the host freezes the process
      // after the response, which is exactly the kind of fault that only shows
      // up in a deployed environment. sendWelcomeEmail never throws — it
      // returns a result — so a mail outage cannot fail a registration that has
      // already been persisted.
      const welcome = await sendWelcomeEmail({
        to: user.email,
        userName: user.name,
        companyName: company.name,
      });

      if (welcome.success) {
        console.log(`[Auth] Welcome email sent to ${user.email} (company: ${company.name})`);
      } else if (welcome.skipped) {
        // SMTP switched off / not configured for this deployment. Distinct from
        // a failure so an operator can tell "never attempted" from "attempted
        // and rejected" when comparing environments.
        console.warn(`[Auth] Welcome email skipped for ${user.email}: ${welcome.error}`);
      } else {
        console.error(`[Auth] Welcome email not delivered to ${user.email}: ${welcome.error}`);
      }

      // Platform-level signal, not a tenant one — super admins only, and
      // deliberately carrying no more than the org name and who registered it.
      void notificationService.notifyRole('super-admin', {
        type: 'system.org.registered',
        message: `${company.name} registered — ${user.name} (${user.email}).`,
        entityType: 'company',
        entityId: String(company.id),
        actionUrl: '/super-admin/organizations',
      });
      // ── Second factor ──────────────────────────────────────────────────────
      // Registration used to issue a session token outright, which meant a brand
      // new account skipped enrolment entirely and — with JWT_EXPIRES_IN at 7d —
      // held full access for a week before ever being challenged. "Mandatory"
      // has to mean mandatory at the point an account first gets a session, not
      // only on its second sign-in.
      //
      // The account and its company are already created above and stay created;
      // only the session is withheld until enrolment completes. Under an
      // optional policy this returns 'proceed' and registration behaves exactly
      // as it always has.
      const decision = await evaluateLogin({ user, deviceToken: null });

      if (decision.action === 'challenge') {
        const challenge = await createChallenge({
          userId: user.id,
          purpose: decision.purpose,
          settings: decision.settings,
          req,
        });

        // 201: the account WAS created. No `token`, so a client that has not
        // been updated fails closed rather than treating this as a signed-in
        // session.
        res.status(201).json({
          twoFactorSetupRequired: true,
          // A skippable policy has to be skippable here too, or a new account
          // gets a stricter experience at registration than the same policy
          // gives it at every later login.
          canSkip: decision.canSkip === true,
          skipsRemaining: decision.skipsRemaining ?? null,
          methods: decision.methods,
          challengeToken: challenge.challengeToken,
          expiresIn: challenge.expiresIn,
          email: user.email,
          user: {
            id: user.id,
            email: user.email,
            name: user.name,
            role: user.role,
          },
          company: {
            id: company.id,
            name: company.name,
          },
        });
        return;
      }

      const token = generateToken(user.id);

      res.status(201).json({
        token,
        user: {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.role,
          isOrgAdmin: user.isOrgAdmin ?? false,
          apiManagementAccess: (user as any).apiManagementAccess ?? false,
          companyIds: user.companyIds,
          activeCompanyId: user.activeCompanyId,
        },
        company: {
          id: company.id,
          name: company.name,
        },
      });
    } catch (error) {
      console.error('Registration error:', error);
      res.status(500).json({ error: 'Registration failed' });
    }
  }
);

// Login
router.post(
  '/login',
  authRateLimiter,
  [
    body('email').isEmail().withMessage('Valid email required'),
    body('password').notEmpty().withMessage('Password is required'),
  ],
  async (req: Request, res: Response) => {
    try {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        res.status(400).json({ errors: safeValidationErrors(errors) });
        return;
      }

      const { User, Company } = getModels();
      // Normalise email to lowercase for exact match (Mongoose schema lowercases on save)
      const email = (req.body.email || '').toLowerCase().trim();
      const { password } = req.body;

      // Exact match, then case-insensitive, then the Gmail dot-equivalence fallback
      const user = await findUserByEmail(User, email);
      if (!user) {
        // No account: answer here and do not go on to compare a password there
        // is nothing to compare against.
        console.log(`Login failed: no user found for email "${email}"`);
        res.status(401).json({ error: LOGIN_EMAIL_NOT_REGISTERED_MESSAGE });
        return;
      }

      const isMatch = await user.comparePassword(password);
      if (!isMatch) {
        console.log(`Login failed: password mismatch for email "${email}"`);
        res.status(401).json({ error: INCORRECT_PASSWORD_MESSAGE });
        return;
      }

      // ── Second factor ──────────────────────────────────────────────────────
      // The password is correct; decide whether a second factor is still owed.
      //
      // With 2FA disabled (the default) `evaluateLogin` returns 'proceed' after a
      // single cached settings read, and everything below runs exactly as it did
      // before this feature existed. The response for that path is unchanged.
      const decision = await evaluateLogin({
        user,
        deviceToken: typeof req.body.deviceToken === 'string' ? req.body.deviceToken : null,
      });

      if (decision.action === 'challenge') {
        const challenge = await createChallenge({
          userId: user.id,
          purpose: decision.purpose,
          settings: decision.settings,
          req,
        });

        // When email is the ONLY way to verify, send the code now rather than
        // making the user press a button to receive something they have no
        // alternative to. With a choice available ("both"), nothing is sent
        // until they actually pick email — otherwise every TOTP login would
        // fire off a pointless email.
        let otpDelivery: SendOtpOutcome | null = null;
        const emailIsOnlyOption =
          decision.methods.includes('email') && !decision.methods.includes('totp');

        if (decision.purpose === 'login' && emailIsOnlyOption) {
          otpDelivery = await sendChallengeOtp({
            jti: challenge.jti,
            user,
            settings: decision.settings,
            req,
          });
        }

        // No `token` field is emitted here. A client that has not been updated
        // for 2FA reads `data.token` as undefined and fails closed rather than
        // treating the response as a successful sign-in.
        res.json({
          ...(decision.purpose === 'enrollment'
            ? { twoFactorSetupRequired: true, canSkip: decision.canSkip === true, skipsRemaining: decision.skipsRemaining ?? null }
            : { twoFactorRequired: true }),
          methods: decision.methods,
          challengeToken: challenge.challengeToken,
          expiresIn: challenge.expiresIn,
          email: user.email,
          ...(otpDelivery
            ? {
              otpSent: otpDelivery.sent,
              ...(otpDelivery.sent
                ? { maskedEmail: otpDelivery.maskedEmail, otpExpiresIn: otpDelivery.otpExpiresIn }
                : { otpError: otpDelivery.error }),
            }
            : {}),
        });
        return;
      }

      res.json(await buildLoginResponse(user, res));
    } catch (error) {
      console.error('Login error:', error);
      res.status(500).json({ error: 'Login failed' });
    }
  }
);

// Logout — stateless JWT, so there is no server session to destroy. This
// endpoint exists so the client can signal a logout that the global
// activity-capture middleware records (with the authenticated user populated).
// The client discards its token regardless of this call's outcome.
router.post('/logout', authenticate, async (_req: Request, res: Response) => {
  res.json({ message: 'Logged out successfully' });
});

// Get current user (accepts both session JWT and API access token)
router.get('/me', authenticateJwtOrApiToken, async (req: Request, res: Response) => {
  try {
    const { Company } = getModels();
    const companies = await Company.find({
      _id: { $in: req.user!.companyIds },
    });

    res.json({
      user: {
        id: req.user!.id,
        email: req.user!.email,
        name: req.user!.name,
        role: req.user!.role,
        isOrgAdmin: req.user!.isOrgAdmin ?? false,
        apiManagementAccess: (req.user as any).apiManagementAccess ?? false,
        companyIds: req.user!.companyIds,
        activeCompanyId: req.user!.activeCompanyId,
        avatar: req.user!.avatar,
      },
      companies: companies.map((c: any) => ({
        id: c.id,
        name: c.name,
        isActive: c.isActive,
      })),
    });
  } catch (error) {
    res.status(500).json({ error: 'Failed to get user data' });
  }
});

// Switch active company
router.post(
  '/switch-company',
  authenticate,
  [body('companyId').notEmpty().withMessage('Company ID is required')],
  async (req: Request, res: Response) => {
    try {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        res.status(400).json({ errors: safeValidationErrors(errors) });
        return;
      }

      const { companyId } = req.body;

      // Verify the company exists
      const { Company } = getModels();
      const company = await Company.findById(companyId);
      if (!company) {
        res.status(404).json({ error: 'Company not found' });
        return;
      }

      // Add company to user's list if missing (handles mock-model sync edge cases)
      if (!req.user!.companyIds.includes(companyId)) {
        req.user!.companyIds.push(companyId);
      }

      req.user!.activeCompanyId = companyId;
      await req.user!.save();

      res.json({
        message: 'Company switched successfully',
        activeCompanyId: companyId,
      });
    } catch (error) {
      res.status(500).json({ error: 'Failed to switch company' });
    }
  }
);

// Update current user's email (accepts both session JWT and API access token)
router.patch(
  '/me/email',
  authenticateJwtOrApiToken,
  [
    body('email').isEmail().withMessage('Valid email required'),
  ],
  async (req: Request, res: Response) => {
    try {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        res.status(400).json({ errors: safeValidationErrors(errors) });
        return;
      }

      const { email } = req.body;
      const { User } = getModels();

      // Check if the new email is already taken by another user (case-insensitive)
      const existingUser = await User.findOne({
        _id: { $ne: req.user!.id },
        email: { $regex: new RegExp(`^${email.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i') },
      });
      if (existingUser) {
        res.status(409).json({ error: 'Email is already in use by another account' });
        return;
      }

      req.user!.email = email.trim().toLowerCase();
      await req.user!.save();

      res.json({
        message: 'Email updated successfully',
        user: {
          id: req.user!.id,
          email: req.user!.email,
          name: req.user!.name,
          role: req.user!.role,
          companyIds: req.user!.companyIds,
          activeCompanyId: req.user!.activeCompanyId,
          avatar: req.user!.avatar,
        },
      });
    } catch (error) {
      res.status(500).json({ error: 'Failed to update email' });
    }
  }
);

// Check if email exists (for forgot password)
router.post(
  '/check-email',
  authRateLimiter,
  [
    body('email').isEmail().withMessage('Valid email required'),
  ],
  async (req: Request, res: Response) => {
    try {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        res.status(400).json({ errors: safeValidationErrors(errors) });
        return;
      }

      const { User } = getModels();
      const user = await findUserByEmail(User, req.body.email);

      if (!user) {
        res.status(404).json({ error: 'This email is not registered.' });
        return;
      }

      res.json({ message: 'Email found', email: user.email });
    } catch (error) {
      console.error('Check email error:', error);
      res.status(500).json({ error: 'Failed to check email' });
    }
  }
);

// ============================================
// PASSWORD RESET
// ============================================

/** How long an emailed reset link stays usable. */
const RESET_TOKEN_TTL_MINUTES = 60;

/**
 * The answer /forgot-password gives once a reset link has actually been sent.
 *
 * Worded conditionally because it used to be the reply for BOTH outcomes — the
 * endpoint deliberately would not say whether an address was registered. That
 * was changed on request: an unknown address now gets an explicit
 * EMAIL_NOT_REGISTERED_MESSAGE instead, which does make this endpoint able to
 * confirm whether a given email has an account (see the note there).
 */
const FORGOT_PASSWORD_MESSAGE =
  'If an account exists for that email, a password reset link has been sent.';

/**
 * Returned for an address with no account.
 *
 * Note this is an account-enumeration oracle: anyone can now test an address
 * here and learn whether it is registered. It is a product decision, requested
 * explicitly, and `passwordResetRateLimiter` (per IP and per address) is what
 * keeps it from being harvested in bulk rather than made impossible.
 */
const EMAIL_NOT_REGISTERED_MESSAGE = 'Email not registered';

/** Shown for a token that is unknown, expired, or already used — all indistinguishable by design. */
const INVALID_TOKEN_MESSAGE = 'This reset link is invalid or has expired. Please request a new one.';

/** Only the hash of a reset token is stored, so a leaked database cannot be used to reset passwords. */
const hashResetToken = (token: string): string =>
  crypto.createHash('sha256').update(token).digest('hex');

/**
 * Where the emailed link points — the host the reset was actually requested
 * from, so a localhost test mails a localhost link and the live app mails a
 * live one. getRequestAppBaseUrl allowlists that origin before using it and
 * falls back to the configured public URL, which is what stops a forged Origin
 * header from redirecting somebody else's reset token.
 */
const buildResetLink = (req: Request, token: string): string => {
  return `${getRequestAppBaseUrl(req)}/reset-password?token=${encodeURIComponent(token)}`;
};

/**
 * Resolve the account a raw reset token belongs to, or null when the token is
 * unknown, expired, or already consumed (a used token has its hash cleared).
 */
async function findUserByResetToken(User: any, rawToken: string): Promise<any | null> {
  if (!rawToken || typeof rawToken !== 'string') return null;

  const user = await User.findOne({ passwordResetTokenHash: hashResetToken(rawToken) });
  if (!user) return null;

  const expiresAt = user.passwordResetExpiresAt ? new Date(user.passwordResetExpiresAt) : null;
  if (!expiresAt || expiresAt.getTime() <= Date.now()) return null;

  return user;
}

// POST /forgot-password — email a reset link
// A registered address gets 200 and a mailed link; an unregistered one gets 404
// and no token. Issuing a new token replaces any outstanding one.
router.post(
  '/forgot-password',
  authRateLimiter,
  // Caps sustained reset requests per IP and per target address. Every request
  // here can send mail, so this is the difference between a reset link and an
  // inbox full of them.
  passwordResetRateLimiter,
  [body('email').isEmail().withMessage('Valid email required')],
  async (req: Request, res: Response) => {
    try {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        res.status(400).json({ errors: safeValidationErrors(errors) });
        return;
      }

      const { User } = getModels();
      const user = await findUserByEmail(User, req.body.email);

      // Unknown address: answer with the error and stop here. No token is
      // generated and no mail is sent — the send below is only reached once an
      // account has been found.
      if (!user) {
        console.log(`Password reset requested for unknown email "${(req.body.email || '').toLowerCase().trim()}"`);
        res.status(404).json({ error: EMAIL_NOT_REGISTERED_MESSAGE });
        return;
      }

      const rawToken = crypto.randomBytes(32).toString('hex');
      user.passwordResetTokenHash = hashResetToken(rawToken);
      user.passwordResetExpiresAt = new Date(Date.now() + RESET_TOKEN_TTL_MINUTES * 60 * 1000);
      await user.save();

      // sendPasswordResetEmail never throws — a mail outage must not tell the
      // caller anything different from a successful send.
      const appBaseUrl = getRequestAppBaseUrl(req);
      const result = await sendPasswordResetEmail({
        to: user.email,
        userName: user.name,
        resetLink: buildResetLink(req, rawToken),
        expiryMinutes: RESET_TOKEN_TTL_MINUTES,
        // Keeps the footer's help/privacy/terms links on the same host as the
        // reset button, instead of bouncing a local tester to production.
        appBaseUrl,
      });

      if (!result.success) {
        console.error(`[Auth] Password reset email not delivered to ${user.email}: ${result.error}`);
      }

      res.json({ message: FORGOT_PASSWORD_MESSAGE });
    } catch (error) {
      console.error('Forgot password error:', error);
      res.status(500).json({ error: 'Failed to process the request. Please try again.' });
    }
  }
);

// POST /reset-password/validate — is this link still usable?
// Lets the reset page show "expired link" before the user types a new password.
router.post(
  '/reset-password/validate',
  authRateLimiter,
  [body('token').isString().trim().notEmpty().withMessage('Reset token is required')],
  async (req: Request, res: Response) => {
    try {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        res.status(400).json({ error: INVALID_TOKEN_MESSAGE });
        return;
      }

      const { User } = getModels();
      const user = await findUserByResetToken(User, req.body.token);
      if (!user) {
        res.status(400).json({ error: INVALID_TOKEN_MESSAGE });
        return;
      }

      res.json({ valid: true, email: user.email });
    } catch (error) {
      console.error('Validate reset token error:', error);
      res.status(500).json({ error: 'Failed to validate the reset link. Please try again.' });
    }
  }
);

// POST /reset-password — set a new password using a reset token
router.post(
  '/reset-password',
  authRateLimiter,
  [
    body('token').isString().trim().notEmpty().withMessage('Reset token is required'),
    passwordPolicyValidator(),
  ],
  async (req: Request, res: Response) => {
    try {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        res.status(400).json({ errors: safeValidationErrors(errors) });
        return;
      }

      const { User } = getModels();
      const user = await findUserByResetToken(User, req.body.token);
      if (!user) {
        res.status(400).json({ error: INVALID_TOKEN_MESSAGE });
        return;
      }

      // The User pre-save hook hashes this with bcrypt before it is stored.
      user.passwordHash = req.body.password;
      // Consume the token in the same save, so it cannot be replayed.
      user.passwordResetTokenHash = undefined;
      user.passwordResetExpiresAt = undefined;
      // A self-service reset satisfies any admin-forced password change.
      user.mustChangePassword = false;
      await user.save();

      res.locals.activityActor = {
        id: user.id,
        email: user.email,
        name: user.name,
        organizationId: user.activeCompanyId || (user.companyIds?.[0] ?? null),
      };

      // In-app security notice, alongside the email below. Personal event —
      // it reaches this account only, whatever role they hold.
      void notificationService.notifyUser(user.id, {
        type: 'auth.password.reset',
        message: 'Your password was changed. If this wasn\'t you, contact support immediately.',
        organizationId: user.activeCompanyId || (user.companyIds?.[0] ?? null),
        actorUserId: user.id,
      });

      // Security notice — best effort, never blocks the reset.
      const notice = await sendPasswordChangedEmail({ to: user.email, userName: user.name });
      if (!notice.success) {
        console.error(`[Auth] Password-changed notice not delivered to ${user.email}: ${notice.error}`);
      }

      res.json({ message: 'Password reset successfully. You can now sign in with your new password.' });
    } catch (error) {
      console.error('Reset password error:', error);
      res.status(500).json({ error: 'Failed to reset the password. Please try again.' });
    }
  }
);

// GET /me/permissions — Resolve effective permissions for the current user
// Any authenticated user can fetch their own resolved permissions.
// This replaces the need for admin users to call /admin/users/:id/permissions
// (which requires super-admin) for their own permission resolution.
router.get('/me/permissions', authenticate, async (req: Request, res: Response) => {
  try {
    const user = req.user!;
    const companyId = user.activeCompanyId || (user.companyIds?.[0] || '');

    // Super-admin and the org main Admin always have full access by default.
    // Admin is never subject to role-based restrictions or user-specific
    // overrides, so we resolve to all permissions directly.
    if (user.role === 'super-admin' || user.role === 'admin') {
      const allModules = [
        'business-profile', 'founders', 'employees', 'products', 'product-categories',
        'icp-personas', 'competitors', 'brand', 'brand-strategy', 'visual-identity',
        'brand-manual', 'brand-assets', 'stationery', 'hr-assets', 'seo',
        'blog-content-os', 'case-studies', 'testimonials', 'faq-bank',
        'website-planner', 'newsletter-content-os', 'social-media-os',
        'landing-pages', 'whatsapp-nurturing', 'sales-scripts', 'sales-collateral',
        'video-content', 'books',
        'commission-tracker', 'sales-targets', 'sales-reports', 'sales-playbooks', 'proposals-quotes',
        'ads', 'pr', 'email-templates', 'intro-scripts',
        'courses', 'events', 'guerrilla-marketing', 'interview-media-prep',
        'speaking-engagements', 'marketing-calendar', 'marketing-channels',
        'influencer', 'gmb', 'loyalty-programme', 'membership-plans',
        'referral-programme', 'sops', 'legal-documents', 'ai-processing',
        'magazine-sponsorship', 'funding', 'ai-chat',
        'dashboard', 'roles', 'users', 'access-control', 'audit-log',
      ];
      const allActions = ['view', 'create', 'edit', 'delete', 'ai-generate', 'export', 'import', 'download', 'upload', 'generate', 'manage', 'assign', 'share', 'approve', 'publish'];

      const modules: Record<string, any> = {};
      for (const mod of allModules) {
        const perm: Record<string, boolean> = {};
        for (const action of allActions) {
          perm[action] = true;
        }
        modules[mod] = perm;
      }

      res.json({ modules, isSuperAdmin: user.role === 'super-admin' });
      return;
    }

    // For non-super-admin users, build the permission map from role + overrides
    const { Role, UserAccessOverride } = getModels();
    const permMap = await buildPermissionMap(user, user._id.toString(), companyId, Role, UserAccessOverride);

    // Convert the flat permMap { module: { "action": true } } to the ResolvedPermissions format
    // that the frontend expects: { modules: { moduleId: { canView, canCreate, ... } } }
    const actionMap: Record<string, string> = {
      'view': 'canView', 'create': 'canCreate', 'edit': 'canEdit', 'delete': 'canDelete',
      'ai-generate': 'canAIGenerate', 'export': 'canExport', 'import': 'canImport',
      'download': 'canDownload', 'upload': 'canUpload', 'generate': 'canGenerate',
      'manage': 'canManage', 'assign': 'canAssign', 'share': 'canShare',
      'approve': 'canApprove', 'publish': 'canPublish',
    };

    const modules: Record<string, any> = {};
    for (const [moduleId, actions] of Object.entries(permMap)) {
      const modPerms: Record<string, boolean> = {};
      // Default all action flags to false
      for (const [, camelKey] of Object.entries(actionMap)) {
        modPerms[camelKey] = false;
      }
      // Set action flags from the permission map
      for (const [actionKey, value] of Object.entries(actions)) {
        const camelKey = actionMap[actionKey];
        if (camelKey) {
          modPerms[camelKey] = value;
        }
      }
      modules[moduleId] = modPerms;
    }

    res.json({ modules, isSuperAdmin: false });
  } catch (error) {
    console.error('Error resolving permissions:', error);
    res.status(500).json({ error: 'Failed to resolve permissions' });
  }
});

export default router;
