import { JWT_CONSTANTS, SessionStore } from '@backend/auth'
import { UserCredentialsRepository, UserRepository } from '@backend/repository'
import { SessionInfo } from '@core/types'
import { ForbiddenException, Injectable, Logger } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import argon2 from 'argon2'
import { InvalidCredentialsException, InvalidPasswordException, UserEmailConflictException } from '../exceptions'
import {
	LoginRequest,
	LoginResponse,
	RequestResetPasswordRequest,
	ResetPasswordRequest,
	UpdatePasswordRequest,
	VerifyEmailRequest,
	VerifyTokenResponse,
} from '../types'
import { AuthEmailService } from './email.service'

@Injectable()
export class AuthService {
	private readonly logger = new Logger(AuthService.name)

	constructor(
		private readonly _jwtService: JwtService,
		private readonly _emailService: AuthEmailService,
		private readonly _userRepository: UserRepository,
		private readonly _userCredentialsRepository: UserCredentialsRepository,
	) {}

	/**
	 * Authenticates a user by validating their credentials and generating an access token.
	 *
	 * @param request - The {@link LoginRequest} containing email and password
	 * @returns A {@link LoginResponse} containing the access token if authentication is successful
	 * @throws An {@link UnauthorizedException} if credentials are invalid
	 */
	async login(request: LoginRequest): Promise<LoginResponse> {
		const user = await this._userRepository.findOne({
			where: { email: request.email },
			relations: { credentials: true },
		})

		if (!user || !user.credentials) {
			if (user && !user.credentials) {
				this.logger.warn({ message: 'User is trying to login, but credentials are not configured', user })
			}

			throw new InvalidCredentialsException()
		}

		if (!(await argon2.verify(user.credentials.password, request.password))) {
			throw new InvalidCredentialsException()
		}

		const profile: SessionInfo = {
			id: user.id,
			firstName: user.firstName,
			lastName: user.lastName,
			email: user.email,
			emailVerified: user.emailVerified,
			tenantId: user.tenantId,
			preferedLanguage: user.preferedLanguage,
		}

		const accessToken = await this._jwtService.signAsync(profile)

		return { access_token: accessToken }
	}

	/**
	 * Starts the email verification process for a user by generating a JWT token and sending a confirmation email.
	 *
	 * @param request - The {@link VerifyEmailRequest} containing the user's email address
	 * @throws A {@link ConflictException} if a user with the same email already exists
	 */
	async startEmailVerification(request: VerifyEmailRequest): Promise<void> {
		const exists = await this._userRepository.exist({ where: { email: request.email } })
		if (exists) throw new UserEmailConflictException()

		const token = await this._jwtService.signAsync(
			{ email: request.email },
			{ secret: JWT_CONSTANTS.SECRET, expiresIn: '1h' },
		)

		await this._emailService.sendConfirmationEmail(request.email, token)
	}

	/**
	 * Sends a reset password email to a user by generating a JWT token and sending a reset password email.
	 *
	 * @param request - The {@link RequestResetPasswordRequest} containing the user's email address
	 */
	async sendResetPasswordEmail(request: RequestResetPasswordRequest): Promise<void> {
		const exists = await this._userRepository.exist({ where: { email: request.email } })
		if (!exists) {
			this.logger.warn({
				message: 'Reset password request received for user that does not exist',
				email: request.email,
			})
			return
		}

		const token = await this._jwtService.signAsync(
			{ email: request.email },
			{ secret: JWT_CONSTANTS.SECRET, expiresIn: '1h' },
		)

		await this._emailService.sendResetPasswordEmail(request.email, token)
	}

	/**
	 * Resets a user's password by verifying the token and updating the password.
	 *
	 * @param request - The {@link ResetPasswordRequest} containing the token and new password
	 * @throws A {@link ForbiddenException} if the token is invalid or the user does not exist
	 */
	async resetPassword(request: ResetPasswordRequest): Promise<void> {
		const { email } = await this._jwtService.verifyAsync<{ email: string }>(request.token, {
			secret: JWT_CONSTANTS.SECRET,
		})

		const user = await this._userRepository.findOne({
			where: { email },
			relations: { credentials: true },
		})
		if (!user) throw new ForbiddenException()

		const password = await argon2.hash(request.password)

		if (user.credentials) {
			await this._userCredentialsRepository.update({ id: user.credentials.id }, { password })
		} else {
			const credentials = await this._userCredentialsRepository.save({ password, tenantId: user.tenantId })
			await this._userRepository.update({ id: user.id }, { credentials, emailVerified: true })
		}
	}

	/**
	 * Updates a user's password by verifying the current password and updating the password.
	 *
	 * @param request - The UpdatePasswordRequest containing the current password and new password
	 * @throws A {@link ForbiddenException} if the user does not exist
	 * @throws A {@link BadRequestException} if the current password is invalid
	 */
	async updatePassword(request: UpdatePasswordRequest): Promise<void> {
		const loggedInUser = SessionStore.get()
		if (!loggedInUser) throw new ForbiddenException()

		const user = await this._userRepository.findOne({
			where: { email: loggedInUser.email },
			relations: { credentials: true },
		})
		if (!user || !user.credentials) throw new ForbiddenException()

		if (!(await argon2.verify(user.credentials.password, request.currentPassword))) {
			throw new InvalidPasswordException()
		}

		const password = await argon2.hash(request.newPassword)
		await this._userCredentialsRepository.update({ id: user.credentials.id }, { password })
	}

	/**
	 * Verifies a JWT token and returns the email address contained in the token.
	 *
	 * @param token - The JWT token to verify
	 * @returns The {@link VerifyTokenResponse} if the token is valid
	 */
	verifyToken(token: string): Promise<VerifyTokenResponse> {
		return this._jwtService.verifyAsync(token, { secret: JWT_CONSTANTS.SECRET })
	}
}
