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

import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import org.python.core.Py;
import org.python.core.PyException;
import org.python.core.PyIndentationError;
import org.python.core.PySyntaxError;
import org.python.core.PyTuple;
import org.python.core.PyType;

import lombok.Getter;
import nz.org.riskscape.engine.RiskscapeException;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problems;

@SuppressWarnings("serial")
public class JythonScriptException extends RiskscapeException {


  private static Problem problemFromPyException(PyException cause) {
    if (cause instanceof PySyntaxError) {
      return Problem.error(descriptionFromPySyntaxError((PySyntaxError) cause));
    } else if (cause instanceof PyIndentationError) {
      return Problem.error(descriptionFromPyIndentationError((PyIndentationError) cause));
    } else if (Py.ImportError.equals(cause.type)) {
      // import error may mean the user is trying to use CPython-style imports
      return toProblem(cause).withChildren(JythonProblems.get().importErrorTip());
    } else {
      return toProblem(cause);
    }
  }

  /**
   * Converts a Python Exception to a RiskScape {@link Problem} that contains the file/line info where the exception
   * occurred (if a traceback is present).
   */
  public static Problem toProblem(PyException cause) {
    String type;
    if (cause.type instanceof PyType) {
      type = ((PyType) cause.type).getName();
    } else {
      type = Objects.toString(cause.type, "<unknown>");
    }

    String reason = cause.value.toString();
    List<Problem> children = new ArrayList<>();

    // If Python the code invoked RiskScape code and then *that* threw an exception,
    // then it could be an i18n Problem-based exception. In which case we need to
    // nest the RS exception as a child problem so it gets displayed correctly
    if (cause.getCause() instanceof RiskscapeException) {
      reason = "";
      children.add(Problems.caught(cause.getCause()));
    }

    if (cause.traceback != null) {
      int lineno = cause.traceback.tb_lineno;
      String filename = cause.traceback.tb_frame.f_code.co_filename;

      return Problem.error(ProblemCodes.EXCEPTION, type.toString(), reason, filename, lineno).withChildren(children);
    } else {
      return Problem.error(ProblemCodes.EXCEPTION_NO_CAUSE, type.toString(), reason).withChildren(children);
    }
  }

  private static String descriptionFromPyIndentationError(PyIndentationError cause) {
    String type = "IndentationError";

    PyTuple value = (PyTuple) cause.value;
    String message = (String) value.get(0);
    PyTuple details = (PyTuple) value.get(1);
    String filename = (String) details.get(0);
    Integer lineno = (Integer) details.get(1);
    Integer column = (Integer) details.get(2);

    return String.format("%s: %s - File \"%s\", line %d, position %d", type, message, filename, lineno, column);
  }

  public static String descriptionFromPySyntaxError(PySyntaxError cause) {
    String type = "SyntaxError";

    PyTuple value = (PyTuple) cause.value;
    String message = (String) value.get(0);
    PyTuple details = (PyTuple) value.get(1);
    String filename = (String) details.get(0);
    Integer lineno = (Integer) details.get(1);
    Integer column = (Integer) details.get(2);
    // we don't use this, but this contains the offending syntax
//    String text = (String) details.get(3);

    return String.format("%s: %s - File \"%s\", line %d, position %d", type, message, filename, lineno, column);
  }

  @Getter
  private final Object typeOfThing;

  @Getter
  private final URI source;

  @Getter
  private final Problem causeProblem;

  public JythonScriptException(Object typeOfThing, URI source, Exception cause) {
    super(cause.toString(), cause);
    this.typeOfThing = typeOfThing;
    this.source = source;

    if (cause instanceof PyException) {
      this.causeProblem = problemFromPyException((PyException) cause);
    } else {
      this.causeProblem = Problems.caught(cause);
    }
  }


  public JythonScriptException(Object typeOfThing, URI source, Problem cause) {
    super(cause);
    this.typeOfThing = typeOfThing;
    this.source = source;
    this.causeProblem = cause;
  }

  @Override
  public Problem getProblem() {
    return Problems.get(JythonProblems.class).couldNotCreateThing(typeOfThing, source)
        .withChildren(causeProblem);
  }

}
