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

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import com.google.common.collect.Lists;

import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.data.ResolvedBookmark;
import nz.org.riskscape.engine.relation.Relation;
import nz.org.riskscape.engine.relation.TupleIterator;
import nz.org.riskscape.engine.types.Enumeration;
import nz.org.riskscape.engine.types.Floating;
import nz.org.riskscape.engine.types.Integer;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Struct.StructMember;
import nz.org.riskscape.engine.types.Text;
import nz.org.riskscape.problem.ProblemSink;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.types.WithinSet;
import nz.org.riskscape.rl.ast.Expression;
import nz.org.riskscape.wizard.bld.IncrementalBuildState;

/**
 * Helper to access attribute information that's helpful when answering questions that require an
 * {@link Expression} answer. Such as the scope (input struct) that the expression has available to it.
 *
 * This helper has distinct modes of operation which are:
 *
 * 1) context is the Struct produced by the last pipeline step
 *
 * 2) context is the type produced by the step named by {@link #TAG_STEP_NAME}
 *
 * 3) context is based on a bookmark, which was the answer to a previous question whose name is
 *    in {@link #TAG_LAYER_QUESTION_NAME}
 */
public class ExpressionHelper {

  /**
   * Annotation whose value ought to be a {@link Question} name that will contain a {@link Relation}
   * containing {@link ResolvedBookmark}. The helper will then use the relations type as the
   * expression scope.
   */
  public static final String TAG_LAYER_QUESTION_NAME = "expression-helper-layer-question";

  /**
   * Annotation whose value is the member name that layer will be stored in.
   */
  public static final String TAG_LAYER_NAME = "expression-helper-feature-member";

  /**
   * Annotation key whose value determines the type of expression required.
   */
  public static final String TAG_EXPRESSION_TYPE = "expression-helper-expression-type";

  /**
   * Value for {@link #TAG_EXPRESSION_TYPE} to require an expression that should return a boolean
   * when evaluated.
   */
  public static final String TAG_EXPRESSION_TYPE_BOOLEAN = "boolean";

  /**
   * Value for {@link #TAG_EXPRESSION_TYPE} to require an expression that should return a numeric value
   * when evaluated.
   */
  public static final String TAG_EXPRESSION_TYPE_NUMERIC = "numeric";

  /**
   * Represents an attribute (member) of the context struct that could be
   * referenced in an expression.
   */
  @RequiredArgsConstructor
  @EqualsAndHashCode
  public static class AttributeMetadata {

    /**
     * The StructMember itself, along with any parent struct(s) that contain it
     */
    private final List<StructMember> memberHierarchy;

    /**
     * @return the short-form version of the attribute name, e.g. 'geom'
     */
    public String getName() {
      return getMember().getKey();
    }

    /**
     * @return the qualified path to the attribute, e.g. 'hazards.sampled.geom'
     */
    public String getFullyQualifiedName() {
      return memberHierarchy.stream()
          .map(StructMember::getKey)
          .collect(Collectors.joining("."));
    }

    /**
     * return the StructMember that represents this attribute
     */
    private StructMember getMember() {
      // the member itself is at the end of the list (bottom of hierarchy)
      return memberHierarchy.get(memberHierarchy.size() - 1);
    }

    /**
     * @return the {@link Type} of the attribute
     */
    public Type getType() {
      return getMember().getType();
    }

    private boolean isA(Class<? extends Type> type) {
      return getType().findAllowNull(type).isPresent();
    }

    private boolean oneOf(List<Class<? extends Type>> types) {
      for (Class<? extends Type> type : types) {
        if (isA(type)) {
          return true;
        }
      }
      return false;
    }
  }

  /**
   * Represents the value that an attribute may contain. This could be potential values in the case
   * of {@link WithinSet} or {@link Enumeration} types. For other types it could be a value that has
   * been found from the actual input data being used.
   */
  @Data
  @RequiredArgsConstructor(access = AccessLevel.PACKAGE)
  public static class AttributeValue implements Comparable<AttributeValue> {

