'''Class to provide namespace manipulation.
'''

import json
import time
from eliot import start_action
from kubernetes.client.rest import ApiException
from kubernetes import client
from .. import LoggableChild


class LSSTNamespaceManager(LoggableChild):
    '''Class to provide namespace manipulation.
    '''

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.namespace = None
        self.service_account = None  # Account for pod to run as

    def set_namespace(self, namespace):
        with start_action(action_type="set_namespace"):
            self.namespace = namespace

    def ensure_namespace(self):
        '''Here we make sure that the namespace exists, creating it if
        it does not.  That requires a ClusterRole that can list and create
        namespaces.

        If we create the namespace, we also create (if needed) a ServiceAccount
        within it to allow the user pod to spawn dask and workflow pods.

        '''
        with start_action(action_type="ensure_namespace"):
            namespace = self.namespace
            if not namespace or namespace == "default":
                raise ValueError("Will not use default namespace!")
            api = self.parent.api
            cfg = self.parent.config
            acd = 'argocd.argoproj.io/'
            ns = client.V1Namespace(
                metadata=client.V1ObjectMeta(
                    name=namespace,
                    labels={acd + 'instance': 'nublado-users'},
                    annotations={
                        acd + 'compare-options': 'IgnoreExtraneous',
                        acd + 'sync-options': 'Prune=false',
                    }))
            try:
                self.log.info(
                    "Attempting to create namespace '%s'" % namespace)
                api.create_namespace(ns)
            except ApiException as e:
                if e.status != 409:
                    estr = "Create namespace '%s' failed: %s" % (ns, str(e))
                    self.log.exception(estr)
                    raise
                else:
                    self.log.info("Namespace '%s' already exists." % namespace)
            # Wait for the namespace to actually appear before creating objects
            #  in it.
            self.wait_for_namespace()
            if cfg.allow_dask_spawn:
                self.log.debug("Ensuring namespaced service account.")
                self.ensure_namespaced_service_account()
            if self.parent.spawner.enable_namespace_quotas:
                # By the time we need this, quota will have been set, because
                #  we needed it for options form generation.
                self.log.debug("Determining resource quota.")
                qm = self.parent.quota_mgr
                qm.ensure_namespaced_resource_quota(qm.quota)
            self.log.debug("Namespace resources ensured.")

    def def_namespaced_account_objects(self):
        '''Define K8s objects for things we need in the namespace.
        '''
        with start_action(action_type="define_namespaced_account_objects"):
            namespace = self.namespace
            username = self.parent.user.escaped_name
            account = "{}-svcacct".format(username)
            self.service_account = account
            acd = 'argocd.argoproj.io/'
            md = client.V1ObjectMeta(
                name=account,
                labels={acd + 'instance': 'nublado-users'},
                annotations={
                    acd + 'compare-options': 'IgnoreExtraneous',
                    acd + 'sync-options': 'Prune=false',
                }
            )
            svcacct = client.V1ServiceAccount(metadata=md)
            # These rules let us manipulate Dask pods, Argo Workflows, and
            #  Multus CNI interfaces
            rules = [
                client.V1PolicyRule(
                    api_groups=["argoproj.io"],
                    resources=["workflows", "workflows/finalizers"],
                    verbs=["get", "list", "watch", "update", "patch", "create",
                           "delete"]
                ),
                client.V1PolicyRule(
                    api_groups=["argoproj.io"],
                    resources=["workflowtemplates",
                               "workflowtemplates/finalizers"],
                    verbs=["get", "list", "watch"],
                ),
                client.V1PolicyRule(
                    api_groups=[""],
                    resources=["secrets"],
                    verbs=["get"]
                ),
                client.V1PolicyRule(
                    api_groups=[""],
                    resources=["pods", "pods/exec", "services", "configmaps"],
                    verbs=["get", "list", "watch", "create", "delete",
                           "update", "patch"]
                ),
                client.V1PolicyRule(
                    api_groups=[""],
                    resources=["pods/log", "serviceaccounts"],
                    verbs=["get", "list", "watch"]
                ),
            ]
            role = client.V1Role(
                rules=rules,
                metadata=md)
            rbstr = 'rbac.authorization.k8s.io'
            rolebinding = client.V1RoleBinding(
                metadata=md,
                role_ref=client.V1RoleRef(api_group=rbstr,
                                          kind="Role",
                                          name=account),
                subjects=[client.V1Subject(
                    kind="ServiceAccount",
                    name=account,
                    namespace=namespace)]
            )
            return svcacct, role, rolebinding

    def ensure_namespaced_service_account(self):
        '''Create a service account with role and rolebinding to allow it
        to manipulate resources in the namespace.
        '''
        with start_action(action_type="ensure_namespaced_service_account"):
            self.log.info("Ensuring namespaced service account.")
            namespace = self.namespace
            api = self.parent.api
            rbac_api = self.parent.rbac_api
            svcacct, role, rolebinding = self.def_namespaced_account_objects()
            account = self.service_account
            try:
                self.log.info("Attempting to create service account.")
                api.create_namespaced_service_account(
                    namespace=namespace,
                    body=svcacct)
            except ApiException as e:
                if e.status != 409:
                    self.log.exception("Create service account '{}' " +
                                       "in namespace '{}' " +
                                       "failed: '{}".format(account,
                                                            namespace, e))
                    raise
                else:
                    self.log.info("Service account '{}' " +
                                  "in namespace '{}' " +
                                  "already exists.".format(account, namespace))
            try:
                self.log.info("Attempting to create role in namespace.")
                rbac_api.create_namespaced_role(
                    namespace,
                    role)
            except ApiException as e:
                if e.status != 409:
                    self.log.exception("Create role '%s' " % account +
                                       "in namespace '%s' " % namespace +
                                       "failed: %s" % str(e))
                    raise
                else:
                    self.log.info("Role '{}' already exists in " +
                                  "namespace '{}'.".format(account, namespace))
            try:
                self.log.info("Attempting to create rolebinding in namespace.")
                rbac_api.create_namespaced_role_binding(
                    namespace,
                    rolebinding)
            except ApiException as e:
                if e.status != 409:
                    self.log.exception("Create rolebinding '%s'" % account +
                                       "in namespace '%s' " % namespace +
                                       "failed: %s", str(e))
                    raise
                else:
                    self.log.info("Rolebinding '%s' " % account +
                                  "already exists in '%s'." % namespace)

    def wait_for_namespace(self, timeout=30):
        '''Wait for namespace to be created.'''
        with start_action(action_type="wait_for_namespace"):
            namespace = self.namespace
            for dl in range(timeout):
                self.log.debug("Checking for namespace " +
                               "{} [{}/{}]".format(namespace, dl, timeout))
                nl = self.parent.api.list_namespace(timeout_seconds=1)
                for ns in nl.items:
                    nsname = ns.metadata.name
                    if nsname == namespace:
                        self.log.debug("Namespace {} found.".format(namespace))
                        return
                    self.log.debug(
                        "Namespace {} not present yet.".format(namespace))
                time.sleep(1)
            raise RuntimeError(
                "Namespace '{}' not created in {} seconds!".format(namespace,
                                                                   timeout))

    def maybe_delete_namespace(self):
        '''Here we try to delete the namespace.  If it has no non-dask
        running pods, and it's not the default namespace, we can delete it."

        This requires a cluster role that can delete namespaces.'''
        with start_action(action_type="maybe_delete_namespace"):
            self.log.debug("Attempting to delete namespace.")
            namespace = self.namespace
            if not namespace or namespace == "default":
                raise RuntimeError("Cannot delete default namespace!")
            podlist = self.parent.api.list_namespaced_pod(namespace)
            clear_to_delete = True
            if podlist and podlist.items:
                clear_to_delete = self._check_pods(podlist.items)
            if not clear_to_delete:
                self.log.info("Not deleting namespace '%s'" % namespace)
                return False
            self.log.info("Deleting namespace '%s'" % namespace)
            self.parent.api.delete_namespace(namespace)
            return True

    def _check_pods(self, items):
        with start_action(action_type="_check_pods"):
            namespace = self.namespace
            for i in items:
                if i and i.status:
                    phase = i.status.phase
                    if (phase == "Running" or phase == "Unknown"
                            or phase == "Pending"):
                        pname = i.metadata.name
                        if pname.startswith("dask-"):
                            self.log.debug(
                                ("Abandoned dask pod '{}' can be " +
                                 "reaped.").format(pname))
                            # We can murder abandoned dask pods
                            continue
                        self.log.warning(("Pod in state '{}'; cannot delete " +
                                          "namespace '{}'.").format(phase,
                                                                    namespace))
                        return False
            # FIXME check on workflows as well.
            return True

    def destroy_namespaced_resource_quota(self):
        '''Remove quotas from namespace.
        You don't usually have to call this, since it will get
        cleaned up as part of namespace deletion.
        '''
        with start_action(action_type="destroy_namespaced_resource_quota"):
            namespace = self.get_user_namespace()
            api = self.parent.api
            qname = "quota-" + namespace
            dopts = client.V1DeleteOptions()
            self.log.info("Deleting resourcequota '%s'" % qname)
            api.delete_namespaced_resource_quota(qname, namespace, dopts)

    def delete_namespaced_svcacct_objs(self):
        '''Remove service accounts, roles, and rolebindings from namespace.
        You don't usually have to call this, since they will get
         cleaned up as part of namespace deletion.
        '''
        with start_action(action_type="delete_namespaced_svcacct_objs"):
            namespace = self.namespace
            account = self.service_account
            if not account:
                self.log.info("Service account not defined.")
                return
            dopts = client.V1DeleteOptions()
            self.log.info("Deleting service accounts/role/rolebinding " +
                          "for %s" % namespace)
            self.parent.rbac_api.delete_namespaced_role_binding(
                account,
                namespace,
                dopts)
            self.parent.rbac_api.delete_namespaced_role(
                account,
                namespace,
                dopts)
            self.parent.api.delete_namespaced_service_account(
                account,
                namespace,
                dopts)

    def dump(self):
        '''Return dict for pretty-printing.
        '''
        nd = {"namespace": self.namespace,
              "service_account": self.service_account,
              "parent": str(self.parent)}
        return nd

    def toJSON(self):
        return json.dumps(self.dump())
