/*
 * 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.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;

import com.google.common.base.Strings;

import lombok.Getter;
import lombok.Setter;
import nz.org.riskscape.engine.Engine;
import nz.org.riskscape.engine.NoSuchObjectException;
import nz.org.riskscape.engine.Reference;
import nz.org.riskscape.engine.bind.BindingContext;
import nz.org.riskscape.engine.bind.Parameter;
import nz.org.riskscape.engine.bind.ParameterSet;
import nz.org.riskscape.engine.cli.Table.Property;
import nz.org.riskscape.engine.data.BookmarkResolver;
import nz.org.riskscape.engine.output.Format;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.picocli.CommandLine.Command;
import nz.org.riskscape.picocli.CommandLine.Parameters;
import nz.org.riskscape.problem.ProblemSink;
import nz.org.riskscape.problem.Problems;

@Command(
    name = "format",
    subcommands = {
        FormatsCommand.FormatInfo.class,
        FormatsCommand.Save.class,
        FormatsCommand.FormatListCommand.class
    }
)
public class FormatsCommand extends StubCommand {

  public abstract static class BaseFormatCommand extends EngineOnlyCommand {

    public BookmarkResolver getResolver(Engine engine, String format) {
      for (BookmarkResolver resolver : engine.getBookmarkResolvers().getAll()) {
        if (resolver.getFormats().contains(format)) {
          return resolver;
        }
      }
      return null;
    }

    protected String getDescription(Parameter param, Object owner, BindingContext bindingContext){
      String description = getMessages().getHelpMessage(owner, "params", param.getName()).orElse("");

      if (param.hasDefaultValue()) {

        description = String.format("%s(default: %s)",
            Strings.isNullOrEmpty(description) ? "" : description + " ",
            param.getDefaultValues(bindingContext).stream()
                .map(o -> getMessages().renderObject(o, getTerminal().getLocale()))
                .collect(Collectors.joining(", "))
        );
      }
      return description;
    }
  }

  @Command(
      name = "list"
  )
  public static class FormatListCommand extends BaseFormatCommand {

    @Override
    public Object doCommand(Engine engine) {
      HashMap<String, HashSet<String>> formatsMap = new HashMap<>(); // key Format: Extensions
      List<String> outputFormats = engine.getFormats().getReferences().stream().map(Reference::getId)
          .collect(Collectors.toList());

      for (BookmarkResolver resolver : engine.getBookmarkResolvers().getAll()) {
        for (String format : resolver.getFormats()) {
          HashSet<String> valueSet = new HashSet<>(resolver.getExtensions(format));
          formatsMap.put(format, valueSet);
        }
      }

      return Table.fromList(new ArrayList<>(formatsMap.keySet()), FormatsCommand.class,
          getMessages().getLabels(),
          Arrays.asList(Property.of("format", format -> format),
              Property.of("file extension", format -> String.join(", ", formatsMap.get(format))),
              Property.of("supports",
                  format -> (outputFormats.contains(format)) ? "input and output data" : "input data")));
    }
  }

  @Command(
      name = "info"
  )
  public static class FormatInfo extends BaseFormatCommand {

    @Getter @Setter
    @Parameters(index="0")
    private String name;

    @Override
    public Object doCommand(Engine engine) {
      BookmarkResolver formatResolver = getResolver(engine, name);
      if (formatResolver == null) {
        throw new ExitException(
            GeneralProblems.get().noSuchObjectExistsDidYouMean(name, Format.class,
                engine.getBookmarkResolvers().getSupportedFormats())
        );
      }

      return Table.fromList(
          formatResolver.getParameterSet().getDeclared().stream().toList(),
          Parameter.class,
          getMessages().getLabels(),
          Arrays.asList(
              Property.of("parameter", Parameter::getName),
              Property.of("description", p -> getDescription(p, formatResolver, engine.newBindingContext()))
          ));
    }

  }

  @Command(name = "save",
          subcommands = {
                  FormatsCommand.Save.OutputOptions.class
          })
  public static class Save extends StubCommand {

    @Command(
            name = "options"
    )
    public static class OutputOptions extends BaseFormatCommand {

      @Getter
      @Setter
      @Parameters(index = "0")
      private String name;

      @Override
      public Table doCommand(Engine engine) {
        Format format;
        try {
          format = engine.getFormats().get(name, ProblemSink.DEVNULL);
        } catch (NoSuchObjectException e) {
          throw new ExitException(Problems.caught(e));
        }

        BindingContext bindingContext = engine.newBindingContext();
        @SuppressWarnings("unchecked")
        ParameterSet parameterSet = format.getWriterParameters();

        return Table.fromList(
                parameterSet.getDeclared().stream().toList(),
                Parameter.class,
                getMessages().getLabels(),
                Arrays.asList(
                        Property.of("parameter", Parameter::getName),
                        Property.of("description", p -> getDescription(p, format, bindingContext))
                ));
      }
    }
  }
}
