import json
import logging
from datetime import datetime
from typing import Any, Dict, List, Iterable

import requests
import nh3
from django.conf import settings as s
from django.contrib.auth import get_user_model
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.core.exceptions import BadRequest, PermissionDenied, ValidationError
from django.core.validators import validate_email
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from oauth2_provider.decorators import protected_resource
from sentry_sdk import capture_exception

from utilities.chem import FormatConverter, IndigoUtils
from utilities.decorators import package_permission_required
from .exceptions import InvalidMolfileException, InvalidSMILESException

from .logic import (
    EPDBURLParser,
    GroupManager,
    PackageManager,
    SearchManager,
    SettingManager,
    UserManager,
)
from .models import (
    AdditionalInformation,
    APIToken,
    Compound,
    CompoundStructure,
    ClassifierPluginModel,
    Edge,
    EnviFormer,
    EnzymeLink,
    EPModel,
    ExpansionSchemeChoice,
    ExternalDatabase,
    ExternalIdentifier,
    Group,
    GroupPackagePermission,
    JobLog,
    License,
    MLRelativeReasoning,
    Node,
    Pathway,
    Permission,
    PropertyPluginModel,
    Reaction,
    Rule,
    RuleBasedRelativeReasoning,
    Scenario,
    SimpleAmbitRule,
    User,
    UserPackagePermission,
)

logger = logging.getLogger(__name__)

Package = s.GET_PACKAGE_MODEL()


def log_post_params(request):
    if s.DEBUG:
        for k, v in request.POST.items():
            logger.debug(f"{k}\t{v}")


def get_error_handler_context(request, for_user=None) -> Dict[str, Any]:
    current_user = _anonymous_or_real(request)

    if for_user:
        current_user = for_user

    ctx = {
        "title": "enviPath",
        "meta": {
            "site_id": s.MATOMO_SITE_ID,
            "version": "0.0.1",
            "server_url": s.SERVER_URL,
            "user": current_user,
            "enabled_features": s.FLAGS,
            "debug": s.DEBUG,
        },
    }
    return ctx


def error(request, message: str, detail: str, code: int = 400):
    context = get_base_context(request)
    error_context = {
        "error_message": message,
        "error_detail": detail,
    }

    if request.headers.get("Accept") == "application/json":
        return JsonResponse(error_context, status=500)

    context.update(**error_context)
    return render(request, "errors/error.html", context, status=code)


def handler400(request, exception):
    """Custom 400 Bad Request error handler"""
    context = get_error_handler_context(request)
    context["public_mode"] = True
    return render(request, "errors/400_bad_request.html", context, status=400)


def handler403(request, exception):
    """Custom 403 Forbidden error handler"""
    context = get_error_handler_context(request)
    context["public_mode"] = True
    return render(request, "errors/403_access_denied.html", context, status=403)


def handler404(request, exception):
    """Custom 404 Not Found error handler"""
    context = get_error_handler_context(request)
    context["public_mode"] = True
    return render(request, "errors/404_not_found.html", context, status=404)


def handler500(request):
    """Custom 500 Internal Server Error handler"""
    context = get_error_handler_context(request)

    error_context = {}
    error_context["error_message"] = "Internal Server Error"
    error_context["error_detail"] = "An unexpected error occurred. Please try again later."

    if request.headers.get("Accept") == "application/json":
        return JsonResponse(error_context, status=500)

    context["public_mode"] = True
    context["error_code"] = 500
    context["error_description"] = (
        "We encountered an unexpected error while processing your request. Our team has been notified and is working to resolve the issue."
    )
    context.update(**error_context)

    return render(request, "errors/error.html", context, status=500)


def login(request):
    context = get_base_context(request)

    if s.CAP_ENABLED:
        context["CAP_ENABLED"] = s.CAP_ENABLED
        context["CAP_API_BASE"] = s.CAP_API_BASE
        context["CAP_SITE_KEY"] = s.CAP_SITE_KEY

    if request.method == "GET":
        context["title"] = "enviPath"
        context["next"] = request.GET.get("next", "")
        return render(request, "static/login.html", context)

    elif request.method == "POST":
        from django.contrib.auth import authenticate, login

        username = request.POST.get("username").strip()
        if username != request.POST.get("username"):
            context["message"] = "Login failed!"
            return render(request, "static/login.html", context)

        password = request.POST.get("password")

        # Get email for username and check if the account is active
        try:
            # Try username and if it fails check if username is a valid email adress and we'll find a user
            try:
                temp_user = get_user_model().objects.get(username=username)
            except get_user_model().DoesNotExist as e:
                # validate_email returns None if input is valid -> check for None
                # Otherwise a ValidationError is raised
                if validate_email(username) is None:
                    temp_user = get_user_model().objects.get(email=username)
                else:
                    raise e

            if not temp_user.is_active:
                context["message"] = "User account is not activated yet!"
                return render(request, "static/login.html", context)

            email = temp_user.email
        except (get_user_model().DoesNotExist, ValidationError):
            context["message"] = "Login failed!"
            return render(request, "static/login.html", context)
        except Exception as e:
            logger.info(f"Uncaught exception while trying to login: {e}")
            context["message"] = "Login failed!"
            return render(request, "static/login.html", context)

        try:
            user = authenticate(username=email, password=password)
        except Exception:
            context["message"] = "Login failed!"
            return render(request, "static/login.html", context)

        if user is not None:
            login(request, user)

            if next := request.POST.get("next"):
                return redirect(next)

            return redirect(reverse("index"))
        else:
            context["message"] = "Login failed!"
            return render(request, "static/login.html", context)
    else:
        return HttpResponseNotAllowed(["GET", "POST"])


def logout(request):
    if request.method == "POST":
        is_logout = bool(request.POST.get("logout", False))

        if is_logout:
            from django.contrib.auth import logout

            logout(request)
            return redirect(s.SERVER_URL)

    return HttpResponseBadRequest()


def register(request):
    context = get_base_context(request)

    if s.CAP_ENABLED:
        context["CAP_ENABLED"] = s.CAP_ENABLED
        context["CAP_API_BASE"] = s.CAP_API_BASE
        context["CAP_SITE_KEY"] = s.CAP_SITE_KEY

    if request.method == "GET":
        # Redirect to unified login page with signup tab
        next_url = request.GET.get("next", "")
        redirect_url = reverse("login") + "#signup"

        if next_url:
            redirect_url += f"?next={next_url}"

        return redirect(redirect_url)
    elif request.method == "POST":
        context["title"] = "enviPath"
        if next := request.POST.get("next"):
            context["next"] = next

        # Catpcha
        if s.CAP_ENABLED:
            cap_token = request.POST.get("cap-token")

            if not cap_token:
                context["message"] = "Missing CAP Token."
                return render(request, "static/login.html", context)

            verify_url = f"{s.CAP_API_BASE}/{s.CAP_SITE_KEY}/siteverify"
            payload = {
                "secret": s.CAP_SECRET_KEY,
                "response": cap_token,
            }

            try:
                resp = requests.post(verify_url, json=payload, timeout=10)
                resp.raise_for_status()
                verify_data = resp.json()
            except requests.RequestException:
                context["message"] = "Captcha verification failed."
                return render(request, "static/login.html", context)

            if not verify_data.get("success"):
                context["message"] = "Captcha check failed. Please try again."
                return render(request, "static/login.html", context)
        # End Captcha

        username = request.POST.get("username", "").strip()
        email = request.POST.get("email", "").strip()
        password = request.POST.get("password", "").strip()
        rpassword = request.POST.get("rpassword", "").strip()

        if not (username and email and password):
            context["message"] = "Invalid username/email/password"
            return render(request, "static/login.html", context)

        try:
            UnicodeUsernameValidator()(username)
        except ValidationError:
            context["message"] = (
                "Enter a valid username. This value may contain only letters, "
                "numbers, and @/./+/-/_ characters."
            )
            return render(request, "static/login.html", context)

        if password != rpassword or password == "":
            context["message"] = "Registration failed, provided passwords differ!"
            return render(request, "static/login.html", context)

        try:
            u = UserManager.create_user(username, email, password)
            logger.info(f"Created user {u.username} ({u.pk})")

            try:
                from .tasks import send_registration_mail

                send_registration_mail.delay(u.pk)
            except Exception as e:
                logger.error(f"Failed to send registration mail to {u.email}: {e}")
                capture_exception(e)

        except Exception:
            context["message"] = "Registration failed! Couldn't create User Account."
            return render(request, "static/login.html", context)

        if s.ADMIN_APPROVAL_REQUIRED:
            context["success_message"] = (
                "Your account has been created! An admin will activate it soon!"
            )
        else:
            context["success_message"] = (
                "Account has been created! You'll receive a mail to activate your account shortly."
            )

        return render(request, "static/login.html", context)
    else:
        return HttpResponseNotAllowed(["GET", "POST"])


def editable(request, user):
    # if user.is_superuser:
    #     return True

    url = request.build_absolute_uri(request.path)
    if PackageManager.is_package_url(url):
        _package = PackageManager.get_package_lp(request.build_absolute_uri())
        return PackageManager.writable(user, _package)
    elif GroupManager.is_group_url(url):
        _group = GroupManager.get_group_lp(request.build_absolute_uri())
        return GroupManager.writable(user, _group)
    elif UserManager.is_user_url(url):
        _user = UserManager.get_user_lp(request.build_absolute_uri())
        return UserManager.writable(user, _user)
    elif url in [
        s.SERVER_URL,
        f"{s.SERVER_URL}/",
        f"{s.SERVER_URL}/package",
        f"{s.SERVER_URL}/user",
        f"{s.SERVER_URL}/group",
        f"{s.SERVER_URL}/search",
    ]:
        return True
    else:
        logger.debug(f"Unknown url: {url}")
        return False


def get_base_context(request, for_user=None) -> Dict[str, Any]:
    current_user = _anonymous_or_real(request)
    can_edit = editable(request, current_user)

    parser = EPDBURLParser(request.build_absolute_uri(request.path))

    url_contains_package = False
    if parser.contains_package_url() or parser.is_package_url():
        url_contains_package = True

    if for_user:
        current_user = for_user

    ctx = {
        "title": "enviPath",
        "meta": {
            "version": "0.0.1",
            "server_url": s.SERVER_URL,
            "user": current_user,
            "can_edit": can_edit,
            "url_contains_package": url_contains_package,
            "readable_packages": PackageManager.get_all_readable_packages(
                current_user, include_reviewed=True
            ),
            "writeable_packages": PackageManager.get_all_writeable_packages(current_user),
            "available_groups": GroupManager.get_groups(current_user),
            "available_settings": SettingManager.get_all_settings(current_user),
            "enabled_features": s.FLAGS,
            "debug": s.DEBUG,
            "external_databases": ExternalDatabase.get_databases(),
            "site_id": s.MATOMO_SITE_ID,
        },
    }

    return ctx


def _anonymous_or_real(request):
    if request.user and (request.user.is_authenticated and not request.user.is_anonymous):
        return request.user
    return get_user_model().objects.get(username="anonymous")


def breadcrumbs(
    first_level_object=None,
    second_level_namespace=None,
    second_level_object=None,
    third_level_namespace=None,
    third_level_object=None,
) -> List[Dict[str, str]]:
    bread = [
        {"Home": s.SERVER_URL},
        {"Package": s.SERVER_URL + "/package"},
    ]
    if first_level_object is not None:
        bread.append({first_level_object.get_name(): first_level_object.url})

    if second_level_namespace is not None:
        bread.append(
            {
                f"{second_level_namespace}".capitalize(): first_level_object.url
                + f"/{second_level_namespace}"
            }
        )

    if second_level_object is not None:
        bread.append({second_level_object.get_name(): second_level_object.url})

    if third_level_namespace is not None:
        bread.append(
            {
                f"{third_level_namespace}".capitalize(): second_level_object.url
                + f"/{third_level_namespace}"
            }
        )

    if third_level_object is not None:
        bread.append({third_level_object.get_name(): third_level_object.url})

    return bread


