/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.wizard.model2.report;

import nz.org.riskscape.wizard.AskerHints;
import static nz.org.riskscape.wizard.model2.DslHelper.*;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import nz.org.riskscape.engine.output.Format;
import nz.org.riskscape.rl.TokenTypes;
import nz.org.riskscape.rl.ast.Expression;
import nz.org.riskscape.rl.ast.PropertyAccess;
import nz.org.riskscape.rl.ast.StructDeclaration;
import nz.org.riskscape.wizard.Choice;
import nz.org.riskscape.wizard.ExpressionHelper;
import nz.org.riskscape.wizard.QuestionSet;
import nz.org.riskscape.wizard.Survey;
import nz.org.riskscape.wizard.bind.AggregationExpression;
import nz.org.riskscape.wizard.bld.dsl.IncompletePipelineChange;
import nz.org.riskscape.wizard.bld.dsl.PipelineChangeInput;
import nz.org.riskscape.wizard.bld.IncrementalBuildState;
import nz.org.riskscape.wizard.bld.PipelineChange;
import nz.org.riskscape.engine.i18n.TranslationContext;
import nz.org.riskscape.wizard.model2.analysis.AnalysisPhase;
import nz.org.riskscape.wizard.model2.input.FilterByAttribute;
import nz.org.riskscape.wizard.survey2.BasePhase;
import nz.org.riskscape.wizard.survey2.Choices;
import nz.org.riskscape.wizard.survey2.DefaultQuestionSet2;

public class ReportPhase extends BasePhase {

  private static final String REPORT_NAME_QSET = "reports";
  private static final String REPORT_NAME_Q = "name";

  private static final String DEFAULT_REPORT = "event-impact";

  public ReportPhase(Survey survey) {
    super(survey);
  }

  @Override
  public List<QuestionSet> getAvailableQuestionSets(IncrementalBuildState buildState) {
    if (!buildState.getRealizedPipeline().getStep(AnalysisPhase.LAST_STEP_NAME).isPresent()) {
      return Collections.emptyList();
    }

    List<QuestionSet> questionSets = new ArrayList<>();

    // add a question-set to customize each individual report/output we have
    for (String reportName : getReportNames(buildState)) {
      if (!buildState.isQuestionSetAnswered("report-" + reportName)) {
        questionSets.add(makeQuestionSet(reportName));
      }
    }

    // let the user optionally specify more report outputs once they added one
    if (buildState.isQuestionSetAnswered("report-" + DEFAULT_REPORT)
        && !buildState.isQuestionSetAnswered(REPORT_NAME_QSET)) {
      DefaultQuestionSet2 qsReportNames = new DefaultQuestionSet2(REPORT_NAME_QSET, this);
      qsReportNames.addQuestion(REPORT_NAME_Q, String.class)
        .customizeQuestion(q -> q.optionalMany())
        .thenNoChange();
      questionSets.add(qsReportNames);
    }

    return questionSets;
  }

  private QuestionSet makeQuestionSet(String reportName) {
    DefaultQuestionSet2 questions = new DefaultQuestionSet2("report", reportName, this);

    questions.addHiddenQuestion("start")
      .then(input ->
        PipelineChange.newChain("%s -> select({*}) as \"report_%s\"", AnalysisPhase.LAST_STEP_NAME, reportName)
    );

    questions.addQuestion("filter", Expression.class)
    .customizeQuestion(q -> q.optionalOne().withAnnotations(
        ExpressionHelper.TAG_EXPRESSION_TYPE, ExpressionHelper.TAG_EXPRESSION_TYPE_BOOLEAN
    ).withAskAs(FilterByAttribute.ASK_AS))
    .then((input, filterExpression) -> PipelineChange.chainStep(
        "filter(%s)",
        filterExpression.toSource()
    ));

    questions.addQuestion("group-by", StructDeclaration.class)
      .customizeQuestion(q -> q.optionalMany()
              .withChoices(bs -> {

                // We are going to augment the list of attributes generated from the input scope with a magic total
                ArrayList<Choice> choices = new ArrayList<>(Choices.from(q, bs));

                // Adds a choice that aggregates by a constant text value, rather than an actual attribute - this is
                // cute, but only works if the user doesn't pick other choices as well - See #1334
                Choice total = new Choice(
                        TokenTypes.quoteText("Total"),
                        "Total",
                        Optional.of("Group all results together"),
                        TokenTypes.quoteText("Total")) {

                  @Override
                  public String getLabel(TranslationContext context) {
                    return context.getMessage("phase.report.total.label", null, "Total");
                  }

                  @Override
                  public String getDescription(TranslationContext context) {
                    return context.getMessage("phase.report.total.desc", null, "Group all results together");
                  }
                };

                choices.add(total);
                return choices;
              }))
      .thenNoChange();

    questions.addQuestion("aggregate", StructDeclaration.class)
      .customizeQuestion(q -> q
        .optionalMany()
        .withAskAs(AggregationExpression.ASK_AS)
      )
      .dependsOn("group-by")
      .thenNoChange();

    questions.addQuestion("bucket-by", PropertyAccess.class)
      .customizeQuestion(q -> q.optionalOne()
              .withAnnotations(AskerHints.PICK_ATTRIBUTE, AskerHints.NUMERIC))
      .dependsOn("group-by")
      .thenNoChange();

    questions.addQuestion("bucket-range", Double.class)
      .customizeQuestion(q -> q.atLeastOne())
      .dependsOn("bucket-by")
      .thenNoChange();

    questions.addQuestion("bucket-aggregation", StructDeclaration.class)
      .customizeQuestion(q -> q
        .atLeastOne()
        .withAskAs(AggregationExpression.ASK_AS)
      )
      .dependsOn("bucket-range")
      .thenNoChange();

    questions.addQuestion("bucket-name", String.class)
      .customizeQuestion(q -> q.requiredOne())
      .dependsOn("bucket-aggregation")
      .thenNoChange();

    questions.addHiddenQuestion("finish-aggregate")
      .dependsOn("group-by")
      .then(input -> aggregationStepPipelineChange(input, questions.getId()));

    questions.addQuestion("select", StructDeclaration.class)
    .customizeQuestion(q -> q.optionalMany())
    .thenForEach((input, responses) ->
      // combine all the select answers into a single expression
      PipelineChange.chainStep("select({ %s })", asStructMembers(responses))
    );

    questions.addQuestion("sort-by", SortExpression.class)
      .customizeQuestion(q -> q.optionalMany())
      .thenForEach((input, responses) -> {
        String attributes = responses.stream()
            .map(se -> se.attribute.toSource())
            .collect(Collectors.joining(", "));
        String directions = responses.stream().map(se -> se.direction.name())
            .collect(Collectors.joining("', '", "['", "']"));

        return PipelineChange.chainStep("sort([%s], direction: %s)",
            attributes, directions);
      }
    );

    questions.addQuestion("format", Format.class)
      .customizeQuestion(q -> q.optionalOne())
      .then((input, format) ->
        PipelineChange.chainStep("save(name: '%s', format: '%s') as \"save_%s\"",
            reportName, format.getId(), reportName)
    );

    questions.addHiddenQuestion("finish")
      .ifNotAnswered("format")
      .then(input ->
        PipelineChange.chainStep("save(name: '%s') as \"save_%s\"", reportName, reportName)
    );
    return questions;
  }

