# Copyright 2014: Mirantis Inc.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import collections
import copy
import uuid

from rally.common import broker
from rally.common import cfg
from rally.common import logging
from rally.common import validation
from rally import exceptions

from rally_openstack.common import consts
from rally_openstack.common import credential
from rally_openstack.common import osclients
from rally_openstack.common.services.identity import identity
from rally_openstack.common.wrappers import network
from rally_openstack.task import context


LOG = logging.getLogger(__name__)

CONF = cfg.CONF

RESOURCE_MANAGEMENT_WORKERS_DESCR = ("The number of concurrent threads to use "
                                     "for serving users context.")
PROJECT_DOMAIN_DESCR = "ID of domain in which projects will be created."
USER_DOMAIN_DESCR = "ID of domain in which users will be created."


@validation.add("required_platform", platform="openstack", users=True)
@context.configure(name="users", platform="openstack", order=100)
class UserGenerator(context.OpenStackContext):
    """Creates specified amount of keystone users and tenants."""

    CONFIG_SCHEMA = {
        "type": "object",
        "$schema": consts.JSON_SCHEMA,
        "anyOf": [
            {"description": "Create new temporary users and tenants.",
             "properties": {
                "tenants": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "The number of tenants to create."
                },
                 "users_per_tenant": {
                     "type": "integer",
                     "minimum": 1,
                     "description": "The number of users to create per one "
                                    "tenant."},
                 "user_password": {
                     "type": "string",
                     "description": "Specify custom user password instead of "
                                    "randomly generated in case of password "
                                    "requirements."},
                 "resource_management_workers": {
                     "type": "integer",
                     "minimum": 1,
                     "description": RESOURCE_MANAGEMENT_WORKERS_DESCR},
                 "project_domain": {
                     "type": "string",
                     "description": PROJECT_DOMAIN_DESCR},
                 "user_domain": {
                     "type": "string",
                     "description": USER_DOMAIN_DESCR},
                 "user_choice_method": {
                     "$ref": "#/definitions/user_choice_method"}},
             "additionalProperties": False},
            # TODO(andreykurilin): add ability to specify users here.
            {"description": "Use existing users and tenants.",
             "properties": {
                 "user_choice_method": {
                     "$ref": "#/definitions/user_choice_method"}
             },
             "additionalProperties": False}
        ],
        "definitions": {
            "user_choice_method": {
                "enum": ["random", "round_robin"],
                "description": "The mode of balancing usage of users between "
                               "scenario iterations."}

        }
    }

    DEFAULT_CONFIG = {"user_choice_method": "random"}

    DEFAULT_FOR_NEW_USERS = {
        "tenants": 1,
        "users_per_tenant": 1,
        "resource_management_workers":
            cfg.CONF.openstack.users_context_resource_management_workers,
    }

    def __init__(self, context):
        super(UserGenerator, self).__init__(context)

        creds = self.env["platforms"]["openstack"]
        if creds.get("admin"):
            admin_cred = copy.deepcopy(creds["admin"])
            api_info = copy.deepcopy(creds.get("api_info", {}))
            if "api_info" in admin_cred:
                api_info.update(creds["admin"]["api_info"])
            admin_cred["api_info"] = api_info
            context["admin"] = {
                "credential": credential.OpenStackCredential(**admin_cred)
            }

        if creds["users"] and not (set(self.config) - {"user_choice_method"}):
            self.existing_users = creds["users"]
        else:
            self.existing_users = []
            self.credential = context["admin"]["credential"]
            project_domain = (self.credential["project_domain_name"]
                              or cfg.CONF.openstack.project_domain)
            user_domain = (self.credential["user_domain_name"]
                           or cfg.CONF.openstack.user_domain)
            self.DEFAULT_FOR_NEW_USERS["project_domain"] = project_domain
            self.DEFAULT_FOR_NEW_USERS["user_domain"] = user_domain
            with self.config.unlocked():
                for key, value in self.DEFAULT_FOR_NEW_USERS.items():
                    self.config.setdefault(key, value)

    def _remove_default_security_group(self):
        """Delete default security group for tenants."""
        clients = osclients.Clients(self.credential)

        if consts.Service.NEUTRON not in clients.services().values():
            return

        use_sg, msg = network.wrap(clients, self).supports_extension(
            "security-group")
        if not use_sg:
            LOG.debug("Security group context is disabled: %s" % msg)
            return

        for user, tenant_id in self._iterate_per_tenants():
            with logging.ExceptionLogger(
                    LOG, "Unable to delete default security group"):
                uclients = osclients.Clients(user["credential"])
                security_groups = uclients.neutron()\
                    .list_security_groups(tenant_id=tenant_id)
                default = [sg for sg in security_groups["security_groups"]
                           if sg["name"] == "default"]
                if default:
                    clients.neutron().delete_security_group(default[0]["id"])

    def _create_tenants(self, threads):
        tenants = collections.deque()

        def publish(queue):
            for i in range(self.config["tenants"]):
                args = (self.config["project_domain"], self.task["uuid"], i)
                queue.append(args)

        def consume(cache, args):
            domain, task_id, i = args
            if "client" not in cache:
                clients = osclients.Clients(self.credential)
                cache["client"] = identity.Identity(
                    clients, name_generator=self.generate_random_name)
            tenant = cache["client"].create_project(domain_name=domain)
            tenant_dict = {"id": tenant.id, "name": tenant.name, "users": []}
            tenants.append(tenant_dict)

        # NOTE(msdubov): consume() will fill the tenants list in the closure.
        broker.run(publish, consume, threads)
        tenants_dict = {}
        for t in tenants:
            tenants_dict[t["id"]] = t

        return tenants_dict

    def _create_users(self, threads):
        # NOTE(msdubov): This should be called after _create_tenants().
        users_per_tenant = self.config["users_per_tenant"]
        default_role = cfg.CONF.openstack.keystone_default_role

        users = collections.deque()

        def publish(queue):
            for tenant_id in self.context["tenants"]:
                for user_id in range(users_per_tenant):
                    username = self.generate_random_name()
                    password = (str(uuid.uuid4())
                                if self.config.get("user_password") is None
                                else self.config["user_password"])
                    args = (username, password, self.config["project_domain"],
                            self.config["user_domain"], tenant_id)
                    queue.append(args)

        def consume(cache, args):
            username, password, project_dom, user_dom, tenant_id = args
            if "client" not in cache:
                clients = osclients.Clients(self.credential)
                cache["client"] = identity.Identity(
                    clients, name_generator=self.generate_random_name)
            client = cache["client"]
            user = client.create_user(username, password=password,
                                      project_id=tenant_id,
                                      domain_name=user_dom,
                                      default_role=default_role)
            user_credential = credential.OpenStackCredential(
                auth_url=self.credential["auth_url"],
                username=user.name,
                password=password,
                tenant_name=self.context["tenants"][tenant_id]["name"],
                permission=consts.EndpointPermission.USER,
                project_domain_name=project_dom,
                user_domain_name=user_dom,
                endpoint_type=self.credential["endpoint_type"],
                https_insecure=self.credential["https_insecure"],
                https_cacert=self.credential["https_cacert"],
                region_name=self.credential["region_name"],
                profiler_hmac_key=self.credential["profiler_hmac_key"],
                profiler_conn_str=self.credential["profiler_conn_str"],
                api_info=self.credential["api_info"])
            users.append({"id": user.id,
                          "credential": user_credential,
                          "tenant_id": tenant_id})

        # NOTE(msdubov): consume() will fill the users list in the closure.
        broker.run(publish, consume, threads)
        return list(users)

    def _get_consumer_for_deletion(self, func_name):
        def consume(cache, resource_id):
            if "client" not in cache:
                clients = osclients.Clients(self.credential)
                cache["client"] = identity.Identity(clients)
            getattr(cache["client"], func_name)(resource_id)
        return consume

    def _delete_tenants(self):
        threads = self.config["resource_management_workers"]

        def publish(queue):
            for tenant_id in self.context["tenants"]:
                queue.append(tenant_id)

        broker.run(publish, self._get_consumer_for_deletion("delete_project"),
                   threads)
        self.context["tenants"] = {}

    def _delete_users(self):
        threads = self.config["resource_management_workers"]

        def publish(queue):
            for user in self.context["users"]:
                queue.append(user["id"])

        broker.run(publish, self._get_consumer_for_deletion("delete_user"),
                   threads)
        self.context["users"] = []

    def create_users(self):
        """Create tenants and users, using the broker pattern."""

        threads = min(self.config["resource_management_workers"],
                      self.config["tenants"])

        LOG.debug("Creating %(tenants)d tenants using %(threads)s threads"
                  % {"tenants": self.config["tenants"], "threads": threads})
        self.context["tenants"] = self._create_tenants(threads)

        if len(self.context["tenants"]) < self.config["tenants"]:
            raise exceptions.ContextSetupFailure(
                ctx_name=self.get_name(),
                msg="Failed to create the requested number of tenants.")

        users_num = self.config["users_per_tenant"] * self.config["tenants"]
        threads = min(self.config["resource_management_workers"], users_num)
        LOG.debug("Creating %(users)d users using %(threads)s threads"
                  % {"users": users_num, "threads": threads})
        self.context["users"] = self._create_users(threads)
        for user in self.context["users"]:
            self.context["tenants"][user["tenant_id"]]["users"].append(user)

        if len(self.context["users"]) < users_num:
            raise exceptions.ContextSetupFailure(
                ctx_name=self.get_name(),
                msg="Failed to create the requested number of users.")

    def use_existing_users(self):
        LOG.debug("Using existing users for OpenStack platform.")
        api_info = copy.deepcopy(self.env["platforms"]["openstack"].get(
            "api_info", {}))
        for user_credential in self.existing_users:
            user_credential = copy.deepcopy(user_credential)
            if "api_info" in user_credential:
                api_info.update(user_credential["api_info"])
            user_credential["api_info"] = api_info
            user_credential = credential.OpenStackCredential(**user_credential)
            user_clients = osclients.Clients(user_credential)
            user_id = user_clients.keystone.auth_ref.user_id
            tenant_id = user_clients.keystone.auth_ref.project_id

            if tenant_id not in self.context["tenants"]:
                self.context["tenants"][tenant_id] = {
                    "id": tenant_id,
                    "name": user_credential.tenant_name
                }

            self.context["users"].append({
                "credential": user_credential,
                "id": user_id,
                "tenant_id": tenant_id
            })

    def setup(self):
        self.context["users"] = []
        self.context["tenants"] = {}
        self.context["user_choice_method"] = self.config["user_choice_method"]

        if self.existing_users:
            self.use_existing_users()
        else:
            self.create_users()

    def cleanup(self):
        """Delete tenants and users, using the broker pattern."""
        if self.existing_users:
            # nothing to do here.
            return
        else:
            self._remove_default_security_group()
            self._delete_users()
            self._delete_tenants()
