/*
 * 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.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import com.google.common.collect.Iterables;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import nz.org.riskscape.engine.bind.Parameter;
import nz.org.riskscape.engine.relation.EmptyRelation;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.pipeline.StepNamingPolicy;
import nz.org.riskscape.rl.ast.StepDefinition;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.problem.ResultOrProblems;

/**
 * A {@link Step} that has been realized, such that it has produced valid output.  A {@link RealizedStep} is a linked
 * traversable DAG in itself, and so is a sort of realized {@link Pipeline} whose outputs can be used.
 */
@RequiredArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(exclude = {"result"})
public class RealizedStep implements AutoCloseable {

  /**
   * @return a RealizedStep useful for testing scenarios where you want to test realization of some pipeline bits
   * appended to *something*
   */
  public static RealizedStep emptyInput(String name, Struct type) {
    return new RealizedStep(
        name,
        NullStep.INSTANCE,
        new StepDefinition(name),
        ResultOrProblems.of(new EmptyRelation(type)),
        Collections.emptyMap(),
        Collections.emptyList(),
        type
    );
  }

  /**
   * @return a RealizedStep with a name and nothing else
   */
  public static RealizedStep named(String named) {
    return new RealizedStep(
        named,
        NullStep.INSTANCE,
        new StepDefinition(named),
        ResultOrProblems.of(new EmptyRelation(Struct.EMPTY_STRUCT)),
        Collections.emptyMap(),
        Collections.emptyList(),
        Struct.EMPTY_STRUCT
    );
  }

  /**
   * @return a RealizedStep with name, ast, parameters, and dependencies set from the given realization input (but not
   * the {@link Realized} object and its type)
   */
  public static RealizedStep fromInput(RealizationInput input) {
    return new RealizedStep(
        input.getName(),
        NullStep.INSTANCE,
        input.getStepDefinition(),
        ResultOrProblems.of(new EmptyRelation(Struct.EMPTY_STRUCT)),
        input.getParameters(),
        input.getDependencies(),
        Struct.EMPTY_STRUCT
    );
  }

  /**
   * The name that was assigned to this step - unique within the pipeline
   */
  private final String name;

  /**
   * The {@link Step} that produced a realized output that went in to this {@link RealizedStep}
   */
  @NonNull
  private final Step implementation;

  /**
   * The part of the syntax tree that defined this step
   */
  private final StepDefinition ast;

  /**
   * The output of the step.
   * TODO drop the {@link ResultOrProblems}?
   */
  @NonNull
  private final ResultOrProblems<? extends Realized> result;

  /**
   * The complete set of {@link Parameter}s that were used to realize this step.
   */
  @NonNull
  private final Map<String, List<?>> boundParameters;

  /**
   * The complete set of {@link RealizedStep}s that were used to realize this step
   */
  @NonNull
  private final List<RealizedStep> dependencies;

  @NonNull
  private final Struct produces;

  /**
   * @return true if this step or a dependency failed
   */
  public boolean isFailed() {
    return result.hasErrors() || Iterables.any(dependencies, RealizedStep::isFailed);
  }

  /**
   * @return true if this step failed, but not if that failure was because of a dependency
   */
  public boolean isDirectlyFailed() {
    // TODO a future commit will change this to sniff a problem code
    return result.hasErrors() && !Iterables.any(dependencies, RealizedStep::isFailed);
  }

  /**
   * @return true if this step has any direct dependencies
   */
  public boolean hasDependencies() {
    return !dependencies.isEmpty();
  }

  /**
   *
   * @return false if this step has any direct dependenices
   */
  public boolean hasNoDependencies() {
    return dependencies.isEmpty();
  }

  /**
   * @return the name assigned to this step, either explicitly in the AST, or a generated one based on the
   * {@link StepNamingPolicy}
   */
  public String getName() {
    return name;
  }

  // keep this to avoid change churn
  public String getStepName() {
    return name;
  }

  @Override
  public String toString() {
    return name + ":" + getProduces();
  }

  /**
   * @return a composite problem for all errors associated with realizing this step, or empty if there were no
   * errors
   */
  public Optional<Problem> getFailureProblem() {
    if (result.hasErrors()) {
      Problem parentProblem = Problems.get(PipelineProblems.class).cannotRealize(ast);

      return Optional.of(parentProblem.withChildren(result.getProblems()));
    } else {
      return Optional.empty();
    }
  }

