package com.capgemini.reports.database.manager;

import com.capgemini.framework.common.DateHelper;
import com.capgemini.framework.common.SettingsHelper;
import com.capgemini.framework.common.StringHelper;
import com.capgemini.reports.ReportEnum;
import com.capgemini.reports.database.HibernateUtil;
import com.capgemini.reports.database.javabean.DbCriteriaVal;
import com.capgemini.reports.database.javabean.DbReport;
import com.capgemini.reports.database.javabean.DbReportOwner;
import com.capgemini.reports.database.javabean.DbReportType;
import com.capgemini.reports.database.javabean.DbUser;
import com.capgemini.reports.database.javabean.enumeration.ReportStateEnum;
import com.capgemini.reports.database.manager.MngCriteriaVal.CriteriaValue;
import com.capgemini.reports.exceptions.InvalidUserInputException;
import com.capgemini.reports.exceptions.NotFoundException;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;

import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Hibernate manager for DBReport JAVA bean.
 **/
@Getter
@Slf4j
public class MngReport extends AbstractHibernateManager<DbReport> {

  private static final int QUERY_DBREPORT_POS = 0;
  private static final int QUERY_LOGIN_POS = 1;
  private static final int QUERY_SURNAME_POS = 2;
  private static final int QUERY_FIRST_NAME_POS = 3;
  private static final int QUERY_PRIORITY_POS = 4;
  private static final int QUERY_REQUEST_DATE_POS = 5;

  // Constants
  private static final String DATEFORMAT_DD_MM_YYYY_HH_MM_SS = "dd/MM/yyyy HH:mm:ss";

  // Settings
  private static final int MAX_JSP_PAGE_SIZE = SettingsHelper.getJspPaging();

  // Queries
  private static final String QUERY_USER_REPORTS_COUNT;
  private static final String QUERY_RUNNING_REPORTS_DATA;
  private static final String QUERY_WAITING_REPORTS_DATA;
  private static final String QUERY_USER_REPORTS_DATA;
  private static final String QUERY_ALL_USERS_AND_REPORTS;
  private static final String QUERY_WAITING_REPORTS_DATA_WITH_TYPE;
  private static final String QUERY_ALL_REPORTS_SCHEDULING;

  // Query keys (internal use only)
  private static final String COLUMN_SCHEDULING_ID = "schedulingReportId";
  private static final String KEY_REPORT_UID = "reportUid";
  private static final String KEY_DB_USER_LOGIN = "DbUserLogin";
  private static final String KEY_DEADLINE = "deadline";
  private static final String KEY_REPORT_CODE = "reportCode";
  private static final String KEY_START_PERIOD = "startPeriod";
  private static final String KEY_END_PERIOD = "endPeriod";

  static {
    String selectData = "select DISTINCT T0, T2.login, T2.surname, T2.firstname, T3.priority, T1.requestDate ";
    String from0123 = "from " + DbReport.class.getName() + " T0, " + DbReportOwner.class.getName() + " T1, " + DbUser.class.getName() + " T2, " + DbReportType.class.getName() + " T3 ";
    String whereCoherence0123 = "where T0.id = T1.reportId AND T1.userId = T2.id AND T0.reportCode = T3.code ";
    String readyOrWait = "and (T0.state = '" + ReportStateEnum.READY + "' OR T0.state = '" + ReportStateEnum.WAIT + "') ";
    String readyOrExecuting = "and (T0.state = '" + ReportStateEnum.READY + "' OR T0.state = '" + ReportStateEnum.EXECUTING + "') ";
    String andUserLogin = "and T2.login = :" + KEY_DB_USER_LOGIN + " ";
    String andReportCode = "and T0.reportCode = :" + KEY_REPORT_CODE + " ";
    String orderBy = "order by T0.startGenerationDate desc, T1.requestDate desc";
    QUERY_RUNNING_REPORTS_DATA = StringHelper.concatToString(selectData, from0123, whereCoherence0123, readyOrExecuting, orderBy);
    QUERY_WAITING_REPORTS_DATA = StringHelper.concatToString(selectData, from0123, whereCoherence0123, readyOrWait, orderBy);
    QUERY_WAITING_REPORTS_DATA_WITH_TYPE =
      StringHelper.concatToString(selectData, from0123, whereCoherence0123, readyOrWait, andReportCode, orderBy);
    QUERY_USER_REPORTS_DATA = StringHelper.concatToString(selectData, from0123, whereCoherence0123, andUserLogin, orderBy);
  }

