#    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 logging
import random
import string

from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from glanceclient import exc as glance_exceptions
from horizon.utils import memoized
from openstack_dashboard.api import base
from openstack_dashboard.api import glance
from openstack_dashboard.api import neutron
from os_cloud_config import keystone_pki
from tuskarclient import client as tuskar_client

from tuskar_ui.api import flavor
from tuskar_ui.cached_property import cached_property  # noqa
from tuskar_ui.handle_errors import handle_errors  # noqa

LOG = logging.getLogger(__name__)
MASTER_TEMPLATE_NAME = 'plan.yaml'
ENVIRONMENT_NAME = 'environment.yaml'
TUSKAR_SERVICE = 'management'

SSL_HIDDEN_PARAMS = ('SSLCertificate', 'SSLKey')
KEYSTONE_CERTIFICATE_PARAMS = (
    'KeystoneSigningCertificate', 'KeystoneCACertificate',
    'KeystoneSigningKey')


@memoized.memoized
def tuskarclient(request, password=None):
    api_version = "2"
    insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
    ca_file = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
    endpoint = base.url_for(request, TUSKAR_SERVICE)

    LOG.debug('tuskarclient connection created using token "%s" and url "%s"' %
              (request.user.token.id, endpoint))

    client = tuskar_client.get_client(api_version,
                                      tuskar_url=endpoint,
                                      insecure=insecure,
                                      ca_file=ca_file,
                                      username=request.user.username,
                                      password=password,
                                      os_auth_token=request.user.token.id)
    return client


def password_generator(size=40, chars=(string.ascii_uppercase +
                                       string.ascii_lowercase +
                                       string.digits)):
    return ''.join(random.choice(chars) for _ in range(size))


def strip_prefix(parameter_name):
    return parameter_name.split('::', 1)[-1]


def _is_blank(parameter):
    return not parameter['value'] or parameter['value'] == 'unset'


def _should_generate_password(parameter):
    # TODO(lsmola) Filter out SSL params for now. Once it will be generated
    # in TripleO add it here too. Note: this will also affect how endpoints are
    # created
    key = parameter['name']
    return all([
        parameter['hidden'],
        _is_blank(parameter),
        strip_prefix(key) not in SSL_HIDDEN_PARAMS,
        strip_prefix(key) not in KEYSTONE_CERTIFICATE_PARAMS,
        key != 'SnmpdReadonlyUserPassword',
    ])


def _should_generate_keystone_cert(parameter):
    return all([
        strip_prefix(parameter['name']) in KEYSTONE_CERTIFICATE_PARAMS,
        _is_blank(parameter),
    ])


def _should_generate_neutron_control_plane(parameter):
    return all([
        strip_prefix(parameter['name']) == 'NeutronControlPlaneID',
        _is_blank(parameter),
    ])


