/*
 * 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.engine.cli;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import com.google.common.base.CaseFormat;
import com.google.common.collect.ImmutableList;

import nz.org.riskscape.engine.Engine;
import nz.org.riskscape.engine.Project;
import nz.org.riskscape.engine.Reference;
import nz.org.riskscape.engine.RiskscapeIOException;
import nz.org.riskscape.engine.auth.SecretBuilder;
import nz.org.riskscape.engine.bind.BindingContext;
import nz.org.riskscape.engine.bind.Parameter;
import nz.org.riskscape.engine.cli.Table.Property;
import nz.org.riskscape.engine.function.IdentifiedFunction;
import nz.org.riskscape.engine.function.IdentifiedFunction.Category;
import nz.org.riskscape.engine.i18n.EnumTranslator;
import nz.org.riskscape.engine.i18n.TranslationContext;
import nz.org.riskscape.engine.output.Format;
import nz.org.riskscape.engine.pipeline.Step;
import nz.org.riskscape.picocli.CommandLine.Command;
import nz.org.riskscape.picocli.CommandLine.Parameters;
import nz.org.riskscape.problem.ResultOrProblems;

@Command(name = "docs", hidden = true)
public class DocsCommand extends ApplicationCommand {

  @Parameters(arity = "0..1", index = "0")
  public String location;

  private static final Path DEFAULT_ROOT = Path.of("docs", "source", "reference");
  private static final Path FUNCTIONS = Path.of("functions", "generated");
  private static final Path PIPELINES = Path.of("pipelines");
  private static final Path CONFIG = Path.of("config", "generated");

  @Override
  public Object doCommand(Project project) {

    Path rootPath = location == null ? DEFAULT_ROOT : Path.of(location);
    Path functionPath = rootPath.resolve(FUNCTIONS);
    Path pipelinePath = rootPath.resolve(PIPELINES);


    TranslationContext translationContext = new TranslationContext(getTerminal().getLocale(),
        getMessages().getLabels());

    try {
      generateFunctionDocs(project, translationContext, functionPath);
      generatePipelineStepDocs(getEngine(), pipelinePath);
      generateSecretBuilderDocs(getEngine(), rootPath.resolve(CONFIG));
      generateOutputFormatDocs(getEngine(), rootPath.resolve("output-formats"));
    } catch (IOException e) {
      return ResultOrProblems.error(
        new RiskscapeIOException("Could not write docs to file resource. Cause: " + e.getMessage(), e)
      );
    }

    return "Generated documentation in " + rootPath.toString();
  }

  private void generatePipelineStepDocs(Engine engine, Path pipelinePath) throws IOException {
    MarkdownHelper markdown = new MarkdownHelper();

    markdown.addHeading("Pipeline steps");

    List<Step> steps = engine.getPipelineSteps().getAll();
    for (Step step : steps) {
      markdown.addHeading("`" + step.getId() + "`", 2);

      List<Parameter> parameters = ImmutableList.copyOf(step.getParameterSet().getDeclared());


      Table table = Table.fromList(parameters,
          Parameter.class,
          getMessages().getLabels(),
          Arrays.asList(
              Property.of("name", Parameter::getName),
              Property.of("typeName", p -> "`" + p.getTypeName() + "`"),
              Property.of("arity", Parameter::getArity),

              Property.of("description", p -> getMessages().getHelpMessage(step, "params", p.getName()).orElse(null))
          )
      );
      markdown.addTable(table);

      Optional<String> description = getDetailedHelp(step, "description");
      if (description.isPresent()) {
        markdown.addLine(description.get());
      }

    }
    markdown.writeToFile(pipelinePath, "generated.md");

  }

  private void generateOutputFormatDocs(Engine engine, Path outputDocsPath) throws IOException {
    MarkdownHelper markdown = new MarkdownHelper();

    markdown.addHeading("Save formats");

    BindingContext context = engine.newBindingContext();

    List<Format> formats = engine.getFormats().getAll()
        .stream()
        .sorted((b1, b2) -> b1.getId().compareTo(b2.getId()))
        .toList();
    for (Format format : formats) {
      markdown.addHeading(inBackticks(format.getId()), 2);

      Optional<String> description = getDetailedHelp(format, "description");
      markdown.addLine(description.orElse(""));

      List<Parameter> parameters = format.getWriterParameters()
          .getDeclared()
          .stream()
          .toList();

      Table table = Table.fromList(parameters,
          Parameter.class,
          getMessages().getLabels(),
          Arrays.asList(
              Property.of("name", p -> inBackticks(p.getName())),
              Property.of("default", p -> processDefaultValues(p.getDefaultValues(context))),

              Property.of("description", p -> getMessages()
                  .getHelpMessage(format, "params", p.getName())
                  .orElse("")
              )
          )
      );
      markdown.addTable(table, Optional.of("Does not support any `save()` step options"));

    }
    markdown.writeToFile(outputDocsPath, "generated.md");

  }

  private void generateFunctionDocs(Project project, TranslationContext context, Path functionPath) throws IOException {
    List<Reference<IdentifiedFunction>> functions = project.getFunctionSet().getReferences();

    for (Category category : Category.values()) {
      MarkdownHelper markdown = new MarkdownHelper();

      EnumTranslator translator = new EnumTranslator(context);
      String categoryName = (translator.getEntry(category)).getLabel();

      String filename = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, category.name());

      markdown.addHeading(categoryName);

      List<Reference<IdentifiedFunction>> filtered = filterFunctions(functions, category);

      for (Reference<IdentifiedFunction> function : filtered) {
        addFunction(markdown, function);
      }

      if (!filtered.isEmpty()) {
        markdown.writeToFile(functionPath, filename + ".md").toString();
      }
    }
  }

  private void addFunction(MarkdownHelper markdown, Reference<IdentifiedFunction> reference) {
    IdentifiedFunction function = reference.get();

    markdown.addHeading("`" + function.getId() + "`", 2);
    markdown.addLine("Arguments: `" + function.getArguments().toString() + "`");
    markdown.addLine("Returns: `" + function.getReturnType().toString() + "`");
    markdown.addLine(getFunctionDescription(function));
    markdown.addLine();
  }

  private String getFunctionDescription(IdentifiedFunction function) {
    // try to use the i18n translatable string if one exists

    Optional<String> help = getDetailedHelp(function, "description");

    if (help.isPresent()) {
      return help.get();
    }

    String description = getMessages().getHelpMessage(function, "description").orElse(null);

    if (description == null) {
      // nope, try the function itself. This covers the user-defined (INI file)
      // description,
      // as well as hard-coded text for built-in functions we haven't converted to
      // i18n yet
      description = function.getDescription();
    }
    return description == null ? "" : description;
  }

  List<Reference<IdentifiedFunction>> filterFunctions(List<Reference<IdentifiedFunction>> allFunctions,
      Category category) {
    return allFunctions.stream()
        // this includes options in the given category *and* failed functions
        .filter(ref -> ref.getResult().map(idf -> idf.getCategory().equals(category)).orElse(false))
        .collect(Collectors.toList());
  }

  private void generateSecretBuilderDocs(Engine engine, Path secretBuilderPath) throws IOException {
    MarkdownHelper markdown = new MarkdownHelper();

    markdown.addHeading("Secret frameworks");

    BindingContext context = getBindingContext();

    List<SecretBuilder> builders = engine.getCollectionByClass(SecretBuilder.class).getAll()
        .stream()
        .sorted((b1, b2) -> b1.getId().compareTo(b2.getId()))
        .toList();
    for (SecretBuilder builder : builders) {
      markdown.addHeading(inBackticks(builder.getId()), 2);

      List<Parameter> parameters = ImmutableList.copyOf(builder.getParameters().getDeclared());

      Table table = Table.fromList(parameters,
          Parameter.class,
          getMessages().getLabels(),
          Arrays.asList(
              Property.of("name", p -> inBackticks(p.getName())),
              Property.of("default", p -> processDefaultValues(p.getDefaultValues(context))),

              Property.of("description", p -> getMessages()
                  .getHelpMessage(builder.getBuildsClass(), "params", p.getName())
                  .orElse("")
              )
          )
      );
      markdown.addTable(table);

      Optional<String> description = getDetailedHelp(builder.getBuildsClass(), "description");
      markdown.addLine(description.orElse(""));

    }
    markdown.writeToFile(secretBuilderPath, "secret-frameworks.md");

  }

  /**
   * Takes a list of default and returns a more human readable value.
   *
   * In most cases this will be to extract the single item from the list.
   */
  private String processDefaultValues(List<?> defaults) {
    if (defaults.isEmpty()) {
      return "";
    } else if (defaults.size() == 1) {
      // we don't want the wrapping [ ] for a single value
      return inBackticks(Objects.toString(defaults.get(0)));
    }
    return inBackticks(Objects.toString(defaults));
  }

  /**
   * @return toQuote wrapped in backticks to beat the spell checker.
   */
  private String inBackticks(String toQuote) {
    return "`" + toQuote + "`";
  }
}