  @Override
  public void close() {
    result.ifPresent(Realized::close);
  }

  public Class<? extends Realized> getStepType() {
    return Realized.getRealizedInterface(getResult().get().getClass());
  }

  /**
   * Returns the realized result of this step, cast to a given type.  Mostly useful for internal workings where the step
   * is assumed to be of a specific type, as it deals in `Optional`s, not `ResultOrProblems`
   *
   * @param stepType The type of the result
   * @return The realized result, or `empty` if this step failed to realize, or if the step isn't of the given step type
   */
  public <T extends Realized> Optional<T> getRealized(Class<T> stepType) {
    return getResult()
        .map(realized -> stepType.isInstance(realized) ? Optional.of(stepType.cast(realized)) : Optional.<T>empty())
        .orElse(Optional.empty());
  }

  /**
   * @return a list of problems from the attempt to realize this step, or an empty list if it didn't have problems
   * during realization
   */
  public List<Problem> getProblems() {
    if (result.hasProblems()) {
      return result.getProblems();
    } else {
      return Collections.emptyList();
    }
  }

  /**
   * Convenience version of {@link #withProblems(List)}
   */
  public RealizedStep withProblems(Problem... problems) {
    return withProblems(Arrays.asList(problems));
  }

  /**
   * Clones this RealizedStep with only given problems, marking it as failed if problem list has errors
   */
  public RealizedStep withProblems(List<Problem> problems) {
    if (Problem.hasErrors(problems)) {
      return new RealizedStep(name, implementation, ast, ResultOrProblems.failed(problems), boundParameters,
          dependencies,
          Struct.EMPTY_STRUCT);
    } else {
      return new RealizedStep(name, implementation, ast, result.withMoreProblems(problems), boundParameters,
          dependencies, produces);
    }
  }

  /**
   * Clone this step, setting the {@link Step} implementation that realized it.
   */
  public RealizedStep realizedBy(Step step) {
    return new RealizedStep(name, step, ast, result, boundParameters, dependencies, produces);
  }

  /**
   * Clone this step, replacing the realized result and setting the produced type to be from that result as well
   * (setting it to the empty struct if it's failed)
   */
  public RealizedStep withResult(ResultOrProblems<? extends Realized> realizedOr) {
    return new RealizedStep(name, implementation, ast, realizedOr, boundParameters, dependencies,
                realizedOr.map(Realized::getProducedType).orElse(Struct.EMPTY_STRUCT));
  }

  /**
   * Clone this step, replacing the realized result and setting the produced type to be from that result as well
   */
  public RealizedStep withResult(Realized realized) {
    return withResult(realized, realized.getProducedType());
  }

  /**
   * Clone this step, replacing the result and realized type
   */
  public RealizedStep withResult(Realized realized, Struct newType) {
    return new RealizedStep(name, implementation, ast, ResultOrProblems.of(realized), boundParameters, dependencies,
        newType);
  }

  /**
   * Clone this step, giving it a new name
   */
  public RealizedStep withName(String newName) {
    return new RealizedStep(newName, implementation, ast, result, boundParameters, dependencies,
        produces);
  }

  /**
   * Convenience version of {@link #withDependencies(List)}
   */
  public RealizedStep withDependencies(RealizedStep... newDependencies) {
    // NB List.of provides null check
    return withDependencies(List.of(newDependencies));
  }

  /**
   * Clone this step, replacing the dependencies
   */
  public RealizedStep withDependencies(List<RealizedStep> newDependencies) {
    return new RealizedStep(name, implementation, ast, result, boundParameters, newDependencies,
        produces);
  }

  /**
   * Clone this step, replacing the parameters with the given ones
   */
  public RealizedStep withParameters(Map<String, List<?>> newParams) {
    return new RealizedStep(name, implementation, ast, result, newParams, dependencies, produces);
  }

  /**
   * Clone this step, replacing the {@link StepDefinition} that is said to have realized this step.
   */
  public RealizedStep withAst(StepDefinition defn) {
    return new RealizedStep(name, implementation, defn, result, boundParameters, dependencies, produces);
  }

  /**
   * @return true if this step was explicitly named via the step's AST
   */
  public boolean isNamed() {
    return getAst().getName().isPresent();
  }
}