def set_scenarios(current_user, attach_object, scenario_urls: List[str]):
    scens = []
    for scenario_url in scenario_urls:
        # As empty lists will be removed in POST request we'll send ['']
        if scenario_url == "":
            continue

        package = PackageManager.get_package_by_url(current_user, scenario_url)
        scen = Scenario.objects.get(package=package, uuid=scenario_url.split("/")[-1])
        scens.append(scen)

    attach_object.set_scenarios(scens)


def set_aliases(current_user, attach_object, aliases: List[str]):
    # As empty lists will be removed in POST request we'll send ['']
    if aliases == [""]:
        aliases = []

    attach_object.aliases = aliases
    attach_object.save()


def copy_object(current_user, target_package: "Package", source_object_url: str):
    # Ensures that source object is readable
    source_package = PackageManager.get_package_by_url(current_user, source_object_url)

    if source_package == target_package:
        raise ValueError(f"Can't copy object {source_object_url} to the same package!")

    parser = EPDBURLParser(source_object_url)

    # if the url don't contain a package or is a plain package
    if not parser.contains_package_url():
        raise ValueError(f"Object {source_object_url} can't be copied!")

    # Gets the most specific object
    source_object = parser.get_object()

    if hasattr(source_object, "copy"):
        mapping = dict()
        copy = source_object.copy(target_package, mapping)

        if s.DEBUG:
            for k, v in mapping.items():
                logger.debug(f"Mapping {k.url} to {v.url}")

        return copy

    raise ValueError(f"Object {source_object} can't be copied!")


def index(request):
    context = get_base_context(request)
    context["title"] = "enviPath - Home"
    context["meta"]["current_package"] = context["meta"]["user"].default_package

    if request.GET.get("getMLServerPath", False):
        return JsonResponse({"mlServerPath": s.SERVER_URL})

    return render(request, "index/index.html", context)


def predict_pathway(request):
    """Top-level predict pathway view using user's default package."""
    if request.method != "GET":
        return HttpResponseNotAllowed(["GET"])

    context = get_base_context(request)
    context["title"] = "enviPath - Predict Pathway"
    context["meta"]["current_package"] = context["meta"]["user"].default_package

    return render(request, "predict_pathway.html", context)


def batch_predict_pathway(request):
    """Top-level predict pathway view using user's default package."""
    if request.method != "GET":
        return HttpResponseNotAllowed(["GET"])

    context = get_base_context(request)
    context["title"] = "enviPath - Batch Predict Pathway"
    context["meta"]["current_package"] = context["meta"]["user"].default_package
    context["batch_predict_max_compounds"] = s.BATCH_PREDICT_MAX_COMPOUNDS

    return render(request, "batch_predict_pathway.html", context)


@package_permission_required()
def package_predict_pathway(request, package_uuid):
    """Package-specific predict pathway view."""
    if request.method != "GET":
        return HttpResponseNotAllowed(["GET"])

    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)

    context = get_base_context(request)
    context["title"] = f"enviPath - {current_package.get_name()} - Predict Pathway"
    context["meta"]["current_package"] = current_package

    return render(request, "predict_pathway.html", context)


def packages(request):
    current_user = _anonymous_or_real(request)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = "enviPath - Packages"
        context["meta"]["current_package"] = context["meta"]["user"].default_package
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Package": s.SERVER_URL + "/package"},
        ]

        # Context for paginated template
        context["entity_type"] = "package"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/packages/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_title"] = "packages"

        return render(request, "collections/packages_paginated.html", context)

    elif request.method == "POST":
        if hidden := request.POST.get("hidden", None):
            if hidden in ["import-legacy-package-json", "import-package-json"]:
                f = request.FILES["file"]

                try:
                    file_data = f.read().decode("utf-8")
                    data = json.loads(file_data)

                    if hidden == "import-legacy-package-json":
                        pack = PackageManager.import_legacy_package(data, current_user)
                    else:
                        pack = PackageManager.import_package(data, current_user)

                    return redirect(pack.url)
                except UnicodeDecodeError:
                    return error(request, "Invalid encoding.", "Invalid encoding, must be UTF-8")
            else:
                return HttpResponseBadRequest()
        else:
            package_name = request.POST.get("package-name")
            package_description = request.POST.get(
                "package-description", s.DEFAULT_VALUES["description"]
            )

            created_package = PackageManager.create_package(
                current_user, package_name, package_description
            )

            return redirect(created_package.url)

    elif request.method == "OPTIONS":
        response = HttpResponse()
        response["allow"] = ",".join(["GET", "POST"])
        return response

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


def compounds(request):
    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = "enviPath - Compounds"
        context["meta"]["current_package"] = context["meta"]["user"].default_package
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Compound": s.SERVER_URL + "/compound"},
        ]

        # Context for paginated template
        context["entity_type"] = "compound"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/compounds/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_mode"] = "tabbed"
        context["list_title"] = "compounds"

        return render(request, "collections/compounds_paginated.html", context)

    elif request.method == "POST":
        # delegate to default package
        current_user = _anonymous_or_real(request)
        default_package = current_user.default_package
        return package_compounds(request, default_package.uuid)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


def rules(request):
    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = "enviPath - Rules"
        context["meta"]["current_package"] = context["meta"]["user"].default_package
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Rule": s.SERVER_URL + "/rule"},
        ]

        # Context for paginated template
        context["entity_type"] = "rule"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/rules/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_title"] = "rules"

        return render(request, "collections/rules_paginated.html", context)

    elif request.method == "POST":
        # delegate to default package
        current_user = _anonymous_or_real(request)
        default_package = current_user.default_package
        return package_rules(request, default_package.uuid)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


def reactions(request):
    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = "enviPath - Reactions"
        context["meta"]["current_package"] = context["meta"]["user"].default_package
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Reaction": s.SERVER_URL + "/reaction"},
        ]

        # Context for paginated template
        context["entity_type"] = "reaction"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/reactions/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_title"] = "reactions"

        return render(request, "collections/reactions_paginated.html", context)

    elif request.method == "POST":
        # delegate to default package
        current_user = _anonymous_or_real(request)
        default_package = current_user.default_package
        return package_reactions(request, default_package.uuid)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


def pathways(request):
    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = "enviPath - Pathways"
        context["meta"]["current_package"] = context["meta"]["user"].default_package
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Pathway": s.SERVER_URL + "/pathway"},
        ]

        # Context for paginated template
        context["entity_type"] = "pathway"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/pathways/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_title"] = "pathways"

        return render(request, "collections/pathways_paginated.html", context)

    elif request.method == "POST":
        # delegate to default package
        current_user = _anonymous_or_real(request)
        default_package = current_user.default_package
        return package_pathways(request, default_package.uuid)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


def scenarios(request):
    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = "enviPath - Scenarios"

        context["object_type"] = "scenario"
        context["meta"]["current_package"] = context["meta"]["user"].default_package
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Scenario": s.SERVER_URL + "/scenario"},
        ]

        # Context for paginated template
        context["entity_type"] = "scenario"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/scenarios/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_title"] = "scenarios"

        return render(request, "collections/scenarios_paginated.html", context)

    elif request.method == "POST":
        # delegate to default package
        default_package = request.user.default_package
        return package_scenarios(request, default_package.uuid)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


def models(request):
    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = "enviPath - Models"
        context["meta"]["current_package"] = context["meta"]["user"].default_package
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Model": s.SERVER_URL + "/model"},
        ]

        context["entity_type"] = "model"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_title"] = "models"

        # Keep model_types for potential modal/action use
        context["model_types"] = {
            "ML Relative Reasoning": {
                "type": "ml-relative-reasoning",
                "requires_rule_packages": True,
                "requires_data_packages": True,
            },
            "Rule Based Relative Reasoning": {
                "type": "rule-based-relative-reasoning",
                "requires_rule_packages": True,
                "requires_data_packages": True,
            },
        }

        if s.ENVIFORMER_PRESENT:
            context["model_types"]["EnviFormer"] = {
                "type": "enviformer",
                "requires_rule_packages": False,
                "requires_data_packages": True,
            }

        if s.FLAGS.get("PLUGINS", False):
            for k, v in s.CLASSIFIER_PLUGINS.items():
                context["model_types"][v.display()] = {
                    "type": k,
                    "requires_rule_packages": v.requires_rule_packages(),
                    "requires_data_packages": v.requires_data_packages(),
                    "additional_parameters": v.Config.__name__.lower()
                    if v.Config.__name__ != ""
                    else None,
                }
            for k, v in s.PROPERTY_PLUGINS.items():
                context["model_types"][v.display()] = {
                    "type": k,
                    "requires_rule_packages": v.requires_rule_packages(),
                    "requires_data_packages": v.requires_data_packages(),
                }

        return render(request, "collections/models_paginated.html", context)

    elif request.method == "POST":
        current_user = _anonymous_or_real(request)
        default_package = current_user.default_package
        return package_models(request, default_package.uuid)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


def search(request):
    current_user = _anonymous_or_real(request)

    if request.method == "GET":
        package_urls = request.GET.getlist("packages")
        searchterm = request.GET.get("search", "").strip()
        mode = request.GET.get("mode")

        # add HTTP_ACCEPT check to differentiate between index and ajax call
        if "application/json" in request.META.get("HTTP_ACCEPT") and all([searchterm, mode]):
            if package_urls:
                packages = [
                    PackageManager.get_package_by_url(current_user, p) for p in package_urls
                ]
            else:
                packages = PackageManager.get_reviewed_packages()

            search_result = SearchManager.search(packages, searchterm, mode)

            return JsonResponse(search_result, safe=False)

        context = get_base_context(request)
        context["title"] = "enviPath - Search"

        context["object_type"] = "model"
        context["meta"]["current_package"] = context["meta"]["user"].default_package
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Search": s.SERVER_URL + "/search"},
        ]

        reviewed_package_qs = PackageManager.get_reviewed_packages()
        unreviewed_package_qs = PackageManager.get_all_readable_packages(current_user)

        context["reviewed_objects"] = reviewed_package_qs
        context["unreviewed_objects"] = unreviewed_package_qs

        if all([searchterm, mode]):
            if package_urls:
                packages = [
                    PackageManager.get_package_by_url(current_user, p) for p in package_urls
                ]
            else:
                packages = PackageManager.get_reviewed_packages()

            context["search_result"] = SearchManager.search(packages, searchterm, mode)
            context["search_result"]["searchterm"] = searchterm

        return render(request, "search.html", context)

    else:
        return HttpResponseNotAllowed(["GET"])


