/*
 * 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.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

import lombok.Getter;
import nz.org.riskscape.engine.i18n.MessageSource;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.engine.util.Pair;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.wizard.Answer;
import nz.org.riskscape.wizard.EmptyQuestionSet;
import nz.org.riskscape.wizard.Question;
import nz.org.riskscape.wizard.QuestionSet;
import nz.org.riskscape.wizard.Survey;
import nz.org.riskscape.wizard.bld.IncrementalBuildState;
import nz.org.riskscape.wizard.bld.PipelineChange;

/**
 * Convenient super class for a phase-based {@link Survey}.
 *
 * Can also be instantiated, but that should be limited to
 * test cases - sub classing makes it easier to organize your survey.
 */
public class BaseSurvey implements Survey {

  @Getter
  protected final String id;

  @Getter
  protected final MessageSource messageSource;

  @Getter
  protected final List<BasePhase> phases;

  private Pair<IncrementalBuildState, List<QuestionSet>> lastQueriedApplicable;

  public BaseSurvey(String id, MessageSource messageSource, Function<Survey, List<BasePhase>> phasesConstructor) {
    this.id = id;
    this.messageSource = messageSource;
    this.phases = phasesConstructor.apply(this);
  }

  // question set for questions that drive the structure of the survey, rather than do any pipeline building
  // themselves
  private final QuestionSet surveyQuestions = new EmptyQuestionSet("wizard", this) {
    @Override
    public PipelineChange getPipelineChange(IncrementalBuildState buildState, Answer answer) {
      ResultOrProblems<QuestionSet> chosenOr = getQuestionSetChoice(buildState, answer);

      if (chosenOr.hasErrors()) {
        return PipelineChange.failed(chosenOr.getProblems()).apply(answer);
      }

      QuestionSet chosen = chosenOr.get();

      if (chosen instanceof DefaultQuestionSet2) {
        DefaultQuestionSet2 qs2 = (DefaultQuestionSet2) chosen;
        return qs2.getPipelineChange(buildState, answer);
      } else {
        return super.getPipelineChange(buildState, answer);
      }
    }
  };

  private BasePhase getCurrentPhase(IncrementalBuildState buildState) {
    // get to the point before we were asking about what questions to answer
    while (buildState.getQuestionSet().equals(surveyQuestions)) {
      buildState = buildState.getLast();
    }
    // we are at the start, return the first phase
    if (buildState.isEmpty()) {
      return phases.get(0);
    }

    // otherwise, flick through our phases in order, looking for the owner
    for (BasePhase phase : phases) {
      if (phase.contains(buildState.getQuestionSet())) {
        return phase;
      }
    }
    return null;
  }

  @Override
  public List<QuestionSet> getApplicableQuestionSets(IncrementalBuildState buildState) {

    if (lastQueriedApplicable != null && lastQueriedApplicable.getLeft().equals(buildState)) {
      return lastQueriedApplicable.getRight();
    }

    // how many skips have been given
    int numPhaseSkips = countPhaseSkipsInARow(buildState);

    // this is the last phase we were on
    BasePhase phase = getCurrentPhase(buildState);
    if (phase == null) {
      throw new RuntimeException("unowned phase after " + buildState.getQuestion());
    }

    // move past the last phase if it is all done
    if (phase.isComplete(buildState)) {
      phase = getNextPhase(phase);
    }

    // now increment through phases based on the number of skips we saw
    for (int i = 0; i < numPhaseSkips; i++) {
      phase = getNextPhase(phase);
    }

    // done
    if (phase == null) {
      return Collections.emptyList();
    }

    List<QuestionSet> all = new ArrayList<>();
    all.addAll(phase.getAvailableQuestionSets(buildState));

    // All the question sets can be skipped, let the user pick a 'skip' question set which triggers the skipping logic
    // above
    if (phase.canSkip(buildState)) {
      all.add(QuestionSet.SKIP);
    }

    lastQueriedApplicable = Pair.of(buildState, all);

    return all;
  }

  private boolean isQuestionSetSkip(Answer answer) {
    return answer.getQuestion().getParameterType().equals(PickQuestionSet.class)
        && answer.getValueAs(PickQuestionSet.class).isSkip();
  }

  /**
   * Helper method for getApplicableQuestionSets to count the number of sequential choices of skipPhase from builtState,
   * returning 0 if buildState is not the result of choosing skipPhase
   */
  private int countPhaseSkipsInARow(IncrementalBuildState buildState) {
    int counted = 0;
    while (isQuestionSetSkip(buildState.getAnswer())) {
      buildState = buildState.getLast();
      counted++;
    }
    return counted;
  }

  private BasePhase getNextPhase(Phase after) {

    Iterator<BasePhase> phasesIter = phases.iterator();
    while (phasesIter.hasNext()) {
      Phase phase = phasesIter.next();

      if (phase.equals(after)) {
        return phasesIter.hasNext() ? phasesIter.next() : null;
      }
    }

    return null;
  }


