package de.jottyfan.camporganizer.module.registration;

import static de.jottyfan.camporganizer.db.jooq.Tables.T_CAMP;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_PERSON;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_PROFILE;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_PROFILEROLE;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_RSS;
import static de.jottyfan.camporganizer.db.jooq.Tables.V_CAMP;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jasypt.util.password.StrongPasswordEncryptor;
import org.jooq.DSLContext;
import org.jooq.DeleteConditionStep;
import org.jooq.InsertResultStep;
import org.jooq.InsertReturningStep;
import org.jooq.InsertValuesStep2;
import org.jooq.Record;
import org.jooq.Record1;
import org.jooq.Record2;
import org.jooq.Record5;
import org.jooq.Record7;
import org.jooq.Record9;
import org.jooq.SelectConditionStep;
import org.jooq.UpdateConditionStep;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL;
import org.jooq.types.DayToSecond;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import de.jottyfan.camporganizer.db.jooq.enums.EnumCamprole;
import de.jottyfan.camporganizer.db.jooq.enums.EnumProgress;
import de.jottyfan.camporganizer.db.jooq.enums.EnumSex;
import de.jottyfan.camporganizer.db.jooq.tables.records.TPersonRecord;
import de.jottyfan.camporganizer.db.jooq.tables.records.TProfileRecord;
import de.jottyfan.camporganizer.db.jooq.tables.records.TRssRecord;
import de.jottyfan.camporganizer.db.jooq.tables.records.VCampRecord;
import de.jottyfan.camporganizer.module.camplist.model.BookingBean;
import de.jottyfan.camporganizer.module.camplist.model.LambdaResultWrapper;
import de.jottyfan.camporganizer.module.registration.model.CampBean;
import de.jottyfan.camporganizer.module.registration.model.ProfileBean;
import de.jottyfan.camporganizer.module.registration.model.RegistrationBean;

/**
 *
 * @author jotty
 *
 */
@Repository
@Transactional(transactionManager = "transactionManager")
public class RegistrationRepository {
	private static final Logger LOGGER = LogManager.getLogger(RegistrationRepository.class);

	@Autowired
	private DSLContext jooq;

	/**
	 * get the camp
	 *
	 * @param pk the ID of the camp
	 * @return the camp bean or null
	 */
	public CampBean getCamp(Integer pk) {
		SelectConditionStep<VCampRecord> sql = jooq.selectFrom(V_CAMP).where(V_CAMP.PK.eq(pk));
		LOGGER.trace(sql.toString());
		VCampRecord r = sql.fetchOne();
		if (r != null) {
			CampBean bean = new CampBean();
			bean.setPk(r.getPk());
			bean.setName(r.getName());
			LocalDateTime arrive = r.getArrive();
			bean.setYear(arrive == null ? null : arrive.getYear());
			bean.setBookingHasStarted(r.getBookingHasStarted());
			bean.setStartBooking(r.getStartBooking());
			return bean;
		} else {
			return null;
		}
	}

	/**
	 * test if the login is available (not yet in use)
	 *
	 * @param login the login
	 * @return true or false
	 */
	public Boolean isLoginNotYetInUse(String login) {
		SelectConditionStep<TProfileRecord> sql = jooq
		// @formatter:off
			.selectFrom(T_PROFILE)
			.where(DSL.lower(T_PROFILE.USERNAME).eq(login));
		// @formatter:on
		LOGGER.trace(sql);
		return sql.fetch().size() < 1;
	}

