/**
 * Auth REST endpoints.
 *
 * POST   /auth/login             — login with email + password
 * POST   /auth/register          — register new user
 * POST   /auth/refresh           — refresh access token
 * GET    /auth/me                — get current user from token
 * PUT    /auth/profile           — update own profile (displayName, phone, bio, avatarUrl)
 * POST   /auth/change-password   — change password (authenticated)
 * DELETE /auth/account           — delete own account (authenticated)
 * POST   /auth/sso               — SSO login (verified external identity)
 * POST   /auth/verify            — service-to-service token verification
 *
 * Password reset (unauthenticated):
 * POST   /auth/forgot-password       — request reset token
 * POST   /auth/reset-password        — reset password with token
 *
 * Email verification:
 * POST   /auth/verify-email          — verify email with token
 * POST   /auth/resend-verification   — resend verification email
 *
 * Admin user management (requires super_admin or admin role):
 * GET    /auth/users          — list users (paginated)
 * GET    /auth/users/count    — total + by-plan counts
 * GET    /auth/users/:id      — get user by id
 * PUT    /auth/users/:id      — update user
 * DELETE /auth/users/:id      — delete user
 */

import type { FastifyInstance } from 'fastify';
import {
  BadRequestError,
  ForbiddenError,
  UnauthorizedError,
  TooManyRequestsError,
  ServiceError,
} from '../../lib/errors.js';
import { bus } from '../../lib/event-bus.js';
import { getProduct, getAllProducts } from '../products/cache.js';
import * as subscriptionRepo from '../subscriptions/repository.js';
import * as licenseRepo from '../licenses/repository.js';
import * as repo from './repository.js';
import * as jwt from './jwt.js';
import * as loginEventRepo from './login-events/repository.js';
import { scoreLoginRisk } from './login-events/risk-scorer.js';
import * as mfaRepo from './mfa/repository.js';
import { createChallenge } from './mfa/challenge-store.js';
import * as deviceRepo from './devices/repository.js';
import {
  LoginSchema,
  RegisterSchema,
  RefreshSchema,
  UpdateUserSchema,
  SsoLoginSchema,
  ProfileUpdateSchema,
  ChangePasswordSchema,
  DeleteAccountSchema,
  ForgotPasswordSchema,
  ResetPasswordSchema,
  VerifyEmailSchema,
  ResendVerificationSchema,
  type UserDoc,
} from './types.js';

// ── Lockout helpers ─────────────────────────────────────────

const LOCKOUT_DURATIONS_MS = [
  15 * 60 * 1000, // 15 min
  30 * 60 * 1000, // 30 min
  60 * 60 * 1000, // 1 hour
  24 * 60 * 60 * 1000, // 24 hours
];
const LOCKOUT_THRESHOLD = 5;

function getLockoutDuration(failedAttempts: number): number {
  const index = Math.min(
    Math.floor((failedAttempts - LOCKOUT_THRESHOLD) / LOCKOUT_THRESHOLD),
    LOCKOUT_DURATIONS_MS.length - 1
  );
  return LOCKOUT_DURATIONS_MS[Math.max(0, index)];
}

// ── Login rate limiter (sliding window, in-memory) ──────────

interface RateLimitEntry {
  timestamps: number[];
}

const loginRateMap = new Map<string, RateLimitEntry>();
const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 min
const RATE_LIMIT_MAX = 10;

function checkLoginRateLimit(ip: string): void {
  const now = Date.now();
  let entry = loginRateMap.get(ip);
  if (!entry) {
    entry = { timestamps: [] };
    loginRateMap.set(ip, entry);
  }
  // Prune old entries
  entry.timestamps = entry.timestamps.filter(t => now - t < RATE_LIMIT_WINDOW_MS);
  if (entry.timestamps.length >= RATE_LIMIT_MAX) {
    throw new TooManyRequestsError('AUTH_RATE_LIMITED');
  }
  entry.timestamps.push(now);
}

