import { HttpException, HttpStatus, Injectable, Res } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { createCipheriv, randomBytes, scrypt } from 'crypto';
import { User } from 'src/users/entities/user.entity';
import { Repository } from 'typeorm';
import { promisify } from 'util';
import { Response } from 'express';
import { GrantTicketDto } from './dto/grantTicket.dto';
import { Game } from './entity/game.entity';
import { ValidateTicketDto } from './dto/validateTicket.dto';
import { TokenGame } from './entity/tokenGame.entity';
import { UsersService } from 'src/users/users.service';
import { CreateGameDto } from './dto/createGame.dto';
import { UpdateGameDto } from './dto/updateGame.dto';
import { FriendshipService } from 'src/friendship/friendship.service';
import { STATUS } from 'src/common/constants/constants';

@Injectable()
export class GameService {
	constructor (
		@InjectRepository(Game)
		private readonly gameRepository :  Repository<Game>,
		@InjectRepository(User)
		private readonly userRepository : Repository<User>,
		@InjectRepository(TokenGame)
		private readonly tokenGameRepository : Repository<TokenGame>,
		private readonly userService : UsersService,
		private readonly friendShipService : FriendshipService
	) { }

	async getMatchesForSpectator() {
		const games = await this.gameRepository.createQueryBuilder("game")
		.where('game.isMatchIsFinished = :isMatchIsFinished', {isMatchIsFinished : false})
		.getMany();
		const gamesToReturn : Partial<Game>[] = []
		for (const game of games)
		{
			gamesToReturn.push({gameServerIdOfTheMatch : game.gameServerIdOfTheMatch,
				gameOptions : game.gameOptions, playerOneUsername : game.playerOneUsername,
				playerTwoUsername : game.playerTwoUsername})
			console.log("Is match is finished : " + game.isMatchIsFinished)
		}
		return gamesToReturn;
	}

	async getRankingForAllUsers(currentUser : User) {
		const users = await this.userRepository.createQueryBuilder("user")
		.leftJoinAndSelect("user.stats", "stats")
		.orderBy('stats.winGame', "DESC")
		.getMany();
		const partialUser : Partial<User>[] = []
		for (const user of users)
		{
			if (await this.friendShipService.findIfUserIsBlockedOrHasBlocked(currentUser.id, user.id) === false)
				partialUser.push({username : user.username, stats : user.stats })
		}
		console.log(...partialUser)
		return partialUser;
	}


	async encryptToken(toEncrypt : string) : Promise<string> {
		const iv = randomBytes(16);
		const password = process.env.TICKET_FOR_PLAYING_GAME_SECRET + new Date();
		const key = (await promisify(scrypt)(password, 'salt', 32)) as Buffer;
		const cipher = createCipheriv('aes-256-ctr', key, iv);
		const encryptedText = Buffer.concat([
			cipher.update(toEncrypt),
			cipher.final(),
		]);
		const encryptedTextToReturn = encryptedText.toString('base64');
		return encryptedTextToReturn
	}

	async deleteToken(user : User){
		const tokenGame  = await this.tokenGameRepository.createQueryBuilder('tokengame')
		.where('tokengame.playerTwoUsername = :playerTwoUsername', {playerTwoUsername : user.username})
		.orWhere('tokengame.playerOneUsername = :playerOneUsername', {playerOneUsername : user.username})
		.getMany();
		if (tokenGame)
			return this.tokenGameRepository.remove(tokenGame);
	}

	async generateToken(user : User, grantTicketDto : GrantTicketDto, @Res() res : Response)
	{
		console.log(user.status);
		if (user.status === STATUS.IN_POOL || user.status === STATUS.IN_GAME)
		{
			await this.deleteToken(user);
			if (user.status === STATUS.IN_POOL) {
				user.status = STATUS.CONNECTED;
			}
			this.userRepository.save(user);
		}
		if (grantTicketDto.isGameIsWithInvitation === true && user.status !== STATUS.IN_GAME)
		{
			const secondUser : Partial<User> = await this.userService.findOne(grantTicketDto.playerTwoUsername)
			if (!secondUser || secondUser.username === user.username)
				return res.status(HttpStatus.NOT_FOUND).json({message : "User not found OR you want to play with yourself."});
			const encryptedTextToReturn = await this.encryptToken(user.username + '_' + secondUser.username + '_'
			+ grantTicketDto.gameOptions + '_' + grantTicketDto.isGameIsWithInvitation + '_' + new Date())
			const tok = this.tokenGameRepository.create(grantTicketDto);
			tok.isSecondUserAcceptedRequest = false;
			tok.numberOfRegisteredUser = 0;
			tok.token = encryptedTextToReturn;
			this.tokenGameRepository.save(tok);
			this.userService.updateStatus(user.id, "In Pool")
			return res.status(HttpStatus.OK).json({ token : encryptedTextToReturn });
		}
		else if (grantTicketDto.isGameIsWithInvitation === false && user.status !== STATUS.IN_GAME) {
			const encryptedTextToReturn = await this.encryptToken(user.username +  '_'
			+ grantTicketDto.gameOptions + '_' +  grantTicketDto.isGameIsWithInvitation + '_' + new Date())
			const tok = this.tokenGameRepository.create(grantTicketDto);
			tok.numberOfRegisteredUser = 0;
			tok.token = encryptedTextToReturn;
			this.tokenGameRepository.save(tok);
			this.userService.updateStatus(user.id, "In Pool")
			return res.status(HttpStatus.OK).json({ token : encryptedTextToReturn });
		}
		return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({message : "Internal Server Error"});
	}

