/*
 *  Copyright 2013-2050 Emmanuel BRUN (contact@amapj.fr)
 * 
 *  This file is part of AmapJ.
 *  
 *  AmapJ is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.

 *  AmapJ is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with AmapJ.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * 
 */
 package fr.amapj.service.services.authentification;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;

import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import fr.amapj.common.AmapjRuntimeException;
import fr.amapj.common.DateUtils;
import fr.amapj.common.FormatUtils;
import fr.amapj.common.RandomUtils;
import fr.amapj.common.SimpleMail;
import fr.amapj.common.StringUtils;
import fr.amapj.model.engine.rdblink.RdbLink;
import fr.amapj.model.engine.transaction.DbRead;
import fr.amapj.model.engine.transaction.DbWrite;
import fr.amapj.model.models.acces.RoleList;
import fr.amapj.model.models.fichierbase.EtatUtilisateur;
import fr.amapj.model.models.fichierbase.Utilisateur;
import fr.amapj.service.services.access.AccessManagementService;
import fr.amapj.service.services.mailer.MailerMessage;
import fr.amapj.service.services.mailer.MailerService;
import fr.amapj.service.services.parametres.ParametresDTO;
import fr.amapj.service.services.parametres.ParametresService;
import fr.amapj.service.services.utilisateur.envoimail.EnvoiMailDTO;
import fr.amapj.service.services.utilisateur.envoimail.EnvoiMailUtilisateurDTO;
import fr.amapj.service.services.utilisateur.envoimail.StatusEnvoiMailDTO;
import fr.amapj.service.services.utilisateur.util.UtilisateurUtil;


/**
 * Permet de positionner le mot de passe d'un utilisateur
 * soit à la création du compte, soit suite à la perte de celui ci 
 * 
 * Cette classe génére aussi tous les mails qui seront envoyés aux utilisateurs 
 * pour les opérations sur les mots de passe 
 *
 */
public class ProcessSetPasswordService
{
	private static final Logger logger = LogManager.getLogger();
	
	/**
	 * Permet de positionner le password d'un utilisateur  
	 */
	@DbWrite
	public boolean setUserPassword(Long userId,String clearPassword,boolean checkPasswordCompliance)
	{
		RdbLink em = RdbLink.get();
		
		// Recherche de l'utilisateur 
		Utilisateur r = em.find(Utilisateur.class, userId);
		if (r==null)
		{
			logger.warn("Impossible de retrouver l'utilisateur avec l'id "+userId);
			return false;
		}
		
		// Verification de la robsutesse du password 
		if (checkPasswordCompliance)
		{
			boolean isCompliant = new PasswordComplianceService().isCompliantPassword(clearPassword);
			if (isCompliant==false)
			{
				return false;
			}
		}
		
		// Calcul du sel si nécessaire 
		if (r.salt==null)
		{
			r.salt = fromByteArray(new PasswordEncryptionService().generateSalt());
		}
		
		byte[] salt = toByteArray(r.salt);
		byte[] encryptedPass = new PasswordEncryptionService().getEncryptedPassword(clearPassword, salt);
		r.password = fromByteArray(encryptedPass);
		
		// A chaque changement du mot de passe on supprime la ré initialisation par mail
		r.processSetPasswordSecret = null;
		r.processSetPasswordDate = null;
		r.processSetPasswordValidityDelay = 0;
		
		// On supprime aussi la date de demande de changement 
		r.dateChangePasswordAsked = null;
		
		return true;
	
	}
	
	
	// Generation des mails
	
	
	
	/**
	 * Retourne le mail initial pour le mail de bienvenue
	 * Ce texte poura ensuite être modifié par le trésorier avant envoi
	 */
	public SimpleMail getInitialMailBienvenue()
	{
		String lineSep="<br/>";
		ParametresDTO param = new ParametresService().getParametres();
		
		SimpleMail res = new SimpleMail();
		res.title = param.nomAmap+" - Bienvenue dans AmapJ";
		
		
		StringBuffer buf = new StringBuffer();
		buf.append("<h2>"+param.nomAmap+"</h2>");
		buf.append(lineSep);
		buf.append("Bonjour et bienvenue sur le logiciel AmapJ");
		buf.append(lineSep);
		buf.append(lineSep);
		buf.append("Votre AMAP utilise le logiciel AmapJ pour la gestion des contrats entre les producteurs et les amapiens.");
		buf.append(lineSep);
		buf.append(lineSep);
		buf.append("Avec votre compte AmapJ, vous pourrez ainsi saisir vos contrats, visualiser vos livraisons, ...");
		buf.append(lineSep);
		buf.append(lineSep);
		buf.append("Votre email : #EMAIL#");
		buf.append(lineSep);
		buf.append(lineSep);
		buf.append("Merci de cliquer sur le lien ci dessous pour finaliser la création de votre compte AmapJ :");
		buf.append(lineSep);
		buf.append(lineSep);
		buf.append("#LINK#");
		buf.append(lineSep);
		buf.append(lineSep);
		buf.append("Merci de noter que ce lien est valable 30 jours uniquement. Passé ce délai, vous devrez demander au collectif de votre AMAP de vous envoyer un nouveau lien d'activation.");
		buf.append(lineSep);
		buf.append(lineSep);
		buf.append("Une fois votre compte créé, vous pourrez accéder à votre espace AmapJ en utilisant le lien ci dessous :");
		buf.append(lineSep);
		buf.append("<a href=\""+param.url+"\">Accéder à AmapJ</a>");
		buf.append(lineSep);
		buf.append(lineSep);
		
		buf.append("Bonne journée !!");
		buf.append(lineSep);
		
		res.content = buf.toString();
		
		return res;
	}
	
		
	
