/*
 * 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.survey2;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.google.common.collect.Lists;

import nz.org.riskscape.engine.Identified;
import nz.org.riskscape.engine.IdentifiedCollection;
import nz.org.riskscape.engine.Project;
import nz.org.riskscape.engine.data.ResolvedBookmark;
import nz.org.riskscape.engine.function.IdentifiedFunction;
import nz.org.riskscape.engine.function.IdentifiedFunction.Category;
import nz.org.riskscape.engine.i18n.DefaultMessages;
import nz.org.riskscape.engine.i18n.EnumTranslator;
import nz.org.riskscape.engine.i18n.MessageSource;
import nz.org.riskscape.engine.i18n.TranslationContext;
import nz.org.riskscape.engine.rl.FunctionCallPrototype;
import nz.org.riskscape.engine.rl.agg.AggregationFunction;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.rl.ast.PropertyAccess;
import nz.org.riskscape.rl.ast.StructDeclaration;
import nz.org.riskscape.wizard.AskerHints;
import nz.org.riskscape.wizard.Choice;
import nz.org.riskscape.wizard.ExpressionHelper;
import nz.org.riskscape.wizard.Question;
import nz.org.riskscape.wizard.bld.IncrementalBuildState;

/**
 * Helper for generating suitable {@link Choice}s for wizard questions.
 */
public class Choices {

  // uses the EnumTranslator to find appropriate label/help for an enum entry
  private static Choice toChoice(Enum<?> enumVal) {
    return new Choice(enumVal.name(), enumVal.name(), Optional.empty(), enumVal) {

      public String getDescription(TranslationContext context) {
        EnumTranslator enumTranslator = new EnumTranslator(context);
        return enumTranslator.getEntry(enumVal).getDescription();
      }

      public String getLabel(TranslationContext context) {
        EnumTranslator enumTranslator = new EnumTranslator(context);
        return enumTranslator.getEntry(enumVal).getLabel();
      }
    };
  }

  /**
   * @return choices the given enum entries. Useful when you want to only present
   *         a subset of the enum values to the user
   */
  public static List<Choice> forEnums(List<? extends Enum<?>> enums) {
    return enums.stream()
          .map(e -> toChoice(e))
          .collect(Collectors.toList());
  }

  /**
   * @return choices for all the entries in the given enum
   */
  public static List<Choice> forEnum(Class<? extends Enum<?>> enumClass) {
    return forEnums(Arrays.asList(enumClass.getEnumConstants()));
  }

  /**
   * @return choices that are suitable based on the given parameter type
   */
  @SuppressWarnings("unchecked")
  public static List<Choice> from(Question question, IncrementalBuildState buildState) {
    Class<?> parameterType = question.getParameterType();
    if (parameterType.isEnum()) {
      return Choices.forEnum((Class<Enum<?>>) parameterType);
    } else if (parameterType.equals(ResolvedBookmark.class)) {
      return Choices.forBookmarks(buildState);
    } else if (StructDeclaration.class.isAssignableFrom(parameterType)
        || PropertyAccess.class.isAssignableFrom(parameterType)) {
      return Choices.forScope(buildState, question);
    } else if (parameterType.equals(PickQuestionSet.class)) {
      return Lists.transform(buildState.getSurvey().getApplicableQuestionSets(buildState),
          qs -> new PickQuestionSet(qs).asChoice());
    } else if (question.getParameterType().equals(FunctionCallPrototype.class)
        && question.hasAnnotation(AskerHints.AGGREGATION)) {
      return Choices.forAggregationNonInteractive(buildState);
    } else if (Identified.class.isAssignableFrom(parameterType)) {
      return Choices.forIdentified(question, buildState);
    }
    return Collections.emptyList();
  }

  /**
   * @return choices for all the avilable bookmarks in the project
   */
  public static List<Choice> forBookmarks(IncrementalBuildState buildState) {
    return buildState.getProject().getBookmarks().getAll().stream()
      .sorted((b1, b2) -> b1.getId().compareTo(b2.getId()))
      .map(bm -> new Choice(bm.getId(), bm.getId(), Optional.of(bm.getDescription()), bm))
      .collect(Collectors.toList());
  }

  private static Choice toChoice(IdentifiedFunction function, MessageSource helpMessages) {

    Optional<String> description = Optional.of(function.getDescription()).filter(d -> !d.equals(""));
    return new Choice(function.getId(), function.getId(), description, function) {

      public String getDescription(TranslationContext context) {

        // First check if there's already something there
        if (this.description.isPresent()) {
          return description.get();
        }

        // Otherwise check the help messages - needed to get a description for
        // is_exposed, and any other built in functions that we may add in future
        String code = DefaultMessages.getShortCode(function, "description");
        return helpMessages.getMessage(code);
      }
    };
  }

