package com.capgemini.reports.definition.r13report.printer;

import com.capgemini.framework.common.StringHelper;
import com.capgemini.framework.teamcenter.authent.TcSession;
import com.capgemini.framework.teamcenter.exceptions.KurtException;
import com.capgemini.framework.teamcenter.object.pom.status.TcReleaseStatus;
import com.capgemini.reports.changeobjects.change.AbstractChangeObject;
import com.capgemini.reports.changeobjects.change.TcChangeNoticeRevision;
import com.capgemini.reports.definition.r13report.R13Keys;
import com.capgemini.reports.definition.r13report.execution.AbstractExecutionR13;
import com.capgemini.reports.execution.mapconstructor.AbstractStaticTemplateMapConstructor;
import com.capgemini.reports.printing.PrintParameters;
import com.capgemini.reports.reports.parameters.EffectivityDate;
import com.capgemini.reports.users.SnecmaPerson;
import com.capgemini.reports.users.SnecmaUser;
import com.capgemini.reports.workflows.Task;
import com.google.common.base.Joiner;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Printer of a R13 Report.
 *
 * @param <T> {@link AbstractExecutionR13} - the abstract execution report
 * @param <U> The change objects required to make a reprot line (FPP or ChangeRequest)
 */
@Slf4j
@EqualsAndHashCode(callSuper = true)
public abstract class AbstractR13Printer<T extends AbstractExecutionR13<T, U>, U extends AbstractChangeObject>
        extends AbstractStaticTemplateMapConstructor<T, List<U>> {

    private static final long serialVersionUID = 1510278082994267162L;

    private static final long MILLISECONDS_IN_SECONDS = 1000;
    private static final long SECONDS_IN_MINUTES = 60;
    private static final long MINUTES_IN_HOURS = 60;
    private static final long HOURS_IN_DAY = 24;
    private static final long MILLIS_PER_DAY = HOURS_IN_DAY * MINUTES_IN_HOURS * SECONDS_IN_MINUTES * MILLISECONDS_IN_SECONDS;

    private static final String INACTIF = "Inactif";
    private static final String ACTIF = "Actif";
    private static final String EMPTY_STRING = "";

    @Override
    public void constructDataMaps(TcSession session, final PrintParameters printParameters, final List<U> changeList) throws KurtException {
        long start = System.currentTimeMillis();
        List<Map<String, ?>> reportLines = new ArrayList<>();
        for (U rootChangeObject : changeList) {
            Map<String, String> typeObjectParam = generateChangeObjectParameters(rootChangeObject);
            Map<? extends AbstractChangeObject, Map<String, String>> expandedChangeObject = getChangeObjectData(rootChangeObject, typeObjectParam);
            for (Entry<? extends AbstractChangeObject, Map<String, String>> entry : expandedChangeObject.entrySet()) {
                AbstractChangeObject changeObject = entry.getKey();
                Map<String, String> prefix = entry.getValue();
              try {
                reportLines.addAll(discoverWorkflows(changeObject, prefix));
              }
              catch (KurtException e) {
                throw new KurtException("Erreur sur la MOD " + changeObject.getItemId(), e);
              }
            }
        }
        if (reportLines.isEmpty()) {
            reportLines.add(putEmptyLine());
        }
        printParameters.addReportData(reportLines);
        printParameters.addReportParameters(new EnumMap<>(R13Keys.class));
        log.trace("Execution time : {} ms for {}", System.currentTimeMillis() - start, "data filtering");
    }

    /**
     * Creates a map of one or several {@link AbstractChangeObject} to data relative to said change object. <br>
     * The change objects are related to the provided change object as follows:
     * <ul>
     * <li>In the case of a FPP report, the only key is the FPP passed as parameter</li>
     * <li>In the case of a CR report, the <b>first</b> key is the CR passed as parameter, the rest are the CNs that depend on the CR. As the order is important, the return type must be a {@link LinkedHashMap} and not a simple {@link Map}/li>
     * </ul>
     * The provided prefix is used on each line, it may NOT be modified.
     *
     * @param data            the change object to represent
     * @param typeObjectParam prefix (DO NOT MODIFY)
     * @return a
     */
    protected abstract Map<? extends AbstractChangeObject, Map<String, String>> getChangeObjectData(final U data,
                                                                                                    final Map<String, String> typeObjectParam);

    private Map<String, String> generateChangeObjectParameters(final U change) {
        // Users parameters
        Map<String, String> mapPrinter = makeUserParameters(change);

        // Object parameters
        mapPrinter.put(R13Keys.ITEM_ID.toString(), change.getItemId());
        mapPrinter.put(R13Keys.ITEM_REVISION.toString(), change.getRevisionId());
        mapPrinter.put(R13Keys.DATE_CREATION.toString(), new EffectivityDate(change.getDateCreation()).getFormattedDateForFrenchUser());
        mapPrinter.put(R13Keys.DESIGNATION.toString(), change.getChangeName());
        String concatenatedLigneProduit = Joiner.on(StringHelper.LINE_FEED).join(change.getLignesProduit()); // Line jump within the cell
        mapPrinter.put(R13Keys.LIGNE_PRODUIT.toString(), concatenatedLigneProduit);

        // Validated status of object parameters
        mapPrinter.putAll(makeReleaseStatusParameters(change));
        return mapPrinter;
    }

    /**
     * These properties are present on both CR and FPP.
     */
    Map<String, String> makeUserParameters(final AbstractChangeObject change) {
        final Map<String, String> param = new HashMap<>();
        SnecmaUser user = change.getUser();
        SnecmaPerson person = user.getPerson();
        String codeCourrier = person.getCodeCourrier();
        if (change instanceof TcChangeNoticeRevision) {
            param.put(R13Keys.CR.ATTACHE_PILOTE_CN.toString(), (codeCourrier == null || codeCourrier.isEmpty()) ? EMPTY_STRING : codeCourrier);
            param.put(R13Keys.CR.NOM_PILOTE_CN.toString(), person.getPersonName());
            param.put(R13Keys.CR.STATUT_PILOTE_CN.toString(), userStatus(user));
        } else {
            param.put(R13Keys.ATTACHE_PILOTE.toString(), (codeCourrier == null || codeCourrier.isEmpty()) ? EMPTY_STRING : codeCourrier);
            param.put(R13Keys.NOM_PILOTE.toString(), person.getPersonName());
            param.put(R13Keys.STATUT_PILOTE.toString(), userStatus(user));
        }
        return param;
    }

    Map<String, String> makeReleaseStatusParameters(final AbstractChangeObject change) {
        TcReleaseStatus validatedStatus = findLastValidatedStatus(change);
        boolean isCN = change instanceof TcChangeNoticeRevision;
        String valueStatus;
        String valueDate;
        if (validatedStatus == null) {
            valueStatus = EMPTY_STRING;
            valueDate = EMPTY_STRING;
        } else {
            valueStatus = validatedStatus.getDisplayableName();
            valueDate = new EffectivityDate(validatedStatus.dateReleased()).getFormattedDateForFrenchUser();
        }
        String keyStatus = (isCN ? R13Keys.CR.STATUT_CN : R13Keys.STATUT).toString();
        String keyDate = (isCN ? R13Keys.CR.DATE_STATUT_CN : R13Keys.DATE_STATUT).toString();
        Map<String, String> param = new HashMap<>();
        param.put(keyStatus, valueStatus);
        param.put(keyDate, valueDate);
        return param;
    }

    /**
     * Returns the last validated status. If no (valid) status, returns null.
     */
    private TcReleaseStatus findLastValidatedStatus(final AbstractChangeObject change) {
        TcReleaseStatus selectedStatus = null;
        for (TcReleaseStatus status : change.getReleaseStatusContainingMaturity()) {
            if (isValid(status, change) && (selectedStatus == null || status.dateReleased().after(selectedStatus.dateReleased()))) {
                selectedStatus = status;
            }
        }
        return selectedStatus;
    }

    /**
     * Validates a status of a change.
     *
     * @param status the TcReleaseStatus of the change
     * @param change the change
     * @return true if the status of the change is valid (should be kept) or false if the status ishould be ignored.
     */
    protected abstract boolean isValid(final TcReleaseStatus status, final AbstractChangeObject change);

    /**
     * Returns a description of the status of all Workflows found to be associated with the provided change item. The provided prefix will be added to each line, unmodified.
     *
     * @param change the {@link AbstractChangeObject}
     * @param prefix the first few columns, pre-calculated
     * @return aLine for each task of each workflow (each line contains the provided prefix)
     */
    private List<Map<String, String>> discoverWorkflows(final AbstractChangeObject change, final Map<String, String> prefix) throws KurtException {
        List<Task> wkfList = change.getWkfList();
        List<Map<String, String>> result = new ArrayList<>();
        if (wkfList == null || wkfList.isEmpty()) {
            result.add(makeEmptyWkfAndTask(prefix));
        } else {
            boolean allWorkflowsAreCompleted = true;
            for (Task wkf : wkfList) {
                if (wkf.isRunning()) { // Remove workflows when not currently running (aborted, skipped, failed, pending, etc.)
                    allWorkflowsAreCompleted = false;
                    Map<String, String> wkfParam = new HashMap<>(prefix); // Copy input prefix

                    // Workflow parameters
                    wkfParam.put(R13Keys.NOM_TEMPLATE_WKF.toString(), wkf.getName());

                    // Users parameters associated to WKF
                    SnecmaUser wkfResponsibleUser = wkf.getResponsibleUser() ;
                    SnecmaPerson wkfResponsiblePerson = wkfResponsibleUser.getPerson();
                    String codeCourrier = wkfResponsiblePerson.getCodeCourrier();
                    wkfParam.put(R13Keys.ATTACHE_PILOTE_WKF.toString(), (codeCourrier == null || codeCourrier.isEmpty()) ? EMPTY_STRING : codeCourrier);
                    wkfParam.put(R13Keys.NOM_PILOTE_WKF.toString(), wkfResponsiblePerson.getPersonName());
                    wkfParam.put(R13Keys.STATUT_PILOTE_WKF.toString(), userStatus(wkfResponsibleUser));

                    // Active task parameters
                    result.addAll(discoverActiveTasks(wkf, change, wkfParam));
                }
            }
            if (allWorkflowsAreCompleted) {
                result.add(makeEmptyWkfAndTask(prefix));
            }
        }
        return result;
    }

    /**
     * Returns a line for each Active Task of the Workflow. The provided prefix will be added to each line, unmodified.
     *
     * @param change the change item
     * @param prefix the first few columns, pre-calculated
     * @return a Line for each active task of the Workflow (each containing the provided prefix)
     */
    private List<Map<String, String>> discoverActiveTasks(final Task rootTask, final AbstractChangeObject change, final Map<String, String> prefix) throws KurtException {
        List<Task> activeMotherTasks = rootTask.getActiveChildTasks();
        List<Map<String, String>> result = new ArrayList<>();
        if (activeMotherTasks == null || activeMotherTasks.isEmpty()) {
            // Prepare a line containing the prefix, and "" everywhere else
            HashMap<String, String> copy = new HashMap<>(prefix);
            addEmptyTask(copy);
            result.add(copy);
        } else {
            for (Task activeMotherTask : activeMotherTasks) {
                // Level 0: task representing the whole "Workflow"
                // Level 1: 'Mother' tasks --> obtain name & due date here
                // Level 2: 'Daughter' tasks --> These are the 'ACTIVE' tasks, obtain users here

                // Here, we always are on the mother task's level
                Map<String, String> line = new HashMap<>(prefix); // Copy prefix without modify it

                // Warning : if a CR has a CN and both of them have workflows, the CR will have its own workflow and the CN's
                if (activeMotherTask.isInTargetAttachments(change)) {
                    // Task name
                    line.put(R13Keys.NOM_TACHE.toString(), activeMotherTask.getName());

                    // Due date
                    Calendar parentDueDate = activeMotherTask.getDueDate();
                    if (parentDueDate == null) {
                        line.put(R13Keys.DATE_CIBLE.toString(), EMPTY_STRING);
                        line.put(R13Keys.AVANCE_RETARD.toString(), EMPTY_STRING);
                    } else {
                        line.put(R13Keys.DATE_CIBLE.toString(), new EffectivityDate(parentDueDate).getFormattedDateForFrenchUser());
                        line.put(R13Keys.AVANCE_RETARD.toString(), compareWithDateSystem(parentDueDate));
                    }

                    // The users are found on the daughter task if it exists
                    SnecmaUser motherOwner = activeMotherTask.getResponsibleUser();
                    List<Task> activeChildTasks = activeMotherTask.getActiveChildTasks();
                    if (!activeChildTasks.isEmpty()) {
                        for (Task childTask : activeChildTasks) {
                            // Add a line for each users (they, however, are found on the daughter task...)
                            result.addAll(discoverUserTasks(childTask, line, motherOwner));
                        }
                    } else {
                        // Add a line for each users (they, however, are found on the daughter task...)
                        result.addAll(discoverUserTasks(activeMotherTask, line, motherOwner));
                    }
                }
            }
        }
        return result;
    }

    private List<Map<String, String>> discoverUserTasks(final Task childTask, final Map<String, String> taskParam, SnecmaUser motherOwner) {
        List<SnecmaUser> validSignoffsValues = childTask.getPendingSignoffsUser(); // Only users that have not yet validated their task
        List<Map<String, String>> result = new ArrayList<>();
        if (!validSignoffsValues.isEmpty()) {
            for (SnecmaUser signoffUser : validSignoffsValues) {
                Map<String, String> personParam = new HashMap<>(taskParam); // Defensive copy of prefix

                // Person parameters
                SnecmaPerson signoffPerson = signoffUser.getPerson();
                String codeCourrier = signoffPerson.getCodeCourrier();
                personParam.put(R13Keys.ATTACHE_ACTEUR_TACHE.toString(),
                        (codeCourrier == null || codeCourrier.isEmpty()) ? EMPTY_STRING : codeCourrier);
                personParam.put(R13Keys.NOM_ACTEUR_TACHE.toString(), signoffPerson.getPersonName());
                personParam.put(R13Keys.STATUT_ACTEUR_TACHE.toString(), userStatus(signoffUser));

                // Add to results list
                result.add(personParam);
            }
        } else {
            // Default behavior when no users are active on the daughter task
            Map<String, String> personParam = new HashMap<>(taskParam); // Defensive copy of prefix
            SnecmaPerson responsibleUser = motherOwner.getPerson();
            String codeCourrier = responsibleUser.getCodeCourrier();
            personParam.put(R13Keys.ATTACHE_ACTEUR_TACHE.toString(), (codeCourrier == null || codeCourrier.isEmpty()) ? EMPTY_STRING : codeCourrier);
            personParam.put(R13Keys.NOM_ACTEUR_TACHE.toString(), responsibleUser.getPersonName());
            personParam.put(R13Keys.STATUT_ACTEUR_TACHE.toString(), userStatus(motherOwner));
            result.add(personParam);
        }
        return result;
    }

    String compareWithDateSystem(final Calendar dueDate) {
        long epochToDueDate = daysSinceEpoch(dueDate);
        long epochToToday = daysSinceEpoch(Calendar.getInstance());
        long diff = epochToDueDate - epochToToday;
        return String.valueOf(diff);
    }

    /**
     * Returns the number of days elapsed since {@link Calendar}'s Epoch (Jan, 1st 1970) until the beggining of the provided Calendar's day : midnight 00:00:000). <br>
     * Ex:
     * <ul>
     * <li>if Calendar is Jan, 1st 1970, 00h:00m:00s:000): 000000000 ms</li>
     * <li>if Calendar is Jan, 1st 1970, 00h:00m:00s:125): 000000125 ms</li>
     * <li>if Calendar is Jan, 1st 1970, 00h:00m:05s:125): 000005125 ms</li>
     * <li>if Calendar is Jan, 1st 1970, 00h:01m:05s:125): 000065125 ms</li>
     * <li>if Calendar is Jan, 1st 1970, 01h:00m:05s:125): 003605125 ms</li>
     * <li>if Calendar is Jan, 3rd 1970, 00h:00m:00s:000): 259200000 ms</li>
     * <li>if Calendar is Jan, 3rd 1970, 00h:00m:05s:125): 259205125 ms</li>
     * </ul>
     *
     * @param dueDate the Date to calculate
     * @return the number of milliseconds elapsed since {@link Calendar}'s Epoch (Jan, 1st 1970) until the beggining of the provided Calendar's day : midnight 00:00:000)
     */
    private long daysSinceEpoch(final Calendar dueDate) {
        return dueDate.getTimeInMillis() / MILLIS_PER_DAY; // TRUNCATE to remove any trace of time spend past midnight
    }

    private Map<String, String> makeEmptyWkfAndTask(final Map<String, String> param) {
        param.put(R13Keys.NOM_TEMPLATE_WKF.toString(), EMPTY_STRING);
        param.put(R13Keys.ATTACHE_PILOTE_WKF.toString(), EMPTY_STRING);
        param.put(R13Keys.NOM_PILOTE_WKF.toString(), EMPTY_STRING);
        param.put(R13Keys.STATUT_PILOTE_WKF.toString(), EMPTY_STRING);
        addEmptyTask(param);
        return param;
    }

    private void addEmptyTask(final Map<String, String> taskParam) {
        taskParam.put(R13Keys.NOM_TACHE.toString(), EMPTY_STRING);
        taskParam.put(R13Keys.ATTACHE_ACTEUR_TACHE.toString(), EMPTY_STRING);
        taskParam.put(R13Keys.NOM_ACTEUR_TACHE.toString(), EMPTY_STRING);
        taskParam.put(R13Keys.STATUT_ACTEUR_TACHE.toString(), EMPTY_STRING);
        taskParam.put(R13Keys.DATE_CIBLE.toString(), EMPTY_STRING);
        taskParam.put(R13Keys.AVANCE_RETARD.toString(), EMPTY_STRING);
    }

    private Map<String, String> putEmptyLine() {
        Map<String, String> mapPrinter = makeEmptyWkfAndTask(new HashMap<String, String>());
        mapPrinter.put(R13Keys.ATTACHE_PILOTE.toString(), EMPTY_STRING);
        mapPrinter.put(R13Keys.NOM_PILOTE.toString(), EMPTY_STRING);
        mapPrinter.put(R13Keys.STATUT_PILOTE.toString(), EMPTY_STRING);
        mapPrinter.put(R13Keys.ITEM_ID.toString(), EMPTY_STRING);
        mapPrinter.put(R13Keys.ITEM_REVISION.toString(), EMPTY_STRING);
        mapPrinter.put(R13Keys.DATE_CREATION.toString(), EMPTY_STRING);
        mapPrinter.put(R13Keys.DESIGNATION.toString(), EMPTY_STRING);
        mapPrinter.put(R13Keys.LIGNE_PRODUIT.toString(), EMPTY_STRING);
        mapPrinter.put(R13Keys.TYPE.toString(), EMPTY_STRING);
        mapPrinter.put(R13Keys.STATUT.toString(), EMPTY_STRING);
        mapPrinter.put(R13Keys.DATE_STATUT.toString(), EMPTY_STRING);
        mapPrinter.put(R13Keys.CR.PARTS_CHANGE_REQUEST.toString(), EMPTY_STRING);
        mapPrinter.put(R13Keys.FPP.PARTS_FPP.toString(), EMPTY_STRING);
        specificTreatmentEmptyLines(mapPrinter);
        return mapPrinter;
    }

    private String userStatus(final SnecmaUser user) {
        return (user.isActive()) ? ACTIF : INACTIF;
    }

    protected abstract void specificTreatmentEmptyLines(final Map<String, String> mapPrinter);
}