class Plan(base.APIResourceWrapper):
    _attrs = ('uuid', 'name', 'description', 'created_at', 'modified_at',
              'roles', 'parameters')

    def __init__(self, apiresource, request=None):
        super(Plan, self).__init__(apiresource)
        self._request = request

    @classmethod
    def create(cls, request, name, description):
        """Create a Plan in Tuskar

        :param request: request object
        :type  request: django.http.HttpRequest

        :param name: plan name
        :type  name: string

        :param description: plan description
        :type  description: string

        :return: the created Plan object
        :rtype:  tuskar_ui.api.tuskar.Plan
        """
        plan = tuskarclient(request).plans.create(name=name,
                                                  description=description)
        return cls(plan, request=request)

    @classmethod
    def patch(cls, request, plan_id, parameters):
        """Update a Plan in Tuskar

        :param request: request object
        :type  request: django.http.HttpRequest

        :param plan_id: id of the plan we want to update
        :type  plan_id: string

        :param parameters: new values for the plan's parameters
        :type  parameters: dict

        :return: the updated Plan object
        :rtype:  tuskar_ui.api.tuskar.Plan
        """
        parameter_list = [{
            'name': unicode(name),
            'value': unicode(value),
        } for (name, value) in parameters.items()]
        plan = tuskarclient(request).plans.patch(plan_id, parameter_list)
        return cls(plan, request=request)

    @classmethod
    @memoized.memoized
    def list(cls, request):
        """Return a list of Plans in Tuskar

        :param request: request object
        :type  request: django.http.HttpRequest

        :return: list of Plans, or an empty list if there are none
        :rtype:  list of tuskar_ui.api.tuskar.Plan
        """
        plans = tuskarclient(request).plans.list()
        return [cls(plan, request=request) for plan in plans]

    @classmethod
    @handle_errors(_("Unable to retrieve plan"))
    def get(cls, request, plan_id):
        """Return the Plan that matches the ID

        :param request: request object
        :type  request: django.http.HttpRequest

        :param plan_id: id of Plan to be retrieved
        :type  plan_id: int

        :return: matching Plan, or None if no Plan matches
                 the ID
        :rtype:  tuskar_ui.api.tuskar.Plan
        """
        plan = tuskarclient(request).plans.get(plan_uuid=plan_id)
        return cls(plan, request=request)

    # TODO(lsmola) before will will support multiple overclouds, we
    # can work only with overcloud that is named overcloud. Delete
    # this once we have more overclouds. Till then, this is the overcloud
    # that rules them all.
    # This is how API supports it now, so we have to have it this way.
    # Also till Overcloud workflow is done properly, we have to work
    # with situations that overcloud is deleted, but stack is still
    # there. So overcloud will pretend to exist when stack exist.
    @classmethod
    def get_the_plan(cls, request):
        plan_list = cls.list(request)
        for plan in plan_list:
            return plan
        # if plan doesn't exist, create it
        plan = cls.create(request, 'overcloud', 'overcloud')
        return plan

    @classmethod
    def delete(cls, request, plan_id):
        """Delete a Plan

        :param request: request object
        :type  request: django.http.HttpRequest

        :param plan_id: plan id
        :type  plan_id: int
        """
        tuskarclient(request).plans.delete(plan_uuid=plan_id)

    @cached_property
    def role_list(self):
        return [Role.get(self._request, role.uuid)
                for role in self.roles]

    @cached_property
    def _roles_by_name(self):
        return dict((role.name, role) for role in self.role_list)

    def get_role_by_name(self, role_name):
        """Get the role with the given name."""
        return self._roles_by_name[role_name]

    def get_role_node_count(self, role):
        """Get the node count for the given role."""
        return int(self.parameter_value(role.node_count_parameter_name,
                                        0) or 0)

    @cached_property
    def templates(self):
        return tuskarclient(self._request).plans.templates(self.uuid)

    @cached_property
    def master_template(self):
        return self.templates.get(MASTER_TEMPLATE_NAME, '')

    @cached_property
    def environment(self):
        return self.templates.get(ENVIRONMENT_NAME, '')

    @cached_property
    def provider_resource_templates(self):
        template_dict = dict(self.templates)
        del template_dict[MASTER_TEMPLATE_NAME]
        del template_dict[ENVIRONMENT_NAME]
        return template_dict

    def parameter_list(self, include_key_parameters=True):
        params = self.parameters
        if not include_key_parameters:
            key_params = []
            for role in self.role_list:
                key_params.extend([role.node_count_parameter_name,
                                   role.image_parameter_name,
                                   role.flavor_parameter_name])
            params = [p for p in params if p['name'] not in key_params]
        return [Parameter(p, plan=self) for p in params]

    def parameter(self, param_name):
        for parameter in self.parameters:
            if parameter['name'] == param_name:
                return Parameter(parameter, plan=self)

    def parameter_value(self, param_name, default=None):
        parameter = self.parameter(param_name)
        if parameter is not None:
            return parameter.value
        return default

    def list_generated_parameters(self, with_prefix=True):
        if with_prefix:
            key_format = lambda key: key
        else:
            key_format = strip_prefix

        # Get all password like parameters
        return dict(
            (key_format(parameter['name']), parameter)
            for parameter in self.parameter_list()
            if any([
                _should_generate_password(parameter),
                _should_generate_keystone_cert(parameter),
                _should_generate_neutron_control_plane(parameter),
            ])
        )

    def _make_keystone_certificates(self, wanted_generated_params):
        generated_params = {}
        for cert_param in KEYSTONE_CERTIFICATE_PARAMS:
            if cert_param in wanted_generated_params.keys():
                # If one of the keystone certificates is not set, we have
                # to generate all of them.
                generate_certificates = True
                break
        else:
            generate_certificates = False

        # Generate keystone certificates
        if generate_certificates:
            ca_key_pem, ca_cert_pem = keystone_pki.create_ca_pair()
            signing_key_pem, signing_cert_pem = (
                keystone_pki.create_signing_pair(ca_key_pem, ca_cert_pem))
            generated_params['KeystoneSigningCertificate'] = (
                signing_cert_pem)
            generated_params['KeystoneCACertificate'] = ca_cert_pem
            generated_params['KeystoneSigningKey'] = signing_key_pem
        return generated_params

    def make_generated_parameters(self):
        wanted_generated_params = self.list_generated_parameters(
            with_prefix=False)

        # Generate keystone certificates
        generated_params = self._make_keystone_certificates(
            wanted_generated_params)

        # Generate passwords and control plane id
        for (key, param) in wanted_generated_params.items():
            if _should_generate_password(param):
                generated_params[key] = password_generator()
            elif _should_generate_neutron_control_plane(param):
                generated_params[key] = neutron.network_list(
                    self._request, name='ctlplane')[0].id

        # Fill all the Tuskar parameters with generated content. There are
        # parameters that has just different prefix, such parameters should
        # have the same values.
        wanted_prefixed_params = self.list_generated_parameters(
            with_prefix=True)
        tuskar_params = {}

        for (key, param) in wanted_prefixed_params.items():
            tuskar_params[key] = generated_params[strip_prefix(key)]

        return tuskar_params

    @property
    def id(self):
        return self.uuid