    /**
     * The value, or at least a {@link String} that could be parsed as an expression to the required
     * value. Type is {@link Comparable} to allow for sensible sorting of the value set.
     */
    private final Comparable value;
    private final String label;

    AttributeValue(Comparable value) {
      this.value = value;
      this.label = value.toString();
    }

    @Override
    @SuppressWarnings("unchecked")
    public int compareTo(AttributeValue other) {
      // we sort on values
      return value.compareTo(other.value);
    }
  }

  /**
   * Creates an attribute helper based on the context struct deduced from the given question/answers.
   * @return an optional expression helper if question/answers contains required metadata, empty otherwise
   */
  public static Optional<ExpressionHelper> create(IncrementalBuildState buildState, Question question) {
    Optional<String> layerQuestion = question.getAnnotation(TAG_LAYER_QUESTION_NAME);
    if (layerQuestion.isPresent()) {

      ResolvedBookmark bookmark = layerQuestion
          .map(name -> buildState.getAnswerTo(buildState.getQuestionSet().getId(), name, ResolvedBookmark.class))
          .orElse(null);

      Optional<String> layerName = question.getAnnotation(TAG_LAYER_NAME);
      if (bookmark != null) {
        if (layerName.isPresent()) {
          return bookmark.getData(Relation.class)
              .map(relation -> Optional.of(new ExpressionHelper(question, relation, layerName.get())))
              .orElse(Optional.empty());
        } else {
          return bookmark.getData(Relation.class)
              .map(relation -> Optional.of(new ExpressionHelper(question, relation)))
              .orElse(Optional.empty());
        }
      }
    }

    // default to using whatever struct is available as input to this question
    // (note we need the orElse fallback to keep the unit tests happy)
    // Struct inputType = buildState.getInputStruct(question);

    return Optional.of(new ExpressionHelper(question, buildState.getInputStruct(question)));
  }

  /**
   * The {@link Question} that this helper is to help answer.
   */
  @Getter
  private final Question question;

  /**
   * The fully qualified context that expressions would have available when they are executed.
   */
  @Getter
  private final Struct context;

  /**
   * An optional relation that would return data of the type of the single context entry.
   * For example context would be expected to be: '{member-name: relationType}'
   * The underlying relation is useful for extracting possible values from the input data.
   */
  private final Optional<Relation> relation;

  /**
   * Creates a helper for the given struct context. In this case there is no underlying relation
   * associated with the struct.
   */
  public ExpressionHelper(Question question, Struct context) {
    this.question = question;
    this.context = context;
    this.relation = Optional.empty();
  }

  /**
   * Creates a helper for building expressions where only the relation's type is available, but we
   * know the layerName that the relation struct will take in the pipeline (and so can infer what the
   * struct will look like when the pipeline is run).
   */
  ExpressionHelper(Question question, Relation relation, String layerName) {
    this.question = question;
    // wrap up the relation type as a struct (i.e. how it will appear in the pipeline tuple)
    this.context = Struct.of(layerName, relation.getType());
    this.relation = Optional.of(relation);
  }

  /**
   * Creates a helper for building expressions based on a relation that we haven't loaded into
   * the pipeline yet. In this case, the relation has no input() 'name' wrapping it in a struct.
   */
  ExpressionHelper(Question question, Relation relation) {
    this.question = question;
    this.context = relation.getType();
    this.relation = Optional.of(relation);
  }

  /**
   * Obtains a list of attributes that are found by re-cursing into the context struct.
   *
   * @param allowedTypes that attributes must be to be included in returned list. If empty all types
   * are allowed.
   * @return list of found attributes that are of an allowed type
   */
  public List<AttributeMetadata> getAttributeMetadata(List<Class<? extends Type>> allowedTypes) {
    List<AttributeMetadata> attributes = new ArrayList<>();
    findAllAttributes(context, Collections.emptyList(), attr -> attributes.add(attr));

    if (allowedTypes == null || allowedTypes.isEmpty()) {
      return attributes;
    }

    return attributes.stream()
        .filter(attr -> attr.oneOf(allowedTypes))
        .collect(Collectors.toList());
  }