@package_permission_required()
def package_models(request, package_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = f"enviPath - {current_package.get_name()} - Models"

        context["meta"]["current_package"] = current_package
        context["object_type"] = "model"
        context["breadcrumbs"] = breadcrumbs(current_package, "model")
        context["entity_type"] = "model"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/model/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_title"] = "models"

        reviewed_model_qs = EPModel.objects.none()
        unreviewed_model_qs = EPModel.objects.none()

        if current_package.reviewed:
            reviewed_model_qs = EPModel.objects.filter(package=current_package).order_by("name")
        else:
            unreviewed_model_qs = EPModel.objects.filter(package=current_package).order_by("name")

        if request.GET.get("all"):
            return JsonResponse(
                {
                    "objects": [
                        {"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
                        for pw in (
                            reviewed_model_qs if current_package.reviewed else unreviewed_model_qs
                        )
                    ]
                }
            )

        context["model_types"] = {
            "ML Relative Reasoning": {
                "type": "ml-relative-reasoning",
                "requires_rule_packages": True,
                "requires_data_packages": True,
            },
            "Rule Based Relative Reasoning": {
                "type": "rule-based-relative-reasoning",
                "requires_rule_packages": True,
                "requires_data_packages": True,
            },
        }
        if s.ENVIFORMER_PRESENT:
            context["model_types"]["EnviFormer"] = {
                "type": "enviformer",
                "requires_rule_packages": False,
                "requires_data_packages": True,
            }

        if s.FLAGS.get("PLUGINS", False):
            for k, v in s.CLASSIFIER_PLUGINS.items():
                context["model_types"][v.display()] = {
                    "type": k,
                    "requires_rule_packages": v.requires_rule_packages(),
                    "requires_data_packages": v.requires_data_packages(),
                    "additional_parameters": v.Config.__name__.lower()
                    if v.Config.__name__ != ""
                    else None,
                }
            for k, v in s.PROPERTY_PLUGINS.items():
                context["model_types"][v.display()] = {
                    "type": k,
                    "requires_rule_packages": v.requires_rule_packages(),
                    "requires_data_packages": v.requires_data_packages(),
                }

        return render(request, "collections/models_paginated.html", context)

    elif request.method == "POST":
        log_post_params(request)

        name = request.POST.get("model-name")
        description = request.POST.get("model-description")

        model_type = request.POST.get("model-type")

        # Generic fields for ML and Rule Based
        rule_packages = request.POST.getlist("model-rule-packages")
        data_packages = request.POST.getlist("model-data-packages")

        # Generic params
        params = {
            "package": current_package,
            "name": name,
            "description": description,
            "data_packages": [
                PackageManager.get_package_by_url(current_user, p) for p in data_packages
            ],
        }

        if model_type == "enviformer":
            threshold = float(request.POST.get("model-threshold", 0.5))
            params["threshold"] = threshold

            mod = EnviFormer.create(**params)
        elif model_type == "ml-relative-reasoning":
            # ML Specific
            threshold = float(request.POST.get("model-threshold", 0.5))
            # TODO handle additional fingerprinter
            # fingerprinter = request.POST.get("model-fingerprinter")

            params["rule_packages"] = [
                PackageManager.get_package_by_url(current_user, p) for p in rule_packages
            ]

            # App Domain related parameters
            build_ad = request.POST.get("build-app-domain", False) == "on"
            num_neighbors = request.POST.get("num-neighbors", 5)
            reliability_threshold = request.POST.get("reliability-threshold", 0.5)
            local_compatibility_threshold = request.POST.get("local-compatibility-threshold", 0.5)

            params["threshold"] = threshold
            # params['fingerprinter'] = fingerprinter
            params["build_app_domain"] = build_ad
            params["app_domain_num_neighbours"] = num_neighbors
            params["app_domain_reliability_threshold"] = reliability_threshold
            params["app_domain_local_compatibility_threshold"] = local_compatibility_threshold

            mod = MLRelativeReasoning.create(**params)
        elif model_type == "rule-based-relative-reasoning":
            params["rule_packages"] = [
                PackageManager.get_package_by_url(current_user, p) for p in rule_packages
            ]

            mod = RuleBasedRelativeReasoning.create(**params)
        elif s.FLAGS.get("PLUGINS", False) and model_type in s.CLASSIFIER_PLUGINS:
            params["plugin_identifier"] = model_type
            impl = s.CLASSIFIER_PLUGINS[model_type]

            if impl.requires_rule_packages():
                params["rule_packages"] = [
                    PackageManager.get_package_by_url(current_user, p) for p in rule_packages
                ]
            else:
                params["rule_packages"] = []

            if not impl.requires_data_packages():
                params["data_packages"] = []

            params["config"] = impl.parse_config(request.POST.dict())

            mod = ClassifierPluginModel.create(**params)
        elif s.FLAGS.get("PLUGINS", False) and model_type in s.PROPERTY_PLUGINS:
            params["plugin_identifier"] = model_type
            impl = s.PROPERTY_PLUGINS[model_type]

            if impl.requires_rule_packages():
                params["rule_packages"] = [
                    PackageManager.get_package_by_url(current_user, p) for p in rule_packages
                ]
            else:
                params["rule_packages"] = []

            if not impl.requires_data_packages():
                del params["data_packages"]

            mod = PropertyPluginModel.create(**params)
        else:
            return error(
                request, "Invalid model type.", f'Model type "{model_type}" is not supported."'
            )

        from .tasks import build_model, dispatch

        dispatch(current_user, build_model, mod.pk)

        return redirect(mod.url)
    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_model(request, package_uuid, model_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)
    current_model = get_object_or_404(EPModel, package=current_package, uuid=model_uuid)

    if request.method == "GET":
        classify = request.GET.get("classify", False)
        ad_assessment = request.GET.get("app-domain-assessment", False)
        # TODO this needs to be generic
        half_life = request.GET.get("half_life", False)

        if any([classify, ad_assessment, half_life]):
            smiles = request.GET.get("smiles", "").strip()

            # Check if smiles is non empty and valid
            if smiles == "":
                return JsonResponse({"error": "Received empty SMILES"}, status=400)

            stereo = FormatConverter.has_stereo(smiles)

            try:
                stand_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
            except ValueError:
                return JsonResponse({"error": f'"{smiles}" is not a valid SMILES'}, status=400)

            if classify:
                from epdb.tasks import dispatch_eager, predict_simple

                _, pred_res = dispatch_eager(
                    current_user, predict_simple, current_model.pk, stand_smiles
                )

                res = {"pred": [], "stereo": stereo}

                for pr in pred_res:
                    if len(pr) > 0:
                        products = []
                        for prod_set in pr.product_sets:
                            logger.debug(f"Checking {prod_set}")
                            products.append(tuple([x for x in prod_set]))

                        res["pred"].append(
                            {
                                "products": list(set(products)),
                                "probability": pr.probability,
                                "btrule": {k: getattr(pr.rule, k) for k in ["url", "name"]}
                                if pr.rule is not None
                                else None,
                            }
                        )

                        # Sort data by prob desc
                        res["pred"] = sorted(
                            res["pred"], key=lambda x: x["probability"], reverse=True
                        )

                return JsonResponse(res, safe=False)

            elif half_life:
                from epdb.tasks import dispatch_eager, predict_simple

                _, run_res = dispatch_eager(
                    current_user, predict_simple, current_model.pk, stand_smiles, include_svg=True
                )

                # Here we expect a single result
                if isinstance(run_res.result, Iterable):
                    return JsonResponse(run_res.result[0].model_dump(mode="json"), safe=False)

                return JsonResponse(run_res.result.model_dump(mode="json"), safe=False)

            else:
                app_domain_assessment = current_model.app_domain.assess(stand_smiles)
                return JsonResponse(app_domain_assessment, safe=False)

        context = get_base_context(request)
        context["title"] = f"enviPath - {current_package.get_name()} - {current_model.get_name()}"

        context["meta"]["current_package"] = current_package
        context["object_type"] = "model"
        context["breadcrumbs"] = breadcrumbs(current_package, "model", current_model)

        context["model"] = current_model
        context["current_object"] = current_model

        if isinstance(current_model, PropertyPluginModel):
            context["plugin_identifier"] = current_model.plugin_identifier
            return render(request, "objects/model/property_model.html", context)
        else:
            return render(request, "objects/model/classification_model.html", context)

    elif request.method == "POST":
        if hidden := request.POST.get("hidden", None):
            if hidden == "delete":
                current_model.delete()
                return redirect(current_package.url + "/model")
            elif hidden == "evaluate":
                from .tasks import dispatch, evaluate_model

                eval_type = request.POST.get("model-evaluation-type")

                if eval_type not in ["sg", "mg"]:
                    return error(
                        request,
                        "Invalid evaluation type",
                        f'Evaluation type "{eval_type}" is not supported. Only "sg" and "mg" are supported.',
                    )

                multigen = eval_type == "mg"

                eval_packages = request.POST.getlist("model-evaluation-packages")
                eval_package_ids = [
                    PackageManager.get_package_by_url(current_user, p).id for p in eval_packages
                ]
                dispatch(current_user, evaluate_model, current_model.pk, multigen, eval_package_ids)

                return redirect(current_model.url)
            elif hidden == "retrain":
                from .tasks import dispatch, retrain

                dispatch(current_user, retrain, current_model.pk)

                return redirect(current_model.url)
            else:
                return HttpResponseBadRequest()
        else:
            # TODO: Move cleaning to property updater
            name = request.POST.get("model-name")
            if name is not None:
                name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()

            description = request.POST.get("model-description")
            if description is not None:
                description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()

            if any([name, description]):
                if name:
                    current_model.name = name

                if description:
                    current_model.description = description

                current_model.save()
                return redirect(current_model.url)

            return HttpResponseBadRequest()

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package(request, package_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)

    if request.method == "GET":
        if request.GET.get("export", False) == "true":
            filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json"
            pack_json = PackageManager.export_package(current_package)
            response = JsonResponse(pack_json, content_type="application/json")
            response["Content-Disposition"] = f'attachment; filename="{filename}"'

            return response

        context = get_base_context(request)
        context["title"] = f"enviPath - {current_package.get_name()}"

        context["meta"]["current_package"] = current_package
        context["object_type"] = "package"
        context["breadcrumbs"] = breadcrumbs(current_package)

        context["package"] = current_package

        user_perms = UserPackagePermission.objects.filter(package=current_package)
        users = get_user_model().objects.exclude(
            id__in=UserPackagePermission.objects.filter(package=current_package).values_list(
                "user_id", flat=True
            )
        )
        users = users.filter(is_active=True)

        group_perms = GroupPackagePermission.objects.filter(package=current_package)
        groups = Group.objects.exclude(
            id__in=GroupPackagePermission.objects.filter(package=current_package).values_list(
                "group_id", flat=True
            )
        )

        context["users"] = users
        context["groups"] = groups
        context["user_permissions"] = user_perms
        context["group_permissions"] = group_perms

        return render(request, "objects/package.html", context)

    elif request.method == "POST":
        log_post_params(request)

        if hidden := request.POST.get("hidden", None):
            if hidden == "delete":
                if current_user.default_package == current_package:
                    return error(
                        request,
                        f'Package "{current_package.get_name()}" is the default and cannot be deleted!',
                        "You cannot delete the default package. If you want to delete this package you have to set another default package first.",
                    )

                logger.debug(current_package.delete())
                return redirect(s.SERVER_URL + "/package")
            elif hidden == "publish-package":
                for g in Group.objects.filter(public=True):
                    PackageManager.grant_read(current_user, current_package, g)
                return redirect(current_package.url)
            elif hidden == "copy":
                object_to_copy = request.POST.get("object_to_copy")

                if not object_to_copy:
                    return error(request, "No object to copy", "There was no object to copy.")

                try:
                    copied_object = copy_object(current_user, current_package, object_to_copy)
                except ValueError:
                    return JsonResponse(
                        {"error": f"Can't copy object {object_to_copy} to the same package!"},
                        status=400,
                    )

                return JsonResponse({"success": copied_object.url})
            else:
                return HttpResponseBadRequest()

        # TODO: Move cleaning to property updater
        new_package_name = request.POST.get("package-name")
        if new_package_name is not None:
            new_package_name = nh3.clean(new_package_name, tags=s.ALLOWED_HTML_TAGS).strip()

        new_package_description = request.POST.get("package-description")
        if new_package_description is not None:
            new_package_description = nh3.clean(
                new_package_description, tags=s.ALLOWED_HTML_TAGS
            ).strip()

        grantee_url = request.POST.get("grantee")
        read = request.POST.get("read") == "on"
        write = request.POST.get("write") == "on"
        owner = request.POST.get("owner") == "on"

        cc_string = request.POST.get("license")

        if new_package_name:
            current_package.name = new_package_name

        if new_package_description:
            current_package.description = new_package_description

        if any([new_package_name, new_package_description]):
            current_package.save()
            return redirect(current_package.url)

        elif any([grantee_url, read, write, owner]):
            if "user" in grantee_url:
                grantee = UserManager.get_user_lp(grantee_url)
            else:
                grantee = GroupManager.get_group_lp(grantee_url)

            max_perm = None
            if read:
                max_perm = Permission.READ[0]
            if write:
                max_perm = Permission.WRITE[0]
            if owner:
                max_perm = Permission.ALL[0]

            PackageManager.update_permissions(current_user, current_package, grantee, max_perm)
            return redirect(current_package.url)

        elif cc_string is not None:
            cc_string = cc_string.strip()
            if cc_string == "no-license":  # Reset the package's license
                current_package.license = None
                current_package.save()
                return redirect(current_package.url)
            else:  # Get the license and assign it to the package
                current_package.license = License.objects.get(cc_string=cc_string)
                current_package.save()

                return redirect(current_package.url)
        else:
            return HttpResponseBadRequest()

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_compounds(request, package_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = f"enviPath - {current_package.get_name()} - Compounds"

        context["meta"]["current_package"] = current_package
        context["object_type"] = "compound"
        context["breadcrumbs"] = breadcrumbs(current_package, "compound")
        context["entity_type"] = "compound"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/compound/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_mode"] = "tabbed"
        context["list_title"] = "compounds"

        reviewed_compound_qs = Compound.objects.none()
        unreviewed_compound_qs = Compound.objects.none()

        if current_package.reviewed:
            reviewed_compound_qs = Compound.objects.filter(package=current_package).order_by("name")
        else:
            unreviewed_compound_qs = Compound.objects.filter(package=current_package).order_by(
                "name"
            )

        if request.GET.get("all"):
            return JsonResponse(
                {
                    "objects": [
                        {"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
                        for pw in (
                            reviewed_compound_qs
                            if current_package.reviewed
                            else unreviewed_compound_qs
                        )
                    ]
                }
            )

        return render(request, "collections/compounds_paginated.html", context)

    elif request.method == "POST":
        compound_name = request.POST.get("compound-name")
        compound_smiles = request.POST.get("compound-smiles")
        compound_molfile = request.POST.get("compound-molfile")
        compound_description = request.POST.get("compound-description")

        try:
            c = Compound.create(
                current_package,
                compound_smiles,
                molfile=compound_molfile,
                name=compound_name,
                description=compound_description,
            )
        except (InvalidSMILESException, InvalidMolfileException) as e:
            raise BadRequest(str(e))

        return redirect(c.url)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_compound(request, package_uuid, compound_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)
    current_compound = get_object_or_404(Compound, package=current_package, uuid=compound_uuid)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = (
            f"enviPath - {current_package.get_name()} - {current_compound.get_name()}"
        )

        context["meta"]["current_package"] = current_package
        context["object_type"] = "compound"
        context["breadcrumbs"] = breadcrumbs(current_package, "compound", current_compound)

        context["compound"] = current_compound
        context["current_object"] = current_compound

        return render(request, "objects/compound.html", context)

    elif request.method == "POST":
        if hidden := request.POST.get("hidden", None):
            if hidden == "delete":
                current_compound.delete()
                return redirect(current_package.url + "/compound")
            else:
                return HttpResponseBadRequest()

        if "selected-scenarios" in request.POST:
            selected_scenarios = request.POST.getlist("selected-scenarios")
            set_scenarios(current_user, current_compound, selected_scenarios)
            return redirect(current_compound.url)

        if "aliases" in request.POST:
            aliases = request.POST.getlist("aliases")

            try:
                set_aliases(current_user, current_compound, aliases)
            except Exception as e:
                return JsonResponse({"error": str(e)}, status=400)

            return JsonResponse({"success": current_compound.url})

        # TODO: Move cleaning to property updater
        new_compound_name = request.POST.get("compound-name")
        if new_compound_name is not None:
            new_compound_name = nh3.clean(new_compound_name, tags=s.ALLOWED_HTML_TAGS).strip()

        new_compound_description = request.POST.get("compound-description")
        if new_compound_description is not None:
            new_compound_description = nh3.clean(
                new_compound_description, tags=s.ALLOWED_HTML_TAGS
            ).strip()

        if new_compound_name:
            current_compound.name = new_compound_name

        if new_compound_description:
            current_compound.description = new_compound_description

        if any([new_compound_name, new_compound_description]):
            current_compound.save()
            return redirect(current_compound.url)

        selected_database = request.POST.get("selected-database", "").strip()
        external_identifier = request.POST.get("identifier", "").strip()

        if selected_database and external_identifier:
            db = ExternalDatabase.objects.get(id=int(selected_database))
            ExternalIdentifier.objects.create(
                content_object=current_compound,
                database=db,
                identifier_value=external_identifier,
                url=db.url_pattern.format(id=external_identifier),
                is_primary=False,
            )
            return redirect(current_compound.url)
        else:
            return HttpResponseBadRequest()

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_compound_structures(request, package_uuid, compound_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)
    current_compound = Compound.objects.get(package=current_package, uuid=compound_uuid)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = (
            f"enviPath - {current_package.get_name()} - {current_compound.get_name()} - Structures"
        )

        context["meta"]["current_package"] = current_package
        context["object_type"] = "structure"
        context["breadcrumbs"] = breadcrumbs(
            current_package, "compound", current_compound, "structure"
        )
        context["entity_type"] = "structure"
        context["page_title"] = f"{current_compound.get_name()} - Structures"
        context["api_endpoint"] = (
            f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/compound/{current_compound.uuid}/structure/"
        )
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["compound"] = current_compound
        context["list_mode"] = "combined"
        context["list_title"] = "structures"

        return render(request, "collections/structures_paginated.html", context)

    elif request.method == "POST":
        structure_name = request.POST.get("structure-name")
        structure_smiles = request.POST.get("structure-smiles")
        structure_molfile = request.POST.get("structure-molfile")
        structure_description = request.POST.get("structure-description")

        try:
            cs = current_compound.add_structure(
                structure_smiles,
                molfile=structure_molfile,
                name=structure_name,
                description=structure_description,
            )
        except ValueError:
            return error(
                request,
                "Adding structure failed!",
                "The structure could not be added as normalized structures don't match!",
            )

        return redirect(cs.url)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_compound_structure(request, package_uuid, compound_uuid, structure_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)
    current_compound = get_object_or_404(Compound, package=current_package, uuid=compound_uuid)
    current_structure = get_object_or_404(
        CompoundStructure, compound=current_compound, uuid=structure_uuid
    )

    if request.method == "GET":
        is_image_request = request.GET.get("image")

        if is_image_request:
            if is_image_request == "svg":
                return HttpResponse(current_structure.as_svg, content_type="image/svg+xml")
            else:
                return HttpResponseBadRequest("Currently only SVG as image formate supported!")

        context = get_base_context(request)
        context["title"] = (
            f"enviPath - {current_package.get_name()} - {current_compound.get_name()} - {current_structure.get_name()}"
        )

        context["meta"]["current_package"] = current_package
        context["object_type"] = "structure"

        context["compound_structure"] = current_structure
        context["current_object"] = current_structure
        context["breadcrumbs"] = breadcrumbs(
            current_package, "compound", current_compound, "structure", current_structure
        )

        return render(request, "objects/compound_structure.html", context)

    elif request.method == "POST":
        if hidden := request.POST.get("hidden", None):
            if hidden == "delete":
                # Check if we have to delete the compound as no structure is left
                if len(current_structure.compound.structures.all()) == 1:
                    # This will delete the structure as well
                    current_compound.delete()
                    return redirect(current_package.url + "/compound")
                else:
                    if current_structure.normalized_structure:
                        current_compound.delete()
                        return redirect(current_package.url + "/compound")
                    else:
                        if current_compound.default_structure == current_structure:
                            current_structure.delete()
                            current_compound.default_structure = (
                                current_compound.structures.all().first()
                            )
                            return redirect(current_compound.url + "/structure")
                        else:
                            current_structure.delete()
                            return redirect(current_compound.url + "/structure")
            else:
                return HttpResponseBadRequest()

        # TODO: Move cleaning to property updater
        new_structure_name = request.POST.get("compound-structure-name")
        if new_structure_name is not None:
            new_structure_name = nh3.clean(new_structure_name, tags=s.ALLOWED_HTML_TAGS).strip()

        new_structure_description = request.POST.get("compound-structure-description")
        if new_structure_description is not None:
            new_structure_description = nh3.clean(
                new_structure_description, tags=s.ALLOWED_HTML_TAGS
            ).strip()

        if new_structure_name:
            current_structure.name = new_structure_name

        if new_structure_description:
            current_structure.description = new_structure_description

        if any([new_structure_name, new_structure_description]):
            current_structure.save()
            return redirect(current_structure.url)

        if "selected-scenarios" in request.POST:
            selected_scenarios = request.POST.getlist("selected-scenarios")

            set_scenarios(current_user, current_structure, selected_scenarios)
            return redirect(current_structure.url)

        if "aliases" in request.POST:
            aliases = request.POST.getlist("aliases")

            try:
                set_aliases(current_user, current_structure, aliases)
            except Exception as e:
                return JsonResponse({"error": str(e)}, status=400)

            return JsonResponse({"success": current_structure.url})

        selected_database = request.POST.get("selected-database", "").strip()
        external_identifier = request.POST.get("identifier", "").strip()

        if selected_database and external_identifier:
            db = ExternalDatabase.objects.get(id=int(selected_database))
            ExternalIdentifier.objects.create(
                content_object=current_structure,
                database=db,
                identifier_value=external_identifier,
                url=db.url_pattern.format(id=external_identifier),
                is_primary=False,
            )
            return redirect(current_structure.url)

        return HttpResponseBadRequest()
    else:
        return HttpResponseNotAllowed(
            [
                "GET",
            ]
        )


@package_permission_required()
def package_rules(request, package_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = f"enviPath - {current_package.get_name()} - Rules"

        context["meta"]["current_package"] = current_package
        context["object_type"] = "rule"
        context["breadcrumbs"] = breadcrumbs(current_package, "rule")
        context["entity_type"] = "rule"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/rule/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_title"] = "rules"

        reviewed_rule_qs = Rule.objects.none()
        unreviewed_rule_qs = Rule.objects.none()

        if current_package.reviewed:
            reviewed_rule_qs = Rule.objects.filter(package=current_package).order_by("name")
        else:
            unreviewed_rule_qs = Rule.objects.filter(package=current_package).order_by("name")

        if request.GET.get("all"):
            return JsonResponse(
                {
                    "objects": [
                        {"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
                        for pw in (
                            reviewed_rule_qs if current_package.reviewed else unreviewed_rule_qs
                        )
                    ]
                }
            )

        return render(request, "collections/rules_paginated.html", context)

    elif request.method == "POST":
        log_post_params(request)

        # Generic params
        rule_name = request.POST.get("rule-name")
        rule_description = request.POST.get("rule-description")

        rule_type = request.POST.get("rule-type")

        params = {}

        # Obtain parameters as required by rule type
        if rule_type == "SimpleAmbitRule":
            params["smirks"] = request.POST.get("rule-smirks")
            params["reactant_filter_smarts"] = request.POST.get("rule-reactant-smarts")
            params["product_filter_smarts"] = request.POST.get("rule-product-smarts")
        elif rule_type == "SimpleRDKitRule":
            params["reaction_smarts"] = request.POST.get("rule-reaction-smarts")
        elif rule_type == "ParallelRule":
            pass
        elif rule_type == "SequentialRule":
            pass
        else:
            return HttpResponseBadRequest()

        r = Rule.create(
            rule_type=rule_type,
            package=current_package,
            name=rule_name,
            description=rule_description,
            **params,
        )
        return redirect(r.url)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_rule(request, package_uuid, rule_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)
    current_rule = get_object_or_404(Rule, package=current_package, uuid=rule_uuid)

    if request.method == "GET":
        context = get_base_context(request)

        if smiles := request.GET.get("smiles", False):
            stand_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
            res = current_rule.apply(stand_smiles)
            if len(res) > 1:
                logger.info(
                    f"Rule {current_rule.uuid} returned multiple product sets on {smiles}, picking the first one."
                )
            # Some Rules are touching unrelated areas which might result in ~ indicating
            # any bond (-, =, #). For drawing we need a concrete bond. -> use single bond
            product_smiles = [x.replace("~", "-") for x in res[0]]
            smirks = f"{stand_smiles}>>{'.'.join(sorted(product_smiles))}"
            # Usually the functional groups are a mapping of fg -> count
            # As we are doing it on the fly here fake a high count to ensure that its properly highlighted

            if isinstance(current_rule, SimpleAmbitRule):
                educt_functional_groups = {current_rule.reactants_smarts: 1000}
                product_functional_groups = {current_rule.products_smarts: 1000}
            else:
                educt_functional_groups = {x: 1000 for x in current_rule.reactants_smarts}
                product_functional_groups = {x: 1000 for x in current_rule.products_smarts}

            return HttpResponse(
                IndigoUtils.smirks_to_svg(
                    smirks,
                    False,
                    0,
                    0,
                    educt_functional_groups=educt_functional_groups,
                    product_functional_groups=product_functional_groups,
                ),
                content_type="image/svg+xml",
            )

        context["title"] = f"enviPath - {current_package.get_name()} - {current_rule.get_name()}"

        context["meta"]["current_package"] = current_package
        context["object_type"] = "rule"
        context["breadcrumbs"] = breadcrumbs(current_package, "rule", current_rule)

        context["rule"] = current_rule
        context["current_object"] = current_rule

        if isinstance(current_rule, SimpleAmbitRule):
            return render(request, "objects/simple_rule.html", context)
        else:  # isinstance(current_rule, ParallelRule) or isinstance(current_rule, SequentialRule):
            return render(request, "objects/composite_rule.html", context)

    elif request.method == "POST":
        if hidden := request.POST.get("hidden", None):
            if hidden == "delete":
                current_rule.delete()
                return redirect(current_package.url + "/rule")
            else:
                return HttpResponseBadRequest()

        if "selected-scenarios" in request.POST:
            selected_scenarios = request.POST.getlist("selected-scenarios")

            set_scenarios(current_user, current_rule, selected_scenarios)
            return redirect(current_rule.url)

        if "aliases" in request.POST:
            aliases = request.POST.getlist("aliases")

            try:
                set_aliases(current_user, current_rule, aliases)
            except Exception as e:
                return JsonResponse({"error": str(e)}, status=400)

            return JsonResponse({"success": current_rule.url})

        # TODO: Move cleaning to property updater
        rule_name = request.POST.get("rule-name")
        if rule_name is not None:
            rule_name = nh3.clean(rule_name, tags=s.ALLOWED_HTML_TAGS).strip()

        rule_description = request.POST.get("rule-description")
        if rule_description is not None:
            rule_description = nh3.clean(rule_description, tags=s.ALLOWED_HTML_TAGS).strip()

        if rule_name:
            current_rule.name = rule_name

        if rule_description:
            current_rule.description = rule_description

        if any([rule_name, rule_description]):
            current_rule.save()
            return redirect(current_rule.url)
        else:
            return HttpResponseBadRequest()

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_rule_enzymelink(request, package_uuid, rule_uuid, enzymelink_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)
    current_rule = Rule.objects.get(package=current_package, uuid=rule_uuid)
    current_enzymelink = EnzymeLink.objects.get(rule=current_rule, uuid=enzymelink_uuid)

    if request.method == "GET":
        context = get_base_context(request)

        context["title"] = f"enviPath - {current_package.get_name()} - {current_rule.get_name()}"

        context["meta"]["current_package"] = current_package
        context["object_type"] = "enzyme"
        context["breadcrumbs"] = breadcrumbs(
            current_package, "rule", current_rule, "enzymelink", current_enzymelink
        )

        context["enzymelink"] = current_enzymelink
        context["current_object"] = current_enzymelink

        return render(request, "objects/enzymelink.html", context)

    return HttpResponseNotAllowed(["GET"])


@package_permission_required()
def package_reactions(request, package_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = f"enviPath - {current_package.get_name()} - Reactions"

        context["meta"]["current_package"] = current_package
        context["object_type"] = "reaction"
        context["breadcrumbs"] = breadcrumbs(current_package, "reaction")
        context["entity_type"] = "reaction"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/reaction/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_title"] = "reactions"

        reviewed_reaction_qs = Reaction.objects.none()
        unreviewed_reaction_qs = Reaction.objects.none()

        if current_package.reviewed:
            reviewed_reaction_qs = Reaction.objects.filter(package=current_package).order_by("name")
        else:
            unreviewed_reaction_qs = Reaction.objects.filter(package=current_package).order_by(
                "name"
            )

        if request.GET.get("all"):
            return JsonResponse(
                {
                    "objects": [
                        {"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
                        for pw in (
                            reviewed_reaction_qs
                            if current_package.reviewed
                            else unreviewed_reaction_qs
                        )
                    ]
                }
            )

        return render(request, "collections/reactions_paginated.html", context)

    elif request.method == "POST":
        reaction_name = request.POST.get("reaction-name")
        reaction_description = request.POST.get("reaction-description")
        reaction_smiles = request.POST.get("reaction-smiles")

        if reaction_smiles is None or reaction_smiles.strip() == "":
            return error(
                request,
                "Reaction SMILES is empty / missing",
                "No reaction SMILES provided. Please provide a SMILES for the reaction.",
            )

        if not FormatConverter.is_valid_smirks(reaction_smiles):
            return error(
                request,
                "Reaction SMILES is invalid",
                f"The provided reactions SMILES {reaction_smiles} is invalid",
            )

        educts = reaction_smiles.split(">>")[0].split(".")
        products = reaction_smiles.split(">>")[1].split(".")

        r = Reaction.create(
            current_package,
            name=reaction_name,
            description=reaction_description,
            educts=educts,
            products=products,
        )

        return redirect(r.url)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_reaction(request, package_uuid, reaction_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)
    current_reaction = get_object_or_404(Reaction, package=current_package, uuid=reaction_uuid)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = (
            f"enviPath - {current_package.get_name()} - {current_reaction.get_name()}"
        )

        context["meta"]["current_package"] = current_package
        context["object_type"] = "reaction"
        context["breadcrumbs"] = breadcrumbs(current_package, "reaction", current_reaction)

        context["reaction"] = current_reaction
        context["current_object"] = current_reaction

        return render(request, "objects/reaction.html", context)

    elif request.method == "POST":
        if hidden := request.POST.get("hidden", None):
            if hidden == "delete":
                current_reaction.delete()
                return redirect(current_package.url + "/reaction")
            else:
                return HttpResponseBadRequest()

        if "selected-scenarios" in request.POST:
            selected_scenarios = request.POST.getlist("selected-scenarios")

            set_scenarios(current_user, current_reaction, selected_scenarios)
            return redirect(current_reaction.url)

        if "aliases" in request.POST:
            aliases = request.POST.getlist("aliases")

            try:
                set_aliases(current_user, current_reaction, aliases)
            except Exception as e:
                return JsonResponse({"error": str(e)}, status=400)

            return JsonResponse({"success": current_reaction.url})

        # TODO: Move cleaning to property updater
        new_reaction_name = request.POST.get("reaction-name")
        if new_reaction_name is not None:
            new_reaction_name = nh3.clean(new_reaction_name, tags=s.ALLOWED_HTML_TAGS).strip()

        new_reaction_description = request.POST.get("reaction-description")
        if new_reaction_description is not None:
            new_reaction_description = nh3.clean(
                new_reaction_description, tags=s.ALLOWED_HTML_TAGS
            ).strip()

        if new_reaction_name:
            current_reaction.name = new_reaction_name

        if new_reaction_description:
            current_reaction.description = new_reaction_description

        if any([new_reaction_name, new_reaction_description]):
            current_reaction.save()
            return redirect(current_reaction.url)

        selected_database = request.POST.get("selected-database", "").strip()
        external_identifier = request.POST.get("identifier", "").strip()

        if selected_database and external_identifier:
            db = ExternalDatabase.objects.get(id=int(selected_database))
            ExternalIdentifier.objects.create(
                content_object=current_reaction,
                database=db,
                identifier_value=external_identifier,
                url=db.url_pattern.format(id=external_identifier),
                is_primary=False,
            )
            return redirect(current_reaction.url)

        return HttpResponseBadRequest()

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_pathways(request, package_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = f"enviPath - {current_package.get_name()} - Pathways"

        context["meta"]["current_package"] = current_package
        context["object_type"] = "pathway"
        context["breadcrumbs"] = breadcrumbs(current_package, "pathway")
        context["entity_type"] = "pathway"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/pathway/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_title"] = "pathways"

        reviewed_pathway_qs = Pathway.objects.none()
        unreviewed_pathway_qs = Pathway.objects.none()

        if current_package.reviewed:
            reviewed_pathway_qs = Pathway.objects.filter(package=current_package).order_by("name")
        else:
            unreviewed_pathway_qs = Pathway.objects.filter(package=current_package).order_by("name")

        if request.GET.get("all"):
            return JsonResponse(
                {
                    "objects": [
                        {"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
                        for pw in (
                            reviewed_pathway_qs
                            if current_package.reviewed
                            else unreviewed_pathway_qs
                        )
                    ]
                }
            )

        return render(request, "collections/pathways_paginated.html", context)

    elif request.method == "POST":
        log_post_params(request)

        name = request.POST.get("name")
        description = request.POST.get("description")

        smiles = request.POST.get("smiles", "").strip()
        pw_mode = request.POST.get("predict", "predict").strip()

        if "smiles" in request.POST and smiles == "":
            return error(
                request,
                "Pathway prediction failed!",
                "Pathway prediction failed due to missing or empty SMILES",
            )
        try:
            stand_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
        except ValueError:
            return error(
                request,
                "Pathway prediction failed!",
                f'Pathway prediction failed as standardization of SMILES "{smiles}" failed!',
            )

        modes = ["predict", "build", "incremental"]
        if pw_mode not in modes:
            return error(
                request,
                "Pathway prediction failed!",
                f'Pathway prediction failed as received mode "{pw_mode}" is none of {modes}',
            )

        prediction_setting = request.POST.get("prediction-setting", None)
        if prediction_setting:
            prediction_setting = SettingManager.get_setting_by_url(current_user, prediction_setting)
        else:
            prediction_setting = current_user.prediction_settings()

        is_predict_mode = pw_mode in {"predict", "incremental"}

        pw = Pathway.create(
            current_package,
            stand_smiles if is_predict_mode else smiles,
            name=name,
            description=description,
            predicted=is_predict_mode,
        )

        # set mode
        pw.kv.update({"mode": pw_mode})
        pw.save()

        if pw_mode == "predict" or pw_mode == "incremental":
            # unlimited pred (will be handled by setting)
            limit = None

            # For incremental predict first level and return
            if pw_mode == "incremental":
                limit = 1

            pw.setting = prediction_setting
            pw.kv.update({"status": "running"})
            pw.save()

            from .tasks import dispatch, predict

            dispatch(current_user, predict, pw.pk, prediction_setting.pk, limit=limit)

        return redirect(pw.url)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_pathway(request, package_uuid, pathway_uuid):
    current_user: User = _anonymous_or_real(request)
    current_package: Package = PackageManager.get_package_by_id(current_user, package_uuid)
    current_pathway: Pathway = get_object_or_404(
        Pathway, package=current_package, uuid=pathway_uuid
    )

    if request.method == "GET":
        if request.GET.get("last_modified", False):
            return JsonResponse(
                {"modified": current_pathway.modified.strftime("%Y-%m-%d %H:%M:%S")}
            )

        if request.GET.get("status", False):
            return JsonResponse(
                {
                    "status": current_pathway.status(),
                    "modified": current_pathway.modified.strftime("%Y-%m-%d %H:%M:%S"),
                    "emptyDueToThreshold": current_pathway.empty_due_to_threshold(),
                }
            )

        if request.GET.get("download", False) == "true":
            filename = f"{current_pathway.get_name().replace(' ', '_')}_{current_pathway.uuid}.csv"
            csv_pw = current_pathway.to_csv()
            response = HttpResponse(csv_pw, content_type="text/csv")
            response["Content-Disposition"] = f'attachment; filename="{filename}"'

            return response

        if (
            request.GET.get("identify-missing-rules", False) == "true"
            and request.GET.get("rule-package") is not None
        ):
            from .tasks import dispatch_eager, identify_missing_rules

            rule_package = PackageManager.get_package_by_url(
                current_user, request.GET.get("rule-package")
            )
            _, res = dispatch_eager(
                current_user, identify_missing_rules, [current_pathway.pk], rule_package.pk
            )

            filename = f"{current_pathway.get_name().replace(' ', '_')}_{current_pathway.uuid}.csv"
            response = HttpResponse(res, content_type="text/csv")
            response["Content-Disposition"] = f'attachment; filename="{filename}"'

            return response

        # Pathway d3_json() relies on a lot of related objects (Nodes, Structures, Edges, Reaction, Rules, ...)
        # we will again fetch the current pathway identified by this url, but this time together with nearly all
        # related objects

        current_pathway = Pathway.objects.prefetch_related(
            "node_set",
            "node_set__out_edges",
            "node_set__default_node_label",
            "node_set__scenarios",
            "edge_set",
            "edge_set__start_nodes",
            "edge_set__end_nodes",
            "edge_set__edge_label",
            "edge_set__scenarios",
        ).get(uuid=pathway_uuid)

        context = get_base_context(request)
        context["title"] = f"enviPath - {current_package.get_name()} - {current_pathway.get_name()}"

        context["meta"]["current_package"] = current_package
        context["object_type"] = "pathway"
        context["breadcrumbs"] = breadcrumbs(current_package, "pathway", current_pathway)

        context["pathway"] = current_pathway
        context["current_object"] = current_pathway

        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Package": s.SERVER_URL + "/package"},
            {current_package.get_name(): current_package.url},
            {"Pathway": current_package.url + "/pathway"},
            {current_pathway.get_name(): current_pathway.url},
        ]

        return render(request, "objects/pathway.html", context)
        # return render(request, 'pathway_playground2.html', context)

    elif request.method == "POST":
        if hidden := request.POST.get("hidden", None):
            if hidden == "delete":
                current_pathway.delete()
                return redirect(current_package.url + "/pathway")
            else:
                return HttpResponseBadRequest()

        if "selected-scenarios" in request.POST:
            selected_scenarios = request.POST.getlist("selected-scenarios")

            set_scenarios(current_user, current_pathway, selected_scenarios)
            return redirect(current_pathway.url)

        if "aliases" in request.POST:
            aliases = request.POST.getlist("aliases")

            try:
                set_aliases(current_user, current_pathway, aliases)
            except Exception as e:
                return JsonResponse({"error": str(e)}, status=400)

            return JsonResponse({"success": current_pathway.url})

        # TODO: Move cleaning to property updater
        pathway_name = request.POST.get("pathway-name")
        if pathway_name is not None:
            pathway_name = nh3.clean(pathway_name, tags=s.ALLOWED_HTML_TAGS).strip()

        pathway_description = request.POST.get("pathway-description")
        if pathway_description is not None:
            pathway_description = nh3.clean(pathway_description, tags=s.ALLOWED_HTML_TAGS).strip()

        if any([pathway_name, pathway_description]):
            if pathway_name is not None and pathway_name.strip() != "":
                pathway_name = pathway_name.strip()

                current_pathway.name = pathway_name

            if pathway_description is not None and pathway_description.strip() != "":
                pathway_description = pathway_description.strip()

                current_pathway.description = pathway_description

            current_pathway.save()
            return redirect(current_pathway.url)

        node_url = request.POST.get("node")

        if node_url:
            n = current_pathway.get_node(node_url)

            current_pathway.kv.update({"status": "running"})
            current_pathway.save()

            from .tasks import dispatch, predict

            dispatch(
                current_user,
                predict,
                current_pathway.pk,
                current_pathway.setting.pk,
                node_pk=n.pk,
            )

            return JsonResponse({"success": current_pathway.url})

        return HttpResponseBadRequest()

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_pathway_nodes(request, package_uuid, pathway_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)
    current_pathway = get_object_or_404(Pathway, package=current_package, uuid=pathway_uuid)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = (
            f"enviPath - {current_package.get_name()} - {current_pathway.get_name()} - Nodes"
        )

        context["meta"]["current_package"] = current_package
        context["object_type"] = "node"
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Package": s.SERVER_URL + "/package"},
            {current_package.get_name(): current_package.url},
            {"Pathway": current_package.url + "/pathway"},
            {current_pathway.get_name(): current_pathway.url},
            {"Node": current_pathway.url + "/node"},
        ]

        reviewed_node_qs = Node.objects.none()
        unreviewed_node_qs = Node.objects.none()

        if current_package.reviewed:
            reviewed_node_qs = Node.objects.filter(pathway=current_pathway).order_by("name")
        else:
            unreviewed_node_qs = Node.objects.filter(pathway=current_pathway).order_by("name")

        if request.GET.get("all"):
            return JsonResponse(
                {
                    "objects": [
                        {"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
                        for pw in (
                            reviewed_node_qs if current_package.reviewed else unreviewed_node_qs
                        )
                    ]
                }
            )

        context["reviewed_objects"] = reviewed_node_qs
        context["unreviewed_objects"] = unreviewed_node_qs

        return render(request, "collections/objects_list.html", context)

    elif request.method == "POST":
        node_name = request.POST.get("node-name")
        node_description = request.POST.get("node-description")

        node_smiles = request.POST.get("node-smiles")
        node_molfile = request.POST.get("node-molfile")

        try:
            current_pathway.add_node(
                node_smiles, molfile=node_molfile, name=node_name, description=node_description
            )
        except InvalidSMILESException:
            return error(
                request, "Node creation failed!", f"Given SMILES ({node_smiles}) is invalid"
            )

        return redirect(current_pathway.url)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)
    current_pathway = get_object_or_404(Pathway, package=current_package, uuid=pathway_uuid)
    current_node = get_object_or_404(Node, pathway=current_pathway, uuid=node_uuid)

    if request.method == "GET":
        is_image_request = request.GET.get("image")
        is_highlight_request = request.GET.get("highlight", False)
        is_highlight_reactivity = request.GET.get("highlightReactivity", False)
        if is_image_request:
            if is_image_request == "svg":
                # TODO optimize this chain
                if is_highlight_request:
                    # User functional groups covered by the model training data
                    fgs = {}
                    if current_pathway.setting:
                        if current_pathway.setting.model:
                            if current_pathway.setting.model.app_domain:
                                fgs = current_pathway.setting.model.app_domain.functional_groups

                    svg_data = IndigoUtils.mol_to_svg(
                        current_node.default_node_label.smiles, functional_groups=fgs
                    )
                elif is_highlight_reactivity:
                    # Use reactant smarts to show all reaction sites
                    # set a high count to obtain a strong color
                    ad_data = current_node.get_app_domain_assessment_data()
                    fgs = {}
                    for t in ad_data.get("assessment", {}).get("transformations", []):
                        r = Rule.objects.get(url=t["rule"]["url"])

                        if isinstance(r, SimpleAmbitRule):
                            fgs[r.reactants_smarts] = 1000
                        else:
                            for sr in r.srs:
                                fgs[sr.reactants_smarts] = 1000

                    svg_data = IndigoUtils.mol_to_svg(
                        current_node.default_node_label.smiles, functional_groups=fgs
                    )
                else:
                    svg_data = current_node.as_svg

                return HttpResponse(svg_data, content_type="image/svg+xml")

        context = get_base_context(request)
        context["title"] = f"enviPath - {current_package.get_name()} - {current_pathway.get_name()}"

        context["meta"]["current_package"] = current_package
        context["object_type"] = "pathway"

        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Package": s.SERVER_URL + "/package"},
            {current_package.get_name(): current_package.url},
            {"Pathway": current_package.url + "/pathway"},
            {current_pathway.get_name(): current_pathway.url},
            {"Node": current_pathway.url + "/node"},
            {current_node.get_name(): current_node.url},
        ]

        context["node"] = current_node
        context["current_object"] = current_node

        context["app_domain_assessment_data"] = json.dumps(
            current_node.get_app_domain_assessment_data()
        )

        return render(request, "objects/node.html", context)

    elif request.method == "POST":
        log_post_params(request)

        if hidden := request.POST.get("hidden", None):
            if hidden == "delete":
                # pre_delete signal will take care of edge deletion
                current_node.delete()

                return redirect(current_pathway.url)
            else:
                return HttpResponseBadRequest()

        if "selected-scenarios" in request.POST:
            selected_scenarios = request.POST.getlist("selected-scenarios")

            set_scenarios(current_user, current_node, selected_scenarios)
            return redirect(current_node.url)

        if "aliases" in request.POST:
            aliases = request.POST.getlist("aliases")

            try:
                set_aliases(current_user, current_node, aliases)
            except Exception as e:
                return JsonResponse({"error": str(e)}, status=400)

            return JsonResponse({"success": current_node.url})

        new_node_name = request.POST.get("node-name")
        new_node_description = request.POST.get("node-description")

        if any([new_node_name, new_node_description]):
            if new_node_name is not None and new_node_name.strip() != "":
                new_node_name = nh3.clean(new_node_name.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
                current_node.name = new_node_name

            if new_node_description is not None and new_node_description.strip() != "":
                new_node_description = nh3.clean(
                    new_node_description.strip(), tags=s.ALLOWED_HTML_TAGS
                ).strip()
                current_node.description = new_node_description

            current_node.save()

            return redirect(current_node.url)

        return error(request, "Node update failed!", "No changes were made to the node")

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_pathway_edges(request, package_uuid, pathway_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)
    current_pathway = get_object_or_404(Pathway, package=current_package, uuid=pathway_uuid)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = (
            f"enviPath - {current_package.get_name()} - {current_pathway.get_name()} - Edges"
        )

        context["meta"]["current_package"] = current_package
        context["object_type"] = "edge"
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Package": s.SERVER_URL + "/package"},
            {current_package.get_name(): current_package.url},
            {"Pathway": current_package.url + "/pathway"},
            {current_pathway.get_name(): current_pathway.url},
            {"Edge": current_pathway.url + "/edge"},
        ]

        reviewed_edge_qs = Edge.objects.none()
        unreviewed_edge_qs = Edge.objects.none()

        if current_package.reviewed:
            reviewed_edge_qs = Edge.objects.filter(pathway=current_pathway).order_by("name")
        else:
            unreviewed_edge_qs = Edge.objects.filter(pathway=current_pathway).order_by("name")

        if request.GET.get("all"):
            return JsonResponse(
                {
                    "objects": [
                        {"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
                        for pw in (
                            reviewed_edge_qs if current_package.reviewed else unreviewed_edge_qs
                        )
                    ]
                }
            )

        context["reviewed_objects"] = reviewed_edge_qs
        context["unreviewed_objects"] = unreviewed_edge_qs

        return render(request, "collections/objects_list.html", context)

    elif request.method == "POST":
        log_post_params(request)

        edge_name = request.POST.get("edge-name")
        edge_description = request.POST.get("edge-description")

        edge_substrates = request.POST.getlist("edge-substrates")
        edge_products = request.POST.getlist("edge-products")

        substrate_nodes = [current_pathway.get_node(url) for url in edge_substrates]
        product_nodes = [current_pathway.get_node(url) for url in edge_products]

        # TODO in the future consider Rules here?
        current_pathway.add_edge(
            substrate_nodes, product_nodes, name=edge_name, description=edge_description
        )

        # Update depths as sideeffect of above operation
        current_pathway.update_depths()

        return redirect(current_pathway.url)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_pathway_edge(request, package_uuid, pathway_uuid, edge_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)
    current_pathway = get_object_or_404(Pathway, package=current_package, uuid=pathway_uuid)
    current_edge = get_object_or_404(Edge, pathway=current_pathway, uuid=edge_uuid)

    if request.method == "GET":
        is_image_request = request.GET.get("image")
        if is_image_request:
            if is_image_request == "svg":
                svg_data = current_edge.as_svg
                return HttpResponse(svg_data, content_type="image/svg+xml")

        context = get_base_context(request)
        context["title"] = (
            f"enviPath - {current_package.get_name()} - {current_pathway.get_name()} - {current_edge.edge_label.get_name()}"
        )

        context["meta"]["current_package"] = current_package
        context["object_type"] = "edge"
        context["breadcrumbs"] = breadcrumbs(
            current_package, "pathway", current_pathway, "edge", current_edge
        )
        context["edge"] = current_edge
        context["current_object"] = current_edge

        return render(request, "objects/edge.html", context)

    elif request.method == "POST":
        log_post_params(request)

        if hidden := request.POST.get("hidden", None):
            if hidden == "delete":
                current_edge.delete()
                return redirect(current_pathway.url)

        if "selected-scenarios" in request.POST:
            selected_scenarios = request.POST.getlist("selected-scenarios")

            set_scenarios(current_user, current_edge, selected_scenarios)
            return redirect(current_edge.url)

        if "aliases" in request.POST:
            aliases = request.POST.getlist("aliases")

            try:
                set_aliases(current_user, current_edge, aliases)
            except Exception as e:
                return JsonResponse({"error": str(e)}, status=400)

            return JsonResponse({"success": current_edge.url})

        return HttpResponseBadRequest()

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


@package_permission_required()
def package_scenarios(request, package_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)

    if request.method == "GET":
        if "application/json" in request.META.get("HTTP_ACCEPT") and not request.GET.get(
            "all", False
        ):
            scens = Scenario.objects.filter(package=current_package).order_by("name")
            res = [{"name": s_.get_name(), "url": s_.url, "uuid": s_.uuid} for s_ in scens]
            return JsonResponse(res, safe=False)

        context = get_base_context(request)
        context["title"] = f"enviPath - {current_package.get_name()} - Scenarios"

        context["meta"]["current_package"] = current_package
        context["object_type"] = "scenario"
        context["breadcrumbs"] = breadcrumbs(current_package, "scenario")
        context["entity_type"] = "scenario"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/scenario/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_title"] = "scenarios"

        reviewed_scenario_qs = Scenario.objects.none()
        unreviewed_scenario_qs = Scenario.objects.none()

        if current_package.reviewed:
            reviewed_scenario_qs = Scenario.objects.filter(package=current_package).order_by("name")
        else:
            unreviewed_scenario_qs = Scenario.objects.filter(package=current_package).order_by(
                "name"
            )

        if request.GET.get("all"):
            return JsonResponse(
                {
                    "objects": [
                        {"name": pw.get_name(), "url": pw.url, "reviewed": current_package.reviewed}
                        for pw in (
                            reviewed_scenario_qs
                            if current_package.reviewed
                            else unreviewed_scenario_qs
                        )
                    ]
                }
            )

        return render(request, "collections/scenarios_paginated.html", context)
    else:
        return HttpResponseNotAllowed(
            [
                "GET",
            ]
        )


@package_permission_required()
def package_scenario(request, package_uuid, scenario_uuid):
    current_user = _anonymous_or_real(request)
    current_package = PackageManager.get_package_by_id(current_user, package_uuid)
    current_scenario = get_object_or_404(Scenario, package=current_package, uuid=scenario_uuid)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = (
            f"enviPath - {current_package.get_name()} - {current_scenario.get_name()}"
        )

        context["meta"]["current_package"] = current_package
        context["object_type"] = "scenario"
        context["breadcrumbs"] = breadcrumbs(current_package, "scenario", current_scenario)

        context["scenario"] = current_scenario

        context["associated_additional_information"] = AdditionalInformation.objects.filter(
            scenario=current_scenario
        )

        # Note: Modals now fetch schemas and data from API endpoints
        # Keeping these for backwards compatibility if needed elsewhere
        # They are no longer used by the main scenario template

        return render(request, "objects/scenario.html", context)

    elif request.method == "POST":
        log_post_params(request)

        if hidden := request.POST.get("hidden", None):
            if hidden == "delete":
                current_scenario.delete()
                return redirect(current_package.url + "/scenario")
            elif hidden == "delete-additional-information":
                uuid = request.POST.get("uuid")
                current_scenario.remove_additional_information(uuid)
                return redirect(current_scenario.url)
            elif hidden == "delete-all-additional-information":
                current_scenario.additional_information = dict()
                current_scenario.save()
                return redirect(current_scenario.url)
            elif hidden == "set-additional-information":
                # Legacy POST handler - no longer used, modals use API endpoints
                return HttpResponseBadRequest(
                    "This endpoint is deprecated. Please use the API endpoints."
                )
            elif hidden == "add-additional-information":
                # Legacy POST handler - no longer used, modals use API endpoints
                return HttpResponseBadRequest(
                    "This endpoint is deprecated. Please use the API endpoints."
                )
            else:
                return HttpResponseBadRequest()

        new_scenario_name = request.POST.get("scenario-name")

        if new_scenario_name is not None:
            new_scenario_name = nh3.clean(new_scenario_name, tags=s.ALLOWED_HTML_TAGS).strip()

            if new_scenario_name:
                current_scenario.name = new_scenario_name

        new_scenario_description = request.POST.get("scenario-description")

        if new_scenario_description is not None:
            new_scenario_description = nh3.clean(
                new_scenario_description, tags=s.ALLOWED_HTML_TAGS
            ).strip()

            if new_scenario_description:
                current_scenario.description = new_scenario_description

        if any([new_scenario_name, new_scenario_description]):
            current_scenario.save()
            return redirect(current_scenario.url)
        else:
            return HttpResponseBadRequest()
    else:
        return HttpResponseNotAllowed(["GET", "POST"])


##############
# User/Group #
##############
def users(request):
    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = "enviPath - Users"

        context["object_type"] = "user"
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"User": s.SERVER_URL + "/user"},
        ]

        context["objects"] = get_user_model().objects.all()

        return render(request, "collections/objects_list.html", context)

    else:
        return HttpResponseNotAllowed(["GET"])


def user(request, user_uuid):
    current_user = _anonymous_or_real(request)

    if request.method == "GET":
        # Check if current user is the one matching to the url
        if str(current_user.uuid) != user_uuid and not current_user.is_superuser:
            return HttpResponseBadRequest()

        requested_user = UserManager.get_user_by_id(current_user, user_uuid)

        context = get_base_context(request, for_user=requested_user)
        context["title"] = "enviPath - User"

        context["object_type"] = "user"
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"User": s.SERVER_URL + "/user"},
            {current_user.username: requested_user.url},
        ]

        context["user"] = requested_user

        accessible_packages = PackageManager.get_all_readable_packages(
            requested_user, include_reviewed=True
        )

        property_models = PropertyPluginModel.objects.filter(
            package__in=accessible_packages
        ).order_by("name")

        tp_prediction_models = (
            EPModel.objects.filter(package__in=accessible_packages)
            .exclude(id__in=[pm.id for pm in property_models])
            .order_by("name")
        )

        context["models"] = tp_prediction_models
        context["property_models"] = property_models

        context["tokens"] = APIToken.objects.filter(user=requested_user)

        return render(request, "objects/user.html", context)

    elif request.method == "POST":
        is_hidden_method = bool(request.POST.get("hidden", False))

        if is_hidden_method and request.POST["hidden"] == "request-api-token":
            name = request.POST.get("name", "No Name")
            expires_days = min(max(int(request.POST.get("valid-for", 90)), 1), 90)

            token, raw_token = APIToken.create_token(
                request.user, name=name, expires_days=expires_days
            )

            return JsonResponse(
                {"raw_token": raw_token, "token": {"id": token.id, "name": token.name}}
            )

        if is_hidden_method and request.POST["hidden"] == "delete":
            token_id = request.POST.get("token-id")
            if token_id is None:
                return HttpResponseBadRequest("Token ID missing!")

            try:
                APIToken.objects.get(user=current_user, id=token_id).delete()
            except APIToken.DoesNotExist:
                return HttpResponseBadRequest("User and Token ID combination invalid!")

            return HttpResponse("success")

        default_package = request.POST.get("default-package")
        default_group = request.POST.get("default-group")
        default_prediction_setting = request.POST.get("default-prediction-setting")

        if any([default_package, default_group, default_prediction_setting]):
            current_user.default_package = PackageManager.get_package_by_url(
                current_user, default_package
            )
            current_user.default_group = GroupManager.get_group_by_url(current_user, default_group)
            current_user.default_setting = SettingManager.get_setting_by_url(
                current_user, default_prediction_setting
            )
            current_user.save()

            return redirect(current_user.url)

        if "change_default" in request.POST:
            new_default_uuid = request.POST["change_default"]
            current_user.default_setting = SettingManager.get_setting_by_id(
                current_user, new_default_uuid
            )
            current_user.save()
            return redirect(current_user.url)

        return HttpResponseBadRequest()

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


def groups(request):
    current_user = _anonymous_or_real(request)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = "enviPath - Groups"

        context["object_type"] = "group"
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Group": s.SERVER_URL + "/group"},
        ]

        # Context for paginated template
        context["entity_type"] = "group"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/groups/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_title"] = "groups"
        context["list_mode"] = "combined"

        return render(request, "collections/groups_paginated.html", context)

    elif request.method == "POST":
        group_name = request.POST.get("group-name")
        group_description = request.POST.get("group-description", s.DEFAULT_VALUES["description"])

        g = GroupManager.create_group(current_user, group_name, group_description)

        return redirect(g.url)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


def group(request, group_uuid):
    current_user = _anonymous_or_real(request)
    current_group = GroupManager.get_group_by_id(current_user, group_uuid)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = f"enviPath - {current_group.get_name()}"

        context["object_type"] = "group"
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Group": s.SERVER_URL + "/group"},
            {current_group.get_name(): current_group.url},
        ]

        context["group"] = current_group

        context["users"] = (
            UserManager.get_users_lp()
            .exclude(id__in=current_group.user_member.all())
            .filter(is_active=True)
        )
        context["groups"] = (
            GroupManager.get_groups_lp()
            .exclude(id__in=current_group.group_member.all())
            .exclude(id=current_group.pk)
        )

        context["packages"] = Package.objects.filter(
            id__in=GroupPackagePermission.objects.filter(group=current_group)
            .values("package")
            .distinct()
        )

        return render(request, "objects/group.html", context)

    elif request.method == "POST":
        log_post_params(request)

        if hidden := request.POST.get("hidden", None):
            if hidden == "delete":
                current_group.delete()
                return redirect(s.SERVER_URL + "/group")
            else:
                return HttpResponseBadRequest()

        member_url = request.POST.get("member")
        action = request.POST.get("action")

        if all([member_url, action]) and action in ["add", "remove"]:
            if "user" in member_url:
                member = UserManager.get_user_lp(member_url)
            else:
                member = GroupManager.get_group_lp(member_url)

            GroupManager.update_members(current_user, current_group, member, action)

            return redirect(current_group.url)

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


def settings(request):
    current_user = _anonymous_or_real(request)
    context = get_base_context(request)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = "enviPath - Settings"

        context["object_type"] = "setting"
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Setting": s.SERVER_URL + "/setting"},
        ]

        # Context for paginated template
        context["entity_type"] = "setting"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/settings/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_title"] = "settings"
        context["list_mode"] = "combined"

        return render(request, "collections/settings_paginated.html", context)

    elif request.method == "POST":
        if s.DEBUG:
            for k, v in request.POST.items():
                logger.info("Parameters received:")
                logger.info(f"{k}\t{v}")

        name = request.POST.get("prediction-setting-name")
        description = request.POST.get("prediction-setting-description")

        new_default = request.POST.get("prediction-setting-new-default", "off") == "on"

        # min 2, max s.DEFAULT_MAX_NUMBER_OF_NODES
        max_nodes = min(
            max(
                int(request.POST.get("prediction-setting-max-nodes", 1)),
                2,
            ),
            s.DEFAULT_MAX_NUMBER_OF_NODES,
        )

        # min 1, max s.DEFAULT_MAX_DEPTH
        max_depth = min(
            max(int(request.POST.get("prediction-setting-max-depth", 1)), 1),
            s.DEFAULT_MAX_DEPTH,
        )

        tp_gen_method = request.POST.get("tp-generation-method")

        params = {}

        if tp_gen_method == "model-based-prediction-setting":
            model_url = request.POST.get("model-based-prediction-setting-model")

            model_uuid = model_url.split("/")[-1]
            params["model"] = EPModel.objects.get(uuid=model_uuid)
            params["model_threshold"] = request.POST.get(
                "model-based-prediction-setting-threshold", s.DEFAULT_MODEL_THRESHOLD
            )

            if not PackageManager.readable(current_user, params["model"].package):
                raise PermissionDenied("You're not allowed to access this model!")

            expansion_scheme = request.POST.get(
                "model-based-prediction-setting-expansion-scheme", "BFS"
            )

            if expansion_scheme not in ExpansionSchemeChoice.values:
                raise BadRequest(f"Unknown expansion scheme: {expansion_scheme}")

            params["expansion_scheme"] = ExpansionSchemeChoice(expansion_scheme)

        elif tp_gen_method == "rule-based-prediction-setting":
            rule_packages = request.POST.getlist("rule-based-prediction-setting-packages")
            params["rule_packages"] = [
                PackageManager.get_package_by_url(current_user, p) for p in rule_packages
            ]

        else:
            raise BadRequest("Neither Model-Based nor Rule-Based as Method selected!")

        property_model_urls = request.POST.getlist("prediction-setting-property-models")

        if property_model_urls:
            mods = []
            for pm_url in property_model_urls:
                model = PropertyPluginModel.objects.get(url=pm_url)

                if PackageManager.readable(current_user, model.package):
                    mods.append(model)

            params["property_models"] = mods

        created_setting = SettingManager.create_setting(
            current_user,
            name=name,
            description=description,
            max_nodes=max_nodes,
            max_depth=max_depth,
            **params,
        )

        if new_default:
            current_user.default_setting = created_setting
            current_user.save()

        return HttpResponse("Success!")

    else:
        return HttpResponseNotAllowed(["GET", "POST"])


def setting(request, setting_uuid):
    current_user = _anonymous_or_real(request)
    current_setting = SettingManager.get_setting_by_id(current_user, setting_uuid)

    if request.method == "GET":
        context = get_base_context(request)
        context["title"] = f"enviPath - {current_setting.get_name()}"

        context["object_type"] = "setting"
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Setting": s.SERVER_URL + "/setting"},
            {f"{current_setting.get_name()}": current_setting.url},
        ]

        context["setting"] = current_setting
        context["current_object"] = current_setting

        return render(request, "objects/setting.html", context)
    else:
        return HttpResponseNotAllowed(["GET"])


