import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import { v4 as uuidv4 } from 'uuid';
import crypto from 'crypto';
import { User } from '../models/user.model';
import { IUserRepository } from '../interfaces/repositories/user.repository.interface';
import { IAuthService, ApiClient, ApiClientWithSecret, TokenPayload } from '../interfaces/services/auth.service.interface';
import logger from '../../config/logger';
import authConfig from '../../config/auth';
import db from '../../infrastructure/database/connection';
import { AuthError } from '../../utils/errors/auth.error';

/**
 * Authentication Service Implementation
 * Handles user authentication and API client management
 */
export class AuthService implements IAuthService {
  constructor(private userRepository: IUserRepository) {}

  /**
   * Authenticate user with email and password
   * @param email User email
   * @param password User password
   * @returns Tokens and user data
   */
  async authenticateUser(email: string, password: string): Promise<{
    accessToken: string;
    refreshToken: string;
    expiresIn: number;
    user: Omit<User, 'password'>;
  }> {
    // Find user by email
    const user = await this.userRepository.findByEmail(email);

    if (!user) {
      throw AuthError.invalidCredentials();
    }

    // Check if user is active
    if (!user.active) {
      throw AuthError.accessDenied('Your account is inactive');
    }

    // Verify password
    const isPasswordValid = await bcrypt.compare(password, user.password);

    if (!isPasswordValid) {
      throw AuthError.invalidCredentials();
    }

    // Update last login
    await this.userRepository.updateLastLogin(user.id);

    // Generate tokens
    const accessToken = this.generateUserAccessToken(user);
    const refreshToken = this.generateRefreshToken(user.id, 'user');

    // Get token expiry time in seconds
    const expiresIn = this.getExpirySeconds(authConfig.jwt.accessTokenExpiry);

    // Remove password from user object
    const { password: _, ...userWithoutPassword } = user;

    return {
      accessToken,
      refreshToken,
      expiresIn,
      user: userWithoutPassword
    };
  }

  /**
   * Authenticate API client with client credentials
   * @param clientId Client ID
   * @param clientSecret Client secret
   * @param scope Requested scopes (space-separated)
   * @returns Access token and expiry
   */
  async authenticateClient(
    clientId: string,
    clientSecret: string,
    scope?: string
  ): Promise<{
    accessToken: string;
    expiresIn: number;
    scope: string;
  }> {
    try {
      // Find client
      const client = await db('api_clients')
        .where({ client_id: clientId, active: true })
        .first();

      if (!client) {
        throw AuthError.invalidCredentials('Invalid client credentials');
      }

      // Verify client secret using constant-time comparison to prevent timing attacks
      const isSecretValid = crypto.timingSafeEqual(
        Buffer.from(clientSecret),
        Buffer.from(client.client_secret)
      );

      if (!isSecretValid) {
        throw AuthError.invalidCredentials('Invalid client credentials');
      }

      // Parse allowed scopes from the client record
      // const allowedScopes = JSON.parse(client.allowed_scopes || '[]');
      const allowedScopes = client.allowed_scopes;

      // If scope is requested, validate it against allowed scopes
      let grantedScopes = allowedScopes;
      if (scope) {
        const requestedScopes = scope.split(' ');

        // Only grant scopes that are allowed for this client
        grantedScopes = requestedScopes.filter(s => allowedScopes.includes(s));

        // If no valid scopes remain, return empty scope
        if (grantedScopes.length === 0) {
          grantedScopes = [];
        }
      }

      // Generate access token with granted scopes
      const accessToken = this.generateClientAccessToken(
        client.client_id,
        grantedScopes
      );

      // Get token expiry time in seconds
      const expiresIn = this.getExpirySeconds(authConfig.jwt.clientTokenExpiry);

      return {
        accessToken,
        expiresIn,
        scope: grantedScopes.join(' ')
      };
    } catch (error) {
      logger.error('Error authenticating client:', error);

      // Rethrow AuthError instances
      if (error instanceof AuthError) {
        throw error;
      }

      // Wrap other errors
      throw AuthError.invalidCredentials('Error during client authentication');
    }
  }

