// src/config/passport.ts
import passport from 'passport';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { Strategy as LocalStrategy } from 'passport-local';
import { Strategy as GoogleStrategy, Profile as GoogleProfile } from 'passport-google-oauth20';
import { Strategy as GitHubStrategy, Profile as GitHubProfile } from 'passport-github2';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
import * as bcrypt from 'bcrypt';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { Request, Response, NextFunction } from 'express';

import * as db from '../services/db/index.db';
import { logger } from '../services/logger.server';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { UserProfile } from '../types';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { createMockUserProfile } from '../tests/utils/mockFactories';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { ForbiddenError } from '../services/db/errors.db';

const JWT_SECRET = process.env.JWT_SECRET!;

const MAX_FAILED_ATTEMPTS = 5;
const LOCKOUT_DURATION_MINUTES = 15;

/**
 * A type guard to check if an object is a UserProfile.
 * This is useful for safely accessing properties on `req.user`.
 * @param user The user object to check.
 * @returns True if the object is a UserProfile, false otherwise.
 */
function isUserProfile(user: unknown): user is UserProfile {
  return (
    typeof user === 'object' &&
    user !== null &&
    'role' in user &&
    'user' in user &&
    typeof (user as { user: unknown }).user === 'object' &&
    (user as { user: unknown }).user !== null &&
    'user_id' in ((user as { user: unknown }).user as object)
  );
}

// --- Passport Local Strategy (for email/password login) ---
passport.use(
  new LocalStrategy(
    {
      usernameField: 'email',
      passReqToCallback: true, // Pass the request object to the callback
    },
    async (req: Request, email, password, done) => {
      try {
        // 1. Find the user by email, including their profile data for the JWT payload.
        const userprofile = await db.userRepo.findUserWithProfileByEmail(email, req.log);

        if (!userprofile) {
          // User not found
          req.log.warn(`Login attempt failed for non-existent user: ${email}`);
          return done(null, false, { message: 'Incorrect email or password.' });
        }

        // Check if the account is currently locked.
        if (
          userprofile.failed_login_attempts >= MAX_FAILED_ATTEMPTS &&
          userprofile.last_failed_login
        ) {
          const lockoutTime = new Date(userprofile.last_failed_login).getTime();
          const timeSinceLockout = Date.now() - lockoutTime;
          const lockoutDurationMs = LOCKOUT_DURATION_MINUTES * 60 * 1000;

          if (timeSinceLockout < lockoutDurationMs) {
            req.log.warn(`Login attempt for locked account: ${email}`);
            // Refresh the lockout timestamp on each attempt to prevent probing.
            await db.adminRepo.incrementFailedLoginAttempts(userprofile.user.user_id, req.log);
            return done(null, false, {
              message: `Account is temporarily locked. Please try again in ${LOCKOUT_DURATION_MINUTES} minutes.`,
            });
          }
        }

        if (!userprofile.password_hash) {
          // User exists but signed up via OAuth, so they don't have a password.
          req.log.warn(`Password login attempt for OAuth user: ${email}`);
          return done(null, false, {
            message:
              'This account was created using a social login. Please use Google or GitHub to sign in.',
          });
        }

        // 2. Compare the submitted password with the hashed password in your DB.
        req.log.debug(
          `[Passport] Verifying password for ${email}. Hash length: ${userprofile.password_hash.length}`,
        );
        const isMatch = await bcrypt.compare(password, userprofile.password_hash);
        req.log.debug(`[Passport] Password match result: ${isMatch}`);

        if (!isMatch) {
          // Password does not match
          req.log.warn(`Login attempt failed for user ${email} due to incorrect password.`);
          // Increment failed attempts and get the new count.
          const newAttemptCount = await db.adminRepo.incrementFailedLoginAttempts(
            userprofile.user.user_id,
            req.log,
          );

          // Log this security event.
          await db.adminRepo.logActivity(
            {
              userId: userprofile.user.user_id,
              action: 'login_failed_password',
              displayText: `Failed login attempt for user ${userprofile.user.email}.`,
              icon: 'shield-alert',
              details: { source_ip: req.ip ?? null, new_attempt_count: newAttemptCount }, // The user.email is correct here as it's part of the Omit type
            },
            req.log,
          );

          // If this attempt just locked the account, inform the user immediately.
          if (newAttemptCount >= MAX_FAILED_ATTEMPTS) {
            return done(null, false, {
              message: `Account is temporarily locked. Please try again in ${LOCKOUT_DURATION_MINUTES} minutes.`,
            });
          }

          return done(null, false, { message: 'Incorrect email or password.' });
        }

        // 3. Success! Return the user object (without password_hash for security).
        // Reset failed login attempts upon successful login.
        await db.adminRepo.resetFailedLoginAttempts(
          userprofile.user.user_id,
          req.ip ?? 'unknown',
          req.log,
        );

        req.log.info(`User successfully authenticated: ${email}`);

        // The `user` object from `findUserWithProfileByEmail` is now a fully formed
        // UserProfile object with additional authentication fields. We must strip these
        // sensitive fields before passing the profile to the session.
        // The `...userProfile` rest parameter will contain the clean UserProfile object,
        // which no longer has a top-level email property.
        // SECURITY: password_hash and refresh_token are intentionally discarded - never send to client.
        const {
          password_hash: _password_hash,
          failed_login_attempts,
          last_failed_login,
          refresh_token: _refresh_token,
          ...cleanUserProfile
        } = userprofile;

        // Log login metadata for audit purposes (non-sensitive fields only)
        req.log.debug(
          { failed_login_attempts, last_failed_login },
          'User login metadata stripped from session',
        );
        return done(null, cleanUserProfile);
      } catch (err: unknown) {
        req.log.error({ error: err }, 'Error during local authentication strategy:');
        return done(err);
      }
    },
  ),
);

