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

import static nz.org.riskscape.problem.StandardCodes.*;

import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import com.google.common.base.Strings;
import com.google.common.collect.Lists;

import nz.org.riskscape.engine.RiskscapeException;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.engine.problem.ProblemFactory;
import nz.org.riskscape.engine.problem.ProblemFactoryProxy;
import nz.org.riskscape.engine.problem.ProblemPlaceholder;
import nz.org.riskscape.problem.Problem.Severity;

/**
 * The Problems interface exists to abstract over a single {@link Problem} or a collection of them (ProblemList),
 * because most of the time when dealing with errors it doesn't matter whether you have one or many, you just want to
 * pass one or more around.
 *
 * Helpers for generating Problem objects.
 *
 * General tips:
 * - Try to reuse existing {@link ProblemFactory} APIs where possible.
 * - To help make messages reusable, try to think in generic terms - use '[thing]' as a
 *   placeholder in your message. E.g. use "[thing] was invalid" over the more specific
 *   "pipeline step '[name]' was invalid".
 * - Use objects as building blocks in your message, i.e. just pass in the whole NamedStep
 *   as an arg rather than using NamedStep.getName(). This means we can display things in
 *   a consistent, user-friendly, translatable way. Displaying these objects args is handled
 *   in ObjectRenderer.
 * - If you don't have an instance of a particular object, you may be able to use
 *   {@link ProblemPlaceholder} as a placeholder.
 * - If it's an exception, always use {@link #caught(Throwable)}.
 *   Never use {@link Exception#getMessage()} directly.
 * - If you just want to group or nest problems together,
 *   use {@link #foundWith(Object, List)}.
 */
public interface Problems extends Iterable<Problem> {

  /**
   * Creates a Problem  for an error found *with* a given object. I.e. there is
   * clearly a *thing* (Parameter, Step, Bookmark, etc) that is associated with or
   * affected by this Problem.
   * This results in the same end message as {@link Problem#error(ProblemCode, Object...)},
   * but stores some extra contextual info with the Problem.
   * Note that the 'thing' should always be the first MessageFormat argument,
   * i.e. {0} in the .properties file. The affected thing's contextual info (i.e. class,
   * name) is preserved and can be interrogated later (i.e. by {@link Problem#getAffectedClass()}).
   */
  static Problem errorWith(ProblemCode code, Object thing, Object... otherArgs) {
    // prepend the affected 'thing' to the start of the args list
    List<Object> args = Lists.newArrayList(otherArgs);
    args.add(0, thing);
    return Problem.error(code, args.toArray()).affecting(thing);
  }

  static Problem foundWith(Object thing, Problems children) {
    // NB reverse this with the other foundWith
    return foundWith(thing, children.toList());
  }

  /**
   * Collates the children into a top-level problem: "Problems found with {thing}"
   */
  static Problem foundWith(Object thing, List<Problem> children) {
    Problem problem = new Problem(children, PROBLEMS_FOUND, thing).affecting(thing);
    if (children.isEmpty()) {
      problem = problem.withSeverity(Severity.ERROR);
    }
    return problem;
  }

  /**
   * Creates a simple "Problems found with {thing}" problem with optional child
   * problems. This can be a useful placeholder when you want to nest problems but
   * you don't have all the child problems yet. Or you only have a single child
   * problem rather than a list.
   */
  static Problem foundWith(Object thing, Problem... child) {
    return Problems.foundWith(thing, Arrays.asList(child));
  }


  /* same as above, but the problem occurred before we could create the context object
   * (but we know the desired class and its name) */
  static Problem foundWith(Class<?> thing, String name,
      List<Problem> children) {
    return Problems.foundWith(ProblemPlaceholder.of(thing, name), children);
  }

  /* same as above, but the problem occurred before we could create the context object
   * (but we know the desired class and its name) */
  static Problem foundWith(Class<?> thing, String name, Problem... child) {
    return Problems.foundWith(thing, name, Arrays.asList(child));
  }


  /**
   * Helper for wrapping a list of problems with a single problem - useful where you go from a multiple problem API to
   * a single problem API (sigh).
   *
   * Returns the 0th element if children is of size 1
   */
  static Problem toSingleProblem(List<Problem> children) {
    if (children.size() == 1) {
      return children.get(0);
    } else {
      return GeneralProblems.get().multipleProblems().withChildren(children).withSeverity(Problem.max(children));
    }
  }

  static Problem caught(Throwable ex) {
    String message;
    if (ex instanceof RiskscapeException) {
      RiskscapeException rex = (RiskscapeException) ex;
      if (rex.getProblem() != null) {
        // return the original problem unadulterated (note we don't want to overwrite
        // any exception info that the original problem may already have associated with it)
        return rex.getProblem();
      } else {
        message = Strings.isNullOrEmpty(ex.getMessage()) ? rex.toString() : rex.getMessage();
      }
    } else if (ex instanceof ProblemException) {
      return Problems.toSingleProblem(((ProblemException) ex).getProblems());
    } else if (ex.getCause() instanceof RiskscapeException) {
      // chances are the outer exception will just be a toString() of the nested RiskscapeException.
      // Strip off the outer exception and just display the original problem/exception
      return caught(ex.getCause());
    } else {
      message = ex.toString();
    }

    return Problem.error(CAUGHT_EXCEPTION, message).withException(ex);
  }

  /**
   * Produces an error: "Failed to parse [expectedThing]'
   * E.g. Failed to parse pipeline 'foo'
   * Similar to {@link nz.org.riskscape.rl.ast.ExpressionProblems#cannotParse(Object, String)}
   * but useful when parsing something other than an expressions, or when we don't have easy access
   * to the problematic expression itself.
   */
  static Problem parseError(Object expected) {
    return Problem.error(CANT_PARSE, expected, "").affecting(expected);
  }

  /**
   * Gets a (proxied) ProblemFactory that can be used to generate Problems.
   * The standard usage would be:
   *   Problem problem = Problem.create(MyFactory.class).myErrorApi(args);
   */
  @SuppressWarnings("unchecked")
  static <T extends ProblemFactory> T get(Class<T> factory) {
    return (T) Proxy.newProxyInstance(
        factory.getClassLoader(),
        new Class[] {factory},
        new ProblemFactoryProxy());
  }

  /**
   * An empty ProblemList
   */
  Problems NONE = ProblemList.of();

  /**
   * @return the correct thing from a list of children, either NONE, the individual problem, or a {@link ProblemList}
   */
  static Problems from(List<Problem> children) {
    if (children.size() == 0) {
      return NONE;
    } else if (children.size() == 1) {
      return children.get(0);
    } else {
      return ProblemList.copyOf(children);
    }
  }

  static Problems from(Problem... children) {
    if (children.length == 0) {
      return NONE;
    } else if (children.length == 1) {
      return children[0];
    } else {
      return ProblemList.copyOf(List.of(children));
    }
  }

  /**
   * Convert this Problems in to a single problem, or empty if this is Problems.NONE
   */
  Optional<Problem> toProblem();

  /**
   * Convert this Problems in to a (possibly empty) list of Problems
   * @return
   */
  List<Problem> toList();

  /**
   * @return a stream over any individual {@link Problem}s
   */
  Stream<Problem> stream();

  /**
   * @return true if this {@link Problems} will yield one or more {@link Problem}s
   */
  default boolean isPresent() {
    return !isEmpty();
  }

  /**
   * @return true if this Problems is NONE
   * @return
   */
  boolean isEmpty();

  /**
   * @return the maximum severity of any {@link Problem}s, or INFO if empty.
   */
  Severity getSeverity();
}