  /**
   * Refresh access token using refresh token
   * @param refreshToken Refresh token
   * @returns New access token and expiry
   */
  async refreshToken(refreshToken: string): Promise<{
    accessToken: string;
    expiresIn: number;
  }> {
    try {
      // Verify and decode refresh token
      const decoded = jwt.verify(refreshToken, authConfig.jwt.secret, {
        issuer: authConfig.jwt.issuer,
        audience: authConfig.jwt.audience
      }) as TokenPayload;

      // Check if it's a refresh token
      if (decoded.type !== 'user' && decoded.type !== 'client') {
        throw AuthError.invalidToken('Invalid token type');
      }

      // Handle based on token type
      if (decoded.type === 'user') {
        // Get user
        const user = await this.userRepository.findById(decoded.sub);

        if (!user) {
          throw AuthError.invalidToken('User not found');
        }

        // Check if user is active
        if (!user.active) {
          throw AuthError.accessDenied('Your account is inactive');
        }

        // Generate new access token
        const accessToken = this.generateUserAccessToken(user);

        // Get token expiry time in seconds
        const expiresIn = this.getExpirySeconds(authConfig.jwt.accessTokenExpiry);

        return {
          accessToken,
          expiresIn
        };
      } else {
        // For client tokens
        const client = await db('api_clients')
          .where({ client_id: decoded.client_id, active: true })
          .first();

        if (!client) {
          throw AuthError.invalidToken('Client not found or inactive');
        }

        // Generate new access token
        const accessToken = this.generateClientAccessToken(
          client.client_id,
          decoded.scopes || []
        );

        // Get token expiry time in seconds
        const expiresIn = this.getExpirySeconds(authConfig.jwt.clientTokenExpiry);

        return {
          accessToken,
          expiresIn
        };
      }
    } catch (error) {
      logger.error('Error refreshing token:', error);

      if (error instanceof jwt.JsonWebTokenError) {
        throw AuthError.invalidToken('Invalid or expired refresh token');
      }

      // Rethrow AuthError instances
      if (error instanceof AuthError) {
        throw error;
      }

      // Wrap other errors
      throw AuthError.invalidToken('Error during token refresh');
    }
  }

  /**
   * Validate access token
   * @param token Access token
   * @returns Decoded token payload
   */
  validateToken(token: string): TokenPayload {
    try {
      const decoded = jwt.verify(token, authConfig.jwt.secret, {
        issuer: authConfig.jwt.issuer,
        audience: authConfig.jwt.audience
      }) as TokenPayload;

      return decoded;
    } catch (error) {
      if (error instanceof jwt.TokenExpiredError) {
        throw AuthError.expiredToken();
      }

      throw AuthError.invalidToken();
    }
  }

  /**
   * Generate access token for users
   * @param user User object
   * @returns JWT access token
   */
  private generateUserAccessToken(user: User): string {
    return jwt.sign(
      {
        sub: user.id,
        type: 'user',
        email: user.email,
        roles: user.roles,
        permissions: user.permissions
      },
      authConfig.jwt.secret,
      {
        expiresIn: authConfig.jwt.accessTokenExpiry,
        issuer: authConfig.jwt.issuer,
        audience: authConfig.jwt.audience
      }
    );
  }

  /**
   * Generate access token for API clients
   * @param clientId Client ID
   * @param scopes Granted scopes
   * @returns JWT access token
   */
  private generateClientAccessToken(clientId: string, scopes: string[]): string {
    return jwt.sign(
      {
        sub: clientId,
        type: 'client',
        client_id: clientId,
        scopes
      },
      authConfig.jwt.secret,
      {
        expiresIn: authConfig.jwt.clientTokenExpiry,
        issuer: authConfig.jwt.issuer,
        audience: authConfig.jwt.audience
      }
    );
  }

