package com.oguerreiro.resilient.service;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.stream.Collectors;

import org.apache.poi.ooxml.POIXMLProperties;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.oguerreiro.resilient.domain.DashboardComponent;
import com.oguerreiro.resilient.domain.DashboardComponentDetail;
import com.oguerreiro.resilient.domain.DashboardComponentDetailValue;
import com.oguerreiro.resilient.domain.DashboardComponentOrganization;
import com.oguerreiro.resilient.domain.Organization;
import com.oguerreiro.resilient.domain.enumeration.DashboardComponentView;
import com.oguerreiro.resilient.repository.DashboardComponentRepository;
import com.oguerreiro.resilient.repository.OrganizationRepository;
import com.oguerreiro.resilient.service.dto.DashboardComponentDTO;
import com.oguerreiro.resilient.service.mapper.DashboardComponentMapper;

/**
 * Service Implementation for managing {@link com.oguerreiro.resilient.domain.DashboardComponent}.
 */
@Service
@Transactional
public class DashboardComponentService
    extends AbstractResilientService<DashboardComponentDTO, DashboardComponent, Long> {

  private OrganizationRepository organizationRepository;
  private DashboardComponentMapper dashboardComponentMapper;

  public DashboardComponentService(DashboardComponentRepository dashboardComponentRepository,
      DashboardComponentMapper dashboardComponentMapper, OrganizationRepository organizationRepository) {
    super(DashboardComponent.class, dashboardComponentRepository, dashboardComponentMapper);

    this.organizationRepository = organizationRepository;
    this.dashboardComponentMapper = dashboardComponentMapper;
  }

  /*
   * Reads ALL orgs and data. Then:<br>
   * <ul>
   * <li>organizationId == 1 - Then, group's the loaded data and builds a single Organization list values</li>
   * <li>organizationId != 1 - Then, group's the loaded data and builds a Organization list values for NOVA (same when organizationId == 1). But also returns the given organizationId data.</li>
   * </ul>
   * 
   * TODO: Probably the best way to do this it's NOT loading all and sum in memory. The best is probably 2 separate query's, that already sum the values. 
   *       And this solves another problem. Now I fixed the top organization ID. With this solution, I should build 2 list's of organization ID's:<br>
   *       <ul>
   *       <li>From the given organizationId, search the TOP most Organization. Then build a list of ALL children. This will give the sum values for top org</li>
   *       <li>From the given organizationId, get the list of ALL children. This will give the sum values for the selected Org</li>
   *       </ul> 
   *       This also enables the possibility to output all levels between top and the selected organizationId
   */
  public Map<String, List<DashboardComponentDetailValue>> buildDashboardComponentView(Long organizationId,
      Long dashboardComponentId, Long periodVersionId) {
    List<DashboardComponentDetailValue> finalValues = new ArrayList<DashboardComponentDetailValue>();

    // Step 1: Load all output data for the dashboardComponent
    Long searchOrganizationId = null; //Forces to load all data
    List<DashboardComponentDetailValue> values = this.getDashboardComponentRepository().loadAllDashboardComponentDetailValues(
        searchOrganizationId, periodVersionId, dashboardComponentId);

    // Step 2: Group by variableId and sum values. To have NOVA org values
    Organization novaOrg = this.organizationRepository.getReferenceById(1L);
    Map<DashboardComponentDetail, BigDecimal> mainOrgSummedValues = Collections.emptyMap();

    //Group values
    Map<DashboardComponentDetail, BigDecimal> summedValues = values.stream().collect(
        Collectors.groupingBy(DashboardComponentDetailValue::getDashboardComponentDetail, Collectors.mapping(
            DashboardComponentDetailValue::getValue, Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));

    // Sort the map by DashboardComponentDetailValue.getDashboardComponentDetail().getName()
    // @formatter:off
    mainOrgSummedValues = summedValues.entrySet()
        .stream()
        .sorted(Comparator.comparingInt(entry -> { 
          return entry.getKey().getIndexOrder();
        }))
        .collect(Collectors.toMap(
            (Map.Entry<DashboardComponentDetail, BigDecimal> entry) -> entry.getKey(),
            (Map.Entry<DashboardComponentDetail, BigDecimal> entry) -> entry.getValue(),
            (e1, e2) -> e1,  // Merge function (not needed here)
            LinkedHashMap::new // Preserve insertion order
        ));
    // @formatter:on

    // Step 3: Extract values for the selected organizationId (if any)
    List<DashboardComponentDetailValue> filterOrgValues = null;
    if (organizationId != 1) {
      //Only if its NOT the main org
      filterOrgValues = values.stream().filter(value -> value.getOwner().getId().equals(organizationId)).collect(
          Collectors.toList());
    }

    // Step 4: Add both collections to finalValues
    // #1 - Selected Org (must be the last one)
    if (filterOrgValues != null) {
      finalValues.addAll(filterOrgValues);
    }

    // #2 - Main Org (must be the last one)
    for (Map.Entry<DashboardComponentDetail, BigDecimal> entry : mainOrgSummedValues.entrySet()) {
      finalValues.add(new DashboardComponentDetailValue(entry.getKey(), entry.getKey().getTargetDashboardComponent(),
          novaOrg, entry.getValue()));
    }

    // Step 5: Group the list, into a MAP. Key=Organization; Value=List of DashboardComponentDetailValue
    //@formatter:off
    Map<String, List<DashboardComponentDetailValue>> mapValues = finalValues.stream()
        .collect(Collectors.groupingBy(
            value -> value.getOwner().getId().toString(),
            LinkedHashMap::new,  // Ensures LinkedHashMap is used
            Collectors.toList()
    ));
    //@formatter:on

    // Step 6: Guarantee that TOP Org is last
    if (mapValues.containsKey("1")) {
      List<DashboardComponentDetailValue> value = mapValues.remove("1"); // Remove first
      mapValues.put("1", value); // Reinsert at the end
    }

    // Get DashboardComponent and check the Organization's list
    List<DashboardComponentOrganization> orgs = this.getDashboardComponentRepository().getReferenceById(
        dashboardComponentId).getDashboardComponentOrganizations();
    if (orgs != null && !orgs.isEmpty()) {
      List<String> keysToRemove = new ArrayList<>();
      for (String key : mapValues.keySet()) {
        boolean anyMatch = orgs.stream().anyMatch(org -> org.getOrganization().getId().equals(Long.valueOf(key)));
        if (!anyMatch) {
          keysToRemove.add(key);
        }
      }
      keysToRemove.forEach(key -> mapValues.remove(key));
    }

    return mapValues;
  }

  private DashboardComponentRepository getDashboardComponentRepository() {
    return (DashboardComponentRepository) super.getRepository();
  }

  @Transactional(readOnly = true)
  public List<DashboardComponentDTO> findAllActive() {
    log.debug("Request to get all active DashboardComponent's.");
    return this.getDashboardComponentRepository().findAllActive().stream().map(dashboardComponentMapper::toDto).collect(
        Collectors.toCollection(LinkedList::new));
  }

  @Transactional(readOnly = true)
  public List<DashboardComponentDTO> findAllActiveForView(DashboardComponentView dashboardComponentView) {
    log.debug("Request to findAllActiveForView.");
    return this.getDashboardComponentRepository().findAllActiveForView(dashboardComponentView).stream().map(
        dashboardComponentMapper::toDto).collect(Collectors.toCollection(LinkedList::new));
  }

  @Transactional(readOnly = true)
  public byte[] export(Long organizationId, Long dashboardComponentId, Long periodversionId) {
    DashboardComponent dashboardComponent = this.getDashboardComponentRepository().getReferenceById(
        dashboardComponentId);

    //Build a Workbook
    try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
      Sheet sheet = workbook.createSheet("Export");

      // Get the TOP component. This will be used as a template definer
      Map<String, List<DashboardComponentDetailValue>> mapValues = this.buildDashboardComponentView(organizationId,
          dashboardComponentId, periodversionId);

      // Set title
      String pageTitle = "";
      String pageSubTitle = "";
      if (dashboardComponent.getView().equals(DashboardComponentView.EMISSIONS)) {
        pageTitle = "Emissões de Gases com Efeito de Estufa";
        pageSubTitle = "t CO2e";
      } else if (dashboardComponent.getView().equals(DashboardComponentView.INDICATORS)) {
        pageTitle = "Indicadores";
      }

      int c = 0;
      Row headerRow = sheet.createRow(0);
      headerRow.createCell(0).setCellValue(pageTitle);
      headerRow = sheet.createRow(1);
      headerRow.createCell(0).setCellValue(pageSubTitle);

      LocalDateTime now = LocalDateTime.now();
      DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
      headerRow = sheet.createRow(2);
      headerRow.createCell(0).setCellValue("Exportado em " + now.format(formatter));
      headerRow = sheet.createRow(3); //Empty row

      // Set metadata
      // Access the POI document properties
      POIXMLProperties props = workbook.getProperties();

      // Core properties
      POIXMLProperties.CoreProperties coreProps = props.getCoreProperties();
      coreProps.setCreator("Resilient framework");
      coreProps.setTitle(pageTitle);
      coreProps.setSubjectProperty(pageTitle);
      coreProps.setDescription("Exported from InNOVA App.");

      // Extended properties
      POIXMLProperties.ExtendedProperties extProps = props.getExtendedProperties();
      extProps.getUnderlyingProperties().setCompany("powerd by Resilient, @OGuerreiro.com");
      //extProps.getUnderlyingProperties().setManager("Jane Manager");

      // Build UO header names
      Map<String, String> UOs = new HashMap<String, String>(); // Maps a key to a UO code. <key_to_mapValues, UO_Code>
      for (String key : mapValues.keySet()) {
        String code = this.organizationRepository.getReferenceById(Long.valueOf(key)).getCode();
        UOs.put(key, code);
      }

      // Header row
      c = 0;
      headerRow = sheet.createRow(4);
      sheet.setColumnWidth(c, 50 * 256);  // 50 characters wide. The unit of width is 1/256 of a char. 
      headerRow.createCell(c++).setCellValue("");
      for (String code : UOs.values()) {
        headerRow.createCell(c++).setCellValue(code);
      }

      // Get data with a stream (query cursor)
      this.recursiveExport(UOs, sheet, organizationId, dashboardComponentId, periodversionId, "");

      workbook.write(out);
      return out.toByteArray();
    } catch (IOException e) {
      throw new RuntimeException("Failed to generate Excel file", e);
    }
  }

  private void recursiveExport(Map<String, String> UOs, Sheet sheet, Long organizationId, Long dashboardComponentId,
      Long periodversionId, String ident) {
    Map<String, List<DashboardComponentDetailValue>> mapValues = this.buildDashboardComponentView(organizationId,
        dashboardComponentId, periodversionId);

    // Break recursivity
    if (mapValues == null || mapValues.isEmpty() || mapValues.values() == null || mapValues.values().isEmpty())
      return;

    int listSize = mapValues.values().stream().filter(Objects::nonNull).findFirst().orElse(
        Collections.emptyList()).size();
    if (listSize == 0) {
      return;
    }

    for (int i = 0; i < listSize; i++) {
      // Create row for this entry
      int cc = 0;
      Row row = sheet.createRow(sheet.getLastRowNum() + 1);

      // Get detail name from the first entry. They are all the same
      DashboardComponentDetail referenceDashboardComponentDetail = mapValues.values().stream().findFirst().orElse(
          Collections.emptyList()).get(i).getDashboardComponentDetail();
      String detailName = referenceDashboardComponentDetail.getName();
      row.createCell(cc++).setCellValue(ident + detailName);

      // Write all values (MAX 2, for now)
      for (Entry<String, String> UO : UOs.entrySet()) {
        //Extract value from mapValues
        BigDecimal value = mapValues.get(UO.getKey()).get(i).getValue();
        row.createCell(cc++).setCellValue(value.setScale(8).toPlainString());
      }

      // Drill down
      if (referenceDashboardComponentDetail.getTargetDashboardComponent() != null) {
        Long targetDashboardComponentId = referenceDashboardComponentDetail.getTargetDashboardComponent().getId();
        this.recursiveExport(UOs, sheet, organizationId, targetDashboardComponentId, periodversionId, ident + "     ");
      }

    }
  }
}