  @Override
  public boolean canSkip(IncrementalBuildState buildState) {
    return true;
  }

  private List<String> getReportNames(IncrementalBuildState buildState) {
    List<String> reports = new ArrayList<>();

    // start off with just an 'event-impact' output by default
    reports.add(DEFAULT_REPORT);

    // add in as many other reports as the user wants
    List<String> names = buildState.getAnswersTo(REPORT_NAME_QSET, REPORT_NAME_Q, String.class);

    // make the IDs safer to use as question-set IDs, filenames, etc
    names = names.stream()
        .map(str -> str.replaceAll(" ", "-"))
        .collect(Collectors.toList());

    reports.addAll(names);

    return reports;
  }


  /**
   * Generate the pipeline code for the group-by-expression.
   * In a separate function because we have to deal with the added complexity of
   * users answering the simple aggregation, the bucket aggregation, or both (or neither!)
   */
  private IncompletePipelineChange aggregationStepPipelineChange(PipelineChangeInput input, String questionSetId) {

    IncrementalBuildState buildState = input.getBuildState();

    class AnswerGetter {
      public <T> T getAnswer(String questionName, Class<T> type) {
        return buildState.getAnswerTo(questionSetId, questionName, type);
      }

      public <T> List<T> getAnswers(String questionName, Class<T> type) {
        return buildState.getAnswersTo(questionSetId, questionName, type);
      }
    }
    AnswerGetter getter = new AnswerGetter();

    List<StructDeclaration> groupBys = getter.getAnswers("group-by", StructDeclaration.class);

    ArrayList<String> selects = new ArrayList<>(List.of("*"));

    if (buildState.isResponseGiven(questionSetId, "aggregate")) {
      List<StructDeclaration> aggregateExpressions = getter.getAnswers("aggregate", StructDeclaration.class);
      selects.add(asStructMembers(aggregateExpressions));
    }

    if (buildState.isResponseGiven(questionSetId, "bucket-by")) {
      PropertyAccess bucketBy = getter.getAnswer("bucket-by", PropertyAccess.class);
      List<Double> range = getter.getAnswers("bucket-range", Double.class);
      List<StructDeclaration> aggregateExpressions = getter.getAnswers("bucket-aggregation", StructDeclaration.class);
      String bucketName = getter.getAnswer("bucket-name", String.class);

      String ranges = range.stream().map(bound -> {
        // We ask for double values, but if the user gives us whole numbers there's no
        // reason to clutter up the pipeline (or the column names in the output)
        if (bound % 1 == 0) {
          return String.valueOf(bound.intValue());
        }
        return String.valueOf(bound);
      }).collect(Collectors.joining(", "));

      selects.add(String.format("bucket_range(pick: %s, select: { %s }, range: [ %s ]) as %s",
              bucketBy.toSource(), asStructMembers(aggregateExpressions), ranges, bucketName));
    }

    return PipelineChange.chainStep("group(by: { %s }, select: { %s })",
            asStructMembers(groupBys), String.join(", ", selects));
  }
}