	/**
	 * save the content in t_person; also, create a profile for the user if
	 * registerInKeycloak is true
	 *
	 * @param bean the bean
	 * @return number of data base rows; should be > 0
	 */
	public Integer register(RegistrationBean bean) {
		LambdaResultWrapper lrw = new LambdaResultWrapper();
		jooq.transaction(t -> {
			if (bean.getLogin() != null && !bean.getLogin().isEmpty()) {
				Boolean loginNotYetInUse = isLoginNotYetInUse(bean.getLogin().toLowerCase());
				if (bean.getRegisterInKeycloak() && !loginNotYetInUse) {
					LOGGER.error("login already in use: {}", bean.getLogin().toLowerCase());
					throw new DataAccessException("login already in use: " + bean.getLogin().toLowerCase());
				}
				Integer fkProfile = null;
				if (loginNotYetInUse) {
					String oldPassword = new StrongPasswordEncryptor().encryptPassword(bean.getPassword());
					InsertResultStep<TProfileRecord> sql1 = DSL.using(t)
					// @formatter:off
						.insertInto(T_PROFILE,
								        T_PROFILE.FORENAME,
								        T_PROFILE.SURNAME,
								        T_PROFILE.USERNAME,
								        T_PROFILE.PASSWORD,
								        T_PROFILE.DUEDATE,
								        T_PROFILE.UUID)
						.values(bean.getForename(), bean.getSurname(), bean.getLogin().toLowerCase(), oldPassword, LocalDateTime.now().plus(356, ChronoUnit.DAYS), UUID.nameUUIDFromBytes(bean.getLogin().getBytes()).toString())
						.returning(T_PROFILE.PK);
					// @formatter:on
					LOGGER.trace(sql1.toString());
					fkProfile = sql1.fetchOne().getPk();

					InsertValuesStep2<TRssRecord, String, String> sql2 = jooq
					// @formatter:off
					  .insertInto(T_RSS,
					  		        T_RSS.MSG,
					  		        T_RSS.RECIPIENT)
					  .values(new StringBuilder(bean.getFullname()).append(" hat sich als Nutzer im CampOrganizer2 registriert.").toString(), "admin");
					// @formatter:on
					LOGGER.trace(sql2);
					sql2.execute();
				} else {
					SelectConditionStep<Record1<Integer>> sql1 = DSL.using(t)
					// @formatter:off
						.select(T_PROFILE.PK)
						.from(T_PROFILE)
						.where(DSL.lower(T_PROFILE.USERNAME).eq(bean.getLogin().toLowerCase()));
					// @formatter:on
					LOGGER.trace(sql1.toString());
					fkProfile = sql1.fetchOne().get(T_PROFILE.PK);
				}
				// register the person for camp participation
				InsertReturningStep<TPersonRecord> sql2 = DSL.using(t)
				// @formatter:off
					.insertInto(T_PERSON,
							       T_PERSON.FORENAME,
							       T_PERSON.SURNAME,
							       T_PERSON.SEX,
							       T_PERSON.BIRTHDATE,
							       T_PERSON.STREET,
							       T_PERSON.ZIP,
							       T_PERSON.CITY,
							       T_PERSON.EMAIL,
							       T_PERSON.PHONE,
							       T_PERSON.CAMPROLE,
							       T_PERSON.FK_CAMP,
							       T_PERSON.COMMENT,
							       T_PERSON.FK_PROFILE)
					.values(bean.getForename(), bean.getSurname(), bean.getSex(),
							bean.getBirthDate(), bean.getStreet(), bean.getZip(), bean.getCity(), bean.getEmail(),
							bean.getPhone(), bean.getCampRole(), bean.getFkCamp(), bean.getComment(), fkProfile)
					.onConflict(T_PERSON.FORENAME, T_PERSON.SURNAME, T_PERSON.BIRTHDATE, T_PERSON.FK_CAMP)
					.doNothing();
				// @formatter:on
				LOGGER.trace(sql2.toString());
				lrw.add(sql2.execute());
				// register the login for the portal
			} else {
				InsertReturningStep<TPersonRecord> sql = DSL.using(t)
				// @formatter:off
					.insertInto(T_PERSON,
							       T_PERSON.FORENAME,
							       T_PERSON.SURNAME,
							       T_PERSON.SEX,
							       T_PERSON.BIRTHDATE,
							       T_PERSON.STREET,
							       T_PERSON.ZIP,
							       T_PERSON.CITY,
							       T_PERSON.EMAIL,
							       T_PERSON.PHONE,
							       T_PERSON.CAMPROLE,
							       T_PERSON.FK_CAMP,
							       T_PERSON.COMMENT)
					.values(bean.getForename(), bean.getSurname(), bean.getSex(),
							bean.getBirthDate(), bean.getStreet(), bean.getZip(), bean.getCity(), bean.getEmail(),
							bean.getPhone(), bean.getCampRole(), bean.getFkCamp(), bean.getComment())
					.onConflict(T_PERSON.FORENAME, T_PERSON.SURNAME, T_PERSON.BIRTHDATE, T_PERSON.FK_CAMP)
					.doNothing();
				// @formatter:on
				LOGGER.trace(sql.toString());
				lrw.add(sql.execute());
			}
		});
		return lrw.getCounter();
	}