  static {
    String select = "select T2.surname, T2.firstname, T0.reportCode, count(DISTINCT T1.reportId) ";
    String from = "from DbReport T0, DbReportOwner T1, DbUser T2 ";
    String whereCoherence = "where T0.id = T1.reportId and T1.userId = T2.id ";
    String andWhereCriteria = "and T1.requestDate between TO_DATE(:" + KEY_START_PERIOD + ") and TO_DATE(:" + KEY_END_PERIOD + ") + 1 ";
    String andIsNotReportScheduling = "and T0.schedulingReportId IS NULL ";
    String groupBy = "group by T0.reportCode, T2.surname, T2.firstname ";
    String orderBy = "order by T2.surname, T2.firstname, T0.reportCode ";
    QUERY_ALL_USERS_AND_REPORTS = select + from + whereCoherence + andWhereCriteria + andIsNotReportScheduling + groupBy + orderBy;
  }

  static {
    String selectCount = "select count(*) ";
    String selectStats = "select 'Administrateur', 'planification', T0.reportCode, count(DISTINCT T1.reportId) ";
    String from012 = "from DbReport T0, DbReportOwner T1, DbUser T2 ";
    String whereCoherence012 = "where T0.id = T1.reportId and T1.userId = T2.id ";
    String andLogin = "and T2.login = :" + KEY_DB_USER_LOGIN + " ";
    String andPeriod = "and T1.requestDate between TO_DATE(:" + KEY_START_PERIOD + ") and TO_DATE(:" + KEY_END_PERIOD + ") + 1 ";
    String andIsReportScheduling = "and T0.schedulingReportId IS NOT NULL ";
    String groupBy = "group by T0.reportCode ";
    String orderBy = "order by T0.reportCode ";
    QUERY_ALL_REPORTS_SCHEDULING =
      StringHelper.concatToString(selectStats, from012, whereCoherence012, andPeriod, andIsReportScheduling, groupBy, orderBy);
    QUERY_USER_REPORTS_COUNT = StringHelper.concatToString(selectCount, from012, whereCoherence012, andLogin);
  }

  /**
   * Create a new report in database setting its mandatory attributes.
   *
   * @param session
   * @param aReportCode        String - Code of the report (ie: "R01", "R21"...)
   * @param uuid               String - the Universal Unique Identifier of the Report Execution
   * @param aState             {@link ReportStateEnum} - State of the report
   * @param schedulingReportId String - the id of the scheduling if it exists (mandatory for a scheduling).<br>
   *                           Else, this is not a scheduling and the value should be null or empty string.
   * @return The created report instance
   */
  private DbReport insert(Session session, final String aReportCode, final String uuid, final ReportStateEnum aState, final Long schedulingReportId) {
    log.debug("Inserting DbReport entry UID={} {}", uuid, schedulingReportId == null ? "" : ("for Scheduling " + schedulingReportId));
    DbReport theReport = new DbReport();

    // Assign default attribute values
    theReport.setReportUid(uuid);
    theReport.setState(aState.toString());
    if (schedulingReportId != null) {
      theReport.setSchedulingReportId(schedulingReportId);
    }

    // Set specific attribute values
    theReport.setReportCode(aReportCode);

    // Persist object
    session.save(theReport);

    return theReport;
  }

  /**
   * Create a new report in database setting its mandatory attributes and inject its associated criteria list and list of users data.
   *
   * @param session
   * @param aReportCode        String - Code of the report (ie: "R01", "R21"...)
   * @param uuid               String - the Universal Unique Identifier of the Report Execution
   * @param aState             {@link ReportStateEnum} - State of the report
   * @param criteriaList       List<{@link CriteriaValue}> - list containing all {@link CriteriaValue}s
   * @param theUsers           List<{@link DbUser}> - all the users asking for the report generation
   * @param schedulingReportId Long - the id of the scheduling if it exists (mandatory for a scheduling).<br>
   *                           Else, this is not a scheduling and the value should be null or empty string.
   * @return The created report instance
   * @throws InvalidUserInputException Occurs only when the login of the request sender is unknown in database
   */
  public DbReport insertWithCriteriaAndOwners(Session session, final String aReportCode, final String uuid, final ReportStateEnum aState,
                                              final List<CriteriaValue> criteriaList, final List<DbUser> theUsers, final Long schedulingReportId) throws InvalidUserInputException {
    // Create the report
    DbReport theNewReport = insert(session, aReportCode, uuid, aState, schedulingReportId);

    MngReportOwner mngReportOwner = new MngReportOwner();
    for (DbUser user : theUsers) {
      // Assign the execution request entry to all the users
      mngReportOwner.insert(session, theNewReport, user);
    }

    // Create all criteria
    Long theReportId = theNewReport.getId();
    MngCriteriaVal theCriteriaValManager = new MngCriteriaVal();
    for (CriteriaValue criterion : criteriaList) {
      theCriteriaValManager.insertByCriteriaName(session, criterion.getName(), criterion.getValue(), theReportId);
    }

    return theNewReport;
  }