def jobs(request):
    current_user = _anonymous_or_real(request)
    context = get_base_context(request)

    if request.method == "GET":
        context["object_type"] = "joblog"
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Jobs": s.SERVER_URL + "/jobs"},
        ]
        # if current_user.is_superuser:
        #     context["jobs"] = JobLog.objects.all().order_by("-created")
        # else:
        #     context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")

        # Context for paginated template
        context["entity_type"] = "joblog"
        context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/joblog/"
        context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
        context["list_title"] = "joblog"
        context["list_mode"] = "combined"

        return render(request, "collections/joblog_paginated.html", context)

        # return render(request, "collections/joblog.html", context)

    elif request.method == "POST":
        job_name = request.POST.get("job-name")

        if job_name == "engineer-pathway":
            pathway_to_engineer = request.POST.get("pathway-to-engineer")
            engineer_setting = request.POST.get("engineer-setting")

            if not all([pathway_to_engineer, engineer_setting]):
                raise BadRequest(
                    f"Unable to run {job_name} as it requires 'pathway-to-engineer' and 'engineer-setting' parameters."
                )

            pathway_package = PackageManager.get_package_by_url(current_user, pathway_to_engineer)
            pathway_to_engineer = Pathway.objects.get(
                url=pathway_to_engineer, package=pathway_package
            )

            engineer_setting = SettingManager.get_setting_by_url(current_user, engineer_setting)

            target_package = PackageManager.create_package(
                current_user,
                f"Autogenerated Package for Pathway Engineering of {pathway_to_engineer.get_name()}",
                f"This Package was generated automatically for the engineering Task of {pathway_to_engineer.get_name()}.",
            )

            from .tasks import dispatch, engineer_pathways

            res = dispatch(
                current_user,
                engineer_pathways,
                [pathway_to_engineer.pk],
                engineer_setting.pk,
                target_package.pk,
            )

            return redirect(f"{s.SERVER_URL}/jobs/{res.task_id}")

        elif job_name == "batch-predict":
            substrates = request.POST.get("substrates")
            prediction_setting_url = request.POST.get("prediction-setting")
            num_tps = request.POST.get("num-tps")

            if substrates is None or substrates.strip() == "":
                raise BadRequest("No substrates provided.")

            pred_data = []
            for pair in substrates.split("\n"):
                parts = pair.split(",")

                try:
                    smiles = FormatConverter.standardize(parts[0], remove_stereo=True)
                except ValueError:
                    raise BadRequest(f"Couldn't standardize SMILES {parts[0]}!")

                # name is optional
                name = ",".join(parts[1:]) if len(parts) > 1 else None
                pred_data.append([smiles, name])

            max_tps = 50
            if num_tps is not None and num_tps.strip() != "":
                try:
                    num_tps = int(num_tps)
                    max_tps = max(min(num_tps, 50), 1)
                except ValueError:
                    raise BadRequest(f"Parameter for num-tps {num_tps} is not a valid integer.")

            batch_predict_setting = SettingManager.get_setting_by_url(
                current_user, prediction_setting_url
            )

            target_package = PackageManager.create_package(
                current_user,
                f"Autogenerated Package for Batch Prediction {datetime.now()}",
                "This Package was generated automatically for the batch prediction task.",
            )

            from .tasks import batch_predict, dispatch

            res = dispatch(
                current_user,
                batch_predict,
                pred_data,
                batch_predict_setting.pk,
                target_package.pk,
                num_tps=max_tps,
            )

            return redirect(f"{s.SERVER_URL}/jobs/{res.task_id}")

        else:
            raise BadRequest(f"Job {job_name} is not supported!")
    else:
        return HttpResponseNotAllowed(["GET", "POST"])