	/**
	 * get the booking from the database
	 *
	 * @param id the id of the booking
	 * @return the booking bean or null
	 */
	public BookingBean getBooking(Integer id) {
		SelectConditionStep<Record7<Integer, String, String, EnumCamprole, String, LocalDateTime, LocalDateTime>> sql = jooq
		// @formatter:off
			.select(T_PERSON.PK,
					    T_PERSON.FORENAME,
					    T_PERSON.SURNAME,
					    T_PERSON.CAMPROLE,
					    T_CAMP.NAME,
					    T_CAMP.ARRIVE,
					    T_CAMP.DEPART)
			.from(T_PERSON)
			.leftJoin(T_CAMP).on(T_CAMP.PK.eq(T_PERSON.FK_CAMP))
			.where(T_PERSON.PK.eq(id));
		// @formatter:on
		LOGGER.trace(sql.toString());
		Record7<Integer, String, String, EnumCamprole, String, LocalDateTime, LocalDateTime> r = sql.fetchOne();
		if (r != null) {
			BookingBean bean = new BookingBean();
			bean.setPk(r.get(T_PERSON.PK));
			bean.setForename(r.get(T_PERSON.FORENAME));
			bean.setSurname(r.get(T_PERSON.SURNAME));
			bean.setCampName(r.get(T_CAMP.NAME));
			bean.setArrive(r.get(T_CAMP.ARRIVE));
			bean.setDepart(r.get(T_CAMP.DEPART));
			bean.setCamprole(r.get(T_PERSON.CAMPROLE));
			return bean;
		}
		return null;
	}

	/**
	 * set the revoke state of a booking
	 *
	 * @param id the ID of the booking
	 */
	public void revokeBooking(Integer id) {
		jooq.transaction(t -> {
			SelectConditionStep<Record5<String, String, String, String, LocalDateTime>> sql0 = DSL.using(t)
			// @formatter:off
				.select(T_PROFILE.USERNAME, T_PERSON.FORENAME, T_PERSON.SURNAME, T_CAMP.NAME, T_CAMP.ARRIVE)
				.from(T_PERSON)
				.leftJoin(T_CAMP).on(T_CAMP.PK.eq(T_PERSON.FK_CAMP))
				.leftJoin(T_PROFILE).on(T_PROFILE.PK.eq(T_PERSON.FK_PROFILE))
				.where(T_PERSON.PK.eq(id));
			// @formatter:on
			LOGGER.trace(sql0.toString());
			Record5<String, String, String, String, LocalDateTime> r = sql0.fetchOne();
			if (r == null) {
				throw new DataAccessException("no such entry in t_person with id = " + id);
			}
			String username = r.get(T_PROFILE.USERNAME);
			String forename = r.get(T_PERSON.FORENAME);
			String surname = r.get(T_PERSON.SURNAME);
			String campname = r.get(T_CAMP.NAME);
			LocalDateTime arrive = r.get(T_CAMP.ARRIVE);

			StringBuilder rssMessage = new StringBuilder(username);
			rssMessage.append(" hat die Stornierung der Buchung von ");
			rssMessage.append(forename).append(" ").append(surname);
			rssMessage.append(" an ");
			rssMessage.append(campname).append(" ")
					.append(arrive == null ? "" : arrive.format(DateTimeFormatter.ofPattern("YYYY")));
			rssMessage.append(" ausgelöst.");

			UpdateConditionStep<TPersonRecord> sql1 = jooq
			// @formatter:off
				.update(T_PERSON)
				.set(T_PERSON.PROGRESS, EnumProgress.revoked)
				.where(T_PERSON.PK.eq(id));
			// @formatter:on
			LOGGER.trace(sql1);
			sql1.execute();

			InsertValuesStep2<TRssRecord, String, String> sql2 = DSL.using(t)
			// @formatter:off
				.insertInto(T_RSS,
                    T_RSS.MSG,
                    T_RSS.RECIPIENT)
				.values(rssMessage.toString(), "registrator");
			// @formatter:on
			LOGGER.trace("{}", sql2.toString());
			sql2.execute();
		});
	}

