"""
The following are helper functions to handle the volumes of a Notebook.
The new API Volume will work with objects of the following format:

volume:
  mount: "mount path"
  newPvc?:
    metadata: ...
    spec: ...
  existingSource?:
    nfs?: ...
    persistentVolumeClaim?: ...
    ...

These functions will parse such objects and map them to K8s constructs.
"""
from kubernetes import client
from werkzeug.exceptions import BadRequest

from kubeflow.kubeflow.crud_backend import api, logging

from . import utils

log = logging.getLogger(__name__)

PVC_SOURCE = "persistentVolumeClaim"
EXISTING_SOURCE = "existingSource"
NEW_PVC = "newPvc"
MOUNT = "mount"
NAME = "name"


def check_volume_format(api_volume):
    """
    Ensure that the JSON object received has the expected structure.

    api_volume: The JSON API Volume object
    """
    if MOUNT not in api_volume:
        raise BadRequest("Volume should have a mount: %s" % api_volume)

    if EXISTING_SOURCE not in api_volume and NEW_PVC not in api_volume:
        raise BadRequest("Volume has neither %s nor %s: %s"
                         % (EXISTING_SOURCE, NEW_PVC, api_volume))

    if EXISTING_SOURCE in api_volume and NEW_PVC in api_volume:
        raise BadRequest("Volume has both %s and %s: %s"
                         % (EXISTING_SOURCE, NEW_PVC, api_volume))


def get_volume_name(api_volume):
    """
    Return the name of the K8s V1Volume given an API volume with an existing
    source.

    api_volume: The API Volume submitted from client/UI
    """
    # if the volume source is an existing PVC then use the requested PVC's name
    # as the V1Volume.name
    if EXISTING_SOURCE not in api_volume:
        raise BadRequest("Failed to retrieve a volume name from '%s'"
                         % api_volume)
    if PVC_SOURCE in api_volume[EXISTING_SOURCE]:
        if "claimName" not in api_volume[EXISTING_SOURCE][PVC_SOURCE]:
            raise BadRequest("Failed to retrieve the PVC name from '%s'"
                             % api_volume)
        return api_volume[EXISTING_SOURCE][PVC_SOURCE]["claimName"]

    # A user requested a different source for the V1Volume. In this case we
    # use a randomly generated name
    return "existing-source-volume-%s" % utils.random_string(8)


def get_pod_volume(api_volume, pvc):
    """
    Return a V1Volume dict object based on the API Volume the client/UI sent.

    api_volume: The API Volume submitted from client/UI
    pvc: The created PVC, whose spec was defined in the api_volume
    """
    check_volume_format(api_volume)

    if pvc is not None:
        # Mount a new PVC. We use the created PVC since the api_volume.newPvc
        # spec might have defined a metadata.generateName. In this case, the
        # name is set after the creation of the PVC
        return {"name": pvc.metadata.name,
                "persistentVolumeClaim": {"claimName": pvc.metadata.name}}

    # User has explicitly asked to use an existing volume source
    v1_volume = {"name": get_volume_name(api_volume)}
    v1_volume.update(api_volume[EXISTING_SOURCE])
    return v1_volume


def get_container_mount(api_volume, volume_name):
    """
    Return a V1VolumeMount dict object from the request's JSON API Volume

    api_volume: The API Volume submitted from client/UI
    volume_name: The name of the V1Volume which the mount should refer to
    """
    check_volume_format(api_volume)

    return {"name": volume_name, "mountPath": api_volume["mount"]}


def get_new_pvc(api_volume) -> client.V1PersistentVolumeClaim:
    """
    Return a V1PersistentVolumeClaim dict object from the request's JSON
    API Volume.

    api_volume: The JSON V1Volume object, in cammelCase as defined in the docs
    """
    check_volume_format(api_volume)
    if NEW_PVC not in api_volume:
        return None

    pvc = api.deserialize(api_volume[NEW_PVC], "V1PersistentVolumeClaim")

    # don't allow users to explicitly set the Namespace
    if pvc.metadata.namespace is not None:
        raise BadRequest("PVC should not specify the namespace.")

    return pvc


def add_notebook_volume(notebook, volume):
    """
    Add the provided podvolume (dict V1Volume) to the Notebook's PodSpec.

    notebook: Notebook CR dict
    volume: Podvolume dict
    """
    podspec = notebook["spec"]["template"]["spec"]
    if "volumes" not in podspec:
        podspec["volumes"] = []

    podspec["volumes"].append(volume)
    return notebook


def add_notebook_container_mount(notebook, container_mount):
    """
    Add the provided container mount (dict V1VolumeMount) to the Notebook's
    PodSpec.

    notebook: Notebook CR dict
    volume: Podvolume dict
    """
    container = notebook["spec"]["template"]["spec"]["containers"][0]
    if "volumeMounts" not in container:
        container["volumeMounts"] = []

    container["volumeMounts"].append(container_mount)
    return notebook
