package com.oguerreiro.resilient.service;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.oguerreiro.resilient.domain.InputData;
import com.oguerreiro.resilient.domain.InputDataUpload;
import com.oguerreiro.resilient.domain.InventoryData;
import com.oguerreiro.resilient.domain.Organization;
import com.oguerreiro.resilient.domain.Period;
import com.oguerreiro.resilient.domain.enumeration.OrganizationNature;
import com.oguerreiro.resilient.domain.enumeration.UnitValueType;
import com.oguerreiro.resilient.domain.enumeration.UploadType;
import com.oguerreiro.resilient.error.VariableUnitConverterMoreThanOneFoundException;
import com.oguerreiro.resilient.error.VariableUnitConverterNotFoundException;
import com.oguerreiro.resilient.error.VariableUnitConverterWithoutFormulaException;
import com.oguerreiro.resilient.repository.EmissionFactorRepository;
import com.oguerreiro.resilient.repository.InputDataRepository;
import com.oguerreiro.resilient.repository.InputDataUploadRepository;
import com.oguerreiro.resilient.repository.OrganizationRepository;
import com.oguerreiro.resilient.repository.PeriodRepository;
import com.oguerreiro.resilient.repository.UnitRepository;
import com.oguerreiro.resilient.repository.VariableRepository;
import com.oguerreiro.resilient.service.dto.InputDataDTO;
import com.oguerreiro.resilient.service.dto.InventoryDataDTO;
import com.oguerreiro.resilient.service.mapper.InputDataMapper;
import com.oguerreiro.resilient.service.mapper.InventoryDataMapper;
import com.oguerreiro.resilient.web.rest.errors.BadRequestAlertException;

/**
 * Service Implementation for managing
 * {@link com.oguerreiro.resilient.domain.InputData}.
 */
@Service
@Transactional
public class InputDataService {

  private final Logger log = LoggerFactory.getLogger(InputDataService.class);

  private final InputDataRepository inputDataRepository;
  private final InputDataUploadRepository inputDataUploadRepository;
  private final VariableRepository variableRepository;
  private final PeriodRepository periodRepository;
  private final UnitRepository unitRepository;
  private final OrganizationRepository organizationRepository;
  private final OrganizationService organizationService;
  private final UnitConverterService unitConverterService;

  private final InputDataMapper inputDataMapper;
  private final InventoryDataMapper inventoryDataMapper;

  public InputDataService(InputDataRepository inputDataRepository, InputDataMapper inputDataMapper,
      EmissionFactorRepository emissionFactorRepository, VariableRepository variableRepository,
      PeriodRepository periodRepository, UnitRepository unitRepository, OrganizationRepository organizationRepository,
      InputDataUploadRepository inputDataUploadRepository, UnitConverterService unitConverterService,
      OrganizationService organizationService, InventoryDataMapper inventoryDataMapper) {
    this.inputDataRepository = inputDataRepository;
    this.inputDataMapper = inputDataMapper;
    this.variableRepository = variableRepository;
    this.periodRepository = periodRepository;
    this.unitRepository = unitRepository;
    this.organizationRepository = organizationRepository;
    this.inputDataUploadRepository = inputDataUploadRepository;
    this.unitConverterService = unitConverterService;
    this.organizationService = organizationService;
    this.inventoryDataMapper = inventoryDataMapper;
  }

  /**
   * Save a inputData.
   *
   * @param  inputDataDTO the entity to save.
   * @return              the persisted entity.
   */
  public InputDataDTO save(InputDataDTO inputDataDTO) {
    log.debug("Request to save InputData : {}", inputDataDTO);
    InputData inputData = this.hydrateEntity(inputDataDTO);

    // Evaluate unit convertion
    try {
      this.evaluateUnitConversion(inputData);
    } catch (VariableUnitConverterNotFoundException | VariableUnitConverterWithoutFormulaException
        | VariableUnitConverterMoreThanOneFoundException e) {
      throw new BadRequestAlertException("Error converting value", "inputData", "idnull");
    }

    inputData = inputDataRepository.save(inputData);
    return inputDataMapper.toDto(inputData);
  }

  /**
   * Update a inputData.
   *
   * @param  inputDataDTO the entity to save.
   * @return              the persisted entity.
   */
  public InputDataDTO update(InputDataDTO inputDataDTO) {
    log.debug("Request to update InputData : {}", inputDataDTO);
    InputData inputData = this.hydrateEntity(inputDataDTO);

    // Evaluate unit convertion

    try {
      this.evaluateUnitConversion(inputData);
    } catch (VariableUnitConverterNotFoundException | VariableUnitConverterWithoutFormulaException
        | VariableUnitConverterMoreThanOneFoundException e) {
      throw new BadRequestAlertException("Error converting value", "inputData", "idnull");
    }

    inputData = inputDataRepository.save(inputData);
    return inputDataMapper.toDto(inputData);
  }