	/**
	 * returns true if the end date of the camp is not yet over
	 *
	 * @param fkCamp the camp ID
	 * @return true or false
	 */
	public Boolean campIsNotYetOver(Integer fkCamp) {
		SelectConditionStep<Record1<LocalDateTime>> sql = jooq
		// @formatter:off
			.select(T_CAMP.DEPART)
			.from(T_CAMP)
			.where(T_CAMP.PK.eq(fkCamp));
		// @formatter:on
		LOGGER.trace(sql.toString());
		Iterator<Record1<LocalDateTime>> i = sql.fetch().iterator();
		while (i.hasNext()) {
			LocalDateTime depart = i.next().get(T_CAMP.DEPART);
			LocalDateTime now = LocalDateTime.now();
			return now.isBefore(depart);
		}
		return false;
	}

	/**
	 * remove login
	 *
	 * @param bean containing username of dataset to be removed
	 * @throws DataAccessExceptionF
	 */
	public void removeLogin(ProfileBean bean) throws DataAccessException {
		jooq.transaction(t -> {
			UpdateConditionStep<TPersonRecord> sql = DSL.using(t)
			// @formatter:off
			  .update(T_PERSON)
			  .set(T_PERSON.FK_PROFILE, (Integer) null)
			  .where(T_PERSON.FK_PROFILE.eq(bean.getPk()));
			// @formatter:off
			LOGGER.trace("{}", sql.toString());
			sql.execute();

			DeleteConditionStep<?> sql1 = DSL.using(t)
			// @formatter:off
		    .deleteFrom(T_PROFILEROLE)
		    .where(T_PROFILEROLE.FK_PROFILE.in(
		    		DSL.using(t)
		    		.select(T_PROFILE.PK)
		    		.from(T_PROFILE)
		      .where(T_PROFILE.USERNAME.eq(bean.getUsername())
		    )));
			// @formatter:on
			LOGGER.trace("{}", sql1.toString());
			sql1.execute();

			DeleteConditionStep<?> sql2 = DSL.using(t)
			// @formatter:off
		    .deleteFrom(T_PROFILE)
		    .where(T_PROFILE.USERNAME.eq(bean.getUsername()));
			// @formatter:on
			LOGGER.trace("{}", sql2.toString());
			sql2.execute();

			InsertValuesStep2<TRssRecord, String, String> sql3 = DSL.using(t)
			// @formatter:off
				.insertInto(T_RSS,
						        T_RSS.MSG,
						        T_RSS.RECIPIENT)
				.values(new StringBuilder(bean.getFullname()).append(" hat sich vom Portal CampOrganizer2 abgemeldet.").toString(), "admin");
			// @formatter:on
			LOGGER.trace("{}", sql3.toString());
			sql3.execute();
		});
	}

