/*
 * 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;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import com.google.common.collect.Lists;

import lombok.RequiredArgsConstructor;

import nz.org.riskscape.config.Config;
import nz.org.riskscape.config.ConfigProblems;
import nz.org.riskscape.config.ConfigSection;
import nz.org.riskscape.engine.bind.BindingContext;
import nz.org.riskscape.engine.bind.Parameter;
import nz.org.riskscape.engine.data.BookmarkFactory;
import nz.org.riskscape.engine.function.FunctionProvider;
import nz.org.riskscape.engine.geo.GeometryValidation;
import nz.org.riskscape.engine.ini.IdentifiedObjectBuilder;
import nz.org.riskscape.engine.ini.IniFileFunctionBuilder;
import nz.org.riskscape.engine.ini.IniFileModelBuilder2;
import nz.org.riskscape.engine.ini.IniFileParameterBuilder;
import nz.org.riskscape.engine.plugin.Plugin;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.engine.resource.UriHelper;
import nz.org.riskscape.engine.types.TypeProvider;
import nz.org.riskscape.engine.typexp.IniFileTypeBuilder;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.TokenTypes;

@RequiredArgsConstructor
public class DefaultProjectBuilder implements ProjectBuilder {

  public static final String PROJECT_KEY = "project";
  private static final String VALIDATE_GEOMETRY_KEY = "validate-geometry";

  private Config config;
  private final LinkedList<Problem> problems = new LinkedList<>();

  @Override
  public ResultOrProblems<Project> init(Project project) {
    Engine engine = project.getEngine();
    List<Plugin> plugins = engine.getPlugins();

    config = project.getConfig();
    project.setRelativeTo(config.getRootLocation());
    // Add resources to engine from the project ini, types/bookmarks.
    List<IdentifiedObjectBuilder<?>> iniBuilders = Arrays.asList(
        new BookmarkFactory(),
        new IniFileTypeBuilder(project),
        new IniFileModelBuilder2(project),
        new IniFileFunctionBuilder(project),
        new IniFileParameterBuilder(project)
    );

    for (ConfigSection section : config.getAll()) {
      String sectionKey = section.getName();

      if (sectionKey.startsWith(PROJECT_KEY)) {
        // parse the various `[project]` section settings
        parseProjectSection(section, project, iniBuilders);
      } else {
        boolean ignored = true;

        // lazily load any item-specific sections, e.t. `[bookmark foo]`, `[type bar]`, etc
        for (IdentifiedObjectBuilder<?> itemBuilder : iniBuilders) {
          if (sectionKey.startsWith(itemBuilder.getKeyword())) {
            // first we remove the item prefix
            String id = sectionKey.replaceFirst(itemBuilder.getKeyword(), "");

            // We strip any quotes that may be wrapping the identifier. This is because in some cases
            // an identifier will need to be quoted and that becomes tricky if the identier itself
            // looks like a quoted identifer.
            id = TokenTypes.stripQuotes(id).trim();
            if (id.isBlank()) {
              problems.add(Problems.foundWith("[" + sectionKey + "]", GeneralProblems.get().required("id")));
            } else {
              itemBuilder.addToProject(project, id, section);
            }

            ignored = false;
            break;
          }
        }

        if (ignored) {
          List<String> allowedKeys = Lists.newArrayList(PROJECT_KEY);
          iniBuilders.stream().forEach(b -> allowedKeys.add(b.getKeyword()));
          problems.add(ConfigProblems.get()
              .spuriousSectionInProject("[" + sectionKey + "]", section.getLocation(), allowedKeys)
          );
        }
      }
    }

    for (Plugin plugin : plugins) {
      plugin.supportsFeature(TypeProvider.class).ifPresent(tp ->
        tp.addTypes(project, p -> problems.add(p)));
    }

    for (Plugin plugin : plugins) {
      plugin.supportsFeature(FunctionProvider.class).ifPresent(tp ->
        tp.addFunctions(project, p -> problems.add(p)));
    }

 // go through the plugins, getting them to populate the project
    for (Plugin plugin : plugins) {
      try {
        problems.addAll(plugin.initializeProject(project, engine));
      } catch (ObjectAlreadyExistsException e) {
        // We don't want to wrap this exception up in ResultOrProblems as doing so changes how the error
        // renders, depending on whether the duplicate is detected down in plugin called code or not.
        // And we would rather these object already exist errors always look the same.
        throw e;
      } catch (Throwable ex) {
        return ResultOrProblems.failed(Problem.error(ex, "Failed to initialize plugin %s", plugin.getDescriptor())
            .withChildren(Problems.caught(ex)));
      }
    }

    // this is one of those odd places where we return a failed result along with the obhect
    return ResultOrProblems.of(project, problems);
  }

  private void parseProjectSection(ConfigSection section, Project project,
      List<IdentifiedObjectBuilder<?>> iniBuilders) {

    // a binding context to help with binding values. This won't be a full binding context, in that many
    // of the project's resources haven't been added, so won't be available. But if all you want is to
    // bind to Enum's (or stuff like that) you'll be fine
    BindingContext bindingContext = project.newBindingContext();

    // handle any other project-specific settings
    section.getOne("output-base-location").ifPresent(
      or -> or.flatMap(value ->
        UriHelper.uriFromLocation(value, project.getRelativeTo())
      ).ifElse(
        uri -> project.setOutputBaseLocation(uri),
        ps -> problems.add(Problems.foundWith(Parameter.class, "output-base-location", ps))
      )
    );

    section.getOne(VALIDATE_GEOMETRY_KEY).ifPresent(
        or -> or.flatMap(value ->
          bindingContext.bind(value, GeometryValidation.class)
        ).ifElse(
          mode -> project.setGeometryValidation(mode),
          ps -> problems.add(Problems.foundWith(Parameter.class, VALIDATE_GEOMETRY_KEY, ps))
        )
      );
  }


}