// --- Passport Google OAuth 2.0 Strategy ---
// Only register the strategy if the required environment variables are set.
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
  passport.use(
    new GoogleStrategy(
      {
        clientID: process.env.GOOGLE_CLIENT_ID,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET,
        callbackURL: '/api/auth/google/callback',
        scope: ['profile', 'email'],
      },
      async (
        _accessToken: string,
        _refreshToken: string,
        profile: GoogleProfile,
        done: (error: Error | null, user?: UserProfile | false) => void,
      ) => {
        try {
          const email = profile.emails?.[0]?.value;
          if (!email) {
            return done(new Error('No email found in Google profile.'), false);
          }

          // Check if user already exists in our database
          const existingUserProfile = await db.userRepo.findUserWithProfileByEmail(email, logger);

          if (existingUserProfile) {
            // User exists, proceed to log them in.
            logger.info(`Google OAuth successful for existing user: ${email}`);
            // Strip sensitive fields before returning
            const {
              password_hash: _password_hash,
              failed_login_attempts: _failed_login_attempts,
              last_failed_login: _last_failed_login,
              refresh_token: _refresh_token,
              ...cleanUserProfile
            } = existingUserProfile;
            return done(null, cleanUserProfile);
          } else {
            // User does not exist, create a new account for them.
            logger.info(`Google OAuth: creating new user for email: ${email}`);

            // Since this is an OAuth user, they don't have a password.
            // We pass `null` for the password hash.
            const newUserProfile = await db.userRepo.createUser(
              email,
              null, // No password for OAuth users
              {
                full_name: profile.displayName,
                avatar_url: profile.photos?.[0]?.value,
              },
              logger,
            );

            return done(null, newUserProfile);
          }
        } catch (err) {
          logger.error({ error: err }, 'Error during Google authentication strategy');
          return done(err as Error, false);
        }
      },
    ),
  );
  logger.info('[Passport] Google OAuth strategy registered.');
} else {
  logger.warn(
    '[Passport] Google OAuth strategy NOT registered: GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET not set.',
  );
}