class Role(base.APIResourceWrapper):
    _attrs = ('uuid', 'name', 'version', 'description', 'created')

    def __init__(self, apiresource, request=None):
        super(Role, self).__init__(apiresource)
        self._request = request

    @classmethod
    @memoized.memoized
    @handle_errors(_("Unable to retrieve overcloud roles"), [])
    def list(cls, request):
        """Return a list of Overcloud Roles in Tuskar

        :param request: request object
        :type  request: django.http.HttpRequest

        :return: list of Overcloud Roles, or an empty list if there
                 are none
        :rtype:  list of tuskar_ui.api.tuskar.Role
        """
        roles = tuskarclient(request).roles.list()
        return [cls(role, request=request) for role in roles]

    @classmethod
    @memoized.memoized
    @handle_errors(_("Unable to retrieve overcloud role"))
    def get(cls, request, role_id):
        """Return the Tuskar Role that matches the ID

        :param request: request object
        :type  request: django.http.HttpRequest

        :param role_id: ID of Role to be retrieved
        :type  role_id: int

        :return: matching Role, or None if no matching
                 Role can be found
        :rtype:  tuskar_ui.api.tuskar.Role
        """
        for role in Role.list(request):
            if role.uuid == role_id:
                return role

    @classmethod
    @memoized.memoized
    def _roles_by_image(cls, request, plan):
        roles_by_image = {}

        for role in Role.list(request):
            image = plan.parameter_value(role.image_parameter_name)
            if image in roles_by_image:
                roles_by_image[image].append(role)
            else:
                roles_by_image[image] = [role]

        return roles_by_image

    @classmethod
    @handle_errors(_("Unable to retrieve overcloud role"))
    def get_by_image(cls, request, plan, image):
        """Return the Role whose ImageID parameter matches the image.

        :param request: request object
        :type  request: django.http.HttpRequest

        :param plan: associated plan to check against
        :type  plan: Plan

        :param image: image to be matched
        :type  image: Image

        :return: matching Role, or None if no matching
                 Role can be found
        :rtype:  tuskar_ui.api.tuskar.Role
        """
        roles = cls._roles_by_image(request, plan)
        try:
            return roles[image.name]
        except KeyError:
            return []

    @classmethod
    @memoized.memoized
    def _roles_by_resource_type(cls, request):
        return {role.provider_resource_type: role
                for role in Role.list(request)}

    @classmethod
    @handle_errors(_("Unable to retrieve overcloud role"))
    def get_by_resource_type(cls, request, resource_type):
        roles = cls._roles_by_resource_type(request)
        try:
            return roles[resource_type]
        except KeyError:
            return None

    @property
    def provider_resource_type(self):
        return "Tuskar::{0}-{1}".format(self.name, self.version)

    @property
    def parameter_prefix(self):
        return "{0}-{1}::".format(self.name, self.version)

    @property
    def node_count_parameter_name(self):
        return self.parameter_prefix + 'count'

    @property
    def image_parameter_name(self):
        return self.parameter_prefix + 'Image'

    @property
    def flavor_parameter_name(self):
        return self.parameter_prefix + 'Flavor'

    def image(self, plan):
        image_name = plan.parameter_value(self.image_parameter_name)
        if image_name:
            try:
                return glance.image_list_detailed(
                    self._request, filters={'name': image_name})[0][0]
            except (glance_exceptions.HTTPNotFound, IndexError):
                LOG.error("Couldn't obtain image with name %s" % image_name)
                return None

    def flavor(self, plan):
        flavor_name = plan.parameter_value(
            self.flavor_parameter_name)
        if flavor_name:
            return flavor.Flavor.get_by_name(self._request, flavor_name)

    def parameter_list(self, plan):
        return [p for p in plan.parameter_list() if self == p.role]

    def is_valid_for_deployment(self, plan):
        node_count = plan.get_role_node_count(self)
        pending_required_params = list(Parameter.pending_parameters(
            Parameter.required_parameters(self.parameter_list(plan))))
        return not (
            self.image(plan) is None or
            (node_count and self.flavor(plan) is None) or
            pending_required_params
        )

    @property
    def id(self):
        return self.uuid