  /**
   * Generate refresh token
   * @param id User ID or Client ID
   * @param type Token type (user or client)
   * @returns JWT refresh token
   */
  private generateRefreshToken(id: string, type: 'user' | 'client'): string {
    return jwt.sign(
      {
        sub: id,
        type,
        jti: uuidv4() // Unique token ID
      },
      authConfig.jwt.secret,
      {
        expiresIn: authConfig.jwt.refreshTokenExpiry,
        issuer: authConfig.jwt.issuer,
        audience: authConfig.jwt.audience
      }
    );
  }

  /**
   * Get expiry time in seconds from string format
   * @param expiryTime Expiry time string (e.g., '15m', '1h', '7d')
   * @returns Expiry time in seconds
   */
  private getExpirySeconds(expiryTime: string): number {
    const unit = expiryTime.charAt(expiryTime.length - 1);
    const value = parseInt(expiryTime.slice(0, -1));

    switch (unit) {
      case 's':
        return value;
      case 'm':
        return value * 60;
      case 'h':
        return value * 60 * 60;
      case 'd':
        return value * 24 * 60 * 60;
      default:
        return 900; // 15 minutes default
    }
  }

  // API Client Management methods

  /**
   * Create a new API client
   * @param clientData Client data
   * @returns Created client (with client_secret)
   */
  async createApiClient(clientData: {
    client_name: string;
    client_description?: string;
    allowed_scopes?: string[];
  }): Promise<ApiClientWithSecret> {
    try {
      const client_id = this.generateClientId();
      const client_secret = this.generateClientSecret();

      const client = {
        client_id,
        client_secret,
        client_name: clientData.client_name,
        client_description: clientData.client_description,
        allowed_scopes: JSON.stringify(clientData.allowed_scopes || []),
        active: true,
        created_at: new Date(),
        updated_at: new Date()
      };

      await db('api_clients').insert(client);

      return {
        ...client,
        allowed_scopes: JSON.parse(client.allowed_scopes)
      };
    } catch (error) {
      logger.error('Error creating API client:', error);
      throw new Error('Failed to create API client');
    }
  }

  /**
   * Get all API clients
   * @param page Page number
   * @param limit Items per page
   * @param filters Optional filters
   * @returns API clients and pagination info
   */
  async getApiClients(
    page: number = 1,
    limit: number = 10,
    filters?: Record<string, any>
  ): Promise<{
    clients: Omit<ApiClient, 'client_secret'>[];
    pagination: {
      page: number;
      limit: number;
      totalCount: number;
      totalPages: number;
      hasNext: boolean;
      hasPrev: boolean;
    };
  }> {
    try {
      const offset = (page - 1) * limit;

      // Build query
      let query = db('api_clients')
        .select(
          'client_id',
          'client_name',
          'client_description',
          'allowed_scopes',
          'active',
          'created_at',
          'updated_at'
        );

      // Apply filters
      if (filters) {
        Object.entries(filters).forEach(([key, value]) => {
          if (key === 'client_name' && typeof value === 'string') {
            query = query.where(key, 'ilike', `%${value}%`);
          } else if (key === 'active' && typeof value === 'boolean') {
            query = query.where(key, value);
          }
        });
      }

      // Get total count
      const countResult = await query
        .clone()
        .count('client_id as count')
        .groupBy('client_id')
        .first();
      const totalCount = parseInt(countResult?.count as string) || 0;

      // Get clients with pagination
      const clients = await query
        .offset(offset)
        .limit(limit)
        .orderBy('created_at', 'desc');

      // Format clients
      const formattedClients = clients.map(client => ({
        ...client,
        // allowed_scopes: JSON.parse(client.allowed_scopes)
      }));

      // Calculate pagination info
      const totalPages = Math.ceil(totalCount / limit);

      return {
        clients: formattedClients,
        pagination: {
          page,
          limit,
          totalCount,
          totalPages,
          hasNext: page < totalPages,
          hasPrev: page > 1
        }
      };
    } catch (error) {
      logger.error('Error getting API clients:', error);
      throw new Error('Failed to get API clients');
    }
  }