	async validateToken(validateTicketDto : ValidateTicketDto) {
		if (validateTicketDto.isGameIsWithInvitation === true)
		{
			const tokenGame : TokenGame  = await this.tokenGameRepository.createQueryBuilder('tokengame')
			.where('tokengame.playerOneUsername = :playerOneUsername', {playerOneUsername : validateTicketDto.playerOneUsername})
			.andWhere('tokengame.playerTwoUsername = :playerTwoUsername', {playerTwoUsername : validateTicketDto.playerTwoUsername})
			.andWhere('tokengame.gameOptions = :gameOption', {gameOption : validateTicketDto.gameOptions})
			.andWhere('tokengame.isGameIsWithInvitation = :isGameIsWithInvitation', {isGameIsWithInvitation: true})
			.andWhere('tokengame.isSecondUserAcceptedRequest = :choice', {choice : true})
			.andWhere('tokengame.token = :token', {token : validateTicketDto.token})
			.getOne();
			if (tokenGame)
			{
				tokenGame.numberOfRegisteredUser++;
				if (tokenGame.numberOfRegisteredUser === 2)
				{
					this.tokenGameRepository.remove(tokenGame)
					const userOne : User = await this.userRepository.createQueryBuilder('user')
					.where("user.username = :username", {username : tokenGame.playerOneUsername})
					.getOne();
					this.userService.updateStatus(userOne.id, "In Game")
					const userTwo : User = await this.userRepository.createQueryBuilder('user')
					.where("user.username = :username", {username : tokenGame.playerTwoUsername})
					.getOne();
					this.deleteToken(userOne)
					this.deleteToken(userTwo)
					this.userService.updateStatus(userTwo.id, "In Game")
				}
				return true;
			}
		}
		else if (validateTicketDto.isGameIsWithInvitation === false)
		{
			const tokenGame : TokenGame  = await this.tokenGameRepository.createQueryBuilder('tokengame')
			.where('tokengame.playerOneUsername = :playerOneUsername', {playerOneUsername : validateTicketDto.playerOneUsername})
			.andWhere('tokengame.gameOptions = :gameOption', {gameOption : validateTicketDto.gameOptions})
			.andWhere('tokengame.isGameIsWithInvitation = :isGameIsWithInvitation', {isGameIsWithInvitation: false})
			.andWhere('tokengame.token = :token', {token : validateTicketDto.token})
			.getOne();
			if (tokenGame)
			{
				this.tokenGameRepository.remove(tokenGame)
				console.log("USERNAME : " + tokenGame.playerOneUsername)
				const user : User = await this.userRepository.createQueryBuilder('user')
				.where("user.username = :username", {username : tokenGame.playerOneUsername})
				.getOne();
				this.userService.updateStatus(user.id, "In Game")
				this.deleteToken(user)
				return true;
			}
		}
		return false;
	}

	async findInvitations(user : User, @Res() res : Response) {
		const game  = await this.tokenGameRepository.createQueryBuilder('tokengame')
		.where('tokengame.playerTwoUsername = :playerTwoUsername', {playerTwoUsername : user.username})
		.andWhere('tokengame.isGameIsWithInvitation = :invit', {invit : true})
		.andWhere('tokengame.isSecondUserAcceptedRequest = :choice', {choice : false})
		.getMany();
		if (!game)
			return res.status(HttpStatus.NOT_FOUND).send({message : "No invitation found"});
		let partialGame : Partial<TokenGame>[] = [];
		for (const gameToken of game) {
			partialGame.push({
				playerOneUsername : gameToken.playerOneUsername,
				playerTwoUsername : gameToken.playerTwoUsername,
				gameOptions : gameToken.gameOptions,
				token : gameToken.token,
			});
		}
		return res.status(HttpStatus.OK).json(partialGame);
	}

