# -*- coding: utf8 -*-
#
#    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 six
import uuid

from django.conf import settings
import django.forms
from django.utils.translation import ugettext_lazy as _
import horizon.exceptions
import horizon.forms
import horizon.messages
from os_cloud_config import keystone as keystone_config
from os_cloud_config.utils import clients

from tuskar_ui import api
import tuskar_ui.api.heat
import tuskar_ui.api.tuskar
import tuskar_ui.forms
import tuskar_ui.infrastructure.flavors.utils as flavors_utils
import tuskar_ui.utils.utils as tuskar_utils

MATCHING_DEPLOYMENT_MODE = flavors_utils.matching_deployment_mode()
LOG = logging.getLogger(__name__)
MESSAGE_ICONS = {
    'ok': 'fa-check-square-o text-success',
    'pending': 'fa-square-o text-info',
    'error': 'fa-exclamation-circle text-danger',
    'warning': 'fa-exclamation-triangle text-warning',
    None: 'fa-exclamation-triangle text-warning',
}
WEBROOT = getattr(settings, 'WEBROOT', '/')


def validate_roles(request, plan):
    """Validates the roles in plan and returns dict describing the issues"""
    for role in plan.role_list:
        if (
            plan.get_role_node_count(role) and
            not role.is_valid_for_deployment(plan)
        ):
            message = {
                'text': _(u"Configure Roles."),
                'is_critical': True,
                'status': 'pending',
            }
            break
    else:
        message = {
            'text': _(u"Configure Roles."),
            'status': 'ok',
        }
    return message


def validate_global_parameters(request, plan):
    pending_required_global_params = list(
        api.tuskar.Parameter.pending_parameters(
            api.tuskar.Parameter.required_parameters(
                api.tuskar.Parameter.global_parameters(
                    plan.parameter_list()))))
    if pending_required_global_params:
        message = {
            'text': _(u"Global Service Configuration."),
            'is_critical': True,
            'status': 'pending',
        }
    else:
        message = {
            'text': _(u"Global Service Configuration."),
            'status': 'ok',
        }
    return message


def validate_plan(request, plan):
    """Validates the plan and returns a list of dicts describing the issues."""
    messages = []
    requested_nodes = 0
    for role in plan.role_list:
        node_count = plan.get_role_node_count(role)
        requested_nodes += node_count
    available_flavors = len(api.flavor.Flavor.list(request))
    if available_flavors == 0:
        messages.append({
            'text': _(u"Define Flavors."),
            'is_critical': True,
            'status': 'pending',
        })
    else:
        messages.append({
            'text': _(u"Define Flavors."),
            'status': 'ok',
        })
    available_nodes = len(api.node.Node.list(request, associated=False,
                                             maintenance=False))
    if available_nodes == 0:
        messages.append({
            'text': _(u"Register Nodes."),
            'is_critical': True,
            'status': 'pending',
        })
    elif requested_nodes > available_nodes:
        messages.append({
            'text': _(u"Not enough registered nodes for this plan. "
                      u"You need {0} more.").format(
                          requested_nodes - available_nodes),
            'is_critical': True,
            'status': 'error',
        })
    else:
        messages.append({
            'text': _(u"Register Nodes."),
            'status': 'ok',
        })
    messages.append(validate_roles(request, plan))
    messages.append(validate_global_parameters(request, plan))
    if not MATCHING_DEPLOYMENT_MODE:
        # All roles have to have the same flavor.
        default_flavor_name = api.flavor.Flavor.list(request)[0].name
        for role in plan.role_list:
            if role.flavor(plan).name != default_flavor_name:
                messages.append({
                    'text': _(u"Role {0} doesn't use default flavor.").format(
                        role.name,
                    ),
                    'is_critical': False,
                    'statis': 'error',
                })
    roles_assigned = True
    messages.append({
        'text': _(u"Assign roles."),
        'status': lambda: 'ok' if roles_assigned else 'pending',
    })
    try:
        controller_role = plan.get_role_by_name("Controller")
    except KeyError:
        messages.append({
            'text': _(u"Controller Role Needed."),
            'is_critical': True,
            'status': 'error',
            'indent': 1,
        })
        roles_assigned = False
    else:
        if plan.get_role_node_count(controller_role) not in (1, 3):
            messages.append({
                'text': _(u"1 or 3 Controllers Needed."),
                'is_critical': True,
                'status': 'pending',
                'indent': 1,
            })
            roles_assigned = False
        else:
            messages.append({
                'text': _(u"1 or 3 Controllers Needed."),
                'status': 'ok',
                'indent': 1,
            })

    try:
        compute_role = plan.get_role_by_name("Compute")
    except KeyError:
        messages.append({
            'text': _(u"Compute Role Needed."),
            'is_critical': True,
            'status': 'error',
            'indent': 1,
        })
        roles_assigned = False
    else:
        if plan.get_role_node_count(compute_role) < 1:
            messages.append({
                'text': _(u"1 Compute Needed."),
                'is_critical': True,
                'status': 'pending',
                'indent': 1,
            })
            roles_assigned = False
        else:
            messages.append({
                'text': _(u"1 Compute Needed."),
                'status': 'ok',
                'indent': 1,
            })
    for message in messages:
        status = message.get('status')
        if callable(status):
            message['status'] = status = status()
        message['classes'] = MESSAGE_ICONS.get(status, MESSAGE_ICONS[None])
    return messages


