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 from '../utils/logger';
import axios from 'axios';

export class AuthService {
  /**
   * Handle SSO callback from frontend
   * Creates new user or updates existing user based on employeeId
   */
  async handleSSOCallback(userData: SSOUserData): 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');
      }

      // 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';
      }

      // Prepare update/create data - always include required fields
      const userUpdateData: any = {
        email: userData.email,
        oktaSub: userData.oktaSub,
        lastLogin: new Date(),
        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 (userData.employeeId) userUpdateData.employeeId = userData.employeeId; // Optional
      if (userData.department) userUpdateData.department = userData.department;
      if (userData.designation) userUpdateData.designation = userData.designation;
      if (userData.phone) userUpdateData.phone = userData.phone;

      // Check if user exists by email (primary identifier)
      let user = await User.findOne({
        where: { email: userData.email }
      });

      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();
        
        logger.info(`User updated via SSO`, {
          email: userData.email,
          oktaSub: userData.oktaSub,
          updatedFields: Object.keys(userUpdateData),
        });
      } else {
        // Create new user with required fields (email and oktaSub)
        user = await User.create({
          email: userData.email,
          oktaSub: userData.oktaSub,
          employeeId: userData.employeeId || null, // Optional
          firstName: firstName || null,
          lastName: lastName || null,
          displayName: displayName,
          department: userData.department || null,
          designation: userData.designation || null,
          phone: userData.phone || null,
          isActive: true,
          role: 'USER',
          lastLogin: new Date()
        });
        
        logger.info(`New user created via SSO`, {
          email: userData.email,
          oktaSub: userData.oktaSub,
          employeeId: userData.employeeId || 'not provided',
          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,
          role: user.role
        },
        accessToken,
        refreshToken
      };
    } catch (error) {
      logger.error(`SSO callback failed`, {
        email: userData.email,
        oktaSub: userData.oktaSub,
        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): 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
    };

    const options: SignOptions = {
      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'
    };

    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<string> {
    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');
      }

      return this.generateAccessToken(user);
    } catch (error) {
      logger.error('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');
    }
  }

  /**
   * 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): Promise<LoginResponse> {
    try {
      // Validate configuration
      if (!ssoConfig.oktaClientId || ssoConfig.oktaClientId.trim() === '') {
        throw new Error('OKTA_CLIENT_ID is not configured. Please set it in your .env file.');
      }
      if (!ssoConfig.oktaClientSecret || ssoConfig.oktaClientSecret.trim() === '' || ssoConfig.oktaClientSecret.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');
      }
      
      logger.info('Exchanging code with Okta', { 
        redirectUri,
        codePrefix: code.substring(0, 10) + '...',
        oktaDomain: ssoConfig.oktaDomain,
        clientId: ssoConfig.oktaClientId,
        hasClientSecret: !!ssoConfig.oktaClientSecret && !ssoConfig.oktaClientSecret.includes('your_okta_client_secret'),
      });
      
      const tokenEndpoint = `${ssoConfig.oktaDomain}/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: redirectUri, // Frontend URL (e.g., http://localhost:3000/login/callback)
          client_id: ssoConfig.oktaClientId,
          client_secret: ssoConfig.oktaClientSecret,
        }),
        {
          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,
      });

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

      const oktaUser = userInfoResponse.data;

      // Log the full Okta response to see what attributes are available
      logger.info('Okta userinfo response received', {
        availableKeys: Object.keys(oktaUser || {}),
        sub: oktaUser.sub,
        email: oktaUser.email,
        // Log specific fields that might be employeeId
        employeeId: oktaUser.employeeId || oktaUser.employee_id || oktaUser.empId || 'NOT_FOUND',
        // Log other common custom attributes
        customAttributes: Object.keys(oktaUser || {}).filter(key => 
          key.includes('employee') || key.includes('emp') || key.includes('id')
        ),
      });

      // Extract oktaSub (required) - this is the Okta subject identifier
      // IMPORTANT: Do NOT use oktaSub for employeeId - they are separate fields
      const oktaSub = oktaUser.sub || '';
      if (!oktaSub) {
        throw new Error('Okta sub (subject identifier) is required but not found in response');
      }

      // Extract employeeId (optional) - ONLY from custom Okta attributes, NOT from sub
      // Check multiple possible sources for actual employee ID attribute:
      // 1. Custom Okta attribute: employeeId, employee_id, empId, employeeNumber
      // 2. Leave undefined if not found - DO NOT use oktaSub/sub as fallback
      const employeeId = 
        oktaUser.employeeId || 
        oktaUser.employee_id || 
        oktaUser.empId || 
        oktaUser.employeeNumber ||
        undefined; // Explicitly undefined if not found - oktaSub is stored separately

      // Extract user data from Okta response
      // Adjust these mappings based on your Okta user profile attributes
      // Only include fields that have values, leave others undefined for optional handling
      const userData: SSOUserData = {
        oktaSub: oktaSub, // Required - Okta subject identifier (stored in okta_sub column)
        email: oktaUser.email || '',
        employeeId: employeeId, // Optional - Only if provided as custom attribute, NOT oktaSub
      };

      // Validate: Ensure we're not accidentally using oktaSub as employeeId
      if (employeeId === oktaSub) {
        logger.warn('Warning: employeeId matches oktaSub - this should not happen unless explicitly set in Okta', {
          oktaSub,
          employeeId,
        });
        // Clear employeeId to avoid confusion - user can update it later if needed
        userData.employeeId = undefined;
      }

      logger.info('User data extracted from Okta', {
        oktaSub: oktaSub,
        email: oktaUser.email,
        employeeId: employeeId || 'not provided (optional)',
        employeeIdSource: oktaUser.employeeId ? 'employeeId attribute' :
                          oktaUser.employee_id ? 'employee_id attribute' :
                          oktaUser.empId ? 'empId attribute' :
                          'not found',
        note: 'Using email as primary identifier, oktaSub for uniqueness',
      });

      // 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;
      }
      if (oktaUser.phone_number || oktaUser.phone) {
        userData.phone = oktaUser.phone_number || oktaUser.phone;
      }

      logger.info('Extracted user data from Okta', {
        employeeId: userData.employeeId,
        email: userData.email,
        hasFirstName: !!userData.firstName,
        hasLastName: !!userData.lastName,
        hasDisplayName: !!userData.displayName,
        hasDepartment: !!userData.department,
        hasDesignation: !!userData.designation,
        hasPhone: !!userData.phone,
      });

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

      // 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) {
      logger.error('Token exchange with Okta failed:', {
        message: error.message,
        response: error.response?.data,
        status: error.response?.status,
        statusText: error.response?.statusText,
        headers: error.response?.headers,
        code: error.code,
        stack: error.stack,
      });
      
      // 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'}`);
    }
  }
}