	async declineInvitation(user : User, token : string, @Res() res : Response)
	{
		if (user.status !== "Connected")
			return res.status(HttpStatus.FORBIDDEN).json({message : "You must not be in game to decline an invitation"});
		console.log("On décline l'invitation")
		const tokenGame  = await this.tokenGameRepository.createQueryBuilder('tokengame')
		.andWhere('tokengame.playerTwoUsername = :playerTwoUsername', {playerTwoUsername : user.username})
		.andWhere('tokengame.token = :token', {token : token})
		.getOne();
		if (tokenGame)
		{
			this.tokenGameRepository.remove(tokenGame);
			return res.status(HttpStatus.OK).json({message : "Invitation declined."});
		}
		return res.status(HttpStatus.NOT_FOUND).json({message : "No invitation found !"});
	}

	async destroySession(token : string)
	{
		console.log("On détruit le token et la session qui va avec")
		const tokenGame  = await this.tokenGameRepository.createQueryBuilder('tokengame')
		.where('tokengame.token = :token', {token : token})
		.getOne();
		if (tokenGame)
		{
			const playerOne = await this.userRepository.findOneBy({username : tokenGame.playerOneUsername})
			const playerTwo = await this.userRepository.findOneBy({username : tokenGame.playerTwoUsername})
			if (playerOne.status !== "Disconnected")
				this.userService.updateStatus(playerOne.id, "Connected")
			if (playerTwo.status !== "Disconnected")
				this.userService.updateStatus(playerTwo.id, "Connected")
			return this.tokenGameRepository.remove(tokenGame);
		}
		return new HttpException("Token not found !", HttpStatus.NOT_FOUND)
	}

	async acceptInvitation(user : User, token : string, @Res() res : Response)
	{
		if (user.status !== "Connected")
			return res.status(HttpStatus.FORBIDDEN).send("")
		const tokenGame  = await this.tokenGameRepository.createQueryBuilder('tokenGame')
		.andWhere('tokenGame.playerTwoUsername = :playerTwoUsername', {playerTwoUsername : user.username})
		.andWhere('tokenGame.token = :token', {token : token})
		.getOne();
		if (tokenGame)
		{
			tokenGame.isSecondUserAcceptedRequest = true;
			this.tokenGameRepository.save(tokenGame)
			return res.status(HttpStatus.OK).json({message : "Invitation accepted."});
		}
		return res.status(HttpStatus.NOT_FOUND).json({message : "No invitation found !"});
	}

	async createGame(creategameDto : CreateGameDto)
	{
		if (creategameDto.playerOneUsername === "" || creategameDto.playerTwoUsername === ""
			|| creategameDto.playerOneUsername === creategameDto.playerTwoUsername)
			return HttpStatus.INTERNAL_SERVER_ERROR
		const game = this.gameRepository.create(creategameDto)
		game.isMatchIsFinished = false;
		this.gameRepository.save(game);
		if (!game)
			return HttpStatus.INTERNAL_SERVER_ERROR
		console.log("200 retourné pour la création de partie")
		return HttpStatus.OK
	}

	async updateGame(updateGameDto : UpdateGameDto) {
		console.log("Updata game" + updateGameDto)
		const game = await this.gameRepository.createQueryBuilder('game')
		.where("game.gameServerIdOfTheMatch = :gameServerIdOfTheMatch", {gameServerIdOfTheMatch : updateGameDto.gameServerIdOfTheMatch})
		.getOne();
		if (!game)
			throw new HttpException(`The game could not be updated.`,HttpStatus.NOT_FOUND);
		game.isMatchIsFinished = true;
		game.playerOneUsernameResult = updateGameDto.playerOneUsernameResult
		game.playerTwoUsernameResult = updateGameDto.playerTwoUsernameResult
		this.gameRepository.save(game);
		console.log("On a sauvegardé la partie. Game :")
		console.log(game)
		const playerOne = await this.userRepository.findOneBy({username : game.playerOneUsername})
		const playerTwo = await this.userRepository.findOneBy({username : game.playerTwoUsername})
		if (!playerOne || !playerTwo)
			return new HttpException("Internal Server Error. Impossible to update the database", HttpStatus.INTERNAL_SERVER_ERROR);
		if (game.playerOneUsernameResult === game.playerTwoUsernameResult)
		{
			this.userService.incrementDraws(playerOne.id)
			this.userService.incrementDraws(playerTwo.id)
			console.log("DRAW NEST");
		}
		else if (game.playerOneUsernameResult < game.playerTwoUsernameResult)
		{
			this.userService.incrementDefeats(playerOne.id)
			this.userService.incrementVictories(playerTwo.id)
		}
		else
		{
			this.userService.incrementVictories(playerOne.id)
			this.userService.incrementDefeats(playerTwo.id)
		}
		this.userService.updateStatus(playerOne.id, "Connected")
		this.userService.updateStatus(playerTwo.id, "Connected")
		return HttpStatus.OK
	}
}