class EditPlan(horizon.forms.SelfHandlingForm):
    def __init__(self, *args, **kwargs):
        super(EditPlan, self).__init__(*args, **kwargs)
        self.plan = api.tuskar.Plan.get_the_plan(self.request)
        self.fields.update(self._role_count_fields(self.plan))

    def _role_count_fields(self, plan):
        fields = {}
        for role in plan.role_list:
            field = django.forms.IntegerField(
                label=role.name,
                widget=tuskar_ui.forms.NumberPickerInput(attrs={
                    'min': 1 if role.name in ('Controller', 'Compute') else 0,
                    'step': 2 if role.name == 'Controller' else 1,
                }),
                initial=plan.get_role_node_count(role),
                required=False
            )
            field.role = role
            fields['%s-count' % role.id] = field
        return fields

    def handle(self, request, data):
        parameters = dict(
            (field.role.node_count_parameter_name, data[name])
            for (name, field) in self.fields.items() if name.endswith('-count')
        )
        # NOTE(gfidente): this is a bad hack meant to magically add the
        # parameter which enables Neutron L3 HA when the number of
        # Controllers is > 1
        try:
            controller_role = self.plan.get_role_by_name('Controller')
            compute_role = self.plan.get_role_by_name('Compute')
        except Exception as e:
            LOG.warning('Unable to find a required role: %s', e.message)
        else:
            number_controllers = parameters[
                controller_role.node_count_parameter_name]
            if number_controllers > 1:
                for role in [controller_role, compute_role]:
                    l3ha_param = role.parameter_prefix + 'NeutronL3HA'
                    parameters[l3ha_param] = 'True'
                    l3agent_param = (role.parameter_prefix +
                                     'NeutronAllowL3AgentFailover')
                    parameters[l3agent_param] = 'True'
            dhcp_agents_per_net = (number_controllers if number_controllers and
                                   number_controllers > 3 else 3)
            dhcp_agents_param = (controller_role.parameter_prefix +
                                 'NeutronDhcpAgentsPerNetwork')
            parameters[dhcp_agents_param] = dhcp_agents_per_net

            try:
                ceph_storage_role = self.plan.get_role_by_name('Ceph-Storage')
            except Exception as e:
                LOG.warning('Unable to find role: %s', 'Ceph-Storage')
            else:
                if parameters[ceph_storage_role.node_count_parameter_name] > 0:
                    parameters.update({
                        'CephClusterFSID': six.text_type(uuid.uuid4()),
                        'CephMonKey': tuskar_utils.create_cephx_key(),
                        'CephAdminKey': tuskar_utils.create_cephx_key()
                    })

                    cinder_enable_rbd_param = (controller_role.parameter_prefix
                                               + 'CinderEnableRbdBackend')
                    glance_backend_param = (controller_role.parameter_prefix +
                                            'GlanceBackend')
                    nova_enable_rbd_param = (compute_role.parameter_prefix +
                                             'NovaEnableRbdBackend')
                    cinder_enable_iscsi_param = (
                        controller_role.parameter_prefix +
                        'CinderEnableIscsiBackend')

                    parameters.update({
                        cinder_enable_rbd_param: True,
                        glance_backend_param: 'rbd',
                        nova_enable_rbd_param: True,
                        cinder_enable_iscsi_param: False
                    })

        try:
            self.plan = self.plan.patch(request, self.plan.uuid, parameters)
        except Exception as e:
            horizon.exceptions.handle(request, _("Unable to update the plan."))
            LOG.exception(e)
            return False
        return True


class ScaleOut(EditPlan):
    def __init__(self, *args, **kwargs):
        super(ScaleOut, self).__init__(*args, **kwargs)
        for name, field in self.fields.items():
            if name.endswith('-count'):
                field.widget.attrs['min'] = field.initial

    def handle(self, request, data):
        if not super(ScaleOut, self).handle(request, data):
            return False
        plan = self.plan
        try:
            stack = api.heat.Stack.get_by_plan(self.request, plan)
            stack.update(request, plan.name, plan.templates)
        except Exception as e:
            LOG.exception(e)
            if hasattr(e, 'error'):
                horizon.exceptions.handle(
                    request,
                    _(
                        "Unable to deploy overcloud. Reason: {0}"
                    ).format(e.error['error']['message']),
                )
                return False
            else:
                raise
        else:
            msg = _('Deployment in progress.')
            horizon.messages.success(request, msg)
            return True


