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

import java.util.Collections;
import java.util.List;
import java.util.Set;

import nz.org.riskscape.engine.RiskscapeException;
import nz.org.riskscape.engine.bind.BindingContext;
import nz.org.riskscape.engine.bind.BoundParameters;
import nz.org.riskscape.engine.bind.InputFieldProperty;
import nz.org.riskscape.engine.bind.Parameter;
import nz.org.riskscape.engine.bind.ParameterProperties;
import nz.org.riskscape.engine.bind.ParameterProperty;
import nz.org.riskscape.engine.bind.ParameterTemplate;
import nz.org.riskscape.engine.bind.TypedProperty;
import nz.org.riskscape.engine.data.ResolvedBookmark;
import nz.org.riskscape.engine.model.ModelParameter;
import nz.org.riskscape.engine.rl.RealizationContext;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.rl.ExpressionParser;
import nz.org.riskscape.rl.TokenTypes;
import nz.org.riskscape.rl.ast.Expression;
import nz.org.riskscape.rl.ast.FunctionCall;

/**
 * A slightly more specialized {@link ModelParameter} that deals specifically with pipelines,
 * i.e. each parameter ultimately needs to be turned back into a pipeline expression.
 */
public class PipelineModelParameter extends ModelParameter {

  // The id of the LookupBookmark function - the 'bookmark' property type is closely tied to its implementation, but
  // it exists in the defaults plugin, meaning we have to hardcode how it works here
  public static final String LOOKUP_BOOKMARK_FUNCTION_ID = "bookmark";

  public static PipelineModelParameter create(String name, ParameterTemplate template) {
    Class<?> bindTo = ParameterProperties.getBindingClass(template.getProperties())
        .orElse(Expression.class);
    // would be nice to have optional parameters, but they don't really work with the
    // pipeline DSL replacement - you always need *some* value to slot into the pipeline
    Parameter parameter = Parameter.required(name, bindTo);
    return new PipelineModelParameter(parameter, template);
  }

  public PipelineModelParameter(Parameter parameter, ParameterTemplate template) {
    // we always validate the value entered for a pipeline parameter against the
    // parameter properties (if any) defined in the template
    super(parameter.withValidation((c, v) -> template.validate(c, v)), template);
  }

  /**
   * Updates the underlying {@link Parameter} with the default value. If no properties
   * have been defined for the {@link ParameterTemplate}, then we can try to infer
   * some basic properties from the default value, e.g. {@link TypedProperty#NUMERIC}.
   */
  public PipelineModelParameter withDefaultValue(BindingContext context, Object value) {
    Parameter newParam = getParameter()
        .withNewDefaults((c, p) -> Collections.singletonList(c.bind(p, value.toString())));

    // we may be able to infer some properties from the default value (e.g. numeric)
    // If so, update the template while we're at it
    ParameterTemplate updatedTemplate = getTemplate();
    if (updatedTemplate.getProperties().isEmpty()) {
      // NB: for now, we only pass these inferred properties to the UI. We don't update the
      // parameter binding class - that feels heavy-handed to potentially give the user
      // run-time errors based off a guesss
      updatedTemplate = updatedTemplate.withProperties(inferProperties(context, value));
    }

    return new PipelineModelParameter(newParam, updatedTemplate);
  }

  /**
   * Returns the bound value in Expression form. E.g. we can bind against a
   * {@link ResolvedBookmark} value, but then we need to turn that bookmark object
   * back into an expression that will slot into pipeline DSL
   */
  public Expression getExpression(BoundParameters bound) {
    Object value = bound.getValue(getName());

    if (getProperties().contains(TypedProperty.BOOKMARK)) {
      return getBookmarkExpression(value.toString());
    }

    if (value instanceof Expression || value == null) {
      return (Expression) value;
    }
    // take the underlying string and parse it into an expression
    String toParse = value.toString().trim();

    if (value instanceof String && !(toParse.startsWith("'") && toParse.endsWith("'"))) {
      // the String needs to be quoted, otherwise it won't be a valid expression
      toParse = TokenTypes.quoteText(toParse);
    }

    return ExpressionParser.INSTANCE.parse(toParse);
  }

  private Expression getBookmarkExpression(String boundValue) {
    // first see if it's a `bookmark` function call already - we return that as is
    try {
      Expression parsed = ExpressionParser.parseString(boundValue);

      if (parsed instanceof FunctionCall fc) {
        // it's already wrapped up in a bookmark call, return as is
        if (fc.getIdentifier().getValue().equals("bookmark")) {
          return parsed;
        }
      }
    } catch (RiskscapeException e) {
    }

    // see if we need to squash the user supplied value in with a template bookmark
    String bookmarkExpression = getTemplate()
        .getProperty(ParameterProperties.BOOKMARK_TEMPLATE)
        // if it's present, we return an expression that we know will use the custom user-specified location
        // together with a well-known bookmark
        .map(tp -> "%s(%s, {location: %s})".formatted(
            LOOKUP_BOOKMARK_FUNCTION_ID,
            TokenTypes.quoteTextIfNeeded(tp.getValue()),
            TokenTypes.quoteTextIfNeeded(boundValue)
        ))
        // no template, just return as a bookmark expression
        .orElseGet(() -> "%s(%s)".formatted(LOOKUP_BOOKMARK_FUNCTION_ID, TokenTypes.quoteTextIfNeeded(boundValue)));

    // make sure what is evaluated returns a bookmark
    return ExpressionParser.parseString(bookmarkExpression);
  }

  /**
   * Tries to infer basic properties, such as {@link TypedProperty#NUMERIC} or
   * {@link InputFieldProperty#MULTISELECT}, based on the default value. This
   * inference only really works if the default value is a constant, and we expect
   * the user to always enter values of the same type.
   */
  private Set<ParameterProperty> inferProperties(BindingContext context, Object defaultValue) {
    // take a peek at the default value expression. If it's a valid constant expression,
    // then we might be able to infer some basic properties (e.g. numeric, multiselect)
    RealizationContext realizationContext = context.getProject().newRealizationContext();
    return realizationContext.realizeConstant(defaultValue.toString())
        .map(realized -> ParameterProperties.infer(realized))
        .orElse(Collections.emptySet());
  }

  /**
   * Validates the final expression before it gets inserted into the pipeline. This checks that
   * any amendments we've made to the user's value are still valid.
   */
  public List<Problem> validateExpression(BoundParameters bound) {
    Object boundValue = bound.getValue(getName());
    Expression expression = getExpression(bound);
    if (boundValue != null && !boundValue.equals(expression)) {
      // we've tinkered with the bound value on the way through. We should only ever do this
      // for constant expressions, so sanity-check the doctored expression is still valid.
      // For example, this catches problems where a given file might not match the bookmark template
      RealizationContext realizationContext = bound.getContext().getProject().newRealizationContext();
      return realizationContext.realizeConstant(expression.toSource())
          .composeProblems(Problems.foundWith(getParameter()))
          .getProblems();
    }
    return Collections.emptyList();
  }
}