	@DbWrite
	public void sendMailBienvenue(Long idUtilisateur, String mailTitle,String mailContent)
	{
		RdbLink em = RdbLink.get();
		
		Utilisateur u = em.find(Utilisateur.class, idUtilisateur);
		
		// Génère une clé pour le reset du password , de 50 caractères en minuscules
		u.processSetPasswordSecret = RandomUtils.generatePasswordMin(50);
		u.processSetPasswordDate = DateUtils.getDate();
		u.processSetPasswordValidityDelay = 60*24*30; // La demande est valide pendant 30 jours 

		//
		ParametresDTO param = new ParametresService().getParametres();
		String link = "<a href=\""+param.getUrl()+"?resetPassword="+u.processSetPasswordSecret+"\">Cliquez ici pour finaliser votre compte</a>";	
		
		// Remplacement des zones de textes
		mailContent = mailContent.replaceAll("#LINK#", link);
		mailContent = mailContent.replaceAll("#EMAIL#", u.email);
		
		// Construction du message
		MailerMessage message  = new MailerMessage();
		message.setEmail(u.email);
		message.setTitle(mailTitle);
		message.setContent(mailContent);
		
		// Envoi du message
		new MailerService().sendHtmlMail(message);
	}
	
	
	// ENVOI DES E MAILS DE BIENVENUE EN MASSE 

	/**
	 * Utilisé par le popup pour la sélection des emails à envoyer 
	 */
	@DbRead
	public EnvoiMailDTO getEnvoiMailDTO()
	{
		RdbLink em = RdbLink.get();
		
		EnvoiMailDTO dto = new EnvoiMailDTO();
		
		dto.utilisateurs = new ArrayList<>();
		
		em.createQuery("select u from Utilisateur u where u.etatUtilisateur=:etat and u.lastConnexion is null");
		em.setParameter("etat", EtatUtilisateur.ACTIF);
		List<Utilisateur> us = em.result().list(Utilisateur.class); 
		
		// 
		SimpleDateFormat df = FormatUtils.getTimeStd();
		Date dateRef = DateUtils.addDays(DateUtils.getDate(), -15);
		
		for (Utilisateur u : us)
		{
			if (UtilisateurUtil.canSendMailTo(u))
			{
				EnvoiMailUtilisateurDTO emu = new EnvoiMailUtilisateurDTO();
				emu.idUtilisateur = u.getId();
				emu.nom = u.nom;
				emu.prenom = u.prenom;
				emu.sendMail = true;
				emu.email = u.email;
				
				// Si un mail a été envoyé il y a moins de 15 jours, on n'en envoie pas un nouveau
				emu.sendMail = (u.processSetPasswordDate==null || u.processSetPasswordDate.before(dateRef));
				emu.dateEnvoiMail = computeDetails(df,emu,u);
				
				
				dto.utilisateurs.add(emu);
			}
		}
		
		dto.utilisateurs.sort(Comparator.comparing((EnvoiMailUtilisateurDTO e)->e.sendMail).reversed().thenComparing(e->e.nom).thenComparing(e->e.prenom));
		
		return dto;
	}

	private String computeDetails(SimpleDateFormat df, EnvoiMailUtilisateurDTO emu, Utilisateur u) {
		
		if (u.processSetPasswordDate==null)
		{
			return "Aucun mail de bienvenue envoyé";
		}
		
		String str =  "Un mail de bienvenue a déjà été envoyé le "+df.format(u.processSetPasswordDate)+". ";
		if (emu.sendMail)
		{
			return str+"Il est préférable d'envoyer un nouveau mail de bienvenue";
		}
		else
		{
			return str+"Il ne semble pas nécessaire d'envoyer un nouveau mail de bienvenue";
		}
		
	}