  /**
   * @return choices for any functions in the project that match the given categories
   */
  public static List<Choice> forFunctions(IncrementalBuildState buildState, Collection<Category> matchCategories) {

    MessageSource helpMessages = buildState.getProject().getEngine().getMessages().getHelp();

    return buildState.getProject().getFunctionSet().getAll().stream()
      .filter(f -> matchCategories.contains(f.getCategory()))
      .sorted((f1, f2) -> f1.getId().compareTo(f2.getId()))
      .map(f -> toChoice(f, helpMessages))
      .collect(Collectors.toList());
  }

  /**
   * @return Choices representing the attributes in scope for the current question
   */
  public static List<Choice> forScope(IncrementalBuildState buildState, Question q) {
    // NB: there's still some TAG_LAYER_QUESTION_NAME magic at work here in some cases
    // to use a bookmark input as scope, rather than the current pipeline scope
    return ExpressionHelper.create(buildState, q).get().getAttributeChoices();
  }

  /**
   * @return Choices representing the attributes in scope for the given struct.
   *         This can be useful for getting a subset of attributes, e.g. just the
   *         hazard struct
   */
  public static List<Choice> forScope(Struct input, Question q) {
    return new ExpressionHelper(q, input).getAttributeChoices();
  }

  public static List<Choice> percentiles(Project project) {

    IdentifiedFunction percentile = project.getFunctionSet().get("percentile", project.getProblemSink());
    return Stream.of(75L, 90L, 95L, 99L)
        .map(p -> new Choice(
            String.format("percentile(x, %d)", p),
            String.format("%dth percentile", p),
            Optional.empty(),
            percentile))
        .toList();
  }


  /**
   * Returns a list of choices of aggregation functions for the wizard. Does
   * not include non-maths functions or a few functions considered too complicated
   * for wizard users.
   */
  public static List<Choice> forAggregation(IncrementalBuildState buildState) {

    // these are quite advanced and not really pitched at the wizard's target audience
    List<String> excludeFunctions = List.of("fit_curve", "stack_continuous", "aal_trapz");


    Predicate<IdentifiedFunction> predicate = function -> {
      // we want to restrict the list to functions that are both maths and aggregation functions.
      // this is to prevent non-math aggregation functions like `to_list` being included.
      return function.getCategory() == Category.MATHS
          && function.getAggregationFunction().isPresent()
          && !excludeFunctions.contains(function.getId());
    };

    List<Choice> choices = buildState.getProject().getFunctionSet().getAll().stream()
      .filter(predicate)
      .sorted((f1, f2) -> f1.getId().compareTo(f2.getId()))
      .map(f -> new Choice(f.getId(), f.getId(), Optional.of(f.getDescription()), f))
      .toList();

    return choices;
  }

   /**
   * Returns a list of choices of aggregation functions for the wizard. Does
   * not include non-maths functions or a few functions considered too complicated
   * for wizard users.
   *
   * This non-interactive version replaces the multi-argument percentile function
   * with a few hard-coded percentile choices.
   */
  public static List<Choice> forAggregationNonInteractive(IncrementalBuildState buildState) {
    return Stream.concat(
        // non-interactive asking case, so we can only use functions that take exactly 1 arg here
        forAggregation(buildState).stream()
            .filter(choice -> {
              IdentifiedFunction function  = choice.getDerivedFrom(IdentifiedFunction.class);

              // Some functions (ie min / max are both aggregation and scalar functions)
              // We need to get the aggregation function out, so we can check if that works.
              Optional<AggregationFunction> aggregationFunction = function.getAggregationFunction();

              return aggregationFunction.filter(value -> value.getArguments().size() <= 1).isPresent();
            }),
        // but add in some harcoded percentiles to give the user more options
        percentiles(buildState.getProject()).stream()
    ).toList();
  }

  public static List<Choice> forIdentified(Question question, IncrementalBuildState buildState) {

    Class<? extends Identified> parameterType = question.getParameterType().asSubclass(Identified.class);
    IdentifiedCollection<? extends Identified> collection = buildState.getProject().getCollectionByClass(parameterType);

    return collection.getReferences().stream()
        .map(identified ->
            new Choice(identified.getId(), identified.getId(), Optional.empty(), identified.get())
        ).toList();
  }
}