  /**
   * Get API client by ID
   * @param clientId Client ID
   * @returns API client (without client_secret)
   */
  async getApiClientById(clientId: string): Promise<Omit<ApiClient, 'client_secret'> | null> {
    try {
      const client = await db('api_clients')
        .where({ client_id: clientId })
        .select('client_id', 'client_name', 'client_description', 'allowed_scopes',
                'active', 'created_at', 'updated_at')
        .first();

      if (!client) {
        return null;
      }

      return {
        ...client,
        // allowed_scopes: JSON.parse(client.allowed_scopes)
      };
    } catch (error) {
      logger.error('Error getting API client:', error);
      throw new Error('Failed to get API client');
    }
  }

  /**
   * Update API client
   * @param clientId Client ID
   * @param clientData Client data to update
   * @returns Updated client (without client_secret)
   */
  async updateApiClient(
    clientId: string,
    clientData: {
      client_name?: string;
      client_description?: string;
      allowed_scopes?: string[];
      active?: boolean;
    }
  ): Promise<Omit<ApiClient, 'client_secret'> | null> {
    try {
      const client = await db('api_clients')
        .where({ client_id: clientId })
        .first();

      if (!client) {
        return null;
      }

      const updateData: Record<string, any> = {
        updated_at: new Date()
      };

      if (clientData.client_name !== undefined) {
        updateData.client_name = clientData.client_name;
      }

      if (clientData.client_description !== undefined) {
        updateData.client_description = clientData.client_description;
      }

      if (clientData.allowed_scopes !== undefined) {
        updateData.allowed_scopes = JSON.stringify(clientData.allowed_scopes);
      }

      if (clientData.active !== undefined) {
        updateData.active = clientData.active;
      }

      await db('api_clients')
        .where({ client_id: clientId })
        .update(updateData);

      const updatedClient = await db('api_clients')
        .where({ client_id: clientId })
        .select('client_id', 'client_name', 'client_description', 'allowed_scopes',
                'active', 'created_at', 'updated_at')
        .first();

      return {
        ...updatedClient,
        // allowed_scopes: JSON.parse(updatedClient.allowed_scopes)
      };
    } catch (error) {
      logger.error('Error updating API client:', error);
      throw new Error('Failed to update API client');
    }
  }

  /**
   * Delete API client
   * @param clientId Client ID
   * @returns Success flag
   */
  async deleteApiClient(clientId: string): Promise<boolean> {
    try {
      const deleted = await db('api_clients')
        .where({ client_id: clientId })
        .delete();

      return deleted > 0;
    } catch (error) {
      logger.error('Error deleting API client:', error);
      throw new Error('Failed to delete API client');
    }
  }

  /**
   * Regenerate client secret
   * @param clientId Client ID
   * @returns New client secret
   */
  async regenerateClientSecret(clientId: string): Promise<string | null> {
    try {
      const client = await db('api_clients')
        .where({ client_id: clientId })
        .first();

      if (!client) {
        return null;
      }

      const client_secret = this.generateClientSecret();

      await db('api_clients')
        .where({ client_id: clientId })
        .update({
          client_secret,
          updated_at: new Date()
        });

      return client_secret;
    } catch (error) {
      logger.error('Error regenerating client secret:', error);
      throw new Error('Failed to regenerate client secret');
    }
  }

  /**
   * Generate a unique client ID
   * @returns Client ID string
   */
  private generateClientId(): string {
    return `client_${uuidv4().replace(/-/g, '').substring(0, 16)}`;
  }

  /**
   * Generate a secure client secret
   * @returns Client secret string
   */
  private generateClientSecret(): string {
    return crypto.randomBytes(32).toString('hex');
  }
}
