# Copyright 2015 HP Software, LLC
# 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 uuid

from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.decorators.debug import sensitive_variables  # noqa

from horizon import exceptions
from horizon import forms
from horizon import messages
from horizon.utils import memoized
from openstack_dashboard import api
from oslo_log import log as logging

from openstack_dashboard.dashboards.project.instances \
    import utils as instance_utils
from trove_dashboard import api as trove_api
from trove_dashboard.content.database_clusters \
    import cluster_manager
from trove_dashboard.content.databases import db_capability
from trove_dashboard.content.databases.workflows \
    import create_instance
from trove_dashboard.utils import common as common_utils

LOG = logging.getLogger(__name__)


class LaunchForm(forms.SelfHandlingForm):
    name = forms.CharField(label=_("Cluster Name"),
                           max_length=80)
    datastore = forms.ChoiceField(
        label=_("Datastore"),
        help_text=_("Type and version of datastore."),
        widget=forms.Select(attrs={
            'class': 'switchable',
            'data-slug': 'datastore'
        }))
    network = forms.ChoiceField(
        label=_("Network"),
        help_text=_("Network attached to instance."),
        required=False)
    volume = forms.IntegerField(
        label=_("Volume Size"),
        min_value=0,
        initial=1,
        help_text=_("Size of the volume in GB."))
    locality = forms.ChoiceField(
        label=_("Locality"),
        choices=[("", "None"),
                 ("affinity", "affinity"),
                 ("anti-affinity", "anti-affinity")],
        required=False,
        help_text=_("Specify whether instances in the cluster will "
                    "be created on the same hypervisor (affinity) or on "
                    "different hypervisors (anti-affinity)."))
    root_password = forms.CharField(
        label=_("Root Password"),
        required=False,
        help_text=_("Password for root user."),
        widget=forms.PasswordInput(attrs={
            'class': 'switched',
            'data-switch-on': 'datastore',
        }))
    num_instances_vertica = forms.IntegerField(
        label=_("Number of Instances"),
        min_value=3,
        initial=3,
        required=False,
        help_text=_("Number of instances in the cluster. (Read only)"),
        widget=forms.TextInput(attrs={
            'readonly': 'readonly',
            'class': 'switched',
            'data-switch-on': 'datastore',
        }))
    num_shards = forms.IntegerField(
        label=_("Number of Shards"),
        min_value=1,
        initial=1,
        required=False,
        help_text=_("Number of shards. (Read only)"),
        widget=forms.TextInput(attrs={
            'readonly': 'readonly',
            'class': 'switched',
            'data-switch-on': 'datastore',
        }))
    num_instances = forms.IntegerField(
        label=_("Number of Instances"),
        initial=3,
        required=False,
        help_text=_("Number of instances in the cluster."),
        widget=forms.TextInput(attrs={
            'class': 'switched',
            'data-switch-on': 'datastore',
        }))

    # (name of field variable, label)
    default_fields = [
        ('num_instances', _('Number of Instances'))
    ]
    mongodb_fields = default_fields + [
        ('num_shards', _('Number of Shards')),
    ]
    vertica_fields = [
        ('num_instances_vertica', ('Number of Instances')),
        ('root_password', _('Root Password')),
    ]

    def __init__(self, request, *args, **kwargs):
        super(LaunchForm, self).__init__(request, *args, **kwargs)

        self.fields['datastore'].choices = self.populate_datastore_choices(
            request)
        self.fields['network'].choices = self.populate_network_choices(
            request)

    def clean(self):
        datastore_field_value = self.data.get("datastore", None)
        if datastore_field_value:
            datastore, datastore_version = (
                create_instance.parse_datastore_and_version_text(
                    common_utils.unhexlify(datastore_field_value)))

            flavor_field_name = self._build_widget_field_name(
                datastore, datastore_version)
            if not self.data.get(flavor_field_name, None):
                msg = _("The flavor must be specified.")
                self._errors[flavor_field_name] = self.error_class([msg])

            if db_capability.is_vertica_datastore(datastore):
                if not self.data.get("root_password", None):
                    msg = _("Password for root user must be specified.")
                    self._errors["root_password"] = self.error_class([msg])
            else:
                if int(self.data.get("num_instances", 0)) < 1:
                    msg = _("The number of instances must be greater than 1.")
                    self._errors["num_instances"] = self.error_class([msg])

                if db_capability.is_mongodb_datastore(datastore):
                    if int(self.data.get("num_shards", 0)) < 1:
                        msg = _("The number of shards must be greater than 1.")
                        self._errors["num_shards"] = self.error_class([msg])

        if not self.data.get("locality", None):
            self.cleaned_data["locality"] = None

        return self.cleaned_data

    @memoized.memoized_method
    def datastore_flavors(self, request, datastore_name, datastore_version):
        try:
            return trove_api.trove.datastore_flavors(
                request, datastore_name, datastore_version)
        except Exception:
            LOG.exception("Exception while obtaining flavors list")
            self._flavors = []
            redirect = reverse('horizon:project:database_clusters:index')
            exceptions.handle(request,
                              _('Unable to obtain flavors.'),
                              redirect=redirect)

    @memoized.memoized_method
    def populate_network_choices(self, request):
        network_list = []
        try:
            if api.base.is_service_enabled(request, 'network'):
                tenant_id = self.request.user.tenant_id
                networks = api.neutron.network_list_for_tenant(request,
                                                               tenant_id)
                network_list = [(network.id, network.name_or_id)
                                for network in networks]
            else:
                self.fields['network'].widget = forms.HiddenInput()
        except exceptions.ServiceCatalogException:
            network_list = []
            redirect = reverse('horizon:project:database_clusters:index')
            exceptions.handle(request,
                              _('Unable to retrieve networks.'),
                              redirect=redirect)
        return network_list

    @memoized.memoized_method
    def datastores(self, request):
        try:
            return trove_api.trove.datastore_list(request)
        except Exception:
            LOG.exception("Exception while obtaining datastores list")
            self._datastores = []
            redirect = reverse('horizon:project:database_clusters:index')
            exceptions.handle(request,
                              _('Unable to obtain datastores.'),
                              redirect=redirect)

    def filter_cluster_datastores(self, request):
        datastores = []
        for ds in self.datastores(request):
            # TODO(michayu): until capabilities lands
            if db_capability.is_cluster_capable_datastore(ds.name):
                datastores.append(ds)
        return datastores

    @memoized.memoized_method
    def datastore_versions(self, request, datastore):
        try:
            return trove_api.trove.datastore_version_list(request, datastore)
        except Exception:
            LOG.exception("Exception while obtaining datastore version list")
            self._datastore_versions = []
            redirect = reverse('horizon:project:database_clusters:index')
            exceptions.handle(request,
                              _('Unable to obtain datastore versions.'),
                              redirect=redirect)

    def populate_datastore_choices(self, request):
        choices = ()
        datastores = self.filter_cluster_datastores(request)
        if datastores is not None:
            datastore_flavor_fields = {}
            for ds in datastores:
                versions = self.datastore_versions(request, ds.name)
                if versions:
                    # only add to choices if datastore has at least one version
                    version_choices = ()
                    for v in versions:
                        # NOTE(zhaochao): troveclient API resources are lazy
                        # loading objects. When an attribute is not found, the
                        # get() method of the Manager object will be called
                        # with the ID of the resource. However for
                        # datastore_versions, the get() method is expecting two
                        # arguments: datastore and datastore_version(name), so
                        # TypeError will be raised as not enough arguments are
                        # passed. In Python 2.x, hasattr() won't reraise the
                        # exception(which is not correct), but reraise under
                        # Python 3(which should be correct).
                        # Use v.to_dict() to verify the 'active' info instead.
                        if not v.to_dict().get('active', True):
                            continue
                        selection_text = self._build_datastore_display_text(
                            ds.name, v.name)
                        widget_text = self._build_widget_field_name(
                            ds.name, v.name)
                        version_choices = (version_choices +
                                           ((widget_text, selection_text),))
                        k, v = self._add_datastore_flavor_field(request,
                                                                ds.name,
                                                                v.name)
                        datastore_flavor_fields[k] = v
                        self._add_attr_to_optional_fields(ds.name,
                                                          widget_text)

                    choices = choices + version_choices
            self._insert_datastore_version_fields(datastore_flavor_fields)
        return choices

    def _add_datastore_flavor_field(self,
                                    request,
                                    datastore,
                                    datastore_version):
        name = self._build_widget_field_name(datastore, datastore_version)
        attr_key = 'data-datastore-' + name
        field = forms.ChoiceField(
            label=_("Flavor"),
            help_text=_("Size of image to launch."),
            required=False,
            widget=forms.Select(attrs={
                'class': 'switched',
                'data-switch-on': 'datastore',
                attr_key: _("Flavor")
            }))
        valid_flavors = self.datastore_flavors(request,
                                               datastore,
                                               datastore_version)
        if valid_flavors:
            field.choices = instance_utils.sort_flavor_list(
                request, valid_flavors)

        return name, field

    def _build_datastore_display_text(self, datastore, datastore_version):
        return datastore + ' - ' + datastore_version

    def _build_widget_field_name(self, datastore, datastore_version):
        # Since the fieldnames cannot contain an uppercase character
        # we generate a hex encoded string representation of the
        # datastore and version as the fieldname
        return common_utils.hexlify(
            self._build_datastore_display_text(datastore, datastore_version))

    def _insert_datastore_version_fields(self, datastore_flavor_fields):
        fields_to_restore_at_the_end = collections.OrderedDict()
        while True:
            k, v = self.fields.popitem()
            if k == 'datastore':
                self.fields[k] = v
                break
            else:
                fields_to_restore_at_the_end[k] = v

        for k, v in datastore_flavor_fields.items():
            self.fields[k] = v

        for k in reversed(list(fields_to_restore_at_the_end.keys())):
            self.fields[k] = fields_to_restore_at_the_end[k]

    def _add_attr_to_optional_fields(self, datastore, selection_text):
        if db_capability.is_mongodb_datastore(datastore):
            fields = self.mongodb_fields
        elif db_capability.is_vertica_datastore(datastore):
            fields = self.vertica_fields
        else:
            fields = self.default_fields

        for field in fields:
            attr_key = 'data-datastore-' + selection_text
            widget = self.fields[field[0]].widget
            if attr_key not in widget.attrs:
                widget.attrs[attr_key] = field[1]

    def _get_locality(self, data):
        locality = None
        if data.get('locality'):
            locality = data['locality']
        return locality

    @sensitive_variables('data')
    def handle(self, request, data):
        try:
            datastore, datastore_version = (
                create_instance.parse_datastore_and_version_text(
                    common_utils.unhexlify(data['datastore'])))

            flavor_field_name = self._build_widget_field_name(
                datastore, datastore_version)
            flavor = data[flavor_field_name]
            num_instances = data['num_instances']
            root_password = None
            if db_capability.is_vertica_datastore(datastore):
                root_password = data['root_password']
                num_instances = data['num_instances_vertica']
            LOG.info("Launching cluster with parameters "
                     "{name=%s, volume=%s, flavor=%s, "
                     "datastore=%s, datastore_version=%s",
                     "locality=%s",
                     data['name'], data['volume'], flavor,
                     datastore, datastore_version, self._get_locality(data))

            trove_api.trove.cluster_create(request,
                                           data['name'],
                                           data['volume'],
                                           flavor,
                                           num_instances,
                                           datastore=datastore,
                                           datastore_version=datastore_version,
                                           nics=data['network'],
                                           root_password=root_password,
                                           locality=self._get_locality(data))
            messages.success(request,
                             _('Launched cluster "%s"') % data['name'])
            return True
        except Exception:
            redirect = reverse("horizon:project:database_clusters:index")
            exceptions.handle(request,
                              _('Unable to launch cluster.'),
                              redirect=redirect)