  /**
   * Partially update a inputData.
   *
   * @param  inputDataDTO the entity to update partially.
   * @return              the persisted entity.
   */
  public Optional<InputDataDTO> partialUpdate(InputDataDTO inputDataDTO) {
    log.debug("Request to partially update InputData : {}", inputDataDTO);

    return inputDataRepository.findById(inputDataDTO.getId()).map(existingInputData -> {
      inputDataMapper.partialUpdate(existingInputData, inputDataDTO);

      return existingInputData;
    }).map(inputDataRepository::save).map(inputDataMapper::toDto);
  }

  /**
   * Get all the inputData.
   *
   * @return the list of entities.
   */
  @Transactional(readOnly = true)
  public List<InputDataDTO> findAll() {
    log.debug("Request to get all InputData");
    return inputDataRepository.findAll().stream().map(inputDataMapper::toDto).collect(
        Collectors.toCollection(LinkedList::new));
  }

  /**
   * Get all the inputData with eager load of many-to-many relationships.
   *
   * @return the list of entities.
   */
  public Page<InputDataDTO> findAllWithEagerRelationships(Pageable pageable) {
    return inputDataRepository.findAllWithEagerRelationships(pageable).map(inputDataMapper::toDto);
  }

  /**
   * Get one inputData by id.
   *
   * @param  id the id of the entity.
   * @return    the entity.
   */
  @Transactional(readOnly = true)
  public Optional<InputDataDTO> findOne(Long id) {
    log.debug("Request to get InputData : {}", id);
    return inputDataRepository.findOneWithEagerRelationships(id).map(inputDataMapper::toDto);
  }

  /**
   * Delete the inputData by id.
   *
   * @param id the id of the entity.
   */
  public void delete(Long id) {
    log.debug("Request to delete InputData : {}", id);
    inputDataRepository.deleteById(id);
  }

  public void deleteAllInputDataWithSource(InputDataUpload inputDataUpload) {
    log.debug("Request to deleteAllInputDataWithSource InputData : {}", inputDataUpload);
    inputDataRepository.deleteAllInputDataWithSource(inputDataUpload);
  }

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void deleteAllInputDataFromDatafileInNewTransaction(Organization organization, Period period) {
    log.debug("Request to deleteAllInputDataFromDatafile InputData : {}", organization, period);
    inputDataRepository.deleteAllInputDataFromDatafile(organization, period);
  }

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void deleteAllInputDataFromDatafileInNewTransaction(Organization organization, Period period,
      UploadType uploadType) {
    log.debug("Request to deleteAllInputDataFromDatafile InputData : {}", organization, period, uploadType);
    inputDataRepository.deleteAllInputDataFromDatafile(organization, period, uploadType);
  }