// --- Passport GitHub OAuth 2.0 Strategy ---
// Only register the strategy if the required environment variables are set.
if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) {
  passport.use(
    new GitHubStrategy(
      {
        clientID: process.env.GITHUB_CLIENT_ID,
        clientSecret: process.env.GITHUB_CLIENT_SECRET,
        callbackURL: '/api/auth/github/callback',
        scope: ['user:email'],
      },
      async (
        _accessToken: string,
        _refreshToken: string,
        profile: GitHubProfile,
        done: (error: Error | null, user?: UserProfile | false) => void,
      ) => {
        try {
          const email = profile.emails?.[0]?.value;
          if (!email) {
            return done(
              new Error(
                'No public email found in GitHub profile. Please ensure your primary email is public or add one.',
              ),
              false,
            );
          }

          // Check if user already exists in our database
          const existingUserProfile = await db.userRepo.findUserWithProfileByEmail(email, logger);

          if (existingUserProfile) {
            // User exists, proceed to log them in.
            logger.info(`GitHub OAuth successful for existing user: ${email}`);
            // Strip sensitive fields before returning
            const {
              password_hash: _password_hash,
              failed_login_attempts: _failed_login_attempts,
              last_failed_login: _last_failed_login,
              refresh_token: _refresh_token,
              ...cleanUserProfile
            } = existingUserProfile;
            return done(null, cleanUserProfile);
          } else {
            // User does not exist, create a new account for them.
            logger.info(`GitHub OAuth: creating new user for email: ${email}`);

            // Since this is an OAuth user, they don't have a password.
            // We pass `null` for the password hash.
            const newUserProfile = await db.userRepo.createUser(
              email,
              null, // No password for OAuth users
              {
                full_name: profile.displayName || profile.username, // GitHub profile might not have displayName
                avatar_url: profile.photos?.[0]?.value,
              },
              logger,
            );

            return done(null, newUserProfile);
          }
        } catch (err) {
          logger.error({ error: err }, 'Error during GitHub authentication strategy');
          return done(err as Error, false);
        }
      },
    ),
  );
  logger.info('[Passport] GitHub OAuth strategy registered.');
} else {
  logger.warn(
    '[Passport] GitHub OAuth strategy NOT registered: GITHUB_CLIENT_ID or GITHUB_CLIENT_SECRET not set.',
  );
}

// --- Passport JWT Strategy (for protecting API routes) ---
const jwtOptions = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // Expect JWT in 'Authorization: Bearer <token>' header
  secretOrKey: JWT_SECRET,
};

// --- DEBUG LOGGING FOR JWT SECRET ---
if (!JWT_SECRET) {
  logger.fatal(
    '[Passport] CRITICAL: JWT_SECRET is missing or empty in environment variables! JwtStrategy will fail.',
  );
} else {
  logger.info(`[Passport] JWT_SECRET loaded successfully (length: ${JWT_SECRET.length}).`);
}

passport.use(
  new JwtStrategy(jwtOptions, async (jwt_payload, done) => {
    logger.debug(
      { jwt_payload: jwt_payload ? { user_id: jwt_payload.user_id } : 'null' },
      '[JWT Strategy] Verifying token payload:',
    );
    try {
      // The jwt_payload contains the data you put into the token during login (e.g., { user_id: user.user_id, email: user.email }).
      // We re-fetch the user from the database here to ensure they are still active and valid.
      const userProfile = await db.userRepo.findUserProfileById(jwt_payload.user_id, logger);

      // --- JWT STRATEGY DEBUG LOGGING ---
      logger.debug(
        `[JWT Strategy] DB lookup for user ID ${jwt_payload.user_id} result: ${userProfile ? 'FOUND' : 'NOT FOUND'}`,
      );

      if (userProfile) {
        return done(null, userProfile); // User profile object will be available as req.user in protected routes
      } else {
        logger.warn(`JWT authentication failed: user with ID ${jwt_payload.user_id} not found.`);
        return done(null, false); // User not found or invalid token
      }
    } catch (err: unknown) {
      logger.error({ error: err }, 'Error during JWT authentication strategy:');
      return done(err, false);
    }
  }),
);

