/*
 * 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 java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;

import com.google.common.collect.Lists;

import nz.org.riskscape.cli.InterruptHandler;
import nz.org.riskscape.cli.Terminal;
import nz.org.riskscape.engine.Project;
import nz.org.riskscape.engine.RiskscapeException;
import nz.org.riskscape.engine.cli.ExitException;
import nz.org.riskscape.engine.cli.Main;
import nz.org.riskscape.engine.cli.ReadlineInterruptedException;
import nz.org.riskscape.engine.cli.pipeline.CliPipelineRunner;
import nz.org.riskscape.engine.cli.pipeline.CliPipelineRunnerOptions;
import nz.org.riskscape.engine.i18n.MessageSource;
import nz.org.riskscape.engine.pipeline.ExecutionContext;
import nz.org.riskscape.engine.pipeline.Pipeline;
import nz.org.riskscape.engine.pipeline.RealizedPipeline;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.wizard.bld.IncrementalBuildState;
import nz.org.riskscape.wizard.survey2.PickQuestionSet;

/**
 * Drives the wizard answering process on the CLI, this time for Survey2 surveys (rather than the old Survey) interface.
 *
 * Survey2 acts more like a controller in this incarnation, with the processor driving the question/answer loop.
 */
public class CliWizardProcessor2 {

  private final Project project;
  private final CliPrompter prompter;
  private final WizardActions actions;
  private final Question skipAll;
  private final MessageSource wizardMessages;

  private AtomicBoolean interrupted = new AtomicBoolean(false);
  private WizardProcessor processor;

  public CliWizardProcessor2(Project project, CliPrompter prompter, WizardActions actions) {
    this.project = project;
    this.prompter = prompter;
    this.actions = actions;
    this.wizardMessages = prompter.getMessages().newMessageSource("wizard");

    // a question that gets added to the list of question choices when they can be skipped (all optional)
    this.skipAll = new Question("skip", Question.class).inSet(
      new EmptyQuestionSet("cli", new EmptySurvey("cli", wizardMessages))
    );
  }

  void newProcessor(Survey survey) {
    this.processor = new WizardProcessor(project.newExecutionContext(), survey);
  }

  /**
   * Runs through the {@link Survey} and interactively asks the user {@link Question}s.
   */
  public void run(Survey survey) {
    newProcessor(survey);

    prompter.println(Main.COPYRIGHT_NOTICE);

    survey.getDescription(prompter.getLocale()).ifPresent(description -> {
      prompter.println(description);
    });


    // set up interrupt handling so the user can ctrl-c at any time and we still show a menu, not just when prompting
    Terminal terminal = prompter.getTerminal();
    InterruptHandler toRestore = terminal.setFallbackHandler(() -> {
      if (interrupted.get()) {
        // double interrupt, quit now, don't wait for main thread to respond
        System.exit(1);
      } else {
        // handleInterrupted will check this before showing the choice and offer a menu
        interrupted.set(true);
      }
    });

    try (ExecutionContext ctx = processor.getExecutionContext()) {
      try {
        runInsideInterrupt();
        // now that the survey is complete we give the user that chance to save it.
        completionMenu();
        // TODO catch some sort of invalid change exception and give the user a chance to go back to a certain point to
        // try again?
      } finally {
        // restore whatever was there- NB there's a slight chance here that a ctrl-c will fall through the cracks, but
        // user can just hit it again
        terminal.setFallbackHandler(toRestore);
      }
      runSurveyPipeline();
    }
  }

  IncrementalBuildState runInsideInterrupt() {
    while (true) {
      try {
        if (processor.isDone()) {
          break;
        }

        List<Question> next = processor.getNextQuestions();

        Question chosen = chooseQuestion(next);

        // none of the choices were acceptable to our fussy user
        if (chosen == skipAll) {
          processor.skipAll(next);
          continue;
        }

        Answer answer = answerQuestion(chosen);

        processor.applyAnswer(answer);

        // even though the answer was valid, it didn't produce a valid pipeline for some reason
        if (processor.isFailed()) {
          prompter.displayProblems("", processor.getFailures());
          processor.undo();
          continue;
        }


      } catch (UndoException ex) {
        processor.undo();
      }
    }

    return processor.getBuildState();
  }