  @Override
  public PipelineChange getPipelineChange(IncrementalBuildState buildState, Answer answer) {
    QuestionSet questionSet = answer.getQuestionSet();

    return questionSet.getPipelineChange(buildState, answer);
  }

  private QuestionTree getCurrentQuestionTree(IncrementalBuildState buildState) {
    QuestionSet lastQuestionSet = buildState.getQuestionSet();

    // user has just picked a question set to answer, return questions from those
    if (lastQuestionSet.equals(this.surveyQuestions)) {
      // we should have set the questionSet as part of the binding previously
      lastQuestionSet = buildState.getAnswer().getValueAs(PickQuestionSet.class).questionSet;
    }

    return QuestionTree.fromList(lastQuestionSet.getQuestions());
  }

  @Override
  public QuestionTree getQuestionTree(IncrementalBuildState buildState) {
    QuestionTree tree = getCurrentQuestionTree(buildState);
    List<Question> nextQuestions = tree.getNextQuestions(buildState);
    if (!nextQuestions.isEmpty()) {
      return tree;
    } else {
      if (getApplicableQuestionSets(buildState).isEmpty()) {
        return QuestionTree.empty();
      } else {
        // no more questions in the current set, need to find out which phase we are in
        // ask the user which set to ask next
        return nextQuestionSetChoice(buildState);
      }
    }
  }

  /**
   * Convenience method for turning a {@link PickQuestionSet} {@link Answer} in to a {@link QuestionSet}
   */
  private ResultOrProblems<QuestionSet> getQuestionSetChoice(IncrementalBuildState buildState, Answer answer) {
    PickQuestionSet pickQuestionSet = answer.getValueAs(PickQuestionSet.class);

    // a previous call to getQuestionSetChoice has already set it, return it
    // NB hard to say whether this is going to happen in practice
    if (pickQuestionSet.questionSet != null) {
      return ResultOrProblems.of(pickQuestionSet.questionSet);
    }

    List<QuestionSet> list = getApplicableQuestionSets(buildState);

    return list.stream()
        .filter(qs -> qs.getId().equals(pickQuestionSet.questionSetId))
        .findFirst()
        // set the questionSet to our binding object so subsequent look up code doesn't have to deal with rebinding
        // it each time
        .map(qs -> pickQuestionSet.questionSet = qs)
        .map(qs -> ResultOrProblems.of(qs))
        .orElse(ResultOrProblems.failed(GeneralProblems.get().notAnOption(
            pickQuestionSet.questionSetId,
            // Use to parameter as that has nicer formatting already and no way to plug-in new problem rendering code
            answer.getQuestion().toParameter(),
            list.stream().map(QuestionSet::getId).collect(Collectors.toList())))
        );
  }

  /**
   * Look up a Phase by its implementing class (Assuming they are unique).  This has been added mostly so that
   * test cases can find elements of the survey after construction.
   */
  public <T extends Phase> Optional<T> getPhaseOfType(Class<T> clazz) {
    for (Phase phase : phases) {
      if (phase.getClass().equals(clazz)) {
        return Optional.of(clazz.cast(phase));
      }
    }
    return Optional.empty();
  }

  /**
   * @return a {@link QuestionTree} that has a single question which requires the user pick a question set to answer
   * next.
   */
  private QuestionTree nextQuestionSetChoice(IncrementalBuildState buildState) {
    long nextCounter = buildState.buildStateStream()
        .filter(p -> p.getQuestion().getParameterType().equals(PickQuestionSet.class))
        .count() + 1;

    Question pick = new Question("question-choice-" + nextCounter, PickQuestionSet.class)
      .withI18nLookup((question, suffix, locale) ->
        Question.DEFAULT_I18N_LOOKUP.apply(question.withName("question-choice"), suffix, locale)
      );
    return QuestionTree.singleton(pick.inSet(surveyQuestions));
  }

  private boolean wasPhaseAnswered(IncrementalBuildState buildState, BasePhase phase) {
    return buildState.buildStateStream()
        .anyMatch(bs -> phase.contains(bs.getQuestionSet()));
  }

  private List<BasePhase> getUnansweredPhases(IncrementalBuildState buildState) {
    List<BasePhase> unanswered = new ArrayList<>();
    for (BasePhase phase : phases) {
      if (!wasPhaseAnswered(buildState, phase)) {
        unanswered.add(phase);
      }
    }
    return unanswered;
  }

  @Override
  public boolean isFinished(IncrementalBuildState buildState) {
    if (buildState.isEmpty()) {
      return false;
    }

    // if there are any phases still left that we can't skip, then we're not done yet
    boolean phaseNeedsAnswering = getUnansweredPhases(buildState).stream()
        .anyMatch(phase -> !phase.canSkip(buildState));
    if (phaseNeedsAnswering) {
      return false;
    }

    // check if there are required Questions still remaining in the current phase
    boolean questionNeedsAnswering = getCurrentQuestionTree(buildState)
        .getNextQuestions(buildState).stream()
        .anyMatch(q -> q.isRequired());
    return !questionNeedsAnswering;
  }

  @Override
  public List<Phase> getDefinedPhases() {
    return new ArrayList<Phase>(phases);
  }

}
