import { Request, Response, NextFunction } from 'express';
import { PgUserRepository } from '../../infrastructure/repositories/postgresql/user.repository';
import { ApiError } from '../../utils/errors/api.error';
import logger from '../../config/logger';
import bcrypt from 'bcryptjs';
import authConfig from '../../config/auth';
import crypto from 'crypto';
import { CreateUserDto, UpdateUserDto, UserResponseDto } from '../../domain/models/user.model';
import jwt from 'jsonwebtoken';

// Initialize repository
const userRepository = new PgUserRepository();

/**
 * User Controller
 * Handles CRUD operations for users and authentication
 */
export class UserController {
  /**
   * Get all users with pagination
   * @route GET /api/users
   */
  async getAllUsers(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const page = parseInt(req.query.page as string) || 1;
      const limit = parseInt(req.query.limit as string) || 10;

      // Extract filter parameters
      const filters: Record<string, any> = {};
      if (req.query.email) {
        filters.email = req.query.email;
      }
      if (req.query.name) {
        filters.name = req.query.name;
      }
      if (req.query.role) {
        filters.role = req.query.role;
      }
      if (req.query.active !== undefined) {
        filters.active = req.query.active === 'true';
      }

      // Get data with pagination
      const users = await userRepository.findAll(page, limit, filters);
      const totalCount = await userRepository.count(filters);

      // Calculate pagination metadata
      const totalPages = Math.ceil(totalCount / limit);
      const hasNext = page < totalPages;
      const hasPrev = page > 1;

      // Remove passwords from response
      const sanitizedUsers = users.map(this.sanitizeUser);

      res.status(200).json({
        status: 'success',
        data: {
          users: sanitizedUsers,
          pagination: {
            page,
            limit,
            totalCount,
            totalPages,
            hasNext,
            hasPrev
          }
        }
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Get a user by ID
   * @route GET /api/users/:id
   */
  async getUserById(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const userId = req.params.id;
      const user = await userRepository.findById(userId);

      if (!user) {
        throw ApiError.notFound(`User with ID ${userId} not found`);
      }

      // Remove password from response
      const sanitizedUser = this.sanitizeUser(user);

      res.status(200).json({
        status: 'success',
        data: { user: sanitizedUser }
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Create a new user
   * @route POST /api/users
   */
  async createUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const userData: CreateUserDto = req.body;

      // Check if email already exists
      const existingUser = await userRepository.findByEmail(userData.email);
      if (existingUser) {
        throw ApiError.conflict(`User with email ${userData.email} already exists`);
      }

      // Hash password
      const hashedPassword = await bcrypt.hash(userData.password, authConfig.password.saltRounds);

      // Create user with hashed password
      const newUser = await userRepository.create({
        ...userData,
        password: hashedPassword
      });

      // Remove password from response
      const sanitizedUser = this.sanitizeUser(newUser);

      res.status(201).json({
        status: 'success',
        data: { user: sanitizedUser }
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Update a user
   * @route PUT /api/users/:id
   */
  async updateUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const userId = req.params.id;
      const userData: UpdateUserDto = req.body;

      // Check if user exists
      const existingUser = await userRepository.findById(userId);
      if (!existingUser) {
        throw ApiError.notFound(`User with ID ${userId} not found`);
      }

      // If email is being updated, check if it already exists
      if (userData.email && userData.email !== existingUser.email) {
        const userWithEmail = await userRepository.findByEmail(userData.email);
        if (userWithEmail && userWithEmail.id !== userId) {
          throw ApiError.conflict(`User with email ${userData.email} already exists`);
        }
      }

      // If password is being updated, hash it
      let updateData = { ...userData };
      if (userData.password) {
        const hashedPassword = await bcrypt.hash(userData.password, authConfig.password.saltRounds);
        updateData.password = hashedPassword;
      }

      // Update user
      const updatedUser = await userRepository.update(userId, updateData);

      if (!updatedUser) {
        throw ApiError.internal('Failed to update user');
      }

      // Remove password from response
      const sanitizedUser = this.sanitizeUser(updatedUser);

      res.status(200).json({
        status: 'success',
        data: { user: sanitizedUser }
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Delete a user
   * @route DELETE /api/users/:id
   */
  async deleteUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const userId = req.params.id;

      // Check if user exists
      const existingUser = await userRepository.findById(userId);
      if (!existingUser) {
        throw ApiError.notFound(`User with ID ${userId} not found`);
      }

      // Check if user is trying to delete themselves
      if (req.user?.id === userId) {
        throw ApiError.forbidden('You cannot delete your own account');
      }

      // Check if user is the last admin
      if (existingUser.roles.includes(authConfig.roles.admin)) {
        const adminCount = await userRepository.count({
          role: authConfig.roles.admin
        });

        if (adminCount <= 1) {
          throw ApiError.forbidden('Cannot delete the last admin user');
        }
      }

      // Delete user
      const deleted = await userRepository.delete(userId);

      if (!deleted) {
        throw ApiError.internal('Failed to delete user');
      }

      res.status(200).json({
        status: 'success',
        message: `User with ID ${userId} has been deleted`
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Update user roles
   * @route PUT /api/users/:id/roles
   */
  async updateUserRoles(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const userId = req.params.id;
      const { roles } = req.body;

      if (!Array.isArray(roles)) {
        throw ApiError.badRequest('Roles must be an array');
      }

      // Check if user exists
      const existingUser = await userRepository.findById(userId);
      if (!existingUser) {
        throw ApiError.notFound(`User with ID ${userId} not found`);
      }

      // Check if user is trying to update their own roles
      if (req.user?.id === userId) {
        throw ApiError.forbidden('You cannot update your own roles');
      }

      // Check if removing admin role from the last admin
      if (
        existingUser.roles.includes(authConfig.roles.admin) &&
        !roles.includes(authConfig.roles.admin)
      ) {
        const adminCount = await userRepository.count({
          role: authConfig.roles.admin
        });

        if (adminCount <= 1) {
          throw ApiError.forbidden('Cannot remove admin role from the last admin user');
        }
      }

      // Update roles
      const updatedUser = await userRepository.updateRoles(userId, roles);

      if (!updatedUser) {
        throw ApiError.internal('Failed to update user roles');
      }

      // Remove password from response
      const sanitizedUser = this.sanitizeUser(updatedUser);

      res.status(200).json({
        status: 'success',
        data: { user: sanitizedUser }
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Activate a user
   * @route PATCH /api/users/:id/activate
   */
  async activateUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const userId = req.params.id;

      // Check if user exists
      const existingUser = await userRepository.findById(userId);
      if (!existingUser) {
        throw ApiError.notFound(`User with ID ${userId} not found`);
      }

      // Activate user
      const updatedUser = await userRepository.activateUser(userId);

      if (!updatedUser) {
        throw ApiError.internal('Failed to activate user');
      }

      // Remove password from response
      const sanitizedUser = this.sanitizeUser(updatedUser);

      res.status(200).json({
        status: 'success',
        data: { user: sanitizedUser }
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Deactivate a user
   * @route PATCH /api/users/:id/deactivate
   */
  async deactivateUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const userId = req.params.id;

      // Check if user exists
      const existingUser = await userRepository.findById(userId);
      if (!existingUser) {
        throw ApiError.notFound(`User with ID ${userId} not found`);
      }

      // Check if user is trying to deactivate themselves
      if (req.user?.id === userId) {
        throw ApiError.forbidden('You cannot deactivate your own account');
      }

      // Check if user is the last admin
      if (existingUser.roles.includes(authConfig.roles.admin)) {
        const adminCount = await userRepository.count({
          role: authConfig.roles.admin,
          active: true
        });

        if (adminCount <= 1) {
          throw ApiError.forbidden('Cannot deactivate the last active admin user');
        }
      }

      // Deactivate user
      const updatedUser = await userRepository.deactivateUser(userId);

      if (!updatedUser) {
        throw ApiError.internal('Failed to deactivate user');
      }

      // Remove password from response
      const sanitizedUser = this.sanitizeUser(updatedUser);

      res.status(200).json({
        status: 'success',
        data: { user: sanitizedUser }
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Change user password
   * @route PUT /api/users/:id/password
   */
  async changePassword(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const userId = req.params.id;
      const { currentPassword, newPassword } = req.body;

      // Check if user exists
      const user = await userRepository.findById(userId);
      if (!user) {
        throw ApiError.notFound(`User with ID ${userId} not found`);
      }

      // Only allow users to change their own password unless they're an admin
      const isAdmin = req.user?.roles?.includes(authConfig.roles.admin);
      if (req.user?.id !== userId && !isAdmin) {
        throw ApiError.forbidden('You can only change your own password');
      }

      // If it's the user changing their own password, verify current password
      if (req.user?.id === userId) {
        const isPasswordValid = await bcrypt.compare(currentPassword, user.password);
        if (!isPasswordValid) {
          throw ApiError.badRequest('Current password is incorrect');
        }
      }

      // Hash new password
      const hashedPassword = await bcrypt.hash(newPassword, authConfig.password.saltRounds);

      // Update password
      const updated = await userRepository.changePassword(userId, hashedPassword);

      if (!updated) {
        throw ApiError.internal('Failed to change password');
      }

      // Delete any existing password reset tokens
      await userRepository.deleteAllPasswordResetTokensForUser(userId);

      res.status(200).json({
        status: 'success',
        message: 'Password has been changed successfully'
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Request password reset
   * @route POST /api/users/password-reset/request
   */
  async requestPasswordReset(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const { email } = req.body;

      // Check if user exists
      const user = await userRepository.findByEmail(email);

      // Don't reveal if user exists or not for security
      if (!user) {
        res.status(200).json({
          status: 'success',
          message: 'If a user with this email exists, a password reset link will be sent'
        });
        return;
      }

      // Generate reset token
      const resetToken = crypto.randomBytes(32).toString('hex');
      const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours

      // Store reset token
      await userRepository.createPasswordResetToken(user.id, resetToken, expiresAt);

      // In a real application, we would send an email with the reset link
      // For now, we'll just return the token in the response for testing
      logger.info(`Password reset requested for ${email}. Token: ${resetToken}`);

      res.status(200).json({
        status: 'success',
        message: 'If a user with this email exists, a password reset link will be sent',
        // In production, remove the token from the response for security
        // It's only included here for testing purposes
        token: resetToken
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Confirm password reset
   * @route POST /api/users/password-reset/confirm
   */
  async confirmPasswordReset(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const { token, password } = req.body;

      // Find reset token
      const resetToken = await userRepository.findPasswordResetToken(token);

      if (!resetToken) {
        throw ApiError.badRequest('Invalid or expired token');
      }

      // Check if token has expired
      if (new Date(resetToken.expires_at) < new Date()) {
        // Delete expired token
        await userRepository.deletePasswordResetToken(token);
        throw ApiError.badRequest('Token has expired');
      }

      // Hash new password
      const hashedPassword = await bcrypt.hash(password, authConfig.password.saltRounds);

      // Update password
      const updated = await userRepository.changePassword(resetToken.user_id, hashedPassword);

      if (!updated) {
        throw ApiError.internal('Failed to reset password');
      }

      // Delete reset token
      await userRepository.deletePasswordResetToken(token);

      res.status(200).json({
        status: 'success',
        message: 'Password has been reset successfully'
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Get user profile (current user)
   * @route GET /api/users/profile
   */
  async getUserProfile(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const userId = req.user?.id;

      if (!userId) {
        throw ApiError.unauthorized('You must be logged in to access your profile');
      }

      const user = await userRepository.findById(userId);

      if (!user) {
        throw ApiError.notFound('User profile not found');
      }

      // Remove password from response
      const sanitizedUser = this.sanitizeUser(user);

      res.status(200).json({
        status: 'success',
        data: { user: sanitizedUser }
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Update user profile (current user)
   * @route PUT /api/users/profile
   */
  async updateUserProfile(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const userId = req.user?.id;

      if (!userId) {
        throw ApiError.unauthorized('You must be logged in to update your profile');
      }

      const userData: UpdateUserDto = req.body;

      // Don't allow role updates through profile update
      if (userData.roles) {
        delete userData.roles;
      }

      // Don't allow permissions updates through profile update
      if (userData.permissions) {
        delete userData.permissions;
      }

      // Don't allow active status updates through profile update
      if (userData.hasOwnProperty('active')) {
        delete userData.active;
      }

      // If email is being updated, check if it already exists
      if (userData.email) {
        const userWithEmail = await userRepository.findByEmail(userData.email);
        if (userWithEmail && userWithEmail.id !== userId) {
          throw ApiError.conflict(`User with email ${userData.email} already exists`);
        }
      }

      // Update user
      const updatedUser = await userRepository.update(userId, userData);

      if (!updatedUser) {
        throw ApiError.internal('Failed to update profile');
      }

      // Remove password from response
      const sanitizedUser = this.sanitizeUser(updatedUser);

      res.status(200).json({
        status: 'success',
        data: { user: sanitizedUser }
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Login user
   * @route POST /api/users/login
   */
  async login(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const { email, password } = req.body;

      // Find user by email
      const user = await userRepository.findByEmail(email);

      if (!user) {
        throw ApiError.unauthorized('Invalid credentials');
      }

      // Check if user is active
      if (!user.active) {
        throw ApiError.forbidden('Your account is deactivated');
      }

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

      if (!isPasswordValid) {
        throw ApiError.unauthorized('Invalid credentials');
      }

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

      // Generate JWT tokens
      const accessToken = jwt.sign(
        {
          sub: user.id,
          email: user.email,
          roles: user.roles,
          permissions: user.permissions
        },
        authConfig.jwt.secret,
        {
          expiresIn: authConfig.jwt.accessTokenExpiry,
          issuer: authConfig.jwt.issuer,
          audience: authConfig.jwt.audience
        }
      );

      const refreshToken = jwt.sign(
        {
          sub: user.id,
          type: 'refresh'
        },
        authConfig.jwt.secret,
        {
          expiresIn: authConfig.jwt.refreshTokenExpiry,
          issuer: authConfig.jwt.issuer,
          audience: authConfig.jwt.audience
        }
      );

      // Remove password from response
      const sanitizedUser = this.sanitizeUser(user);

      res.status(200).json({
        status: 'success',
        data: {
          user: sanitizedUser,
          accessToken,
          refreshToken,
          expiresIn: this.getExpiryTime(authConfig.jwt.accessTokenExpiry)
        }
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Refresh token
   * @route POST /api/users/refresh-token
   */
  async refreshToken(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const { refreshToken } = req.body;

      if (!refreshToken) {
        throw ApiError.badRequest('Refresh token is required');
      }

      try {
        // Verify refresh token
        const decoded = jwt.verify(refreshToken, authConfig.jwt.secret, {
          issuer: authConfig.jwt.issuer,
          audience: authConfig.jwt.audience
        }) as { sub: string; type: string };

        // Check if it's a refresh token
        if (decoded.type !== 'refresh') {
          throw ApiError.unauthorized('Invalid token type');
        }

        // Get user
        const user = await userRepository.findById(decoded.sub);

        if (!user) {
          throw ApiError.unauthorized('User not found');
        }

        // Check if user is active
        if (!user.active) {
          throw ApiError.forbidden('Your account is deactivated');
        }

        // Generate new access token
        const accessToken = jwt.sign(
          {
            sub: user.id,
            email: user.email,
            roles: user.roles,
            permissions: user.permissions
          },
          authConfig.jwt.secret,
          {
            expiresIn: authConfig.jwt.accessTokenExpiry,
            issuer: authConfig.jwt.issuer,
            audience: authConfig.jwt.audience
          }
        );

        res.status(200).json({
          status: 'success',
          data: {
            accessToken,
            expiresIn: this.getExpiryTime(authConfig.jwt.accessTokenExpiry)
          }
        });
      } catch (error) {
        if (error instanceof jwt.JsonWebTokenError) {
          throw ApiError.unauthorized('Invalid or expired refresh token');
        }
        throw error;
      }
    } catch (error) {
      next(error);
    }
  }

  /**
   * Remove sensitive data from user object
   * @param user User object
   * @returns Sanitized user object
   */
  private sanitizeUser(user: any): UserResponseDto {
    const { password, ...sanitizedUser } = user;
    return sanitizedUser;
  }

  /**
   * Convert expiry time string to seconds
   * @param expiryTime Expiry time string (e.g., '15m', '1h', '7d')
   * @returns Expiry time in seconds
   */
  private getExpiryTime(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
    }
  }
}

// Export controller instance
export const userController = new UserController();