def job(request, job_uuid):
    current_user = _anonymous_or_real(request)
    context = get_base_context(request)

    if request.method == "GET":
        if current_user.is_superuser:
            job = JobLog.objects.get(task_id=job_uuid)
        else:
            job = JobLog.objects.get(task_id=job_uuid, user=current_user)

        # No op if status is already in a terminal state
        job.check_for_update()

        if request.GET.get("download", False) == "true":
            if not job.is_result_downloadable():
                raise BadRequest("Result is not downloadable!")

            if job.job_name == "batch_predict":
                filename = f"{job.job_name.replace(' ', '_')}_{job.task_id}.csv"
            elif job.job_name == "identify_missing_rules":
                filename = f"{job.job_name.replace(' ', '_')}_{job.task_id}.csv"
            else:
                raise BadRequest("Result is not downloadable!")

            res_str = job.task_result
            response = HttpResponse(res_str, content_type="text/csv")
            response["Content-Disposition"] = f'attachment; filename="{filename}"'

            return response

        context["object_type"] = "joblog"
        context["breadcrumbs"] = [
            {"Home": s.SERVER_URL},
            {"Jobs": s.SERVER_URL + "/jobs"},
            {job.job_name: f"{s.SERVER_URL}/jobs/{job.task_id}"},
        ]

        context["job"] = job

        return render(request, "objects/joblog.html", context)

    else:
        return HttpResponseNotAllowed(["GET"])