  /**
   * Returns a complete list of attributes (included those nested within structs) that contained in
   * the context struct.
   */
  public List<AttributeMetadata> getAttributeMetadata() {
    return getAttributeMetadata(Collections.emptyList());
  }

  /**
   * Finds all attribute of currentContext and adds them to the attributeConsumer. Re-curses if a
   * struct attribute is found.
   */
  private void findAllAttributes(Struct currentContext, List<StructMember> parents,
      Consumer<AttributeMetadata> attributeComsumer) {
    for (StructMember member : currentContext.getMembers()) {
      List<StructMember> pathList = Lists.newArrayList(parents);
      pathList.add(member);
      Optional<Struct> isStruct = member.getType().findAllowNull(Struct.class);
      if (isStruct.isPresent()) {
        findAllAttributes(isStruct.get(), pathList, attributeComsumer);
        attributeComsumer.accept(new AttributeMetadata(pathList));
      } else {
        attributeComsumer.accept(new AttributeMetadata(pathList));
      }
    }
  }

  /**
   * Attempt to get a list of possible values of the desired attribute.
   *
   * In the case of types with a restricted set of allowed values such as {@link WithinSet} or
   * {@link Enumeration} then the values are those allowed by the type system.
   *
   * Otherwise the relation (if present) will be scanned, for the values that are actually present
   * in the data.
   *
   * @param attribute attribute to get possible values of
   * @return list of possible values, or empty if no list could be generated
   */
  public List<AttributeValue> getValues(@NonNull AttributeMetadata attribute) {
    List<AttributeValue> values = doGetValues(attribute);
    // listing the values in alphabetical order is nicer for the user
    Collections.sort(values);
    return values;
  }

  private List<AttributeValue> doGetValues(@NonNull AttributeMetadata attribute) {
    if (attribute.isA(WithinSet.class)) {
      return valuesFrom(attribute.getType().find(WithinSet.class).get());
    } else if (attribute.isA(Enumeration.class)) {
      return valuesFrom(attribute.getType().find(Enumeration.class).get());
    }
    if (!relation.isPresent()) {
      return Collections.emptyList();
    }
    List<Comparable> foundValues = new ArrayList<>();
    TupleIterator it = relation.get().skipInvalid(ProblemSink.DEVNULL).iterator();
    while (it.hasNext()) {
      Tuple t = it.next();
      Comparable value = t.fetch(attribute.getMember());
      if (!foundValues.contains(value)) {
        // We only want to add unique values to the list
        foundValues.add(value);
      }
    }
    it.close();

    return foundValues.stream()
        .map(value -> new AttributeValue(value))
        .collect(Collectors.toList());
  }

  private List<AttributeValue> valuesFrom(WithinSet set) {
    return set.getAllowed().stream()
        .map(allowed -> new AttributeValue(allowed.toString()))
        .collect(Collectors.toList());
  }

  private List<AttributeValue> valuesFrom(Enumeration enumeration) {
    return Arrays.stream(enumeration.getValues())
        .map(value -> new AttributeValue(value, value))
        .collect(Collectors.toList());
  }

  /**
   * @return the content of the {@link #TAG_EXPRESSION_TYPE} question annotation, or empty if no annotation given
   */
  public Optional<String> getExpressionType() {
    return question.getAnnotation(TAG_EXPRESSION_TYPE);
  }

  public List<Choice> getAttributeChoices() {
    List<Class<? extends Type>> allowedTypes = Collections.emptyList();

    // let questions refine what type of attributes they're interested in
    if (question.hasAnnotationWithValue(PICK_ATTRIBUTE, NUMERIC)) {
      allowedTypes = List.of(Integer.class, Floating.class);
    } else if (question.hasAnnotationWithValue(PICK_ATTRIBUTE, COMPARABLE)) {
      allowedTypes = List.of(Text.class, Integer.class, Floating.class, Enumeration.class);
    }

    return getAttributeMetadata(allowedTypes).stream()
        .map(attr -> new Choice(attr.getFullyQualifiedName(), attr.getFullyQualifiedName(), Optional.empty(), attr))
        .collect(Collectors.toList());
  }
}
