/*
 * 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.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Formatter;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Consumer;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;

import lombok.RequiredArgsConstructor;
import nz.org.riskscape.engine.FileProblems;
import nz.org.riskscape.engine.cli.ExitException;
import nz.org.riskscape.engine.cli.PipelineRenderer;
import nz.org.riskscape.engine.i18n.Messages;
import nz.org.riskscape.engine.pipeline.RealizedPipeline;
import nz.org.riskscape.engine.pipeline.RealizedStep;
import nz.org.riskscape.engine.pipeline.SinkConstructor;
import nz.org.riskscape.engine.resource.UriHelper;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Struct.StructMember;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.wizard.bld.IncrementalBuildState;

@RequiredArgsConstructor
public class WizardActions implements PipelineRenderer {

  /**
   * The action that is returned when the user selects the 'continue' option - allows calling code to detect that they
   * didn't actually want to do anything
   */
  public static final Action CONTINUE_ACTION = () -> {};

  private final CliPrompter cli;

  // location to write out project files to - uses this instead of project's relative dir to allow tests to change it
  private final URI outTo;

  @Override
  public Messages getMessages() {
    return cli.getMessages();
  }

  @Override
  public Locale getLocale() {
    return cli.getLocale();
  }

  private boolean writeFile(String filename, String fileDescription, Consumer<Formatter> writer) {
    ResultOrProblems<URI> uriResult = UriHelper.uriFromLocation(filename, outTo);

    if (uriResult.hasProblems()) {
      cli.displayProblems("Failed to write file", uriResult.getProblems());
      return false;
    }
    File outputFile = new File(uriResult.get().getPath());

    try (Formatter f = new Formatter(new FileOutputStream(outputFile))) {
      writer.accept(f);
    } catch (IOException ex) {
      cli.displayProblems("Failed to write file",
          FileProblems.get().cantWriteTo(outputFile.toPath()).withChildren(Problems.caught(ex))
      );
      return false;
    }

    cli.showSuccessMessage(String.format("Saved %s to %s", fileDescription, uriResult.get().getPath()));

    return true;
  }

  /**
   * Saves both model INI and a pipeline from the buildState
   */
  public void saveModelIniAndPipeline(IncrementalBuildState buildState) {
    String modelName = cli.readlineWithTitle("Enter a name for the new model");
    saveModelIni(buildState, modelName);
    savePipelineDsl(buildState, modelName, false);
  }

  /**
   * Saves the pipeline DSL to the given relativeTo directory (useful for testing).
   */
  public void savePipelineDsl(IncrementalBuildState buildState, String pipelineName, boolean writeModelFile) {
    String pipelineLocation = String.format("pipeline_%s.txt", pipelineName);
    // first write the pipeline as a plain-text DSL file
    boolean success = writeFile(
        pipelineLocation,
        "pipeline",
        f -> f.format(buildState.getAst().toSource() + "%n"));

    if (success && writeModelFile) {
      writeFile(
          String.format("models_pipeline_%s.ini", pipelineName),
          "pipeline model",
          f -> f.format(
              "[model %s-pipeline]%n"
              + "framework = pipeline%n"
              + "location = %s%n", pipelineName, pipelineLocation));
    }
  }

  /**
   * Saves the pipeline produced by the {@link IncrementalBuildState} as a plain-text DSL file within the
   * project.ini file's directory.
   */
  public void savePipelineDsl(IncrementalBuildState buildState) {
    String pipelineName = cli.readlineWithTitle("Enter a name for your pipeline");
    savePipelineDsl(buildState, pipelineName, true);
    cli.println("");
  }

  /**
   * Saves the wizard content as a model INI file to the given relativeTo
   * directory (useful for testing).
   */
  public void saveModelIni(IncrementalBuildState buildState) {
    String modelName = cli.readlineWithTitle("Enter a name for a new model ini file");
    saveModelIni(buildState, modelName);
    cli.println("");
  }

  private void saveModelIni(IncrementalBuildState buildState, String modelName) {
    ConfigParser parser = new ConfigParser(ignored -> buildState.getSurvey());
    writeFile(
        String.format("models_%s.ini", modelName),
        "model",
        f -> {
          f.format("[model %s]%n"
              + "framework = wizard%n",
                modelName);

          parser.getConfigToWrite(buildState).forEach(pair -> {
            f.format("%s = %s%n", pair.getLeft(), pair.getRight());
          });
        }
    );
  }

  /**
   * Renders a RealizedPipeline, as per {@link PipelineRenderer}.
   *
   * Note that this only renders *valid* pipelines, as that is all the wizard should produce.
   */
  public String getRenderedPipeline(RealizedPipeline pipeline) {
    StringBuilder sb = new StringBuilder();

    this.printPipeline(pipeline, new Formatter(sb));

    return sb.toString();
  }

  public List<CliChoice<Action>> getSaveOrShowActions(IncrementalBuildState buildState) {
    Builder<CliChoice<Action>> builder = ImmutableList.builder();

    builder.add(
      new CliChoice<Action>(
          "Undo",
          "U",
          () -> {
            throw new UndoException();
          }
      )
    );

    builder.addAll(
      Arrays.asList(
        new CliChoice<Action>(
            "List the attributes currently available in the pipeline",
            "L",
            () -> {
              showInputScope(buildState);
          }
        ),

        new CliChoice<Action>(
            "Save your current progress as a model INI file",
            "m",
            () -> saveModelIni(buildState)
        ),

        new CliChoice<Action>(
            "Save your current progress as a pipeline",
            "S",
            () -> {
              savePipelineDsl(buildState);
          }
        ),

        new CliChoice<Action>(
            "Print the current model pipeline",
            "P",
            () -> cli.println(buildState.getAst().toSource()).println()
        ),

        new CliChoice<Action>(
            "Print all answers given so far",
            "a",
            () -> {
              showAnswers(buildState);
          }
        ),

        new CliChoice<Action>(
            "Print very detailed information for the current model pipeline (for advanced users)",
            "d",
            () -> {
              ResultOrProblems<RealizedPipeline> pipeline = buildState.realizePipeline(buildState.getAst());

              if (pipeline.hasErrors()) {
                cli.displayProblems("Failed to construct a realized pipeline", pipeline.getProblems());
              } else {
                cli.println(getRenderedPipeline(pipeline.get()));
              }
            }
        ),

        new CliChoice<Action>(
            "Continue - Go back and continue the wizard process",
            "C",
            CONTINUE_ACTION
        ),

        new CliChoice<Action>(
            "Exit RiskScape",
            "E",
            () -> {
              ExitException.quit();
            }
        )
      )
    );

    return builder.build();
  }

  public List<CliChoice<Action>> getSurveyCompleteActions(IncrementalBuildState buildState) {
    Builder<CliChoice<Action>> builder = ImmutableList.builder();

    builder.addAll(
      Arrays.asList(

        new CliChoice<Action>(
            "Save and run",
            "S",
            () -> {
              saveModelIniAndPipeline(buildState);
              cli.println("");
            }
        ),

        new CliChoice<Action>(
            "Save and quit (without running it)",
            "q",
            () -> {
              saveModelIniAndPipeline(buildState);
              ExitException.quit();
            }
        ),

        new CliChoice<Action>(
            "Run it (without saving it)",
            "R",
            () -> {
              // display what we are about to run
              cli.println(buildState.getAst().toSource()).println("");
            }
        )
      )
    );

    return builder.build();
  }

  /**
   * @return the end step(s) that represents the current scope of the pipeline
   */
  private List<RealizedStep> getStepsInScope(IncrementalBuildState buildState) {
    List<RealizedStep> endSteps = new ArrayList<>();
    int cappedSteps = 0;
    for (RealizedStep step : buildState.getRealizedPipeline().getEndSteps()) {
      // a capped step indicates a finished report - it's essentially no longer in scope
      if (step.getStepType() == SinkConstructor.class) {
        cappedSteps++;
        continue;
      }

      if (step.getProduces().equals(Struct.EMPTY_STRUCT)) {
        continue;
      }
      endSteps.add(step);
    }
    // if we only found capped steps, try defaulting to the step that the reports hang off
    if (endSteps.isEmpty() && cappedSteps > 0) {
      Optional<RealizedStep> stepOr = buildState.getContext().getSurvey().getAnalysisOutputStepName()
          .flatMap(analysisStep -> buildState.getRealizedPipeline().getStep(analysisStep));
      stepOr.ifPresent(step -> endSteps.add(step));
    }
    return endSteps;
  }

  public void showInputScope(IncrementalBuildState buildState) {
    for (RealizedStep step : getStepsInScope(buildState)) {
      cli.println(cli.title("Showing the attributes produced by the current pipeline:")).println();

      cli.println("The current end step '" + step.getStepName() + "' produces:");
      printStruct(step.getProduces(), "");
      cli.println("");
    }
  }

  private void printStruct(Struct struct, String memberPrefix) {
    for (StructMember member : struct.getMembers()) {
      Type memberType = Nullable.strip(member.getType());
      Struct memberStruct = memberType.find(Struct.class).orElse(null);

      if (memberStruct == null) {
        cli.println("  " + memberPrefix + member.getKey() + " - " + memberType);
      } else {
        printStruct(memberStruct, memberPrefix +  member.getKey() + ".");
      }
    }
  }

  public void showAnswers(IncrementalBuildState buildState) {
    List<Answer> answers = buildState.getAllAnswers();
    Collections.reverse(answers);

    cli.println(cli.title("Showing all answers:")).println();

    QuestionSet lastQuestionSet = IncrementalBuildState.EMPTY_QUESTION_SET;
    for (Answer answer : answers) {
      if (!answer.getQuestionSet().equals(lastQuestionSet)) {
        lastQuestionSet = answer.getQuestionSet();
        if (lastQuestionSet != IncrementalBuildState.EMPTY_QUESTION_SET) {
          // add an empty line between question sets to space them out
          cli.println("");
        }
        cli.println(
            lastQuestionSet.getId() + " - " + lastQuestionSet.getDescription(cli.getTerminal().getLocale()));
      }

      cli.println(String.format("  %s - %s", answer.getQuestion().getName(), answer.getOriginalResponses()));
    }
    cli.println("");
  }

}