###########
# KETCHER #
###########


def indigo(request):
    from indigo import Indigo

    return JsonResponse({"Indigo": {"version": Indigo().version()}})


@csrf_exempt
def aromatize(request):
    if request.method == "POST":
        data = json.loads(request.body)
        mol_data = data.get("struct")
        aromatized = IndigoUtils.aromatize(mol_data, False)
        return JsonResponse({"struct": aromatized})
    else:
        return HttpResponseBadRequest()


@csrf_exempt
def dearomatize(request):
    if request.method == "POST":
        data = json.loads(request.body)
        mol_data = data.get("struct")
        dearomatized = IndigoUtils.dearomatize(mol_data, False)
        return JsonResponse({"struct": dearomatized})
    else:
        return HttpResponseBadRequest()


@csrf_exempt
def layout(request):
    if request.method == "POST":
        data = json.loads(request.body)
        mol_data = data.get("struct")
        lay = IndigoUtils.layout(mol_data)
        return JsonResponse({"struct": lay})
    else:
        return HttpResponseBadRequest()


##########################
# Generic/Non-Persistent #
##########################
def depict(request):
    if smiles := request.GET.get("smiles"):
        return HttpResponse(IndigoUtils.mol_to_svg(smiles), content_type="image/svg+xml")

    elif smirks := request.GET.get("smirks"):
        query_smirks = request.GET.get("is_query_smirks", False) == "true"
        return HttpResponse(
            IndigoUtils.smirks_to_svg(smirks, query_smirks), content_type="image/svg+xml"
        )
    else:
        return HttpResponseBadRequest()