class DeployOvercloud(horizon.forms.SelfHandlingForm):
    network_isolation = horizon.forms.BooleanField(
        label=_("Enable Network Isolation"),
        required=False)

    def handle(self, request, data):
        try:
            plan = api.tuskar.Plan.get_the_plan(request)
        except Exception as e:
            LOG.exception(e)
            horizon.exceptions.handle(request,
                                      _("Unable to deploy overcloud."))
            return False

        # If network isolation selected, read environment file data
        # and add to plan
        env_temp = '/usr/share/openstack-tripleo-heat-templates/environments'
        try:
            if self.cleaned_data['network_isolation']:
                with open(env_temp, 'r') as env_file:
                    env_contents = ''.join(
                        [line for line in
                         env_file.readlines() if '#' not in line]
                    )
                    plan.environment += env_contents
        except Exception as e:
            LOG.exception(e)
            pass

        # Auto-generate missing passwords and certificates
        if plan.list_generated_parameters():
            generated_params = plan.make_generated_parameters()
            plan = plan.patch(request, plan.uuid, generated_params)

        # Validate plan and create stack
        for message in validate_plan(request, plan):
            if message.get('is_critical'):
                horizon.messages.success(request, message.text)
                return False
        try:
            stack = api.heat.Stack.get_by_plan(self.request, plan)
            if not stack:
                api.heat.Stack.create(request, plan.name, plan.templates)
        except Exception as e:
            LOG.exception(e)
            horizon.exceptions.handle(
                request, _("Unable to deploy overcloud. Reason: {0}").format(
                    e.error['error']['message']))
            return False
        else:
            msg = _('Deployment in progress.')
            horizon.messages.success(request, msg)
            return True


class UndeployOvercloud(horizon.forms.SelfHandlingForm):
    def handle(self, request, data):
        try:
            plan = api.tuskar.Plan.get_the_plan(request)
            stack = api.heat.Stack.get_by_plan(self.request, plan)
            if stack:
                api.heat.Stack.delete(request, stack.id)
        except Exception as e:
            LOG.exception(e)
            horizon.exceptions.handle(request,
                                      _("Unable to undeploy overcloud."))
            return False
        else:
            msg = _('Undeployment in progress.')
            horizon.messages.success(request, msg)
            return True


class PostDeployInit(horizon.forms.SelfHandlingForm):
    admin_email = horizon.forms.CharField(label=_("Admin Email"))
    public_host = horizon.forms.CharField(
        label=_("Public Host"), initial="", required=False)
    region = horizon.forms.CharField(
        label=_("Region"), initial="regionOne")

    def build_endpoints(self, plan, controller_role):
        return {
            "ceilometer": {
                "password": plan.parameter_value(
                    controller_role.parameter_prefix + 'CeilometerPassword')},
            "cinder": {
                "password": plan.parameter_value(
                    controller_role.parameter_prefix + 'CinderPassword')},
            "cinderv2": {
                "password": plan.parameter_value(
                    controller_role.parameter_prefix + 'CinderPassword')},
            "ec2": {
                "password": plan.parameter_value(
                    controller_role.parameter_prefix + 'GlancePassword')},
            "glance": {
                "password": plan.parameter_value(
                    controller_role.parameter_prefix + 'GlancePassword')},
            "heat": {
                "password": plan.parameter_value(
                    controller_role.parameter_prefix + 'HeatPassword')},
            "neutron": {
                "password": plan.parameter_value(
                    controller_role.parameter_prefix + 'NeutronPassword')},
            "nova": {
                "password": plan.parameter_value(
                    controller_role.parameter_prefix + 'NovaPassword')},
            "novav3": {
                "password": plan.parameter_value(
                    controller_role.parameter_prefix + 'NovaPassword')},
            "swift": {
                "password": plan.parameter_value(
                    controller_role.parameter_prefix + 'SwiftPassword'),
                'path': '/v1/AUTH_%(tenant_id)s',
                'admin_path': '/v1'},
            "horizon": {
                'port': '80',
                'path': WEBROOT,
                'admin_path': '%sadmin' % WEBROOT}}

    def handle(self, request, data):
        try:
            plan = api.tuskar.Plan.get_the_plan(request)
            controller_role = plan.get_role_by_name("Controller")
            stack = api.heat.Stack.get_by_plan(self.request, plan)

            admin_token = plan.parameter_value(
                controller_role.parameter_prefix + 'AdminToken')
            admin_password = plan.parameter_value(
                controller_role.parameter_prefix + 'AdminPassword')
            admin_email = data['admin_email']
            auth_ip = stack.keystone_ip
            auth_url = stack.keystone_auth_url
            auth_tenant = 'admin'
            auth_user = 'admin'

            # do the keystone init
            keystone_config.initialize(
                auth_ip, admin_token, admin_email, admin_password,
                region='regionOne', ssl=None, public=None, user='heat-admin',
                pki_setup=False)

            # retrieve needed Overcloud clients
            keystone_client = clients.get_keystone_client(
                auth_user, admin_password, auth_tenant, auth_url)

            # do the setup endpoints
            keystone_config.setup_endpoints(
                self.build_endpoints(plan, controller_role),
                public_host=data['public_host'],
                region=data['region'],
                os_auth_url=auth_url,
                client=keystone_client)

        except Exception as e:
            LOG.exception(e)
            horizon.exceptions.handle(request,
                                      _("Unable to initialize Overcloud."))
            return False
        else:
            msg = _('Overcloud has been initialized.')
            horizon.messages.success(request, msg)
            return True