	/**
	 * toggle the consent for photo
	 *
	 * @param id the ID of the person
	 */
	public void toggleConsent(Integer id) {
		Boolean consent = jooq.selectFrom(T_PERSON).where(T_PERSON.PK.eq(id)).fetchOne().getConsentCatalogPhoto();
		consent = consent == null ? true : !consent;
		UpdateConditionStep<TPersonRecord> sql = jooq
		// @formatter:off
			.update(T_PERSON)
			.set(T_PERSON.CONSENT_CATALOG_PHOTO, consent)
			.where(T_PERSON.PK.eq(id));
		// @formatter:on
		LOGGER.trace("{}", sql.toString());
		sql.execute();
	}

	/**
	 * check if the age of the teacher in campRole fits to the valid ones
	 *
	 * @param birthDate the birth date of the person
	 * @param fkCamp    the ID of the camp
	 * @param campRole  the role; if it is a teacher, check for the age
	 * @return true if it fits, false otherwise
	 */
	public Boolean checkAgeOfTeacher(LocalDate birthDate, Integer fkCamp, EnumCamprole campRole) {
		LocalDateTime birthDateDay = birthDate.atStartOfDay();
		if (EnumCamprole.teacher.equals(campRole)) {
			SelectConditionStep<Record2<Integer, DayToSecond>> sql = jooq
			// @formatter:off
				.select(T_CAMP.MAX_AGE,
								DSL.localDateTimeDiff(T_CAMP.DEPART, birthDateDay).as("teacherAge"))
				.from(T_CAMP)
				.where(T_CAMP.PK.eq(fkCamp));
			// @formatter:on
			LOGGER.trace(sql.toString());
			Record r = sql.fetchOne();
			Integer minTeacherAge = r.get(T_CAMP.MAX_AGE) + 2; // by default, we need 2 years older teachers at least
			DayToSecond currentTeacherAge = r.get("teacherAge", DayToSecond.class);
			double totalYears = currentTeacherAge.getTotalDays() / 365.25; // in years
			int years = (int) totalYears;
			return years >= minTeacherAge;
		} else {
			return true;
		}
	}

	/**
	 * get the person information of already registered people
	 *
	 * @param currentUser the user that registered people
	 * @return the list of found registrations, may be an empty list; if currentUser is null, return null.
	 */
	public List<RegistrationBean> getRegistrations(String currentUser) {
		SelectConditionStep<Record9<String, String, String, String, String, String, String, EnumSex, LocalDate>> sql = jooq
		// @formatter:off
			.selectDistinct(T_PERSON.FORENAME,
									    T_PERSON.SURNAME,
									    T_PERSON.STREET,
									    T_PERSON.ZIP,
									    T_PERSON.CITY,
									    T_PERSON.PHONE,
									    T_PERSON.EMAIL,
									    T_PERSON.SEX,
									    T_PERSON.BIRTHDATE)
			.from(T_PERSON)
			.innerJoin(T_PROFILE).on(T_PROFILE.PK.eq(T_PERSON.FK_PROFILE))
			.where(DSL.lower(T_PROFILE.USERNAME).eq(currentUser == null ? null : currentUser.toLowerCase()));
		// @formatter:on
		LOGGER.trace(sql);
		List<RegistrationBean> list = new ArrayList<>();
		Iterator<Record9<String, String, String, String, String, String, String, EnumSex, LocalDate>> i = sql.fetch().iterator();
		while (i.hasNext()) {
			Record9<String, String, String, String, String, String, String, EnumSex, LocalDate> r = i.next();
			RegistrationBean bean = new RegistrationBean();
			bean.setForename(r.get(T_PERSON.FORENAME));
			bean.setSurname(r.get(T_PERSON.SURNAME));
			bean.setStreet(r.get(T_PERSON.STREET));
			bean.setZip(r.get(T_PERSON.ZIP));
			bean.setCity(r.get(T_PERSON.CITY));
			bean.setPhone(r.get(T_PERSON.PHONE));
			bean.setEmail(r.get(T_PERSON.EMAIL));
			bean.setSex(r.get(T_PERSON.SEX));
			bean.setBirthDate(r.get(T_PERSON.BIRTHDATE));
			list.add(bean);
		}
		return list;
	}