// ── AUTH_ACCOUNT_LOCKED error (423) ─────────────────────────

class AccountLockedError extends ServiceError {
  constructor(retryAfter?: string) {
    super(423, retryAfter ? `Account locked until ${retryAfter}` : 'Account is locked', {
      code: 'AUTH_ACCOUNT_LOCKED',
      retryAfter: retryAfter ?? null,
    });
  }
}

export async function authRoutes(app: FastifyInstance) {
  // ── Login event recording helper ──────────────────────────
  async function recordLoginEvent(
    userId: string,
    productId: string,
    result: 'success' | 'failed' | 'locked' | 'mfa_required' | 'mfa_failed',
    method: 'password' | 'oauth_google' | 'oauth_microsoft' | 'oauth_apple' | 'passkey' | 'refresh',
    ip: string,
    userAgent?: string,
    fingerprint?: string
  ): Promise<void> {
    try {
      const device = fingerprint ? await deviceRepo.getByFingerprint(userId, fingerprint) : null;
      const recentFailures = await loginEventRepo.countRecentFailures(userId, 15 * 60 * 1000);
      const risk = scoreLoginRisk({
        ip,
        userAgent,
        fingerprint,
        isNewIp: true, // simplified — full impl would check IP history
        isNewDevice: !device,
        isDeviceTrusted: deviceRepo.isDeviceTrusted(device),
        recentFailures,
        method,
        hourOfDay: new Date().getHours(),
      });
      await loginEventRepo.record({
        id: `le_${crypto.randomUUID()}`,
        userId,
        productId,
        result,
        method,
        riskLevel: risk.level,
        riskScore: risk.score,
        ip,
        userAgent,
        fingerprint,
        riskFlags: risk.flags,
        createdAt: new Date().toISOString(),
      });
    } catch {
      // best-effort — never block login
    }
  }

  // Login
  app.post('/auth/login', async req => {
    const parsed = LoginSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const { email, password, productId } = parsed.data;

    // Rate limit by IP
    const ip = req.ip || 'unknown';
    checkLoginRateLimit(ip);

    const user = await repo.getByEmail(email, productId);
    if (!user) throw new UnauthorizedError('AUTH_INVALID_CREDENTIALS');
    if (user.status !== 'active') throw new UnauthorizedError('Account is disabled');

    // Check lockout BEFORE password verification
    if (user.lockedUntil) {
      const lockExpiry = new Date(user.lockedUntil);
      if (lockExpiry > new Date()) {
        throw new AccountLockedError(user.lockedUntil);
      }
      // Lock expired — reset
      await repo.resetFailedLogin(user.id);
    }

    const valid = await repo.verifyPassword(password, user.passwordHash);
    if (!valid) {
      const attempts = (user.failedLoginAttempts ?? 0) + 1;
      let lockedUntil: string | undefined;
      if (attempts >= LOCKOUT_THRESHOLD) {
        const duration = getLockoutDuration(attempts);
        lockedUntil = new Date(Date.now() + duration).toISOString();
        // Emit lockout event
        bus
          .emit(
            'auth.account_locked',
            {
              userId: user.id,
              email: user.email,
              productId,
              lockedUntil,
              failedAttempts: attempts,
            },
            { source: 'auth/login' }
          )
          .catch(() => {});
      }
      await repo.incrementFailedLogin(user.id, lockedUntil);

      // Record failed login event (best-effort)
      recordLoginEvent(
        user.id,
        productId,
        'failed',
        'password',
        ip,
        req.headers['user-agent'] as string | undefined
      ).catch(() => {});

      throw new UnauthorizedError('AUTH_INVALID_CREDENTIALS');
    }

    // Success — reset failed attempts
    if (user.failedLoginAttempts && user.failedLoginAttempts > 0) {
      await repo.resetFailedLogin(user.id);
    }

    // Check if MFA is enabled — issue challenge instead of tokens
    if (user.mfaEnabled) {
      const mfaDoc = await mfaRepo.getByUserId(user.id);
      if (mfaDoc?.verified) {
        const challengeToken = createChallenge({
          userId: user.id,
          productId,
          email: user.email,
          role: user.role,
          plan: user.plan,
        });

        // Record MFA-required login event (best-effort)
        recordLoginEvent(
          user.id,
          productId,
          'mfa_required',
          'password',
          ip,
          req.headers['user-agent'] as string | undefined
        ).catch(() => {});

        return { mfaRequired: true, challengeToken };
      }
    }

    await repo.updateLastLogin(user.id);

    // Record successful login event (best-effort)
    recordLoginEvent(
      user.id,
      productId,
      'success',
      'password',
      ip,
      req.headers['user-agent'] as string | undefined
    ).catch(() => {});

    const accessToken = await jwt.createAccessToken({
      sub: user.id,
      email: user.email,
      role: user.role,
      productId,
      plan: user.plan,
      displayName: user.displayName,
    });
    const refreshToken = await jwt.createRefreshToken({ sub: user.id, productId });

    return {
      accessToken,
      refreshToken,
      user: {
        id: user.id,
        email: user.email,
        role: user.role,
        plan: user.plan,
        displayName: user.displayName,
      },
    };
  });

  // Register
  app.post('/auth/register', async (req, reply) => {
    const parsed = RegisterSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const { email, password, displayName, role, productId } = parsed.data;
    const product = getProduct(productId);
    if (!product || product.status !== 'active') {
      throw new BadRequestError(`Unknown or disabled product: ${productId}`);
    }

    const existing = await repo.getByEmail(email, productId);
    if (existing) throw new BadRequestError('Email already registered');

    const now = new Date().toISOString();
    const user: UserDoc = {
      id: `usr_${crypto.randomUUID()}`,
      productId,
      email: email.toLowerCase(),
      passwordHash: await repo.hashPassword(password),
      plan: product.defaultPlan,
      role,
      displayName,
      status: 'active',
      emailVerified: false,
      lastLoginAt: null,
      createdAt: now,
      updatedAt: now,
    };
    await repo.create(user);

    const nowDate = new Date();
    const nowIso = nowDate.toISOString();
    const trialEnd = new Date(nowDate);
    trialEnd.setDate(trialEnd.getDate() + product.trialDays);
    const hasTrial = product.trialDays > 0;
    const initialPlan = product.defaultPlan;

    // Registration hook: initialize subscription + license using product defaults.
    // Best-effort during migration; auth account creation remains primary.
    try {
      await subscriptionRepo.createSubscription({
        id: `sub_${user.id}_${Date.now()}`,
        productId,
        userId: user.id,
        plan: initialPlan,
        status: hasTrial ? 'trialing' : 'active',
        currentPeriodStart: nowIso,
        currentPeriodEnd: hasTrial ? trialEnd.toISOString() : nowIso,
        cancelAtPeriodEnd: false,
        monthlyPrice: 0,
        tokensIncluded: 0,
        tokensUsed: 0,
        createdAt: nowIso,
        updatedAt: nowIso,
      });
    } catch (err) {
      req.log.warn(
        { err, userId: user.id, productId },
        'Subscription provisioning failed during register'
      );
    }

    try {
      await licenseRepo.create({
        id: `lic_${crypto.randomUUID()}`,
        productId,
        key: licenseRepo.generateKey(product.licensePrefix),
        userId: user.id,
        plan: initialPlan,
        status: 'active',
        activatedAt: null,
        expiresAt: hasTrial ? trialEnd.toISOString() : null,
        deviceIds: [],
        maxDevices: product.deviceLimits[initialPlan],
        createdAt: nowIso,
        updatedAt: nowIso,
      });
    } catch (err) {
      req.log.warn(
        { err, userId: user.id, productId },
        'License provisioning failed during register'
      );
    }

    const accessToken = await jwt.createAccessToken({
      sub: user.id,
      email: user.email,
      role: user.role,
      productId,
      plan: user.plan,
      displayName: user.displayName,
    });
    const refreshToken = await jwt.createRefreshToken({ sub: user.id, productId });

    // Emit user.created event (fire-and-forget)
    bus
      .emit(
        'user.created',
        {
          userId: user.id,
          email: user.email,
          plan: user.plan,
          productId,
        },
        { source: 'auth/register' }
      )
      .catch(() => {});

    // Create email verification token and emit event (best-effort)
    try {
      const verifyRawToken = crypto.randomUUID();
      const verifyTokenHash = repo.hashToken(verifyRawToken);
      const verifyExpiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
      await repo.createEmailVerification({
        id: `evf_${crypto.randomUUID()}`,
        productId,
        userId: user.id,
        email: user.email,
        tokenHash: verifyTokenHash,
        expiresAt: verifyExpiresAt,
        createdAt: nowIso,
      });
      bus
        .emit(
          'user.email_verification_requested',
          {
            userId: user.id,
            email: user.email,
            verificationToken: verifyRawToken,
            displayName,
            productId,
          },
          { source: 'auth/register' }
        )
        .catch(() => {});
    } catch (err) {
      req.log.warn(
        { err, userId: user.id },
        'Email verification provisioning failed during register'
      );
    }

    reply.code(201);
    return {
      accessToken,
      refreshToken,
      user: {
        id: user.id,
        email: user.email,
        role: user.role,
        plan: user.plan,
        displayName: user.displayName,
      },
    };
  });

  // Refresh
  app.post('/auth/refresh', async req => {
    const parsed = RefreshSchema.safeParse(req.body);
    if (!parsed.success) throw new BadRequestError('refreshToken is required');

    try {
      const payload = await jwt.verifyToken(parsed.data.refreshToken);
      if (payload.type !== 'refresh') throw new Error('Not a refresh token');

      const user = await repo.getById(payload.sub);
      if (!user || user.status !== 'active')
        throw new UnauthorizedError('User not found or disabled');

      const accessToken = await jwt.createAccessToken({
        sub: user.id,
        email: user.email,
        role: user.role,
        productId: user.productId,
        plan: user.plan,
        displayName: user.displayName,
      });
      const refreshToken = await jwt.createRefreshToken({
        sub: user.id,
        productId: user.productId,
      });
      return { accessToken, refreshToken };
    } catch {
      throw new UnauthorizedError('Invalid or expired refresh token');
    }
  });

  // Me (enhanced for OneAuth)
  app.get('/auth/me', async req => {
    const auth = req.headers.authorization;
    if (!auth?.startsWith('Bearer ')) throw new UnauthorizedError();
    const token = auth.slice(7);

    try {
      const payload = await jwt.verifyToken(token);
      const user = await repo.getById(payload.sub);
      if (!user) throw new UnauthorizedError('User not found');

      // Lazy-import oauthRepo to avoid circular deps at module load
      const oauthRepo = await import('./oauth/repository.js');
      const providers = await oauthRepo.listByUser(user.id);

      return {
        id: user.id,
        email: user.email,
        role: user.role,
        plan: user.plan,
        displayName: user.displayName,
        emailVerified: user.emailVerified,
        currentProduct: payload.productId,
        products: (user.memberships ?? []).map(m => ({
          productId: m.productId,
          plan: m.plan,
          role: m.role,
        })),
        providers: providers.map(p => ({
          provider: p.provider,
          email: p.email,
          linkedAt: p.linkedAt,
        })),
        mfaEnabled: user.mfaEnabled ?? false,
        mfaMethods: user.mfaMethods ?? [],
      };
    } catch {
      throw new UnauthorizedError('Invalid or expired token');
    }
  });

  // Self-service profile update (authenticated user updates their own profile)
  app.put('/auth/profile', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    const parsed = ProfileUpdateSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    const updated = await repo.update(payload.sub, parsed.data);
    if (!updated) throw new BadRequestError('User not found');

    const { passwordHash: _h, ...safe } = updated;
    void _h;
    return safe;
  });

  // Change password (authenticated)
  app.post('/auth/change-password', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    const parsed = ChangePasswordSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    const user = await repo.getById(payload.sub);
    if (!user) throw new UnauthorizedError('User not found');

    const valid = await repo.verifyPassword(parsed.data.currentPassword, user.passwordHash);
    if (!valid) throw new BadRequestError('Current password is incorrect');

    const newHash = await repo.hashPassword(parsed.data.newPassword);
    await repo.updatePassword(user.id, newHash);

    req.log.info({ userId: user.id }, '[auth] Password changed');
    return { message: 'Password changed successfully.' };
  });

  // Delete own account (authenticated)
  app.delete('/auth/account', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    const parsed = DeleteAccountSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    const user = await repo.getById(payload.sub);
    if (!user) throw new UnauthorizedError('User not found');

    const valid = await repo.verifyPassword(parsed.data.password, user.passwordHash);
    if (!valid) throw new BadRequestError('Incorrect password');

    await repo.remove(user.id);

    bus
      .emit(
        'user.deleted',
        { userId: user.id, email: user.email, productId: user.productId },
        { source: 'auth/delete-account' }
      )
      .catch(() => {});

    req.log.info({ userId: user.id }, '[auth] Account deleted');
    return { message: 'Account deleted successfully.' };
  });

  // SSO login — verified external identity (called by dashboard OAuth callbacks)
  // The caller has already verified the user's identity via Microsoft/Google OAuth.
  // This endpoint finds or creates the user and issues platform JWT tokens.
  app.post('/auth/sso', async (req, reply) => {
    const parsed = SsoLoginSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const { email, productId, provider, displayName } = parsed.data;
    const product = getProduct(productId);
    if (!product || product.status !== 'active') {
      throw new BadRequestError(`Unknown or disabled product: ${productId}`);
    }

    let user = await repo.getByEmail(email, productId);

    if (!user) {
      // Auto-create SSO user with a random password (they'll never use it)
      const now = new Date().toISOString();
      user = {
        id: `usr_${crypto.randomUUID()}`,
        productId,
        email: email.toLowerCase(),
        passwordHash: await repo.hashPassword(crypto.randomUUID()),
        plan: product.defaultPlan,
        role: 'user',
        displayName: displayName || email.split('@')[0],
        status: 'active',
        emailVerified: true,
        lastLoginAt: now,
        createdAt: now,
        updatedAt: now,
      };
      await repo.create(user);

      // Best-effort: provision subscription + license (same as register)
      const nowDate = new Date();
      const trialEnd = new Date(nowDate);
      trialEnd.setDate(trialEnd.getDate() + product.trialDays);
      const hasTrial = product.trialDays > 0;
      try {
        await subscriptionRepo.createSubscription({
          id: `sub_${user.id}_${Date.now()}`,
          productId,
          userId: user.id,
          plan: product.defaultPlan,
          status: hasTrial ? 'trialing' : 'active',
          currentPeriodStart: now,
          currentPeriodEnd: hasTrial ? trialEnd.toISOString() : now,
          cancelAtPeriodEnd: false,
          monthlyPrice: 0,
          tokensIncluded: 0,
          tokensUsed: 0,
          createdAt: now,
          updatedAt: now,
        });
      } catch (err) {
        req.log.warn({ err, userId: user.id }, 'SSO subscription provisioning failed');
      }
      try {
        await licenseRepo.create({
          id: `lic_${crypto.randomUUID()}`,
          productId,
          key: licenseRepo.generateKey(product.licensePrefix),
          userId: user.id,
          plan: product.defaultPlan,
          status: 'active',
          activatedAt: null,
          expiresAt: hasTrial ? trialEnd.toISOString() : null,
          deviceIds: [],
          maxDevices: product.deviceLimits[product.defaultPlan],
          createdAt: now,
          updatedAt: now,
        });
      } catch (err) {
        req.log.warn({ err, userId: user.id }, 'SSO license provisioning failed');
      }

      reply.code(201);
    } else {
      // Existing user — update last login
      await repo.updateLastLogin(user.id);
    }

    req.log.info({ userId: user.id, provider, email }, 'SSO login');

    const accessToken = await jwt.createAccessToken({
      sub: user.id,
      email: user.email,
      role: user.role,
      productId,
      plan: user.plan,
      displayName: user.displayName,
    });
    const refreshToken = await jwt.createRefreshToken({ sub: user.id, productId });

    return {
      accessToken,
      refreshToken,
      user: {
        id: user.id,
        email: user.email,
        role: user.role,
        plan: user.plan,
        displayName: user.displayName,
      },
    };
  });

  // Service-to-service token verification
  app.post('/auth/verify', async req => {
    const { token } = req.body as { token?: string };
    if (!token) throw new BadRequestError('token is required');

    try {
      const payload = await jwt.verifyToken(token);
      return { valid: true, payload };
    } catch {
      return { valid: false, payload: null };
    }
  });

  // ── Password Reset ──────────────────────────────────────────

  // Request a password reset token
  app.post('/auth/forgot-password', async req => {
    const parsed = ForgotPasswordSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const { email, productId } = parsed.data;

    // Always return success to prevent email enumeration
    const user = await repo.getByEmail(email, productId);
    if (!user) {
      return { message: 'If that email exists, a reset link has been sent.' };
    }

    const rawToken = crypto.randomUUID();
    const tokenHash = repo.hashToken(rawToken);
    const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // 1 hour

    await repo.createResetToken({
      id: `rst_${crypto.randomUUID()}`,
      productId,
      userId: user.id,
      tokenHash,
      expiresAt,
      createdAt: new Date().toISOString(),
    });

    req.log.info({ userId: user.id, productId }, '[auth] Password reset token generated');

    // Emit password_reset event — subscriber sends the email
    bus
      .emit(
        'user.password_reset',
        {
          userId: user.id,
          email: user.email,
          resetToken: rawToken,
          displayName: user.displayName,
          productId,
        },
        { source: 'auth/forgot-password' }
      )
      .catch(() => {});

    return { message: 'If that email exists, a reset link has been sent.' };
  });

  // Reset password with token
  app.post('/auth/reset-password', async req => {
    const parsed = ResetPasswordSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const { token, newPassword } = parsed.data;
    const tokenHash = repo.hashToken(token);

    // Search across all registered products — token hash is unique
    let resetDoc = null;
    for (const p of getAllProducts()) {
      resetDoc = await repo.findResetToken(tokenHash, p.id);
      if (resetDoc) break;
    }

    if (!resetDoc) {
      throw new BadRequestError('Invalid or expired reset token');
    }

    // Check expiry
    if (new Date(resetDoc.expiresAt) < new Date()) {
      throw new BadRequestError('Reset token has expired');
    }

    // Update password
    const newHash = await repo.hashPassword(newPassword);
    const updated = await repo.updatePassword(resetDoc.userId, newHash);
    if (!updated) {
      throw new BadRequestError('Failed to update password');
    }

    // Mark token as used
    await repo.markResetTokenUsed(resetDoc.id, resetDoc.productId);

    req.log.info({ userId: resetDoc.userId }, '[auth] Password reset successful');
    return { message: 'Password has been reset successfully.' };
  });

  // ── Email Verification ──────────────────────────────────────

  // Verify email with token
  app.post('/auth/verify-email', async req => {
    const parsed = VerifyEmailSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const tokenHash = repo.hashToken(parsed.data.token);

    let verifyDoc = null;
    for (const p of getAllProducts()) {
      verifyDoc = await repo.findEmailVerification(tokenHash, p.id);
      if (verifyDoc) break;
    }

    if (!verifyDoc) {
      throw new BadRequestError('Invalid or expired verification token');
    }

    if (new Date(verifyDoc.expiresAt) < new Date()) {
      throw new BadRequestError('Verification token has expired');
    }

    await repo.setEmailVerified(verifyDoc.userId, true);
    await repo.markEmailVerified(verifyDoc.id, verifyDoc.productId);

    // Emit email_verified event (fire-and-forget)
    bus
      .emit(
        'user.email_verified',
        {
          userId: verifyDoc.userId,
          email: verifyDoc.email,
          productId: verifyDoc.productId,
        },
        { source: 'auth/verify-email' }
      )
      .catch(() => {});

    req.log.info({ userId: verifyDoc.userId }, '[auth] Email verified');
    return { message: 'Email verified successfully.' };
  });

  // Resend verification email
  app.post('/auth/resend-verification', async req => {
    const parsed = ResendVerificationSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const { email, productId } = parsed.data;

    const user = await repo.getByEmail(email, productId);
    if (!user) {
      return { message: 'If that email exists, a verification link has been sent.' };
    }

    const rawToken = crypto.randomUUID();
    const tokenHash = repo.hashToken(rawToken);
    const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); // 24 hours

    await repo.createEmailVerification({
      id: `evf_${crypto.randomUUID()}`,
      productId,
      userId: user.id,
      email: user.email,
      tokenHash,
      expiresAt,
      createdAt: new Date().toISOString(),
    });

    req.log.info({ userId: user.id, productId }, '[auth] Email verification token generated');

    // Emit event — subscriber sends the email
    bus
      .emit(
        'user.email_verification_requested',
        {
          userId: user.id,
          email: user.email,
          verificationToken: rawToken,
          displayName: user.displayName,
          productId,
        },
        { source: 'auth/resend-verification' }
      )
      .catch(() => {});

    return { message: 'If that email exists, a verification link has been sent.' };
  });

  // ── Admin user management ────────────────────────────────────

  function requireAdminRole(req: import('fastify').FastifyRequest): string {
    const role = req.jwtPayload?.role;
    if (!role || !['super_admin', 'admin'].includes(role)) {
      throw new ForbiddenError('Admin access required');
    }
    const productId = req.jwtPayload?.productId;
    if (!productId) throw new UnauthorizedError('Missing productId in token');
    return productId;
  }

  function stripPasswordHash(user: UserDoc) {
    const { passwordHash: _h, ...rest } = user;
    void _h;
    return rest;
  }

  // List users (paginated)
  app.get('/auth/users', async req => {
    const productId = requireAdminRole(req);
    const { limit, offset } = req.query as { limit?: string; offset?: string };
    const users = await repo.list(productId, Number(limit) || 100, Number(offset) || 0);
    return { users: users.map(stripPasswordHash) };
  });

  // Count users (total + by plan)
  app.get('/auth/users/count', async req => {
    const productId = requireAdminRole(req);
    const [total, byPlan] = await Promise.all([repo.count(productId), repo.countByPlan(productId)]);
    return { total, byPlan };
  });

  // Get user by ID
  app.get('/auth/users/:id', async req => {
    requireAdminRole(req);
    const { id } = req.params as { id: string };
    const user = await repo.getById(id);
    if (!user) throw new BadRequestError('User not found');
    return stripPasswordHash(user);
  });

  // Update user
  app.put('/auth/users/:id', async req => {
    requireAdminRole(req);
    const { id } = req.params as { id: string };
    const parsed = UpdateUserSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const updated = await repo.update(id, parsed.data);
    if (!updated) throw new BadRequestError('User not found');
    return stripPasswordHash(updated);
  });

  // Delete user
  app.delete('/auth/users/:id', async req => {
    requireAdminRole(req);
    const { id } = req.params as { id: string };
    const deleted = await repo.remove(id);
    if (!deleted) throw new BadRequestError('User not found');
    return { success: true };
  });
}