  private void runSurveyPipeline() {
    // once the user has finished the survey, run the pipeline
    RealizedPipeline realized = processor.getBuildState().getRealizedPipeline();
    CliPipelineRunner runner = new CliPipelineRunner(realized.getContext(), prompter.getMessages(),
        prompter.getTerminal(), CliPipelineRunner.DEFAULT_TIME_FUNCTION);

    // update name in metadata so that manifest shows something more useful than 'anonymous'
    realized = realized.withMetadata(realized.getMetadata().withName("wizard"));

    try {
      runner.run(realized, processor.getExecutionContext().getPipelineExecutor(),
          project, new CliPipelineRunnerOptions());
    } catch (RiskscapeException ex) {
      throw new ExitException(Problems.foundWith(Pipeline.class, Problems.caught(ex)), ex);
    }
  }

  private Question chooseQuestion(
      List<Question> questions
  ) {
    List<Question> options = Lists.newArrayList();

    if (questions.isEmpty()) {
      // empty question tree shouldn't make it here
      throw new AssertionError("shouldn't ever happen - empty questions from " + questions);
    }

    options.addAll(questions);

    final boolean allOptional = options.stream().allMatch(q -> !q.isRequired());
    if (allOptional) {
      options.add(skipAll);
    }

    // only one choice, choose it for them (no interaction required)
    // note that a single optional question still needs to be paired with a skip
    if (options.size() == 1) {
      return options.get(0);
    }

    return handleInterrupted(() -> {
      return prompter.choose("Pick A Question", options, q -> q.getTitle(prompter.getLocale()).orElse(q.getId()));
    });
  }

  /**
   * Asks a single chosen Question that might have multiple responses.
   */
  private Answer answerQuestion(Question chosen) {

    List<Answer.Response> collected = new LinkedList<>();
    while (true) {
      Answer.Response value =
          handleInterrupted(() -> {
            if (!chosen.getParameterType().equals(PickQuestionSet.class)) {
              prompter.printBreadcrumb(processor.getQuestionTree(), chosen);
            }
            while (true) {
              Object unbound = prompter.askWithRepeat(processor.getBuildState(), chosen);
              ResultOrProblems<Answer.Response> responseOr =
                  Answer.bind(processor.getBindingContext(), chosen, unbound);

              if (responseOr.hasErrors()) {
                prompter.displayProblems(
                    wizardMessages.getMessage(
                        "cli.could-not-bind",
                        new Object[] {unbound},
                        prompter.getLocale()
                    ),
                  responseOr.getProblems()
                );
                continue;
              } else {
                // the CliPrompter should have already displayed any warnings
                return responseOr.getWithProblemsIgnored();
              }
            }
        });

      collected.add(value);

      if (chosen.isSingleValueQuestion()) {
        break;
      }

      boolean another = handleInterrupted(() -> prompter.askIf(
          "You can specify multiple responses for this question. Do you want to give another response"));

      if (!another) {
        break;
      }
    }

    return new Answer(chosen, collected);
  }

  /**
   * Wrap some code with support for handling ctrl-c interrupts
   */
  private <T> T handleInterrupted(Supplier<T> r) {
    // interrupted before we even began, omg!
    if (interrupted.compareAndSet(true, false)) {
      interruptMenuLoop(processor.getBuildState());
    }

    // we call the supplier in a while loop because if we are interrupted, we need to ask the question again, which we
    // might interrupt (again)
    while (true) {
      try {
        return r.get();
      } catch (ReadlineInterruptedException e) {
        interruptMenuLoop(processor.getBuildState());
      }
    }
  }

  /**
   * Shows the interrupt menu in a loop, showing it until the user dismisses it or selects continue
   */
  private void interruptMenuLoop(IncrementalBuildState buildState) {
    do {
      if (interruptMenu(buildState, false)) {
        break;
      }
    } while (prompter.askIf("See wizard actions menu again"));
  }


  /**
   * Show the interruptMenu, which is just the wizard actions choices, returning true if they selected the 'continue'
   * choice (which means we shouldn't ask them again if they want the wizard actions menu)
   */
  private boolean interruptMenu(IncrementalBuildState buildState, boolean allowBack) {
    List<CliChoice<Action>> choices = actions.getSaveOrShowActions(buildState);
    Action chosen = prompter.choose(prompter.title("Choose an action"), choices).data;

    chosen.run();

    return chosen == WizardActions.CONTINUE_ACTION;
  }

  /**
   * To ask the user what to do once the survey is complete.
   */
  private void completionMenu() {
    List<CliChoice<Action>> choices = actions.getSurveyCompleteActions(processor.getBuildState());
    Action chosen = prompter.choose(
        prompter.title("There are no further questions to answer. What do you want to do with your completed model?"),
        choices
    ).data;

    chosen.run();
  }

}