  /**
   * Create a new report in database setting its mandatory attributes and inject its associated criteria list and user data.
   *
   * @param aReportCode  String - Code of the report (ie: "R01", "R21"...)
   * @param uuid         String - the Universal Unique Identifier of the Report Execution
   * @param aState       {@link ReportStateEnum} - State of the report
   * @param criteriaList List<{@link CriteriaValue}> - list containing all {@link CriteriaValue}s
   * @param theUser      {@link DbUser} - the user asking for the report generation
   * @return The created report instance
   * @throws InvalidUserInputException Occurs only when the login of the request sender is unknown in database
   */
  public DbReport insertWithCriteriaAndOwner(final String aReportCode, final String uuid, final ReportStateEnum aState,
                                             final List<CriteriaValue> criteriaList, final DbUser theUser) throws InvalidUserInputException {
    SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
    Session session = sessionFactory.openSession();
    try {
      Transaction transaction = session.beginTransaction();

      // Create the report
      // This report is not scheduled
      DbReport theNewReport = insert(session, aReportCode, uuid, aState, null);

      // Assign the execution request entry
      MngReportOwner mngReportOwner = new MngReportOwner();
      mngReportOwner.insert(session, theNewReport, theUser);

      // Create all criteria
      Long theReportId = theNewReport.getId();
      MngCriteriaVal theCriteriaValManager = new MngCriteriaVal();
      for (CriteriaValue criterion : criteriaList) {
        theCriteriaValManager.insertByCriteriaName(session, criterion.getName(), criterion.getValue(), theReportId);
      }
      transaction.commit();
      return theNewReport;
    }
    finally {
      session.close();
    }
  }

  /**
   * Search query for retrieving Report using its UUID.
   */
  public DbReport searchByUid(final String aReportUid, Session session) throws NotFoundException {
    DbReport report = searchBy(session, KEY_REPORT_UID, aReportUid);
    if (report == null) {
      throw new NotFoundException("Unable to find a matching entry in the DB for this report:" + aReportUid);
    }
    return report;
  }

  /**
   * Search query for retrieving all the reports using this scheduling name.
   */
  public List<DbReport> searchBySchedulingId(final Long aSchedulingId, Session session) {
    return searchAndReturnAllValues(session, COLUMN_SCHEDULING_ID, aSchedulingId);
  }

  /**
   * Search in database the list of reports that belong to a provided user (using its login as search criteria).
   */
  public List<RunningReportEntry> searchUserReports(Session session, final String aUserLogin, final int aPageNumber) {
    Query theQuery = session.createQuery(QUERY_USER_REPORTS_DATA);
    theQuery.setMaxResults(MAX_JSP_PAGE_SIZE);
    theQuery.setFirstResult(MAX_JSP_PAGE_SIZE * (aPageNumber - 1));
    theQuery.setString(KEY_DB_USER_LOGIN, aUserLogin);
    List<RunningReportEntry> result = new ArrayList<>();
    List<Object[]> rawResults = theQuery.list();
    for (Object[] reportData : rawResults) {
      final DbReport dbReport = (DbReport) reportData[QUERY_DBREPORT_POS];
      final String login = (String) reportData[QUERY_LOGIN_POS];
      final String surname = (String) reportData[QUERY_SURNAME_POS];
      final String firstName = (String) reportData[QUERY_FIRST_NAME_POS];
      final Long priority = (Long) reportData[QUERY_PRIORITY_POS];
      final Timestamp requestDate = (Timestamp) reportData[QUERY_REQUEST_DATE_POS];
      final String requestDateStr = new SimpleDateFormat(DATEFORMAT_DD_MM_YYYY_HH_MM_SS).format(requestDate);
      result.add(new RunningReportEntry(dbReport, login, surname, firstName, priority, requestDateStr));
    }
    return result;
  }