class Parameter(base.APIDictWrapper):

    _attrs = ['name', 'value', 'default', 'description', 'hidden', 'label',
              'parameter_type', 'constraints']

    def __init__(self, apidict, plan=None):
        super(Parameter, self).__init__(apidict)
        self._plan = plan

    @property
    def stripped_name(self):
        return strip_prefix(self.name)

    @property
    def plan(self):
        return self._plan

    @property
    def role(self):
        if self.plan:
            for role in self.plan.role_list:
                if self.name.startswith(role.parameter_prefix):
                    return role

    def is_required(self):
        """Boolean: True if parameter is required, False otherwise."""
        return self.default is None

    def get_constraint_by_type(self, constraint_type):
        """Returns parameter constraint by it's type.

        For available constraint types see HOT Spec:
        http://docs.openstack.org/developer/heat/template_guide/hot_spec.html
        """

        constraints_of_type = [c for c in self.constraints
                               if c['constraint_type'] == constraint_type]
        if constraints_of_type:
            return constraints_of_type[0]
        else:
            return None

    @staticmethod
    def required_parameters(parameters):
        """Yields parameters which are required."""
        for parameter in parameters:
            if parameter.is_required():
                yield parameter

    @staticmethod
    def pending_parameters(parameters):
        """Yields parameters which don't have value set."""
        for parameter in parameters:
            if not parameter.value:
                yield parameter

    @staticmethod
    def global_parameters(parameters):
        """Yields parameters with name without role prefix."""
        for parameter in parameters:
            if '::' not in parameter.name:
                yield parameter