	/**
	 * returns true if and only if this combination of registration has already been done
	 *
	 * @param forename the forename
	 * @param surname the surname
	 * @param birthDate the birth date
	 * @param campId the ID of the camp
	 * @return true or false
	 */
	public Boolean checkAlreadyRegistered(String forename, String surname, LocalDate birthDate, Integer campId) {
		SelectConditionStep<Record1<Integer>> sql = jooq
		// @formatter:off
			.select(T_PERSON.PK)
			.from(T_PERSON)
			.where(T_PERSON.FORENAME.eq(forename))
			.and(T_PERSON.SURNAME.eq(surname))
			.and(T_PERSON.BIRTHDATE.eq(birthDate))
			.and(T_PERSON.FK_CAMP.eq(campId));
		// @formatter:on
		LOGGER.trace(sql);
		Iterator<Integer> i = sql.fetch(T_PERSON.PK).iterator();
		return i.hasNext();
	}

	/**
	 * turn back the revocation and set state to requested
	 *
	 * @param id
	 */
	public void unrevokeBooking(Integer id) {
		jooq.transaction(t -> {
			SelectConditionStep<Record5<String, String, String, String, LocalDateTime>> sql0 = DSL.using(t)
			// @formatter:off
				.select(T_PROFILE.USERNAME, T_PERSON.FORENAME, T_PERSON.SURNAME, T_CAMP.NAME, T_CAMP.ARRIVE)
				.from(T_PERSON)
				.leftJoin(T_CAMP).on(T_CAMP.PK.eq(T_PERSON.FK_CAMP))
				.leftJoin(T_PROFILE).on(T_PROFILE.PK.eq(T_PERSON.FK_PROFILE))
				.where(T_PERSON.PK.eq(id));
			// @formatter:on
			LOGGER.trace(sql0.toString());
			Record5<String, String, String, String, LocalDateTime> r = sql0.fetchOne();
			if (r == null) {
				throw new DataAccessException("no such entry in t_person with id = " + id);
			}
			String username = r.get(T_PROFILE.USERNAME);
			String forename = r.get(T_PERSON.FORENAME);
			String surname = r.get(T_PERSON.SURNAME);
			String campname = r.get(T_CAMP.NAME);
			LocalDateTime arrive = r.get(T_CAMP.ARRIVE);

			StringBuilder rssMessage = new StringBuilder(username);
			rssMessage.append(" hat die Stornierung der Buchung von ");
			rssMessage.append(forename).append(" ").append(surname);
			rssMessage.append(" an ");
			rssMessage.append(campname).append(" ")
					.append(arrive == null ? "" : arrive.format(DateTimeFormatter.ofPattern("YYYY")));
			rssMessage.append(" rückgängig gemacht.");

			UpdateConditionStep<TPersonRecord> sql1 = jooq
			// @formatter:off
				.update(T_PERSON)
				.set(T_PERSON.PROGRESS, EnumProgress.requested)
				.where(T_PERSON.PK.eq(id));
			// @formatter:on
			LOGGER.trace(sql1);
			sql1.execute();

			InsertValuesStep2<TRssRecord, String, String> sql2 = DSL.using(t)
			// @formatter:off
				.insertInto(T_RSS,
                    T_RSS.MSG,
                    T_RSS.RECIPIENT)
				.values(rssMessage.toString(), "registrator");
			// @formatter:on
			LOGGER.trace("{}", sql2.toString());
			sql2.execute();
		});
	}
}