  /**
   * Count in database the report list of a specific user using its login as search criteria.
   *
   * @param session
   * @param aUserLogin String - Login of the searched user
   * @return DbReport list
   */
  public Long countUserReports(Session session, final String aUserLogin) {
    Query theQuery = session.createQuery(QUERY_USER_REPORTS_COUNT);
    theQuery.setString(KEY_DB_USER_LOGIN, aUserLogin);
    return (Long) theQuery.uniqueResult();
  }

  /**
   * Search in database the executing reports.
   *
   * @param session
   * @return List of report entries, each of them being containing: <br/>
   * <ul>
   * <li>DbReport list</li>
   * <li>Surname</li>
   * <li>First name</li>
   * <li>Report priority</li>
   * </ul>
   */
  public List<RunningReportEntry> searchRunningReports(Session session) {
    Query theQuery = session.createQuery(QUERY_RUNNING_REPORTS_DATA);
    List<RunningReportEntry> result = new ArrayList<>();
    for (Object[] reportData : (List<Object[]>) theQuery.list()) {
      DbReport report = (DbReport) reportData[QUERY_DBREPORT_POS];
      String login = (String) reportData[QUERY_LOGIN_POS];
      String surname = (String) reportData[QUERY_SURNAME_POS];
      String firstName = (String) reportData[QUERY_FIRST_NAME_POS];
      Long priority = (Long) reportData[QUERY_PRIORITY_POS];
      Timestamp requestDate = (Timestamp) reportData[QUERY_REQUEST_DATE_POS];
      String requestDateStr = new SimpleDateFormat(DATEFORMAT_DD_MM_YYYY_HH_MM_SS).format(requestDate);
      RunningReportEntry rep = new RunningReportEntry(report, login, surname, firstName, priority, requestDateStr);
      result.add(rep);
    }
    return result;
  }

  /**
   * Search in database the reports which are on the READY (ready to run in queue) or WAITING (scheduled) state.
   */
  public List<RunningReportEntry> searchWaitingReports(Session session) {
    Query theQuery = session.createQuery(QUERY_WAITING_REPORTS_DATA);
    List<RunningReportEntry> result = new ArrayList<>();
    for (Object[] reportData : (List<Object[]>) theQuery.list()) {
      final DbReport dbReport = (DbReport) reportData[QUERY_DBREPORT_POS];
      final String login = (String) reportData[QUERY_LOGIN_POS];
      final String surname = (String) reportData[QUERY_SURNAME_POS];
      final String firstName = (String) reportData[QUERY_FIRST_NAME_POS];
      final Long priority = (Long) reportData[QUERY_PRIORITY_POS];
      final Timestamp requestDate = (Timestamp) reportData[QUERY_REQUEST_DATE_POS];
      final String requestDateStr = new SimpleDateFormat(DATEFORMAT_DD_MM_YYYY_HH_MM_SS).format(requestDate);
      final RunningReportEntry fullReportEntry = new RunningReportEntry(dbReport, login, surname, firstName, priority, requestDateStr);
      result.add(fullReportEntry);
    }
    return result;
  }

  /**
   * Get reports by report code and user login in a specific period.
   *
   * @param session
   * @param aStartPeriod Starting period
   * @param aEndPeriod   End period
   * @return List of {@link ReportLaunchByUser}
   */
  public List<ReportLaunchByUser> searchUsersReports(Session session, final Date aStartPeriod, final Date aEndPeriod) {
    Query theQuery = session.createQuery(QUERY_ALL_USERS_AND_REPORTS);
    theQuery.setDate(KEY_START_PERIOD, aStartPeriod);
    theQuery.setDate(KEY_END_PERIOD, aEndPeriod);
    return parseStatisticsData(theQuery);
  }

