import { User } from '../models/User';
import { SSOUserData, ssoConfig } from '../config/sso';
import jwt, { SignOptions } from 'jsonwebtoken';
import type { StringValue } from 'ms';
import { LoginResponse } from '../types/auth.types';
import logger, { logAuthEvent } from '../utils/logger';
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import { emitToUser } from '../realtime/socket';
import { ACCESS_TOKEN_TTL_MS } from '../config/sessionPolicy';

function parseDeviceFromUserAgent(ua?: string): string {
  if (!ua) return 'Unknown Device';
  let browser = 'Unknown Browser';
  if (ua.includes('Firefox/')) browser = 'Firefox';
  else if (ua.includes('Chrome/') && !ua.includes('Edg/')) browser = 'Chrome';
  else if (ua.includes('Safari/') && !ua.includes('Chrome/')) browser = 'Safari';
  else if (ua.includes('Edg/')) browser = 'Edge';

  let os = 'Unknown OS';
  if (ua.includes('Windows')) os = 'Windows';
  else if (ua.includes('Mac OS')) os = 'macOS';
  else if (ua.includes('Linux')) os = 'Linux';
  else if (ua.includes('Android')) os = 'Android';
  else if (ua.includes('iOS') || ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS';

  return `${browser} on ${os}`;
}


export class AuthService {
  private resolveOktaConfigForRedirectUri(redirectUri: string): {
    domain: string;
    clientId: string;
    clientSecret: string;
    apiToken: string;
    profile: 'localhost' | 'default';
  } {
    let host = '';
    try {
      host = new URL(redirectUri).hostname.toLowerCase();
    } catch {
      host = '';
    }
    const isLocalhostRedirect = host === 'localhost' || host === '127.0.0.1';
    if (isLocalhostRedirect) {
      return {
        domain: process.env.OKTA_DOMAIN_LOCALHOST || ssoConfig.oktaDomain,
        clientId: process.env.OKTA_CLIENT_ID_LOCALHOST || ssoConfig.oktaClientId,
        clientSecret: process.env.OKTA_CLIENT_SECRET_LOCALHOST || ssoConfig.oktaClientSecret,
        apiToken: process.env.OKTA_API_TOKEN_LOCALHOST || ssoConfig.oktaApiToken || '',
        profile: 'localhost',
      };
    }
    return {
      domain: ssoConfig.oktaDomain,
      clientId: ssoConfig.oktaClientId,
      clientSecret: ssoConfig.oktaClientSecret,
      apiToken: ssoConfig.oktaApiToken || '',
      profile: 'default',
    };
  }

  /**
   * Fetch user details from Okta Users API (full profile with manager, employeeID, etc.)
   * Falls back to userinfo endpoint if Users API fails or token is not configured
   */
  private async fetchUserFromOktaUsersAPI(
    oktaSub: string,
    email: string,
    accessToken: string,
    oktaDomainOverride?: string,
    oktaApiTokenOverride?: string
  ): Promise<any> {
    try {
      const oktaDomain = oktaDomainOverride || ssoConfig.oktaDomain;
      const oktaApiToken = oktaApiTokenOverride || ssoConfig.oktaApiToken;
      // Check if API token is configured
      if (!oktaApiToken || oktaApiToken.trim() === '') {
        logger.info('OKTA_API_TOKEN not configured, will use userinfo endpoint as fallback');
        return null;
      }

      // Try to fetch from Users API using email first (as shown in curl example)
      // If email lookup fails, try with oktaSub (user ID)
      let usersApiResponse: any = null;

      // First attempt: Use email (preferred method as shown in curl example)
      if (email) {
        const usersApiEndpoint = `${oktaDomain}/api/v1/users/${encodeURIComponent(email)}`;

        logger.info('Fetching user from Okta Users API (using email)', {
          endpoint: usersApiEndpoint.replace(email, email.substring(0, 5) + '...'),
          hasApiToken: !!oktaApiToken,
        });

        try {
          const response = await axios.get(usersApiEndpoint, {
            headers: {
              'Authorization': `SSWS ${oktaApiToken}`,
              'Accept': 'application/json',
            },
            validateStatus: (status) => status < 500, // Don't throw on 4xx errors
          });

          if (response.status === 200 && response.data) {
            logger.info('Successfully fetched user from Okta Users API (using email)', {
              userId: response.data.id,
              hasProfile: !!response.data.profile,
            });
            return response.data;
          }
        } catch (emailError: any) {
          logger.warn('Users API lookup with email failed, will try with oktaSub', {
            status: emailError.response?.status,
            error: emailError.message,
          });
        }
      }

      // Second attempt: Use oktaSub (user ID) if email lookup failed
      if (oktaSub) {
        const usersApiEndpoint = `${oktaDomain}/api/v1/users/${encodeURIComponent(oktaSub)}`;

        logger.info('Fetching user from Okta Users API (using oktaSub)', {
          endpoint: usersApiEndpoint.replace(oktaSub, oktaSub.substring(0, 10) + '...'),
          hasApiToken: !!oktaApiToken,
        });

        try {
          const response = await axios.get(usersApiEndpoint, {
            headers: {
              'Authorization': `SSWS ${oktaApiToken}`,
              'Accept': 'application/json',
            },
            validateStatus: (status) => status < 500,
          });

          if (response.status === 200 && response.data) {
            logger.info('Successfully fetched user from Okta Users API (using oktaSub)', {
              userId: response.data.id,
              hasProfile: !!response.data.profile,
            });
            return response.data;
          } else {
            logger.warn('Okta Users API returned non-200 status (oktaSub lookup)', {
              status: response.status,
              statusText: response.statusText,
            });
          }
        } catch (oktaSubError: any) {
          logger.warn('Users API lookup with oktaSub also failed', {
            status: oktaSubError.response?.status,
            error: oktaSubError.message,
          });
        }
      }

      return null;
    } catch (error: any) {
      logger.warn('Failed to fetch from Okta Users API, will use userinfo fallback', {
        error: error.message,
        status: error.response?.status,
      });
      return null;
    }
  }

  /**
   * Extract user data from Okta Users API response
   */
  private extractUserDataFromUsersAPI(oktaUserResponse: any, oktaSub: string): SSOUserData | null {
    try {
      const profile = oktaUserResponse.profile || {};

      const userData: SSOUserData = {
        oktaSub: oktaSub || oktaUserResponse.id || '',
        email: profile.email || profile.login || '',
        employeeId: profile.employeeID || profile.employeeId || profile.employee_id || undefined,
        firstName: profile.firstName || undefined,
        lastName: profile.lastName || undefined,
        displayName: profile.displayName || undefined,
        department: profile.department || undefined,
        designation: profile.title || profile.designation || undefined,
        phone: profile.mobilePhone || profile.phone || profile.phoneNumber || undefined,
        manager: profile.manager || undefined, // Store manager name if available
        jobTitle: profile.title || undefined,
        postalAddress: profile.postalAddress || undefined,
        mobilePhone: profile.mobilePhone || undefined,
        secondEmail: profile.secondEmail || profile.second_email || undefined,
        adGroups: Array.isArray(profile.memberOf) ? profile.memberOf : undefined,
        dealerCode: profile.dealer_code || profile.dealerCode || undefined,
        employeeNumber: profile.dealer_code || profile.employeeNumber || profile.employee_number || undefined,
      };

      // Validate required fields
      if (!userData.oktaSub || !userData.email) {
        logger.warn('Users API response missing required fields (oktaSub or email)');
        return null;
      }

      logger.info('Extracted user data from Okta Users API', {
        oktaSub: userData.oktaSub,
        email: userData.email,
        employeeId: userData.employeeId || 'not provided',
        hasManager: !!userData.manager,
        manager: userData.manager || 'not provided',
        hasDepartment: !!userData.department,
        hasDesignation: !!userData.designation,
        designation: userData.designation || 'not provided',
        hasJobTitle: !!userData.jobTitle,
        jobTitle: userData.jobTitle || 'not provided',
        hasTitle: !!(userData.jobTitle || userData.designation),
        hasAdGroups: !!userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0,
        adGroupsCount: userData.adGroups && Array.isArray(userData.adGroups) ? userData.adGroups.length : 0,
        adGroups: userData.adGroups && Array.isArray(userData.adGroups) ? userData.adGroups.slice(0, 5) : 'none', // Log first 5 groups
      });

      return userData;
    } catch (error) {
      logger.error('Error extracting user data from Users API response', error);
      return null;
    }
  }

  /**
   * Extract user data from Okta userinfo endpoint (fallback)
   */
  private extractUserDataFromUserInfo(oktaUser: any, oktaSub: string): SSOUserData {
    // Extract oktaSub (required)
    const sub = oktaSub || oktaUser.sub || '';
    if (!sub) {
      throw new Error('Okta sub (subject identifier) is required but not found in response');
    }

    // Extract employeeId (optional)
    const employeeId =
      oktaUser.employeeId ||
      oktaUser.employee_id ||
      oktaUser.empId ||
      oktaUser.employeeNumber ||
      undefined;

    const userData: SSOUserData = {
      oktaSub: sub,
      email: oktaUser.email || '',
      employeeId: employeeId,
      dealerCode: oktaUser.dealer_code || undefined,
      employeeNumber: oktaUser.dealer_code || oktaUser.employeeNumber || undefined,
    };

    // Validate: Ensure we're not accidentally using oktaSub as employeeId
    if (employeeId === sub) {
      logger.warn('Warning: employeeId matches oktaSub - this should not happen unless explicitly set in Okta', {
        oktaSub: sub,
        employeeId,
      });
      userData.employeeId = undefined;
    }

    // Only set optional fields if they have values
    if (oktaUser.given_name || oktaUser.firstName) {
      userData.firstName = oktaUser.given_name || oktaUser.firstName;
    }
    if (oktaUser.family_name || oktaUser.lastName) {
      userData.lastName = oktaUser.family_name || oktaUser.lastName;
    }
    if (oktaUser.name) {
      userData.displayName = oktaUser.name;
    }
    if (oktaUser.department) {
      userData.department = oktaUser.department;
    }
    if (oktaUser.title || oktaUser.designation) {
      userData.designation = oktaUser.title || oktaUser.designation;
      userData.jobTitle = oktaUser.title || oktaUser.designation;
    }
    if (oktaUser.phone_number || oktaUser.phone) {
      userData.phone = oktaUser.phone_number || oktaUser.phone;
    }
    if (oktaUser.manager) {
      userData.manager = oktaUser.manager;
    }
    if (oktaUser.mobilePhone) {
      userData.mobilePhone = oktaUser.mobilePhone;
    }
    if (oktaUser.address || oktaUser.postalAddress) {
      userData.postalAddress = oktaUser.address || oktaUser.postalAddress;
    }
    if (oktaUser.secondEmail) {
      userData.secondEmail = oktaUser.secondEmail;
    }
    if (Array.isArray(oktaUser.memberOf)) {
      userData.adGroups = oktaUser.memberOf;
    }

    return userData;
  }

  /**
   * Handle SSO callback from frontend
   * Creates new user or updates existing user based on employeeId
   */
  async handleSSOCallback(userData: SSOUserData, userAgent?: string): Promise<LoginResponse> {
    try {
      // Validate required fields - email and oktaSub are required
      if (!userData.email || !userData.oktaSub) {
        throw new Error('Email and Okta sub are required');
      }

      const norm = (value?: unknown): string | undefined => {
        const s = String(value ?? '').trim();
        return s ? s : undefined;
      };
      const limit = (value: unknown, max: number): string | undefined => {
        const s = norm(value);
        return s ? s.slice(0, max) : undefined;
      };
      const normalizedEmail = norm(userData.email)?.toLowerCase();
      const normalizedOktaSub = norm(userData.oktaSub);
      if (!normalizedEmail || !normalizedOktaSub) {
        throw new Error('Email and Okta sub are required');
      }

      // Prepare user data with defaults for missing fields
      // If firstName/lastName are missing, try to extract from displayName
      let firstName = userData.firstName || '';
      let lastName = userData.lastName || '';
      let displayName = userData.displayName || '';

      // If displayName exists but firstName/lastName don't, try to split displayName
      if (displayName && !firstName && !lastName) {
        const nameParts = displayName.trim().split(/\s+/);
        if (nameParts.length > 0) {
          firstName = nameParts[0] || '';
          lastName = nameParts.slice(1).join(' ') || '';
        }
      }

      // If firstName/lastName exist but displayName doesn't, create displayName
      if (!displayName && (firstName || lastName)) {
        displayName = `${firstName} ${lastName}`.trim() || userData.email;
      }

      // Fallback: if still no displayName, use email
      if (!displayName) {
        displayName = userData.email.split('@')[0] || 'User';
      }

      firstName = limit(firstName, 100) || '';
      lastName = limit(lastName, 100) || '';
      displayName = limit(displayName, 200) || normalizedEmail.split('@')[0] || 'User';

      const sessionToken = uuidv4();
      const lastLoginDevice = parseDeviceFromUserAgent(userAgent);

      // Prepare update/create data - always include required fields
      const userUpdateData: any = {
        email: normalizedEmail,
        oktaSub: normalizedOktaSub,
        lastLogin: new Date(),
        sessionToken,
        lastLoginDevice,
        isActive: true,
      };

      // Only set optional fields if they have values (don't overwrite with null/empty)
      if (firstName) userUpdateData.firstName = firstName;
      if (lastName) userUpdateData.lastName = lastName;
      if (displayName) userUpdateData.displayName = displayName;
      if (limit(userData.employeeId, 50)) userUpdateData.employeeId = limit(userData.employeeId, 50); // Optional
      if (limit(userData.department, 100)) userUpdateData.department = limit(userData.department, 100);
      if (limit(userData.designation, 100)) userUpdateData.designation = limit(userData.designation, 100);
      if (limit(userData.phone, 20)) userUpdateData.phone = limit(userData.phone, 20);
      if (limit(userData.manager, 200)) userUpdateData.manager = limit(userData.manager, 200); // Manager name from SSO
      if (limit(userData.jobTitle, 3000)) userUpdateData.jobTitle = limit(userData.jobTitle, 3000); // Job title from SSO
      if (limit(userData.postalAddress, 500)) userUpdateData.postalAddress = limit(userData.postalAddress, 500); // Address from SSO
      if (limit(userData.mobilePhone, 20)) userUpdateData.mobilePhone = limit(userData.mobilePhone, 20); // Mobile phone from SSO
      if (limit(userData.secondEmail, 255)) userUpdateData.secondEmail = limit(userData.secondEmail, 255);
      const employeeNumber = limit(userData.employeeNumber || userData.dealerCode, 50);
      if (employeeNumber) {
        userUpdateData.employeeNumber = employeeNumber;
      }
      if (userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0) {
        userUpdateData.adGroups = userData.adGroups
          .map((group: unknown) => limit(group, 255))
          .filter(Boolean)
          .slice(0, 200); // Group memberships from SSO
      }

      // Prefer matching by oktaSub, then fallback to email.
      // This avoids collisions when email changes in IdP over time.
      let user = await User.findOne({
        where: { oktaSub: normalizedOktaSub }
      });
      if (!user) {
        user = await User.findOne({
          where: { email: normalizedEmail }
        });
      }

      if (user) {
        // Update existing user - update oktaSub if different, and other fields
        await user.update(userUpdateData);
        // Reload to get updated data
        user = await user.reload();

        logAuthEvent('sso_callback', user.userId, {
          email: userData.email,
          action: 'user_updated',
          updatedFields: Object.keys(userUpdateData),
        });

        // Notify previous session via WebSocket to logout immediately
        // This provides real-time "Last-In-Wins" enforcement
        emitToUser(user.userId, 'SESSION_SUPERSEDED', { 
          reason: 'CONCURRENT_LOGIN',
          device: lastLoginDevice,
          timestamp: new Date().toISOString()
        });
      } else {
        // Create new user with required fields (email and oktaSub)
        user = await User.create({
          email: normalizedEmail,
          oktaSub: normalizedOktaSub,
          employeeId: userData.employeeId || null, // Optional
          firstName: firstName || null,
          lastName: lastName || null,
          displayName: displayName,
          department: limit(userData.department, 100) || null,
          designation: limit(userData.designation, 100) || null,
          phone: limit(userData.phone, 20) || null,
          manager: limit(userData.manager, 200) || null, // Manager name from SSO
          jobTitle: limit(userData.jobTitle, 3000) || null, // Job title from SSO
          postalAddress: limit(userData.postalAddress, 500) || null, // Address from SSO
          mobilePhone: limit(userData.mobilePhone, 20) || null,
          secondEmail: limit(userData.secondEmail, 255) || null,
          adGroups: userUpdateData.adGroups && Array.isArray(userUpdateData.adGroups) && userUpdateData.adGroups.length > 0 ? userUpdateData.adGroups : null,
          employeeNumber: limit(userData.employeeNumber || userData.dealerCode, 50) || null,
          isActive: true,
          role: 'USER',
          lastLogin: new Date(),
          sessionToken,
          lastLoginDevice
        });

        logAuthEvent('sso_callback', user.userId, {
          email: userData.email,
          action: 'user_created',
          displayName,
          hasDepartment: !!userData.department,
          hasDesignation: !!userData.designation,
        });
      }

      // Generate JWT tokens
      const accessToken = this.generateAccessToken(user);
      const refreshToken = this.generateRefreshToken(user);

      return {
        user: {
          userId: user.userId,
          employeeId: user.employeeId || null,
          email: user.email,
          firstName: user.firstName || null,
          lastName: user.lastName || null,
          displayName: user.displayName || null,
          department: user.department || null,
          designation: user.designation || null,
          jobTitle: user.jobTitle || null,
          role: user.role
        },
        accessToken,
        refreshToken
      };
    } catch (error) {
      logAuthEvent('auth_failure', undefined, {
        email: userData.email,
        action: 'sso_callback_failed',
        error: error instanceof Error ? error.message : 'Unknown error',
      });
      const errorMessage = error instanceof Error ? error.message : 'Unknown error';
      throw new Error(`SSO callback failed: ${errorMessage}`);
    }
  }

  /**
   * Generate JWT access token
   */
  private generateAccessToken(user: User, expiresIn?: StringValue | number): string {
    if (!ssoConfig.jwtSecret) {
      throw new Error('JWT secret is not configured');
    }

    const payload = {
      userId: user.userId,
      employeeId: user.employeeId,
      email: user.email,
      role: user.role, // Keep uppercase: USER, MANAGEMENT, ADMIN
      sessionToken: user.sessionToken
    };

    const options: SignOptions = {
      expiresIn: expiresIn ?? (ssoConfig.jwtExpiry as StringValue | number)
    };

    return jwt.sign(payload, ssoConfig.jwtSecret, options);
  }

  /**
   * Generate JWT refresh token
   */
  private generateRefreshToken(user: User): string {
    if (!ssoConfig.jwtSecret) {
      throw new Error('JWT secret is not configured');
    }

    const payload = {
      userId: user.userId,
      type: 'refresh',
      sessionToken: user.sessionToken
    };

    const options: SignOptions = {
      expiresIn: ssoConfig.refreshTokenExpiry as StringValue | number
    };

    return jwt.sign(payload, ssoConfig.jwtSecret, options);
  }

  /**
   * Validate JWT token
   */
  async validateToken(token: string): Promise<any> {
    try {
      return jwt.verify(token, ssoConfig.jwtSecret);
    } catch (error) {
      throw new Error('Invalid token');
    }
  }

  /**
   * Refresh access token using refresh token
   */
  async refreshAccessToken(refreshToken: string): Promise<{ accessToken: string; accessTokenTtlMs: number }> {
    try {
      const decoded = jwt.verify(refreshToken, ssoConfig.jwtSecret) as any;

      if (decoded.type !== 'refresh') {
        throw new Error('Invalid refresh token');
      }

      const user = await User.findByPk(decoded.userId);
      if (!user || !user.isActive) {
        throw new Error('User not found or inactive');
      }

      if (decoded.sessionToken !== user.sessionToken) {
        throw new Error('Session expired due to login from another device');
      }

      // Strict 30-minute session timeout from login time.
      const lastLoginTime = user.lastLogin ? new Date(user.lastLogin).getTime() : 0;
      if (!lastLoginTime || Number.isNaN(lastLoginTime)) {
        throw new Error('Session expired');
      }
      const sessionAgeMs = Date.now() - lastLoginTime;
      if (sessionAgeMs > ACCESS_TOKEN_TTL_MS) {
        throw new Error('Session expired');
      }
      // Absolute session deadline: refreshed token must never outlive login + 30m.
      const remainingSessionMs = ACCESS_TOKEN_TTL_MS - sessionAgeMs;
      if (remainingSessionMs <= 0) {
        throw new Error('Session expired');
      }
      const remainingSessionSeconds = Math.max(1, Math.floor(remainingSessionMs / 1000));
      const accessToken = this.generateAccessToken(user, `${remainingSessionSeconds}s` as StringValue);
      return { accessToken, accessTokenTtlMs: remainingSessionSeconds * 1000 };
    } catch (error) {
      logAuthEvent('auth_failure', undefined, {
        action: 'token_refresh_failed',
        error,
      });
      throw new Error('Token refresh failed');
    }
  }

  /**
   * Get user profile by ID
   */
  async getUserProfile(userId: string): Promise<User | null> {
    try {
      return await User.findByPk(userId);
    } catch (error) {
      logger.error(`Failed to get user profile for ${userId}:`, error);
      throw new Error('Failed to get user profile');
    }
  }

  /**
   * Update user profile
   */
  async updateUserProfile(userId: string, updateData: Partial<User>): Promise<User | null> {
    try {
      const user = await User.findByPk(userId);
      if (!user) {
        return null;
      }

      return await user.update(updateData);
    } catch (error) {
      logger.error(`Failed to update user profile for ${userId}:`, error);
      throw new Error('Failed to update user profile');
    }
  }

  /**
   * Authenticate user with username (email) and password via Okta (Resource Owner Password grant).
   * For direct API clients (e.g. Postman) when Okta allows this grant; otherwise use token-exchange.
   * No local or demo password bypass.
   */
  async authenticateWithPassword(username: string, password: string, userAgent?: string): Promise<LoginResponse> {
    try {
      logger.info('Authenticating user with username/password', { username });

      // Authenticate with Okta using Resource Owner Password flow
      // Requires Okta Resource Owner Password grant when used; otherwise use SSO / token-exchange.
      const tokenEndpoint = `${ssoConfig.oktaDomain}/oauth2/default/v1/token`;

      const tokenResponse = await axios.post(
        tokenEndpoint,
        new URLSearchParams({
          grant_type: 'password',
          username: username,
          password: password,
          scope: 'openid profile email',
          client_id: ssoConfig.oktaClientId,
          client_secret: ssoConfig.oktaClientSecret,
        }),
        {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept': 'application/json',
          },
          validateStatus: (status) => status < 500,
        }
      );

      // Check for authentication errors
      if (tokenResponse.status !== 200) {
        logger.error('Okta authentication failed', {
          status: tokenResponse.status,
          data: tokenResponse.data,
        });

        const errorData = tokenResponse.data || {};
        const errorMessage = errorData.error_description || errorData.error || 'Invalid username or password';
        throw new Error(`Authentication failed: ${errorMessage}`);
      }

      const { access_token, refresh_token, id_token } = tokenResponse.data;

      if (!access_token) {
        throw new Error('Failed to obtain access token from Okta');
      }

      logger.info('Successfully authenticated with Okta');

      // Step 2: Get user info from Okta
      const userInfoEndpoint = `${ssoConfig.oktaDomain}/oauth2/default/v1/userinfo`;
      const userInfoResponse = await axios.get(userInfoEndpoint, {
        headers: {
          Authorization: `Bearer ${access_token}`,
        },
      });

      const oktaUserInfo = userInfoResponse.data;
      const oktaSub = oktaUserInfo.sub || '';

      if (!oktaSub) {
        throw new Error('Okta sub (subject identifier) not found in response');
      }

      // Step 3: Try Users API first (provides full profile including manager, employeeID, etc.)
      let userData: SSOUserData | null = null;
      const usersApiResponse = await this.fetchUserFromOktaUsersAPI(oktaSub, oktaUserInfo.email || username, access_token);

      if (usersApiResponse) {
        userData = this.extractUserDataFromUsersAPI(usersApiResponse, oktaSub);
      }

      // Fallback to userinfo endpoint if Users API failed or returned null
      if (!userData) {
        logger.info('Using userinfo endpoint as fallback (Users API unavailable or failed)');
        userData = this.extractUserDataFromUserInfo(oktaUserInfo, oktaSub);
        // Override email with username if needed
        if (!userData.email && username) {
          userData.email = username;
        }
      }

      logger.info('User data extracted from Okta', {
        email: userData.email,
        employeeId: userData.employeeId || 'not provided',
        hasEmployeeId: !!userData.employeeId,
        hasName: !!userData.displayName,
        hasManager: !!(userData as any).manager,
        manager: (userData as any).manager || 'not provided',
        hasDepartment: !!userData.department,
        hasDesignation: !!userData.designation,
        hasJobTitle: !!userData.jobTitle,
        source: usersApiResponse ? 'Users API' : 'userinfo endpoint',
      });

      // Step 4: Create/update user in our database
      const result = await this.handleSSOCallback(userData, userAgent);

      logger.info('User authenticated successfully via password flow', {
        userId: result.user.userId,
        email: result.user.email,
      });

      // Return tokens (including Okta tokens for reference)
      return {
        ...result,
        oktaRefreshToken: refresh_token,
        oktaAccessToken: access_token,
        oktaIdToken: id_token,
      };
    } catch (error: any) {
      logger.error('Password authentication failed', {
        username,
        error: error.message,
        status: error.response?.status,
        oktaError: error.response?.data,
      });

      if (error.response?.data) {
        const errorData = error.response.data;
        if (typeof errorData === 'object' && !Array.isArray(errorData)) {
          const errorMsg = errorData.error_description || errorData.error || error.message;
          throw new Error(`Authentication failed: ${errorMsg}`);
        }
      }

      throw new Error(`Authentication failed: ${error.message || 'Invalid credentials'}`);
    }
  }

  /**
   * Exchange authorization code for tokens with Okta/Auth0
   * 
   * IMPORTANT: redirectUri MUST match the one used in the initial authorization request to Okta.
   * This is the FRONTEND callback URL (e.g., http://localhost:3000/login/callback),
   * NOT the backend URL. Okta verifies this matches to prevent redirect URI attacks.
   */
  async exchangeCodeForTokens(code: string, redirectUri: string, userAgent?: string): Promise<LoginResponse> {
    try {
      const oktaConfigForRequest = this.resolveOktaConfigForRedirectUri(redirectUri);
      // Validate configuration
      if (!oktaConfigForRequest.clientId || oktaConfigForRequest.clientId.trim() === '') {
        throw new Error('OKTA_CLIENT_ID is not configured. Please set it in your .env file.');
      }
      if (!oktaConfigForRequest.clientSecret || oktaConfigForRequest.clientSecret.trim() === '' || oktaConfigForRequest.clientSecret.includes('your_okta_client_secret')) {
        throw new Error('OKTA_CLIENT_SECRET is not configured. Please set it in your .env file.');
      }
      if (!code || code.trim() === '') {
        throw new Error('Authorization code is required');
      }
      if (!redirectUri || redirectUri.trim() === '') {
        throw new Error('Redirect URI is required');
      }

      const normalize = (s: string) => s.trim().replace(/\/+$/, '');
      const providedRedirectUri = normalize(redirectUri);
      const configuredFrontendBases = (process.env.FRONTEND_URL || '')
        .split(',')
        .map((s) => normalize(s))
        .filter(Boolean);
      const providedOrigin = (() => {
        try {
          return normalize(new URL(providedRedirectUri).origin);
        } catch {
          return '';
        }
      })();
      const matchingConfiguredBase = configuredFrontendBases.find((base) => {
        try {
          return normalize(new URL(base).origin).toLowerCase() === providedOrigin.toLowerCase();
        } catch {
          return false;
        }
      });
      const fallbackConfiguredBase = configuredFrontendBases[0] || '';
      const selectedFrontendBase = matchingConfiguredBase || fallbackConfiguredBase;
      const canonicalRedirectUri = selectedFrontendBase ? `${selectedFrontendBase}/login/callback` : providedRedirectUri;
      const isSecureEnv = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'uat';
      const effectiveRedirectUri = isSecureEnv ? canonicalRedirectUri : providedRedirectUri;

      logger.info('Exchanging code with Okta', {
        redirectUri: effectiveRedirectUri,
        providedRedirectUri,
        canonicalRedirectUri,
        configuredFrontendBases,
        selectedFrontendBase,
        oktaProfile: oktaConfigForRequest.profile,
        codePrefix: code.substring(0, 10) + '...',
        oktaDomain: oktaConfigForRequest.domain,
        clientId: oktaConfigForRequest.clientId,
        hasClientSecret: !!oktaConfigForRequest.clientSecret && !oktaConfigForRequest.clientSecret.includes('your_okta_client_secret'),
      });

      const tokenEndpoint = `${oktaConfigForRequest.domain}/oauth2/default/v1/token`;

      // Exchange authorization code for tokens
      // redirect_uri here must match the one used when requesting the authorization code
      const tokenResponse = await axios.post(
        tokenEndpoint,
        new URLSearchParams({
          grant_type: 'authorization_code',
          code,
          redirect_uri: effectiveRedirectUri, // Must match authorize request redirect_uri exactly
          client_id: oktaConfigForRequest.clientId,
          client_secret: oktaConfigForRequest.clientSecret,
        }),
        {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept': 'application/json',
          },
          responseType: 'json', // Explicitly set response type
          validateStatus: (status) => status < 500, // Don't throw on 4xx errors, we'll handle them
        }
      );

      // Check for error response from Okta
      if (tokenResponse.status !== 200) {
        logger.error('Okta token exchange failed', {
          status: tokenResponse.status,
          statusText: tokenResponse.statusText,
          data: tokenResponse.data,
          headers: tokenResponse.headers,
        });

        const errorData = tokenResponse.data || {};
        const errorMessage = errorData.error_description || errorData.error || 'Unknown error from Okta';
        throw new Error(`Okta token exchange failed (${tokenResponse.status}): ${errorMessage}`);
      }

      // Check if response data is valid JSON
      if (!tokenResponse.data || typeof tokenResponse.data !== 'object') {
        logger.error('Invalid response from Okta', {
          dataType: typeof tokenResponse.data,
          isArray: Array.isArray(tokenResponse.data),
          data: tokenResponse.data,
        });
        throw new Error('Invalid response format from Okta');
      }

      const { access_token, refresh_token, id_token } = tokenResponse.data;

      if (!access_token) {
        logger.error('Missing access_token in Okta response', {
          responseKeys: Object.keys(tokenResponse.data || {}),
          hasRefreshToken: !!refresh_token,
          hasIdToken: !!id_token,
        });
        throw new Error('Failed to obtain access token from Okta - access_token missing in response');
      }

      logger.info('Successfully obtained tokens from Okta', {
        hasAccessToken: !!access_token,
        hasRefreshToken: !!refresh_token,
        hasIdToken: !!id_token,
      });

      // Step 1: Try to get user info from Okta Users API (full profile with manager, employeeID, etc.)
      // First, get oktaSub from userinfo to use as user ID
      const userInfoEndpoint = `${oktaConfigForRequest.domain}/oauth2/default/v1/userinfo`;
      const userInfoResponse = await axios.get(userInfoEndpoint, {
        headers: {
          Authorization: `Bearer ${access_token}`,
        },
      });

      const oktaUserInfo = userInfoResponse.data;
      const oktaSub = oktaUserInfo.sub || '';

      if (!oktaSub) {
        throw new Error('Okta sub (subject identifier) is required but not found in response');
      }

      // Try Users API first (provides full profile including manager, employeeID, etc.)
      let userData: SSOUserData | null = null;
      const usersApiResponse = await this.fetchUserFromOktaUsersAPI(
        oktaSub,
        oktaUserInfo.email || '',
        access_token,
        oktaConfigForRequest.domain,
        oktaConfigForRequest.apiToken
      );

      if (usersApiResponse) {
        userData = this.extractUserDataFromUsersAPI(usersApiResponse, oktaSub);
      }

      // Fallback to userinfo endpoint if Users API failed or returned null
      if (!userData) {
        logger.info('Using userinfo endpoint as fallback (Users API unavailable or failed)');
        userData = this.extractUserDataFromUserInfo(oktaUserInfo, oktaSub);
      }

      logger.info('Final extracted user data', {
        oktaSub: userData.oktaSub,
        email: userData.email,
        employeeId: userData.employeeId || 'not provided',
        hasManager: !!(userData as any).manager,
        manager: (userData as any).manager || 'not provided',
        hasDepartment: !!userData.department,
        hasDesignation: !!userData.designation,
        hasJobTitle: !!userData.jobTitle,
        hasPostalAddress: !!userData.postalAddress,
        hasMobilePhone: !!userData.mobilePhone,
        hasSecondEmail: !!userData.secondEmail,
        hasAdGroups: !!userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0,
        source: usersApiResponse ? 'Users API' : 'userinfo endpoint',
      });

      // Handle SSO callback to create/update user and generate our tokens
      const result = await this.handleSSOCallback(userData, userAgent);

      // Return our JWT tokens along with Okta tokens (store Okta refresh token for future use)
      return {
        ...result,
        // Store Okta tokens separately if needed (especially id_token for logout)
        oktaRefreshToken: refresh_token,
        oktaAccessToken: access_token,
        oktaIdToken: id_token, // Include id_token for proper Okta logout
      };
    } catch (error: any) {
      logAuthEvent('auth_failure', undefined, {
        action: 'okta_token_exchange_failed',
        errorMessage: error.message,
        status: error.response?.status,
        statusText: error.response?.statusText,
        oktaError: error.response?.data?.error,
        oktaErrorDescription: error.response?.data?.error_description,
      });

      // Provide a more user-friendly error message
      if (error.response?.data) {
        const errorData = error.response.data;
        // Handle if error response is an object
        if (typeof errorData === 'object' && !Array.isArray(errorData)) {
          const errorMsg = errorData.error_description || errorData.error || error.message;
          throw new Error(`Okta authentication failed: ${errorMsg}`);
        } else {
          logger.error('Unexpected error response format from Okta', {
            dataType: typeof errorData,
            isArray: Array.isArray(errorData),
          });
          throw new Error(`Okta authentication failed: Unexpected response format. Status: ${error.response.status}`);
        }
      }

      throw new Error(`Okta authentication failed: ${error.message || 'Unknown error'}`);
    }
  }

  /**
   * Exchange Tanflow authorization code for tokens
   * Similar to Okta flow but uses Tanflow IAM endpoints
   */
  async exchangeTanflowCodeForTokens(code: string, redirectUri: string, userAgent?: string): Promise<LoginResponse> {
    try {
      // Validate configuration
      if (!ssoConfig.tanflowClientId || ssoConfig.tanflowClientId.trim() === '') {
        throw new Error('TANFLOW_CLIENT_ID is not configured. Please set it in your .env file.');
      }
      if (!ssoConfig.tanflowClientSecret || ssoConfig.tanflowClientSecret.trim() === '') {
        throw new Error('TANFLOW_CLIENT_SECRET is not configured. Please set it in your .env file.');
      }
      if (!code || code.trim() === '') {
        throw new Error('Authorization code is required');
      }
      if (!redirectUri || redirectUri.trim() === '') {
        throw new Error('Redirect URI is required');
      }

      const normalize = (s: string) => s.trim().replace(/\/+$/, '');
      const providedRedirectUri = normalize(redirectUri);
      const configuredFrontendBases = (process.env.FRONTEND_URL || '')
        .split(',')
        .map((s) => normalize(s))
        .filter(Boolean);
      const providedOrigin = (() => {
        try {
          return normalize(new URL(providedRedirectUri).origin);
        } catch {
          return '';
        }
      })();
      const matchingConfiguredBase = configuredFrontendBases.find((base) => {
        try {
          return normalize(new URL(base).origin).toLowerCase() === providedOrigin.toLowerCase();
        } catch {
          return false;
        }
      });
      const fallbackConfiguredBase = configuredFrontendBases[0] || '';
      const selectedFrontendBase = matchingConfiguredBase || fallbackConfiguredBase;
      const canonicalRedirectUri = selectedFrontendBase ? `${selectedFrontendBase}/login/callback` : providedRedirectUri;
      const isSecureEnv = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'uat';
      const effectiveRedirectUri = isSecureEnv ? canonicalRedirectUri : providedRedirectUri;

      logger.info('Exchanging code with Tanflow', {
        redirectUri: effectiveRedirectUri,
        providedRedirectUri,
        canonicalRedirectUri,
        configuredFrontendBases,
        selectedFrontendBase,
        codePrefix: code.substring(0, 10) + '...',
        tanflowBaseUrl: ssoConfig.tanflowBaseUrl,
        clientId: ssoConfig.tanflowClientId,
        hasClientSecret: !!ssoConfig.tanflowClientSecret,
      });

      const tokenEndpoint = `${ssoConfig.tanflowBaseUrl}/protocol/openid-connect/token`;

      // Exchange authorization code for tokens
      const tokenResponse = await axios.post(
        tokenEndpoint,
        new URLSearchParams({
          grant_type: 'authorization_code',
          code,
          redirect_uri: effectiveRedirectUri,
          client_id: ssoConfig.tanflowClientId!,
          client_secret: ssoConfig.tanflowClientSecret!,
        }),
        {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept': 'application/json',
          },
          responseType: 'json',
          validateStatus: (status) => status < 500,
        }
      );

      // Check for error response from Tanflow
      if (tokenResponse.status !== 200) {
        logger.error('Tanflow token exchange failed', {
          status: tokenResponse.status,
          statusText: tokenResponse.statusText,
          data: tokenResponse.data,
        });

        const errorData = tokenResponse.data || {};
        const errorMessage = errorData.error_description || errorData.error || 'Unknown error from Tanflow';
        throw new Error(`Tanflow token exchange failed (${tokenResponse.status}): ${errorMessage}`);
      }

      if (!tokenResponse.data || typeof tokenResponse.data !== 'object') {
        logger.error('Invalid response from Tanflow', {
          dataType: typeof tokenResponse.data,
          isArray: Array.isArray(tokenResponse.data),
          data: tokenResponse.data,
        });
        throw new Error('Invalid response format from Tanflow');
      }

      const { access_token, refresh_token, id_token } = tokenResponse.data;

      if (!access_token) {
        logger.error('Missing access_token in Tanflow response', {
          responseKeys: Object.keys(tokenResponse.data || {}),
          hasRefreshToken: !!refresh_token,
          hasIdToken: !!id_token,
        });
        throw new Error('Failed to obtain access token from Tanflow - access_token missing in response');
      }

      logger.info('Successfully obtained tokens from Tanflow', {
        hasAccessToken: !!access_token,
        hasRefreshToken: !!refresh_token,
        hasIdToken: !!id_token,
      });

      // Get user info from Tanflow userinfo endpoint
      const userInfoEndpoint = `${ssoConfig.tanflowBaseUrl}/protocol/openid-connect/userinfo`;
      const userInfoResponse = await axios.get(userInfoEndpoint, {
        headers: {
          Authorization: `Bearer ${access_token}`,
        },
      });

      const tanflowUserInfo = userInfoResponse.data;
      const tanflowSub = tanflowUserInfo.sub || '';

      if (!tanflowSub) {
        throw new Error('Tanflow sub (subject identifier) is required but not found in response');
      }

      // Log available fields from Tanflow for debugging and planning
      logger.info('Tanflow userinfo response received', {
        availableFields: Object.keys(tanflowUserInfo),
        hasEmail: !!tanflowUserInfo.email,
        hasPreferredUsername: !!tanflowUserInfo.preferred_username,
        hasEmployeeId: !!(tanflowUserInfo.employeeId || tanflowUserInfo.employee_id),
        hasEmployeeType: !!tanflowUserInfo.employeeType,
        hasDepartment: !!tanflowUserInfo.department,
        hasDesignation: !!tanflowUserInfo.designation,
        hasManager: !!tanflowUserInfo.manager,
        hasGroups: Array.isArray(tanflowUserInfo.groups),
        groupsCount: Array.isArray(tanflowUserInfo.groups) ? tanflowUserInfo.groups.length : 0,
        hasLocation: !!(tanflowUserInfo.city || tanflowUserInfo.state || tanflowUserInfo.country),
        hasAddress: !!tanflowUserInfo.address,
        sampleData: {
          sub: tanflowUserInfo.sub?.substring(0, 10) + '...',
          email: tanflowUserInfo.email?.substring(0, 10) + '...',
          name: tanflowUserInfo.name,
          given_name: tanflowUserInfo.given_name,
          family_name: tanflowUserInfo.family_name,
          employeeType: tanflowUserInfo.employeeType,
          designation: tanflowUserInfo.designation,
        }
      });

      // Extract user data from Tanflow userinfo
      // Tanflow uses standard OIDC claims, similar to Okta
      // Also supports custom claims based on Tanflow configuration
      const userData: SSOUserData = {
        oktaSub: tanflowSub, // Reuse oktaSub field for Tanflow sub
        email: tanflowUserInfo.email || tanflowUserInfo.preferred_username || '',
        employeeId: tanflowUserInfo.employeeId || tanflowUserInfo.employee_id || undefined,
        firstName: tanflowUserInfo.given_name || tanflowUserInfo.firstName || undefined,
        lastName: tanflowUserInfo.family_name || tanflowUserInfo.lastName || undefined,
        displayName: tanflowUserInfo.name || tanflowUserInfo.displayName || undefined,
        department: tanflowUserInfo.department || undefined,
        designation: tanflowUserInfo.designation || undefined, // Map designation to designation
        phone: tanflowUserInfo.phone_number || tanflowUserInfo.phone || undefined,
        // Additional fields that may be available from Tanflow (custom claims)
        manager: tanflowUserInfo.manager || undefined,
        jobTitle: tanflowUserInfo.employeeType || undefined, // Map employeeType to jobTitle
        postalAddress: tanflowUserInfo.address ? (typeof tanflowUserInfo.address === 'string' ? tanflowUserInfo.address : JSON.stringify(tanflowUserInfo.address)) : undefined,
        mobilePhone: tanflowUserInfo.mobile_phone || tanflowUserInfo.mobilePhone || undefined,
        dealerCode: tanflowUserInfo.dealer_code || undefined,
        employeeNumber: tanflowUserInfo.dealer_code || tanflowUserInfo.employeeNumber || undefined,
        adGroups: Array.isArray(tanflowUserInfo.groups) ? tanflowUserInfo.groups : undefined,
      };

      // Validate required fields
      if (!userData.oktaSub || !userData.email) {
        throw new Error('Email and Tanflow sub are required');
      }

      logger.info('Extracted user data from Tanflow', {
        tanflowSub: userData.oktaSub,
        email: userData.email,
        employeeId: userData.employeeId || 'not provided',
        hasDepartment: !!userData.department,
        hasDesignation: !!userData.designation,
        hasManager: !!userData.manager,
        hasJobTitle: !!userData.jobTitle,
        hasPostalAddress: !!userData.postalAddress,
        hasMobilePhone: !!userData.mobilePhone,
        hasAdGroups: !!userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0,
        adGroupsCount: userData.adGroups && Array.isArray(userData.adGroups) ? userData.adGroups.length : 0,
      });

      // Handle SSO callback to create/update user and generate our tokens
      const result = await this.handleSSOCallback(userData, userAgent);

      // Return our JWT tokens along with Tanflow tokens
      return {
        ...result,
        // Store Tanflow tokens separately if needed (especially id_token for logout)
        oktaRefreshToken: refresh_token, // Reuse oktaRefreshToken field
        oktaAccessToken: access_token, // Reuse oktaAccessToken field
        oktaIdToken: id_token, // Reuse oktaIdToken field for Tanflow logout
      };
    } catch (error: any) {
      logAuthEvent('auth_failure', undefined, {
        action: 'tanflow_token_exchange_failed',
        errorMessage: error.message,
        status: error.response?.status,
        statusText: error.response?.statusText,
        tanflowError: error.response?.data?.error,
        tanflowErrorDescription: error.response?.data?.error_description,
      });

      if (error.response?.data) {
        const errorData = error.response.data;
        if (typeof errorData === 'object' && !Array.isArray(errorData)) {
          const errorMsg = errorData.error_description || errorData.error || error.message;
          throw new Error(`Tanflow authentication failed: ${errorMsg}`);
        } else {
          logger.error('Unexpected error response format from Tanflow', {
            dataType: typeof errorData,
            isArray: Array.isArray(errorData),
          });
          throw new Error(`Tanflow authentication failed: Unexpected response format. Status: ${error.response.status}`);
        }
      }

      throw new Error(`Tanflow authentication failed: ${error.message || 'Unknown error'}`);
    }
  }

  /**
   * Refresh Tanflow access token using refresh token
   */
  async refreshTanflowToken(refreshToken: string): Promise<string> {
    try {
      if (!ssoConfig.tanflowClientId || !ssoConfig.tanflowClientSecret) {
        throw new Error('Tanflow client credentials not configured');
      }

      const tokenEndpoint = `${ssoConfig.tanflowBaseUrl}/protocol/openid-connect/token`;

      const response = await axios.post(
        tokenEndpoint,
        new URLSearchParams({
          grant_type: 'refresh_token',
          client_id: ssoConfig.tanflowClientId!,
          client_secret: ssoConfig.tanflowClientSecret!,
          refresh_token: refreshToken,
        }),
        {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept': 'application/json',
          },
        }
      );

      if (response.status !== 200 || !response.data.access_token) {
        throw new Error('Failed to refresh Tanflow token');
      }

      return response.data.access_token;
    } catch (error: any) {
      logger.error('Tanflow token refresh failed:', error);
      throw new Error(`Tanflow token refresh failed: ${error.message || 'Unknown error'}`);
    }
  }
}
