/*
 * 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.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;

import lombok.Getter;
import nz.org.riskscape.wizard.QuestionSet;
import nz.org.riskscape.wizard.Survey;
import nz.org.riskscape.wizard.bld.IncrementalBuildState;

/**
 * Base implementation of a phase that takes care of the links between the survey, they phase and the question sets.
 */
public class BasePhase implements Phase {

  /**
   * Create a BasePhase that wraps a single question set
   * @param qsId id to assign to the question set
   * @param skippable true if this question set, and thus the entire phase, can be skipped
   * @param consumer a function that allows you to add questions to the question set that belongs to the phase
   */
  public static BasePhase simple(
      Survey survey,
      String qsId,
      boolean skippable,
      Consumer<DefaultQuestionSet2> consumer
  ) {
    return new BasePhase(survey, phase -> {
      DefaultQuestionSet2 qs = new DefaultQuestionSet2(qsId, phase);
      consumer.accept(qs);

      if (skippable) {
        phase.skippableQuestionSets.add(qsId);
      }
      return Arrays.asList(qs);
    });
  }

  @Getter
  protected final Survey survey;

  protected final Set<String> skippableQuestionSets = new HashSet<>();

  protected List<? extends QuestionSet> builtQuestionSets;

  private Object questionSetsBuiltWith;

  public BasePhase(Survey survey) {
    this(survey, null);
  }

  public BasePhase(Survey survey2, Function<BasePhase, List<DefaultQuestionSet2>> constructor) {
    this.survey = survey2;

    if (constructor != null) {
      questionSetsBuiltWith = Boolean.FALSE;
      this.builtQuestionSets = constructor.apply(this);
    }
  }

  /**
   * @return true if there are no more question sets that can be answered
   */
  public boolean isComplete(IncrementalBuildState buildState) {
    return getAvailableQuestionSets(buildState).isEmpty();
  }

  /**
   * Default implementation of {@link Phase#getAvailableQuestionSets(IncrementalBuildState)} that builds a static
   * list of question sets the first time it sees a build state and then filters them based on what's already been
   * answered
   *
   */
  public List<QuestionSet> getAvailableQuestionSets(IncrementalBuildState buildState) {
    // we build the question sets if they are empty OR if it looks like the questions we built are no longer valid
    // because the build state we built them from is not present in the undo stack of the given buildState
    if (builtQuestionSets == null || builtQuestionsInvalid(buildState)) {
      builtQuestionSets = buildQuestionSets(buildState);
      questionSetsBuiltWith = buildState;
    }

    return builtQuestionSets.stream()
        .filter(qs -> !buildState.isQuestionSetAnswered(qs.getId()))
        .collect(Collectors.toList());
  }

  /**
   * Determine whether question sets need rebuilding, will always return false if the phase was constructed with a
   * single static question set.  Returns true if the given build state doesn't contain the build state (in its stack)
   * that the phase used to build the question sets it already has.
   */
  protected boolean builtQuestionsInvalid(IncrementalBuildState buildState) {
    return questionSetsBuiltWith != Boolean.FALSE
        && !buildState.buildStateStream().anyMatch(bs -> bs == questionSetsBuiltWith);
  }

  protected List<QuestionSet> buildQuestionSets(IncrementalBuildState buildState) {
    return Collections.singletonList(buildQuestionSet(buildState));
  }

  protected QuestionSet buildQuestionSet(IncrementalBuildState buildState) {
    throw new RuntimeException("buildQuestionSets not overridden and no static constructor was supplied");
  }

  /**
   * @return true if the remaining question sets in this phase can be skipped, or false if they must be answered to move
   * on to the next phase
   */
  public boolean canSkip(IncrementalBuildState buildState) {
    return getAvailableQuestionSets(buildState).stream().allMatch(qs -> skippableQuestionSets.contains(qs.getId()));
  }

  /**
   * @return true if the given question set belongs to this phase.  Used by the survey code to track a build state
   * back to a phase
   */
  public boolean contains(QuestionSet questionSet) {
    return Objects.equals(questionSet.getPhase(), this);
  }

  @Override
  public Optional<String> getName(Locale locale) {
    // whilst this class is not abstract direct uses of this class are test only. All non test use
    // is via overriding classes such as AnalysisPhase

    // NB drop off the word phase - it's needless extra typing
    String key = this.getClass().getSimpleName().replace("Phase", "");
    return Optional.ofNullable(getSurvey().getMessageSource()
        .getMessage(String.format("phase.%s.name", key), new Object[0], key, locale));
  }
}