  /**
   * Get report scheduling count by report code and admin in a specific period.
   *
   * @param session
   * @param aStartPeriod Starting period
   * @param aEndPeriod   End period
   * @return List of report entries, each of them being an array containing: <br/>
   * <ul>
   * <li>Index 0: Surname</li><br/>
   * <li>Index 1: First name</li><br/>
   * <li>Index 2: Report code</li><br/>
   * <li>Index 3: Report count</li>
   * <li>Index 4[0]: year</li>
   * <li>Index 4[1]: month</li>
   * </ul>
   */
  public List<ReportLaunchByUser> searchAdminSchedulingReports(Session session, final Date aStartPeriod, final Date aEndPeriod) {
    Query theQuery = session.createQuery(QUERY_ALL_REPORTS_SCHEDULING);
    theQuery.setDate(KEY_START_PERIOD, aStartPeriod);
    theQuery.setDate(KEY_END_PERIOD, aEndPeriod);
    return parseStatisticsData(theQuery);
  }

  /**
   * Executes the query and browses the results. <br>
   * Expecting an array containing:
   * <ul>
   * <li>Index 0: Surname</li><br/>
   * <li>Index 1: First name</li><br/>
   * <li>Index 2: Report code</li><br/>
   * <li>Index 3: Report count</li>
   * <li>Index 4[0]: year</li>
   * <li>Index 4[1]: month</li>
   * </ul>
   *
   * @param theQuery the query to execute
   * @return the browsed {@link List}
   */
  private List<ReportLaunchByUser> parseStatisticsData(final Query theQuery) {
    List<ReportLaunchByUser> result = new ArrayList<>();
    for (Object[] reportData : (List<Object[]>) theQuery.list()) {
      final String surname = (String) reportData[QUERY_DBREPORT_POS];
      final String firstName = (String) reportData[QUERY_LOGIN_POS];
      final String reportCode = (String) reportData[QUERY_SURNAME_POS];
      final Long reportCount = (Long) reportData[QUERY_FIRST_NAME_POS];
      final ReportLaunchByUser launch = new ReportLaunchByUser(reportCode, surname, firstName, reportCount);
      result.add(launch);
    }
    return result;
  }

  /**
   * Searches the DB to find all report of the specified type that match the provided list of criteria.
   */
  public List<RunningReportEntry> searchQueuedReportsWithCriteria(Session session, final ReportEnum reportEnum, final List<CriteriaValue> requestedCriteriaList) {
    // First search all Waiting report executions
    List<RunningReportEntry> recentReports = searchWaitingReportsInstances(session, reportEnum);

    // Prepare requested Criteria: put them in a map for fast retrieval
    Map<String, String> requestedCriteriaMap = new HashMap<>();
    for (CriteriaValue requestedCriteria : requestedCriteriaList) {
      requestedCriteriaMap.put(requestedCriteria.getName(), requestedCriteria.getValue());
    }

    // Then for each recent executions check if the criteria were the same
    List<RunningReportEntry> duplicates = new ArrayList<>();
    for (RunningReportEntry report : recentReports) {
      List<CriteriaValue> criteriaValues = new MngCriteriaVal().searchCriteriasByReportID(session, report.getReport().getId());
      if (hasSameCriteria(requestedCriteriaMap, criteriaValues)) {
        duplicates.add(report);
      }
    }
    return duplicates;
  }

  private List<RunningReportEntry> searchWaitingReportsInstances(Session session, final ReportEnum reportEnum) {
    Query theQuery = session.createQuery(QUERY_WAITING_REPORTS_DATA_WITH_TYPE);
    theQuery.setString(KEY_REPORT_CODE, reportEnum.getReportCode());
    List<RunningReportEntry> result = new ArrayList<>();
    for (Object[] reportData : (List<Object[]>) theQuery.list()) {
      DbReport dbreport = (DbReport) reportData[QUERY_DBREPORT_POS];
      String login = (String) reportData[QUERY_LOGIN_POS];
      String surname = (String) reportData[QUERY_SURNAME_POS];
      String firstname = (String) reportData[QUERY_FIRST_NAME_POS];
      Long priority = (Long) reportData[QUERY_PRIORITY_POS];
      Timestamp requestDate = (Timestamp) reportData[QUERY_REQUEST_DATE_POS];
      String requestDateStr = new SimpleDateFormat(DATEFORMAT_DD_MM_YYYY_HH_MM_SS).format(requestDate);
      RunningReportEntry rep = new RunningReportEntry(dbreport, login, surname, firstname, priority, requestDateStr);
      result.add(rep);
    }
    return result;
  }