// --- Custom Error Class for Unauthorized Access ---
class UnauthorizedError extends Error {
  status: number;
  constructor(message: string) {
    super(message);
    this.name = 'UnauthorizedError';
    this.status = 401;
  }
}

/**
 * A required authentication middleware that returns standardized error responses.
 * Unlike the default passport.authenticate(), this middleware ensures that 401 responses
 * follow our API response format with { success: false, error: { code, message } }.
 *
 * Use this instead of `passport.authenticate('jwt', { session: false })` to ensure
 * consistent error responses per ADR-028.
 */
export const requireAuth = (req: Request, res: Response, next: NextFunction) => {
  passport.authenticate(
    'jwt',
    { session: false },
    (err: Error | null, user: Express.User | false, info: { message: string } | Error) => {
      if (err) {
        // An actual error occurred during authentication
        req.log.error({ error: err }, 'Authentication error');
        return next(err);
      }

      if (!user) {
        // Authentication failed - return standardized error through error handler
        const message =
          info instanceof Error ? info.message : info?.message || 'Authentication required.';
        req.log.warn({ info: message }, 'JWT authentication failed');
        return next(new UnauthorizedError(message));
      }

      // Authentication succeeded - attach user and proceed
      req.user = user;
      next();
    },
  )(req, res, next);
};

// --- Middleware for Admin Role Check ---
export const isAdmin = (req: Request, res: Response, next: NextFunction) => {
  // Use the type guard for safer access to req.user
  const userProfile = req.user;

  if (isUserProfile(userProfile) && userProfile.role === 'admin') {
    next();
  } else {
    // Check if userProfile is a valid UserProfile before accessing its properties for logging.
    const userIdForLog = isUserProfile(userProfile) ? userProfile.user.user_id : 'unknown';
    req.log.warn(`Admin access denied for user: ${userIdForLog}`);
    next(new ForbiddenError('Forbidden: Administrator access required.'));
  }
};

/**
 * A flexible authentication middleware. It attempts to authenticate via JWT but does NOT
 * reject the request if authentication fails. This allows routes to handle both
 * authenticated and anonymous users. If a valid token is present, `req.user` will be
 * populated; otherwise, it will be undefined, and the request proceeds.
 */
export const optionalAuth = (req: Request, res: Response, next: NextFunction) => {
  // The custom callback for passport.authenticate gives us access to `err`, `user`, and `info`.
  passport.authenticate(
    'jwt',
    { session: false },
    (err: Error | null, user: Express.User | false, info: { message: string } | Error) => {
      if (err) {
        // An actual error occurred during authentication (e.g., malformed token).
        // For optional auth, we log this but still proceed without a user.
        req.log.warn({ error: err }, 'Optional auth encountered an error, proceeding anonymously.');
        return next();
      }
      if (info) {
        // The patch requested this specific error handling.
        req.log.info({ info: info.message || info.toString() }, 'Optional auth info:');
      }
      if (user) (req as Express.Request).user = user; // Attach user if authentication succeeds.

      next(); // Always proceed to the next middleware
    },
  )(req, res, next);
};

/**
 * Mock Authentication Middleware for Testing
 *
 * This middleware is ONLY active when `process.env.NODE_ENV` is 'test'.
 * It bypasses the entire JWT authentication flow and directly injects a
 * mock user object into `req.user`. This is essential for integration tests,
 * allowing protected routes to be tested without needing to generate valid JWTs
 * or mock the passport strategy.
 *
 * In any environment other than 'test', it does nothing and immediately passes
 * control to the next middleware.
 */
export const mockAuth = (req: Request, res: Response, next: NextFunction) => {
  if (process.env.NODE_ENV === 'test') {
    // In a test environment, attach a mock user to the request.
    // We use the mock factory to create a consistent, type-safe user profile.
    // We override the default role to 'admin' for broad access in tests.
    req.user = createMockUserProfile({
      role: 'admin',
    });
  }
  // In production or development, this middleware does nothing.
  next();
};

export default passport;
