/*
 * Copyright (c) 2018 OBiBa. All rights reserved.
 *
 * This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.obiba.mica.search.csvexport;

import au.com.bytecode.opencsv.CSVWriter;
import org.obiba.core.translator.JsonTranslator;
import org.obiba.core.translator.Translator;
import org.obiba.mica.core.domain.LocalizedString;
import org.obiba.mica.micaConfig.service.MicaConfigService;
import org.obiba.mica.network.service.NetworkService;
import org.obiba.mica.search.JoinQueryExecutor;
import org.obiba.mica.search.csvexport.generators.CsvReportGeneratorImpl;
import org.obiba.mica.spi.search.QueryType;
import org.obiba.mica.spi.search.Searcher;
import org.obiba.mica.spi.search.support.JoinQuery;
import org.obiba.mica.study.domain.BaseStudy;
import org.obiba.mica.study.domain.Population;
import org.obiba.mica.study.service.PublishedStudyService;
import org.obiba.mica.study.service.StudyService;
import org.obiba.mica.web.model.Mica;
import org.obiba.mica.web.model.MicaSearch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.inject.Inject;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.io.OutputStream;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.function.Function;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.toList;

@Component
@Scope("request")
public class SpecificStudyReportGenerator extends CsvReportGeneratorImpl {

  private static final Logger logger = LoggerFactory.getLogger(SpecificStudyReportGenerator.class);

  @Inject
  private JoinQueryExecutor joinQueryExecutor;

  @Inject
  private Searcher searcher;

  @Inject
  private MicaConfigService micaConfigService;

  @Inject
  private PublishedStudyService publishedStudyService;

  @Inject
  private StudyService studyService;

  @Inject
  private NetworkService networkService;

  private Translator translator;
  private List<String> studyIds;
  private String locale;

  public void report(Translator translator, List<String> studyIds, String locale, OutputStream outputStream) {
    this.translator = translator;
    this.studyIds = studyIds.stream().sorted().collect(toList());
    this.locale = locale;
    this.write(outputStream);
  }

  public void report(String networkId, String locale, OutputStream outputStream) throws IOException {
    List<String> studyIds = networkService.findById(networkId).getStudyIds();
    Translator translator = JsonTranslator.buildSafeTranslator(() -> micaConfigService.getTranslations(locale, false));

    report(translator, studyIds, locale, outputStream);
  }

  public void report(String rqlQuery, OutputStream outputStream) throws IOException {

    JoinQuery joinQuery = searcher.makeJoinQuery(rqlQuery);

    List<String> studyIds = joinQueryExecutor.query(QueryType.STUDY, joinQuery)
        .getStudyResultDto()
        .getExtension(MicaSearch.StudyResultDto.result)
        .getSummariesList()
        .stream()
        .map(Mica.StudySummaryDto::getId)
        .collect(toList());
    Translator translator = JsonTranslator.buildSafeTranslator(() -> micaConfigService.getTranslations(joinQuery.getLocale(), false));

    report(translator, studyIds, joinQuery.getLocale(), outputStream);
  }

  @Override
  protected void writeHeader(CSVWriter writer) {

    List<String> line = new ArrayList<>();

    line.add(tr("report-group.study.name"));
    line.add(tr("report-group.study.acronym"));
    line.add(tr("report-group.study.type"));
    line.add(tr("report-group.study.status"));
    line.add(tr("report-group.study.country"));
    line.add(tr("report-group.study.start-year"));
    line.add(tr("report-group.study.years-of-follow-up"));
    line.add(tr("report-group.study.study-design"));
    line.add(tr("report-group.study.recruitment-target"));
    line.add(tr("report-group.study.number-of-participants"));
    line.add(tr("report-group.study.number-of-bio-samples"));
    line.add(tr("report-group.study.population.name"));
    line.add(tr("report-group.study.population.source-of-recruitment"));
    line.add(tr("report-group.study.population.minimum-age"));
    line.add(tr("report-group.study.population.maximum-age"));
    line.add(tr("report-group.study.population.number-of-participants"));
    line.add(tr("report-group.study.population.number-of-bio-samples"));
    line.add(tr("report-group.study.population.number-of-data-collection-events"));

    writer.writeNext(line.toArray(new String[line.size()]));
  }

  @Override
  protected void writeEachLine(CSVWriter writer) {

    for (String studyId : studyIds) {
      BaseStudy publishedStudy = publishedStudyService.findById(studyId);

      if (publishedStudy != null) {

        List<String> publishedStudyDetails = generatePublishedStudyDetails(publishedStudy);

        Iterator<Population> populationIterator = publishedStudy.getPopulations().iterator();

        if (!populationIterator.hasNext()) {
          writer.writeNext(publishedStudyDetails.toArray(new String[publishedStudyDetails.size()]));
        }

        while (populationIterator.hasNext()) {
          List<String> buildingCompleteLine = new ArrayList<>(publishedStudyDetails);
          Population next = populationIterator.next();
          buildingCompleteLine.addAll(generatePopulationDetails(next));
          writer.writeNext(buildingCompleteLine.toArray(new String[buildingCompleteLine.size()]));
        }
      } else {

        BaseStudy draftStudy = studyService.findStudy(studyId);
        if (draftStudy != null) {
          List<String> lineOfDratStudy = generateDraftStudyDetails(draftStudy);
          writer.writeNext(lineOfDratStudy.toArray(new String[lineOfDratStudy.size()]));
        }
      }
    }
  }

  private Map<String, Locale> getCountries() {
    return Arrays.stream(Locale.getAvailableLocales()).collect(Collectors.toMap(locale -> {
      try {
        return locale.getISO3Country();
      } catch (RuntimeException e) {
        return locale.getCountry();
      }
    }, Function.identity(), (a, b) -> a));
  }

  private String getCountryName(String iso3Country) {
    try {
      return getCountries().get(iso3Country).getDisplayCountry(new Locale(this.locale));
    } catch (RuntimeException e) {
      logger.debug("Translation of country %s is not available in java database", e);
      return iso3Country;
    }
  }

  private List<String> generateDraftStudyDetails(BaseStudy study) {
    List<String> line = new ArrayList<>();

    line.add(localizedField(study::getName));
    line.add(localizedField(study::getAcronym));
    line.add(field(() -> tr("global." + study.getResourcePath())));
    line.add(field(() -> tr("draft")));

    return line;
  }

  private List<String> generatePublishedStudyDetails(BaseStudy study) {
    List<String> line = new ArrayList<>();

    line.add(localizedField(study::getName));
    line.add(localizedField(study::getAcronym));
    line.add(field(() -> tr("global." + study.getResourcePath())));
    line.add(field(() -> tr("published")));
    line.add(arrayField(() -> study.getPopulations().stream().flatMap(population -> {
      Map<String, List<String>> selectionCriteria = (Map<String, List<String>>) population.getModel().get("selectionCriteria");
      return selectionCriteria.get("countriesIso").stream().map(this::getCountryName);
    }).distinct().sorted().collect(toList())));
    line.add(field(() -> study.getModel().get("startYear").toString()));
    line.add(field(() -> calculateYearsOfFollowUp(study)));
    line.add(field(() -> tr("study_taxonomy.vocabulary.methods-design.term." + ((Map<String, Object>) study.getModel().get("methods")).get("design").toString() + ".title")));
    line.add(arrayField(() -> ((List<String>) ((Map<String, Object>)
        study.getModel().get("methods")).get("recruitments"))
        .stream().map(m -> tr("study_taxonomy.vocabulary.methods-recruitments.term." + m + ".title")).sorted().collect(toList())));
    line.add(field(() -> ((Map<String, Object>) ((Map<String, Object>)
        study.getModel().get("numberOfParticipants")).get("participant")).get("number").toString()));
    line.add(field(() -> ((Map<String, Object>) ((Map<String, Object>)
        study.getModel().get("numberOfParticipants")).get("sample")).get("number").toString()));

    return line;
  }

  @NotNull
  private List<String> generatePopulationDetails(Population population) {
    List<String> line = new ArrayList<>();

    line.add(localizedField(() -> population.getName()));
    line.add(arrayField(() -> ((List<String>) ((Map<String, Object>) population.getModel().get("recruitment")).get("dataSources"))
        .stream().map(source -> tr("study_taxonomy.vocabulary.populations-recruitment-dataSources.term." + source + ".title")).sorted().collect(toList())));
    line.add(field(() -> ((Map<String, Object>) population.getModel().get("selectionCriteria")).get("ageMin").toString()));
    line.add(field(() -> ((Map<String, Object>) population.getModel().get("selectionCriteria")).get("ageMax").toString()));
    line.add(field(() -> ((Map<String, Object>) ((Map<String, Object>)
        population.getModel().get("numberOfParticipants")).get("participant")).get("number").toString()));
    line.add(field(() -> ((Map<String, Object>) ((Map<String, Object>)
        population.getModel().get("numberOfParticipants")).get("sample")).get("number").toString()));
    line.add(field(() -> Integer.toString(population.getDataCollectionEvents().size())));

    return line;
  }

  private String calculateYearsOfFollowUp(BaseStudy studySummaryDto) {
    if (studySummaryDto.getModel().get("startYear") == null)
      return "";
    int startYear = (int) studySummaryDto.getModel().get("startYear");
    int endYear = studySummaryDto.getModel().get("endYear") != null ? (int) studySummaryDto.getModel().get("endYear") : Calendar.getInstance().get(Calendar.YEAR);
    return Integer.toString(endYear - startYear);
  }

  private String tr(String key) {
    return translator.translate(key);
  }

  private String field(Callable<String> field) {
    try {
      return field.call();
    } catch (Exception e) {
      logger.debug("Error while generating csv custom report", e);
      return "";
    }
  }

  private String localizedField(Callable<LocalizedString> field) {
    return field(() -> field.call().get(this.locale));
  }

  private String arrayField(final Callable<List<String>> field) {
    return field(() -> {
      List<String> fields = field.call();
      return StringUtils.arrayToDelimitedString(fields.toArray(new String[fields.size()]), ", ");
    });
  }
}