	/**
	 * Envoi un e mail a chaque utilisateur, dans une transaction separée 
	 * pour chaque utilisateur 
	 * 
	 * ATTENTION : NE PAS AJOUTER ICI un tag DbWrite ou DbRead
	 *  
	 */
	public StatusEnvoiMailDTO envoiEmailBienvenue(EnvoiMailDTO envoiMail)
	{
		StatusEnvoiMailDTO ret = new StatusEnvoiMailDTO();
		
		for (EnvoiMailUtilisateurDTO dto : envoiMail.utilisateurs)
		{
			if (dto.sendMail)
			{
				try
				{
					// envoi du mail et positionne le  mot de passe pour cet utilisateur dans une transaction en ecriture independante 
					sendMailBienvenue(dto.idUtilisateur, envoiMail.mailTitle, envoiMail.mailContent);
					
					ret.nbMailOK++;
				}
				catch(Exception e)
				{
					String msg = "Impossible d'envoyer un e mail à "+dto.email+" car :"+e.getMessage();
					ret.erreurs.add(msg);
				}
			}
		}
		
		return ret;
	}
	
	
	// Reset du password par l'utilisateur lui même  
	
	

	@DbWrite
	public String sendMailForResetPassword(String email)
	{
		RdbLink em = RdbLink.get();
		
		Utilisateur u = new AuthentificationService().findUser(email, em);
		
		if (u==null)
		{
			return "Votre adresse e mail est inconnue";
		}
		
		if (u.etatUtilisateur==EtatUtilisateur.INACTIF)
		{
			return "Votre compte a été désactivé car vous n'êtes plus membre de l'AMAP.";
		}
		
		// SI l'utilisateur ne s'est jamais connecté, il ne peut pas faire une demande de reset de password 
		if (u.password==null)
		{
			return "Vous devez contacter le gestionnaire de votre AMAP, lui seul peut ré initialiser votre mot de passe.";
		}
		
		// Impossible pour le master de faire une recuperation de mot de passe 
		List<RoleList> rs = new AccessManagementService().getUserRole(u, em);
		if (rs.contains(RoleList.MASTER))
		{
			return "Votre adresse e mail est inconnue";
		}
		
		
		// Génère une clé pour le reset du password , de 50 caractères en minuscules
		u.processSetPasswordSecret = RandomUtils.generatePasswordMin(50);
		u.processSetPasswordDate = DateUtils.getDate();
		u.processSetPasswordValidityDelay = 15; // La demande est valide pendant 15 minutes 

		ParametresDTO parametresDTO = new ParametresService().getParametres(); 
		
		String link = parametresDTO.getUrl()+"?resetPassword="+u.processSetPasswordSecret;
		
		StringBuffer buf = new StringBuffer();
		buf.append("<h2>"+parametresDTO.nomAmap+"</h2>");
		buf.append("<br/>");
		buf.append("Vous avez demandé la ré initialisation de votre mot de passe AmapJ");
		buf.append("<br/>");
		buf.append("Merci de cliquer sur le lien ci dessous pour saisir votre nouveau mot de passe");
		buf.append("<br/>");
		buf.append("<br/>");
		buf.append("<a href=\""+link+"\">Cliquez ici pour changer votre mot de passe</a>");
		buf.append("<br/>");
		buf.append("<br/>");
		buf.append("Ce lien sera valable durant 15 minutes.");
		buf.append("<br/>");
		buf.append("Si vous n'avez pas demandé à changer de mot de passe, merci de ne pas tenir compte de ce mail");
		buf.append("<br/>");
		
		new MailerService().sendHtmlMail(new MailerMessage(email, "Changement de votre mot de passe AmapJ", buf.toString()));
		
		return null;
		
	}
	
	
	
	
	// Reset du password par le tresorier 
	
	