class ClusterAddInstanceForm(forms.SelfHandlingForm):
    cluster_id = forms.CharField(
        required=False,
        widget=forms.HiddenInput())
    flavor = forms.ChoiceField(
        label=_("Flavor"),
        help_text=_("Size of image to launch."))
    volume = forms.IntegerField(
        label=_("Volume Size"),
        min_value=0,
        initial=1,
        help_text=_("Size of the volume in GB."))
    name = forms.CharField(
        label=_("Name"),
        required=False,
        help_text=_("Optional name of the instance."))
    type = forms.CharField(
        label=_("Instance Type"),
        required=False,
        help_text=_("Optional datastore specific type of the instance."))
    related_to = forms.CharField(
        label=_("Related To"),
        required=False,
        help_text=_("Optional datastore specific value that defines the "
                    "relationship from one instance in the cluster to "
                    "another."))
    network = forms.ChoiceField(
        label=_("Network"),
        help_text=_("Network attached to instance."),
        required=False)

    def __init__(self, request, *args, **kwargs):
        super(ClusterAddInstanceForm, self).__init__(request, *args, **kwargs)
        self.fields['cluster_id'].initial = kwargs['initial']['cluster_id']
        self.fields['flavor'].choices = self.populate_flavor_choices(request)
        self.fields['network'].choices = self.populate_network_choices(
            request)

    @memoized.memoized_method
    def flavors(self, request):
        try:
            datastore = None
            datastore_version = None
            datastore_dict = self.initial.get('datastore', None)
            if datastore_dict:
                datastore = datastore_dict.get('type', None)
                datastore_version = datastore_dict.get('version', None)
            return trove_api.trove.datastore_flavors(
                request,
                datastore_name=datastore,
                datastore_version=datastore_version)
        except Exception:
            LOG.exception("Exception while obtaining flavors list")
            self._flavors = []
            redirect = reverse('horizon:project:database_clusters:index')
            exceptions.handle(request,
                              _('Unable to obtain flavors.'),
                              redirect=redirect)

    def populate_flavor_choices(self, request):
        flavor_list = [(f.id, "%s" % f.name) for f in self.flavors(request)]
        return sorted(flavor_list)

    @memoized.memoized_method
    def populate_network_choices(self, request):
        network_list = []
        try:
            if api.base.is_service_enabled(request, 'network'):
                tenant_id = self.request.user.tenant_id
                networks = api.neutron.network_list_for_tenant(request,
                                                               tenant_id)
                network_list = [(network.id, network.name_or_id)
                                for network in networks]
            else:
                self.fields['network'].widget = forms.HiddenInput()
        except exceptions.ServiceCatalogException:
            network_list = []
            redirect = reverse('horizon:project:database_clusters:index')
            exceptions.handle(request,
                              _('Unable to retrieve networks.'),
                              redirect=redirect)
        return network_list

    def handle(self, request, data):
        try:
            flavor = trove_api.trove.flavor_get(request, data['flavor'])
            manager = cluster_manager.get(data['cluster_id'])
            manager.add_instance(str(uuid.uuid4()),
                                 data.get('name', None),
                                 data['flavor'],
                                 flavor.name,
                                 data['volume'],
                                 data.get('type', None),
                                 data.get('related_to', None),
                                 data.get('network', None))
        except Exception:
            redirect = reverse("horizon:project:database_clusters:index")
            exceptions.handle(request,
                              _('Unable to grow cluster.'),
                              redirect=redirect)
        return True


class ResetPasswordForm(forms.SelfHandlingForm):
    cluster_id = forms.CharField(widget=forms.HiddenInput())
    password = forms.CharField(widget=forms.PasswordInput(),
                               label=_("New Password"),
                               help_text=_("New password for cluster access."))

    @sensitive_variables('data')
    def handle(self, request, data):
        password = data.get("password")
        cluster_id = data.get("cluster_id")
        try:
            trove_api.trove.create_cluster_root(request,
                                                cluster_id,
                                                password)
            messages.success(request, _('Root password updated for '
                                        'cluster "%s"') % cluster_id)
        except Exception:
            redirect = reverse("horizon:project:database_clusters:index")
            exceptions.handle(request, _('Unable to reset password.'),
                              redirect=redirect)
        return True