  private boolean hasSameCriteria(final Map<String, String> requestedCriteriaMap, final List<CriteriaValue> criteriaValues) {
    // Check both list have at least same size
    if (criteriaValues.size() != requestedCriteriaMap.size()) {
      return false;
    }
    // Search if DB entries all exist in requested Criteria
    for (CriteriaValue criteriaValue : criteriaValues) {
      if (!isCriteriaInList(criteriaValue, requestedCriteriaMap)) {
        return false;
      }
    }
    // One set is inside the other set, and both are same size: so both sets are equal!
    return true;
  }

  private boolean isCriteriaInList(final CriteriaValue criteria, final Map<String, String> requestedCriteriaMap) {
    boolean noValue = criteria.getValue() == null || criteria.getValue().isEmpty() || "null".equals(criteria.getValue());
    boolean keyISInMap = requestedCriteriaMap.containsKey(criteria.getName());
    if (keyISInMap) {
      String value = requestedCriteriaMap.get(criteria.getName());
      if (value == null || value.isEmpty() || "null".equals(value)) {
        // If no value requested and no value obtained, it's ok.
        return noValue;
      }
      else {
        return value.equals(criteria.getValue());
      }
    }
    else {
      // Key is not in the map, if value is absent then it's normal
      return noValue;
    }
  }

  /**
   * Search for all reports which were executed before a provided deadline.
   */
  private List<DbReport> searchReportsBeforeDate(Session session, final Timestamp deadline) {
    Query theQuery = session.createQuery(
      "SELECT T0 " +
        "FROM DbReport T0 " +
        "WHERE T0.schedulingReportId IS NULL " +
        "AND (T0.startGenerationDate IS NULL AND T0.endGenerationDate IS NULL) " +
        "OR (T0.startGenerationDate IS NOT NULL AND T0.startGenerationDate < :deadline) " +
        "OR (T0.endGenerationDate IS NOT NULL AND T0.endGenerationDate < :deadline) " +
        "ORDER BY T0.reportUid");
    theQuery.setTimestamp(KEY_DEADLINE, deadline);
    return castAll(theQuery.list());
  }

  /**
   * Search for all reports which were executed before a provided deadline.
   */
  private List<DbReport> searchScheduledReportsBeforeDate(Session session, final Timestamp deadline) {
    Query theQuery = session.createQuery(
      "SELECT T0 " +
        "FROM DbReport T0 " +
        "WHERE T0.schedulingReportId IS NOT NULL " +
        "AND (T0.startGenerationDate IS NULL AND T0.endGenerationDate IS NULL) " +
        "OR (T0.startGenerationDate IS NOT NULL AND T0.startGenerationDate < :deadline) " +
        "OR (T0.endGenerationDate IS NOT NULL AND T0.endGenerationDate < :deadline) " +
        "ORDER BY T0.reportUid");
    theQuery.setTimestamp(KEY_DEADLINE, deadline);
    return castAll(theQuery.list());
  }

  /**
   * Deletes all reports in the DB that were started before the deadline.
   *
   * @param deadline the deadline (literally!)
   * @return the {@link List} of removed Entries
   */
  public List<RemovedReportEntry> deleteReportsBeforeDate(final Calendar deadline) {
    SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
    Session session = sessionFactory.openSession();
    try {
      Transaction transaction = session.beginTransaction();
      List<DbReport> reports = new MngReport().searchReportsBeforeDate(session, DateHelper.toTimestamp(deadline));
      List<RemovedReportEntry> removed = deleteCascade(session, reports);
      transaction.commit();
      return removed;
    }
    finally {
      session.close();
    }
  }

  /**
   * Deletes all scheduling reports in the DB that were started before the deadline.
   *
   * @param deadline the deadline (literally!)
   * @return the {@link List} of removed Entries
   */
  public List<RemovedReportEntry> deleteScheduledReportsBeforeDate(final Calendar deadline) {
    SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
    Session session = sessionFactory.openSession();
    try {
      Transaction transaction = session.beginTransaction();
      // HQL cannot delete from multiple tables, so let's SELECT first...
      List<DbReport> reports = new MngReport().searchScheduledReportsBeforeDate(session, DateHelper.toTimestamp(deadline));
      List<RemovedReportEntry> removed = deleteCascade(session, reports);
      transaction.commit();
      return removed;
    }
    finally {
      session.close();
    }
  }