@protected_resource()
def userinfo(request):
    user = request.resource_owner
    res = {
        "sub": str(user.uuid),
        "email": user.email,
        "username": user.username,
        "name": user.get_full_name() or user.username,
        "email_verified": user.is_active,
    }
    return JsonResponse(res)


# Static Pages
def static_terms_of_use(request):
    context = get_base_context(request)
    context["title"] = "enviPath - Terms of Use"
    context["public_mode"] = True
    return render(request, "static/terms_of_use.html", context)


def static_privacy_policy(request):
    context = get_base_context(request)
    context["title"] = "enviPath - Privacy Policy"
    context["public_mode"] = True
    return render(request, "static/privacy_policy.html", context)


def static_cookie_policy(request):
    context = get_base_context(request)
    context["title"] = "enviPath - Cookie Policy"
    context["public_mode"] = True
    return render(request, "static/cookie_policy.html", context)


def static_about_us(request):
    context = get_base_context(request)
    context["title"] = "enviPath - About Us"
    context["public_mode"] = True
    return render(request, "static/about_us.html", context)


def static_contact_support(request):
    context = get_base_context(request)
    context["title"] = "enviPath - Contact & Support"
    context["public_mode"] = True
    return render(request, "static/contact.html", context)


def static_careers(request):
    context = get_base_context(request)
    context["title"] = "enviPath - Careers"
    context["public_mode"] = True
    return render(request, "static/careers.html", context)


def static_cite(request):
    context = get_base_context(request)
    context["title"] = "enviPath - How to Cite"
    context["public_mode"] = True
    return render(request, "static/cite.html", context)


def static_legal(request):
    context = get_base_context(request)
    context["title"] = "enviPath - Legal Information"
    context["public_mode"] = True
    return render(request, "static/legal.html", context)
