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

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;

import com.google.common.base.CaseFormat;

import lombok.RequiredArgsConstructor;
import nz.org.riskscape.engine.problem.Affecting.Target;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problem.Severity;
import nz.org.riskscape.problem.ProblemCode;

/**
 * Works as a Proxy for a given {@link ProblemFactory}
 */
public class ProblemFactoryProxy implements InvocationHandler {

  @RequiredArgsConstructor
  public static class JavaMethodCode implements ProblemCode {

    private final Method method;

    @Override
    public String toKey() {
      return method.getDeclaringClass().getCanonicalName() + "." + name();
    }

    @Override
    public String name() {
      // stick with enum-style upper-case ProblemCode names
      return CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, method.getName());
    }

    @Override
    public boolean equals(Object obj) {
      if (obj instanceof ProblemCode) {
        ProblemCode rhs = (ProblemCode) obj;
        return rhs.toKey().equals(this.toKey());
      } else {
        return false;
      }
    }

    @Override
    public int hashCode() {
      return toKey().hashCode();
    }

    @Override
    public String toString() {
      return String.format("JavaMethodCode(%s)", toKey());
    }
  }

  private static final Object[] NO_ARGS = new Object[0];

  /**
   * Intercepts the @{link ProblemFactory} method to create a {@link Problem}
   * based on the method name and factory class name.
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (args == null) {
      args = NO_ARGS;
    }

    boolean lastArgsIsChildren = isLastArgChildren(method);

    // if the last argument is an array of problems, we assume those are problem children
    List<Problem> children;
    if (lastArgsIsChildren) {
      children = getChildrenFromArgs(args);

      // remove last arg now we know it's children - this might be unnecessary, as it'll never get used, we might just
      // be burning cpu cycles...
      Object[] newArgs = new Object[args.length - 1];
      System.arraycopy(args, 0, newArgs, 0, args.length - 1);
      args = newArgs;
    } else {
      children = List.of();
    }

    Annotation[][] parameterAnnotations = method.getParameterAnnotations();
    Object affecting = getAffecting(parameterAnnotations, args);

    // The default is for a problem to be an error
    Severity severity = Severity.ERROR;

    // allow annotations to override the default
    SeverityLevel annotatedSeverity = method.getAnnotation(SeverityLevel.class);
    if (annotatedSeverity != null) {
      severity = annotatedSeverity.value();
    }

    // if there are children, then we ignore any default and take the maximum (consistent with the child-accepting
    // problem constructor as well as Problems.foundWith (which is what we're emulating here)
    if (children.size() > 0) {
      severity = Problem.max(children);
    }

    JavaMethodCode problemCode = new JavaMethodCode(method);
    return new Problem(severity, "", args, null, problemCode, Problem.wrapAffectedObject(affecting),
        Problems.from(children));
  }

  /**
   * @return true if this method accepts var-args for child problems
   */
  private boolean isLastArgChildren(Method method) {
    Class<?>[] types = method.getParameterTypes();

    if (types.length == 0) {
      return false;
    }

    if (types[types.length - 1] == Problem[].class) {
      return true;
    }

    return false;
  }

  /**
   * Returns a list of problems from the method invocation's args - only safe to call if isLastArgChildren is true
   */
  private List<Problem> getChildrenFromArgs(Object[] args) {
    Problem[] children = (Problem[]) args[args.length - 1];
    if (children != null) {
      return List.of(children);
    } else {
      return List.of();
    }
  }

  private Object getAffecting(Annotation[][] parameterAnnotations, Object[] args) {
    String name = null;
    Class<?> clazz = null;

    for (int i = 0; i < parameterAnnotations.length; i++) {
      Annotation[] annotations = parameterAnnotations[i];

      for (Annotation annotation : annotations) {
        if (annotation.annotationType().equals(Affecting.class)) {
          Affecting affecting = (Affecting) annotation;
          if (affecting.value() == Target.OBJECT) {
            return args[i];
          } else if (affecting.value() == Target.NAME) {
            name = args[i].toString();
          } else  {
            clazz = (Class<?>) args[i];
          }
        }
      }
    }

    if (name != null) {
      return new ProblemPlaceholder(name, clazz);
    }

    if (args.length == 0) {
      return null;
    } else {
      return args[0];
    }
  }
}