	@DbWrite
	public void sendMailByTresorierForResetPassword(Long idUtilisateur)
	{
		RdbLink em = RdbLink.get();
		
		Utilisateur u = em.find(Utilisateur.class, idUtilisateur); 
		
		// Impossible pour le master de faire une recuperation de mot de passe 
		List<RoleList> rs = new AccessManagementService().getUserRole(u, em);
		if (rs.contains(RoleList.MASTER))
		{
			throw new AmapjRuntimeException();
		}
		
		
		// Génère une clé pour le reset du password , de 50 caractères en minuscules
		u.processSetPasswordSecret = RandomUtils.generatePasswordMin(50);
		u.processSetPasswordDate = DateUtils.getDate();
		u.processSetPasswordValidityDelay = 60*24*7; // La demande est valide durant 7 jours 

		ParametresDTO parametresDTO = new ParametresService().getParametres(); 
		
		String link = parametresDTO.getUrl()+"?resetPassword="+u.processSetPasswordSecret;
		
		String lineSep="<br/>";
		StringBuffer buf = new StringBuffer();
		buf.append("<h2>"+parametresDTO.nomAmap+"</h2>");
		buf.append(lineSep);
		buf.append(lineSep);
		buf.append("Le trésorier de votre AMAP vous envoie les informations nécessaires pour vous connecter à votre compte  AmapJ");
		buf.append(lineSep);
		buf.append(lineSep);
		buf.append("Votre email : "+StringUtils.s(u.email));
		buf.append(lineSep);
		buf.append(lineSep);
		buf.append("Cliquez sur le lien ci dessous pour accèder à AmapJ");
		buf.append(lineSep);
		buf.append(lineSep);
		buf.append("<a href=\""+parametresDTO.url+"\">Accéder à AmapJ</a>");
		buf.append(lineSep);
		buf.append(lineSep);	
		buf.append("Si vous avez perdu votre mot de passe, merci de cliquer sur le lien ci dessous pour saisir un nouveau mot de passe");
		buf.append("<br/>");
		buf.append("<br/>");
		buf.append("<a href=\""+link+"\">Cliquez ici pour changer votre mot de passe</a>");
		buf.append("<br/>");
		buf.append("<br/>");
		buf.append("Ce lien sera valable durant 7 jours");
		buf.append("<br/>");
		buf.append("Si vous n'avez pas demandé à changer de mot de passe, merci de ne pas tenir compte de ce mail");
		buf.append("<br/>");
		
		new MailerService().sendHtmlMail(new MailerMessage(u.email, "Accéder à votre compte AmapJ", buf.toString()));
			
	}
	
	
	
	
	
	
	// Gestion du clic sur les liens envoyés par mail 

	
	static public class  ProcessSetPasswordInfo
	{
		public String errorMessage;
		public Long idUtilisateur;
		public boolean isCreationPassword;
		public boolean isLinkExpired = false;
		public String nomPrenom;
		public String email;
		
		static public ProcessSetPasswordInfo error(String error)
		{
			ProcessSetPasswordInfo p = new ProcessSetPasswordInfo();
			p.errorMessage = error;
			return p;
		}
		
		static public ProcessSetPasswordInfo linkExpired(String email, boolean isCreationPassword)
		{
			ProcessSetPasswordInfo p = new ProcessSetPasswordInfo();
			p.isLinkExpired = true;
			p.isCreationPassword = isCreationPassword;
			p.email = email;
			return p;
		}
	}
	
	
	
	/**
	 * Retrouve l'utilisateur et les informations associées avec ce processSetPasswordSecret
	 */
	@DbRead
	public ProcessSetPasswordInfo findUserFromProcessSetPasswordSecret(String processSetPasswordSecret)
	{
		RdbLink em = RdbLink.get();
		
		if (isResetPasswordSaltCompliant(processSetPasswordSecret)==false)
		{
			return ProcessSetPasswordInfo.error("Lien incorrect.");
		}
		
		em.createQuery("select u from Utilisateur u where u.processSetPasswordSecret=:s");
		em.setParameter("s", processSetPasswordSecret);
		Utilisateur u =  em.result().oneOrZero(Utilisateur.class);
		
		
		if (u==null)
		{
			return ProcessSetPasswordInfo.error("Lien incorrect.");
		}
		
		if (u.etatUtilisateur==EtatUtilisateur.INACTIF)
		{
			return ProcessSetPasswordInfo.error("Votre compte a été désactivé par le trésorier. Merci de le contacter pour qu'il remette votre compte à l'état ACTIF.");
		}
		
		Date datLimit = DateUtils.addMinute(DateUtils.getDate(), -u.processSetPasswordValidityDelay);		
		if (u.processSetPasswordDate.before(datLimit))
		{
			return ProcessSetPasswordInfo.linkExpired(u.email,u.password==null);
		}
		
		ProcessSetPasswordInfo p = new ProcessSetPasswordInfo();
		p.idUtilisateur = u.id;
		p.isCreationPassword = u.password==null;
		p.nomPrenom = u.nom+" "+u.prenom;
		p.email = u.email;
		return p;
	}
	
	
	private boolean isResetPasswordSaltCompliant(String resetPasswordSalt) 
	{
		if ((resetPasswordSalt==null) || resetPasswordSalt.equals("") || resetPasswordSalt.length()!=50)
		{
			return false;
		}
		for (int i = 0; i < 50; i++) 
		{
			if (resetPasswordSalt.charAt(i)>'z' || resetPasswordSalt.charAt(i)<'a') 
			{
				return false;
			}
		}
		return true;
	}
	
	// OUTILS  
	
	/**
	 * Permet de transformer une chaine base 64 en tableau de byte
	 */
	private byte[] toByteArray(String base64str)
	{
		return Base64.decodeBase64(base64str.getBytes());
	}
	
	/**
	 * Permet de transformer un tableau de byte  en une chaine en base 64  
	 */
	private String fromByteArray(byte[] flux)
	{
		return new String(Base64.encodeBase64(flux));
	}
}
