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

import static nz.org.riskscape.wizard.ExpressionHelper.*;

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

import lombok.AccessLevel;
import lombok.Getter;
import nz.org.riskscape.engine.i18n.MessageFactory;
import nz.org.riskscape.engine.i18n.TranslationContext;
import nz.org.riskscape.engine.types.Floating;
import nz.org.riskscape.engine.types.Integer;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.ast.Expression;
import nz.org.riskscape.wizard.Answer;
import nz.org.riskscape.wizard.CliChoice;
import nz.org.riskscape.wizard.CliPrompter;
import nz.org.riskscape.wizard.ExpressionHelper;
import nz.org.riskscape.wizard.ExpressionHelper.AttributeMetadata;
import nz.org.riskscape.wizard.ExpressionHelper.AttributeValue;
import nz.org.riskscape.wizard.Question;
import nz.org.riskscape.wizard.bld.IncrementalBuildState;
import nz.org.riskscape.wizard.survey2.Choices;

/**
 * An {@link Asker} for getting an {@link Expression} answer that allows the user to either enter the
 * expression themselves, or to build the expression interactively via
 * {@link #askInteractively(nz.org.riskscape.wizard.CliPrompter, nz.org.riskscape.wizard.ExpressionHelper) }
 *
 * Intended to be extended to create more specific askers by overriding:
 * - {@link #askInteractively(nz.org.riskscape.wizard.CliPrompter, nz.org.riskscape.wizard.ExpressionHelper) }
 * - {@link #getAllowedExpressionTypes() }
 */
public class ExpressionAsker extends BaseAsker {

  public interface Messages extends MessageFactory {

    String chooseAttribute();

    String answerType(String expressionType);

    String enterExpression(String expressionType);
  }

  /**
   * Determines whether the user wants to provide a custom expression manually, or build an expression
   * interactively in the wizard.
   */
  public enum AnswerType {
    INTERACTIVELY,
    CUSTOM
  }

  /**
   * Used by {@link #canAsk(IncrementalBuildState, Question)} to
   * check if the question is for a supported type. Should be overridden if canAsk() is otherwise correct.
   *
   * @return list of TAG_EXPRESSION_TYPE values that are supported by the asker
   */
  @Getter(AccessLevel.PROTECTED)
  private final List<String> allowedExpressionTypes;
  protected final Messages expressionMessages;

  public ExpressionAsker(TranslationContext messageSource, List<String> allowedExpressionTypes) {
    super(messageSource);
    this.allowedExpressionTypes = allowedExpressionTypes;
    this.expressionMessages = messageSource.newFactory(Messages.class);
  }

  /**
   * Get a list of the {@link AttributeMetadata} {@link CliChoice}s that are available and of an allowedType.
   *
   * @param allowedTypes types that attributes must contain to be allowed in returned list
   * @param helper to obtain attributes from
   * @return list of found attributes
   */
  protected List<CliChoice<AttributeMetadata>> getAttributeChoices(List<Class<? extends Type>> allowedTypes,
      ExpressionHelper helper) {
    return helper.getAttributeMetadata(allowedTypes).stream()
        .map(attr -> new CliChoice<>(attr.getFullyQualifiedName(), attr))
        .collect(Collectors.toList());
  }

  /**
   * @return a list of possible values (as {@link Choices} for the given attribute.
   * This is only possible if the attribute's type has a predetermined set of values
   * (i.e. enum or WithinSet), or we have the underlying relation data we can look at.
   */
  protected List<CliChoice<String>> getValueChoices(AttributeMetadata member, ExpressionHelper helper) {
    List<AttributeValue> values = helper.getValues(member);
    if (values.isEmpty()) {
      return Collections.emptyList();
    }
    List<CliChoice<String>> choiceValues = values.stream()
        .map(s -> new CliChoice<>(s.getLabel(), Objects.toString(s.getValue())))
        .collect(Collectors.toList());

    return choiceValues;
  }

  /**
   * Build an expression interactively in the wizard, i.e. user picks what attribute to use, how to
   * transform it, etc.
   */
  protected String askInteractively(CliPrompter cliPrompter, ExpressionHelper helper) {
    List<Class<? extends Type>> allowedTypes = new ArrayList<>();
    if (TAG_EXPRESSION_TYPE_NUMERIC.equals(helper.getExpressionType().orElse(""))) {
      allowedTypes.add(Integer.class);
      allowedTypes.add(Floating.class);
    }

    CliChoice<AttributeMetadata> targetAttr = cliPrompter.choose(expressionMessages.chooseAttribute(),
        getAttributeChoices(
            allowedTypes,
            helper
        )
    );
    return targetAttr.data.getFullyQualifiedName();
  }

  @Override
  public boolean canAsk(IncrementalBuildState buildState, Question question) {
    return Expression.class.isAssignableFrom(question.getParameterType())
        && (
            // if there aren't any, that means all are tags are fine - allow the base class
            // to act as a catch-all
            getAllowedExpressionTypes().size() > 0
            && getAllowedExpressionTypes().contains(question.getAnnotation(TAG_EXPRESSION_TYPE).orElse(null))
        )
        && ExpressionHelper.create(buildState, question).isPresent();
  }

  @Override
  public ResultOrProblems<Answer.Response> ask(AskRequest input) {
    CliPrompter prompter = input.getCliPrompter();
    IncrementalBuildState state = input.getBuildState();
    Question question = input.getQuestion();

    ExpressionHelper helper = ExpressionHelper.create(state, question).get();

    prompter.getOut().printlnMarkup(question.getDescription(prompter.getLocale()).orElse(question.getId())).println();

    String expressionString = null;

    switch (prompter.choose(
        expressionMessages.answerType(getAnswerTypeLabel(helper)),
        getChoices(AnswerType.class)
        ).data) {
    case CUSTOM:
      expressionString = prompter.readlineWithTitle(expressionMessages.enterExpression(getAnswerTypeLabel(helper)));
      break;
    case INTERACTIVELY:
      expressionString = askInteractively(prompter, helper);
      break;
    default:
      // it would be a programming error to have an unhandled choice
      throw new RuntimeException("Unhandled choice");
    }

    return Answer.bind(input.getBindingContext(), question, expressionString);
  }

  /**
   * @param helper
   * @return a label for the kink of answer that the user should supply
   */
  private String getAnswerTypeLabel(ExpressionHelper helper) {
    switch (helper.getExpressionType().orElse("")) {
      case TAG_EXPRESSION_TYPE_BOOLEAN:
        return "true/false value";
      case TAG_EXPRESSION_TYPE_NUMERIC:
        return "number";
      default:
        // Programmer error, new expression types should have a mapping here
        throw new RuntimeException("Unhandled expression type: " + helper.getExpressionType());
    }
  }

}