  // HQL cannot delete from multiple tables, so we need to browse and delete the entries one by one
  private List<RemovedReportEntry> deleteCascade(Session session, List<DbReport> reports) {
    List<RemovedReportEntry> removed = new ArrayList<>();
    log.trace("Deleting {} DbReport entries and all their associated owners and criteria values", reports.size());
    MngReportOwner mngReportOwner = new MngReportOwner();
    MngCriteriaVal mngCriteriaVal = new MngCriteriaVal();
    for (DbReport dbReport : reports) {
      log.trace(" + Deleting DbRport with Id {} (generated on {})", dbReport.getId(), dbReport.getStartGenerationDate());

      // Search all owners
      List<DbReportOwner> owners = mngReportOwner.searchAndReturnAllValues(session, "reportId", dbReport.getId());
      log.trace("   ->Deleting {} DbReportOwner entries", owners.size());
      for (DbReportOwner dbReportOwner : owners) {
        log.trace("    + Deleting DbRportOwner {} of Report {} user ID = {} ", dbReportOwner.getId(), dbReportOwner.getReportId(),
          dbReportOwner.getUserId());
        session.delete(dbReportOwner); // Delete each owner
      }

      // Search all criteria
      List<DbCriteriaVal> criteria = mngCriteriaVal.searchAndReturnAllValues(session, "reportId", dbReport.getId());
      log.trace("   ->Deleting {} DbCriteriaVal entries", criteria.size());
      for (DbCriteriaVal criteriaValue : criteria) {
        log.trace("    + Deleting DbCriteriaVal {} of Report {} for value {}", criteriaValue.getId(), criteriaValue.getReportId(),
          criteriaValue.getCriteriaValue());
        session.delete(criteriaValue); // Delete each criteria value
      }

      // Delete the report now
      log.trace("   ->Deleting the DbReport {} itself", dbReport.getId());
      session.delete(dbReport); // Delete the report
      removed.add(new RemovedReportEntry(dbReport, owners, criteria));
    }
    return removed;
  }

  @Override
  public Class<DbReport> getManagedClass() {
    return DbReport.class;
  }

  /**
   * Updates the file name of a report. Usually overwriting classical filename with scheduling filename.
   *
   * @param reportTechnicalUid      the report UID
   * @param scheduledReportFileName the filename
   * @throws NotFoundException if the report entry wasn't found
   */
  public void updateFileName(String reportTechnicalUid, String scheduledReportFileName) throws NotFoundException {
    SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
    Session session = sessionFactory.openSession();
    try {
      Transaction transaction = session.beginTransaction();
      DbReport dbReport = searchByUid(reportTechnicalUid, session);
      dbReport.setFileName(scheduledReportFileName);
      update(session, dbReport);
      transaction.commit();
    }
    finally {
      session.close();
    }
  }

  /**
   * Simple data class.
   */
  @Getter
  @ToString
  @AllArgsConstructor(access = AccessLevel.PRIVATE)
  public static class RemovedReportEntry {
    private final DbReport report;
    private final List<DbReportOwner> owner;
    private final List<DbCriteriaVal> criteria;
  }

  /**
   * Represents a report entry in the DB, specifically from the logged-in user's point of view. <br>
   * Contains reference to a {@link DbReport}, the login/surname/firstname of its owner, the priority, and theoriginal request date
   */
  @AllArgsConstructor
  @Getter
  public class RunningReportEntry {
    private final DbReport report;
    private final String login;
    private final String surname;
    private final String firstName;
    private final Long reportPriority;
    private final String requestDate;

    /**
     * Returns a 'safe' username: 'Name Surname' or, if any is unavailable, then returns the login.
     *
     * @return a safe name, which always contains relevant information even with missing naming data
     */
    public String getSafeName() {
      boolean fistNameIsNull = isNull(firstName);
      boolean surNameIsNull = isNull(surname);
      return (fistNameIsNull || surNameIsNull) ? login : firstName + " " + surname;
    }

    private boolean isNull(final String string) {
      return string == null || string.isEmpty() || "null".equals(string);
    }
  }

  /**
   * Represents a report entry in the DB.
   */
  @AllArgsConstructor
  @Getter
  public class ReportLaunchByUser {
    private final String reportCode;
    private final String surname;
    private final String firstName;
    private final Long reportCount;

    @Override
    public String toString() {
      return "ReportLaunchByUser [reportCode=" + reportCode + ", surname=" + surname + ", firstName=" + firstName + ", reportCount="
        + reportCount + "]";
    }

  }
}