  @Transactional(readOnly = true)
  public byte[] export(Long periodId) {
    //Build a Workbook
    try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
      Sheet sheet = workbook.createSheet("Export");

      // Header row
      int c = 0;
      Row headerRow = sheet.createRow(0);
      headerRow.createCell(c++).setCellValue("Periodo");
      headerRow.createCell(c++).setCellValue("UO");
      headerRow.createCell(c++).setCellValue("Variável");
      headerRow.createCell(c++).setCellValue("");
      headerRow.createCell(c++).setCellValue("Valor");
      headerRow.createCell(c++).setCellValue("Unidade");
      headerRow.createCell(c++).setCellValue("Classe");
      headerRow.createCell(c++).setCellValue("");
      headerRow.createCell(c++).setCellValue("Fonte");
      headerRow.createCell(c++).setCellValue("Notas");

      // Get data with a stream (query cursor)
      try (Stream<InputData> stream = this.inputDataRepository.streamAll(periodId)) {
        // Loop data
        stream.forEach(inputData -> {
          int cc = 0;
          // Write row
          Row row = sheet.createRow(sheet.getLastRowNum() + 1);
          row.createCell(cc++).setCellValue(inputData.getPeriod().getName());
          row.createCell(cc++).setCellValue(inputData.getOwner().getCode());
          row.createCell(cc++).setCellValue(inputData.getVariable().getCode());
          row.createCell(cc++).setCellValue(inputData.getVariable().getName());
          row.createCell(cc++).setCellValue(
              inputData.getImputedValue() != null ? inputData.getImputedValue().setScale(8).toPlainString() : "");
          row.createCell(cc++).setCellValue(inputData.getUnit().getSymbol());
          row.createCell(cc++).setCellValue(inputData.getVariableClassCode());
          row.createCell(cc++).setCellValue(inputData.getVariableClassName());
          row.createCell(cc++).setCellValue(inputData.getDataSource());
          row.createCell(cc++).setCellValue(inputData.getDataComments());
        });
      }

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

  @Transactional(readOnly = true)
  public byte[] exportInventory(Long periodId, Long ownerId, Long scopeId, Long categoryId) {
    Period period = this.periodRepository.getReferenceById(periodId);
    Organization owner = this.organizationRepository.getReferenceById(ownerId);

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

      // Header row
      int c = 0;
      Row headerRow = sheet.createRow(0);
      headerRow.createCell(c++).setCellValue("Periodo");
      headerRow.createCell(c++).setCellValue("UO");
      headerRow.createCell(c++).setCellValue("Variável");
      headerRow.createCell(c++).setCellValue("");
      headerRow.createCell(c++).setCellValue("Classe");
      headerRow.createCell(c++).setCellValue("");
      headerRow.createCell(c++).setCellValue("Valor");
      headerRow.createCell(c++).setCellValue("Unidade");

      // Get data with a stream (query cursor)
      try (Stream<InventoryData> stream = this.streamAllInventory(periodId, ownerId, scopeId, categoryId)) {
        // Loop data
        stream.forEach(inventoryData -> {
          int cc = 0;
          // Write row
          Row row = sheet.createRow(sheet.getLastRowNum() + 1);
          row.createCell(cc++).setCellValue(period.getName());
          row.createCell(cc++).setCellValue(owner.getCode());
          row.createCell(cc++).setCellValue(inventoryData.getVariable().getCode());
          row.createCell(cc++).setCellValue(inventoryData.getVariable().getName());
          row.createCell(cc++).setCellValue(inventoryData.getVariableClassCode());
          row.createCell(cc++).setCellValue(inventoryData.getVariableClassName());
          row.createCell(cc++).setCellValue(inventoryData.getValue().setScale(8).toPlainString());
          row.createCell(cc++).setCellValue(inventoryData.getUnit().getSymbol());
        });
      }

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

  /**
   * Used ONLY when the Entity is received as DTO. In this case, the relations must be correctly hydrated, because DTO
   * only have ID; Version; Name (or some other functional info)
   * 
   * @param inputData
   */
  private InputData hydrateEntity(InputDataDTO inputDataDTO) {
    // Unmarshall the entity
    InputData inputData = inputDataMapper.toEntity(inputDataDTO);

    // Hydrate relations
    if (inputData.getVariable() != null)
      inputData.setVariable(
          variableRepository.findByIdAndVersion(inputData.getVariable().getId(), inputData.getVariable().getVersion()));
    if (inputData.getPeriod() != null)
      inputData.setPeriod(
          periodRepository.findByIdAndVersion(inputData.getPeriod().getId(), inputData.getPeriod().getVersion()));
    if (inputData.getOwner() != null)
      inputData.setOwner(
          organizationRepository.findByIdAndVersion(inputData.getOwner().getId(), inputData.getOwner().getVersion()));
    if (inputData.getSourceInputData() != null)
      inputData.setSourceInputData(inputDataRepository.findByIdAndVersion(inputData.getSourceInputData().getId(),
          inputData.getSourceInputData().getVersion()));
    if (inputData.getSourceInputDataUpload() != null)
      inputData.setSourceInputDataUpload(inputDataUploadRepository.findByIdAndVersion(
          inputData.getSourceInputDataUpload().getId(), inputData.getSourceInputDataUpload().getVersion()));
    if (inputData.getSourceUnit() != null)
      inputData.setSourceUnit(
          unitRepository.findByIdAndVersion(inputData.getSourceUnit().getId(), inputData.getSourceUnit().getVersion()));
    if (inputData.getUnit() != null)
      inputData.setUnit(
          unitRepository.findByIdAndVersion(inputData.getUnit().getId(), inputData.getUnit().getVersion()));

    return inputData;
  }

  private void evaluateUnitConversion(InputData inputData) throws VariableUnitConverterNotFoundException,
      VariableUnitConverterWithoutFormulaException, VariableUnitConverterMoreThanOneFoundException {
    // STRING type doesn't need convertion
    if (inputData.getSourceUnit().getUnitType().getValueType().equals(UnitValueType.STRING))
      return;

    BigDecimal convertedValue = this.unitConverterService.convertIfNeeded(inputData.getPeriod(),
        inputData.getVariable(), inputData.getSourceUnit(), inputData.getSourceValue(),
        inputData.getVariableClassCode());

    inputData.setVariableValue(convertedValue);
    inputData.setImputedValue(convertedValue);
  }

  public List<InventoryDataDTO> searchInventory(Long periodId, Long ownerId, Long scopeId, Long categoryId) {
    List<InventoryData> inventoryData = streamAllInventory(periodId, ownerId, scopeId, categoryId).collect(
        Collectors.toList());

    List<InventoryDataDTO> entityList = inventoryDataMapper.toDto(inventoryData);
    return entityList;
  }

  private Stream<InventoryData> streamAllInventory(Long periodId, Long ownerId, Long scopeId, Long categoryId) {
    boolean hideForMainOrg = false;

    // Must convert the ownerId into a list of descendant Orgs
    List<Long> orgIds = new ArrayList<Long>();
    if (ownerId != null) {
      // Always add it self
      orgIds.add(ownerId);

      // And also his children
      List<Organization> orgs = organizationService.getAllChildrenOf(ownerId, OrganizationNature.ORGANIZATION);
      orgs.stream().forEach(org -> orgIds.add(org.getId()));

      //Set flag for main org
      Organization owner = organizationRepository.getReferenceById(ownerId);
      if (owner.getParent() == null) {
        // Its top ORG, define it as the main org
        hideForMainOrg = true;
      }
    }

    return inputDataRepository.streamAllInventory(periodId, orgIds.isEmpty() ? null : orgIds, scopeId, categoryId,
        hideForMainOrg);
  }
}
