# Copyright (c) 2014 NetApp, 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.

from django.conf import settings
from django.forms import ValidationError
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from horizon.utils.memoized import memoized

from manila_ui.api import manila
from manila_ui.dashboards import utils
from manila_ui import features
from manilaclient.common.apiclient import exceptions as m_exceptions
import time


class CreateForm(forms.SelfHandlingForm):
    name = forms.CharField(max_length="255", label=_("Share Name"))
    description = forms.CharField(
        label=_("Description"), required=False,
        widget=forms.Textarea(attrs={'rows': 3}))
    share_proto = forms.ChoiceField(label=_("Share Protocol"), required=True)
    size = forms.IntegerField(min_value=1, label=_("Size (GiB)"))
    share_type = forms.ChoiceField(
        label=_("Share Type"), required=True,
        widget=forms.Select(
            attrs={'class': 'switchable', 'data-slug': 'sharetype'}))
    availability_zone = forms.ChoiceField(
        label=_("Availability Zone"),
        required=False)

    def __init__(self, request, *args, **kwargs):
        super(CreateForm, self).__init__(request, *args, **kwargs)
        # NOTE(vkmc): choose only those share protocols that are enabled
        # FIXME(vkmc): this should be better implemented by having a
        # capabilities endpoint on the control plane.
        manila_features = getattr(settings, 'OPENSTACK_MANILA_FEATURES', {})
        self.enabled_share_protocols = manila_features.get(
            'enabled_share_protocols',
            ['NFS', 'CIFS', 'GlusterFS', 'HDFS', 'CephFS', 'MapRFS'])
        self.enable_public_shares = manila_features.get(
            'enable_public_shares', True)
        share_networks = manila.share_network_list(request)
        share_types = manila.share_type_list(request)
        self.fields['share_type'].choices = (
            [("", "")] +
            [(utils.transform_dashed_name(st.name), st.name)
             for st in share_types]
        )

        availability_zones = manila.availability_zone_list(request)
        self.fields['availability_zone'].choices = (
            [("", "")] + [(az.name, az.name) for az in availability_zones])

        if features.is_share_groups_enabled():
            share_groups = manila.share_group_list(request)
            self.fields['sg'] = forms.ChoiceField(
                label=_("Share Group"),
                choices=[("", "")] + [(sg.id, sg.name or sg.id)
                                      for sg in share_groups],
                required=False)

        self.sn_field_name_prefix = 'share-network-choices-'
        for st in share_types:
            extra_specs = st.get_keys()
            dhss = extra_specs.get('driver_handles_share_servers')
            # NOTE(vponomaryov): Set and tie share-network field only for
            # share types with enabled handling of share servers.
            if (isinstance(dhss, str) and dhss.lower() in ['true', '1']):
                sn_choices = (
                    [('', '')] +
                    [(sn.id, sn.name or sn.id) for sn in share_networks])
                sn_field_name = (
                    self.sn_field_name_prefix +
                    utils.transform_dashed_name(st.name)
                )
                sn_field = forms.ChoiceField(
                    label=_("Share Network"), required=True,
                    choices=sn_choices,
                    widget=forms.Select(attrs={
                        'class': 'switched',
                        'data-switch-on': 'sharetype',
                        'data-sharetype-%s' % utils.transform_dashed_name(
                            st.name): _("Share Network"),
                    }))
                self.fields[sn_field_name] = sn_field

        self.fields['share_source_type'] = forms.ChoiceField(
            label=_("Share Source"), required=False,
            widget=forms.Select(
                attrs={'class': 'switchable', 'data-slug': 'source'}))
        self.fields['snapshot'] = forms.ChoiceField(
            label=_("Use snapshot as a source"),
            widget=forms.fields.SelectWidget(
                attrs={'class': 'switched',
                       'data-switch-on': 'source',
                       'data-source-snapshot': _('Snapshot')},
                data_attrs=('size', 'name'),
                transform=lambda x: "%s (%sGiB)" % (x.name, x.size)),
            required=False)
        self.fields['metadata'] = forms.CharField(
            label=_("Metadata"), required=False,
            widget=forms.Textarea(attrs={'rows': 4}))

        if self.enable_public_shares:
            self.fields['is_public'] = forms.BooleanField(
                label=_("Make visible to users from all projects"),
                required=False)

        self.fields['share_proto'].choices = [(sp, sp) for sp in
                                              self.enabled_share_protocols]
        if ("snapshot_id" in request.GET or
                kwargs.get("data", {}).get("snapshot")):
            try:
                snapshot = self.get_snapshot(
                    request,
                    request.GET.get("snapshot_id",
                                    kwargs.get("data", {}).get("snapshot")))
                self.fields['name'].initial = snapshot.name
                self.fields['size'].initial = snapshot.size
                self.fields['snapshot'].choices = ((snapshot.id, snapshot),)
                try:
                    # Set the share type and az from the original share
                    orig_share = manila.share_get(request, snapshot.share_id)
                    self.fields['share_type'].initial = (
                        orig_share.share_type_name)
                    self.fields['availability_zone'].initial = (
                        orig_share.availability_zone)
                except Exception:
                    pass
                self.fields['size'].help_text = _(
                    'Share size must be equal to or greater than the snapshot '
                    'size (%sGiB)') % snapshot.size
                del self.fields['share_source_type']
            except Exception:
                exceptions.handle(request,
                                  _('Unable to load the specified snapshot.'))
        else:
            source_type_choices = []

            try:
                snapshot_list = manila.share_snapshot_list(request)
                snapshots = [s for s in snapshot_list
                             if s.status == 'available']
                if snapshots:
                    source_type_choices.append(("snapshot",
                                                _("Snapshot")))
                    choices = [('', _("Choose a snapshot"))] + \
                              [(s.id, s) for s in snapshots]
                    self.fields['snapshot'].choices = choices
                else:
                    del self.fields['snapshot']
            except Exception:
                exceptions.handle(request, _("Unable to retrieve "
                                             "share snapshots."))

            if source_type_choices:
                choices = ([('no_source_type',
                             _("No source, empty share"))] +
                           source_type_choices)
                self.fields['share_source_type'].choices = choices
            else:
                del self.fields['share_source_type']

    def clean(self):
        cleaned_data = super(CreateForm, self).clean()
        form_errors = list(self.errors)

        chosen_share_type = cleaned_data.get('share_type')
        if chosen_share_type:
            # NOTE(vponomaryov): skip errors for share-network fields that are
            # not related to chosen share type.
            for error in form_errors:
                st_name = error.split(self.sn_field_name_prefix)[-1]
                if (error.startswith(self.sn_field_name_prefix) and
                        st_name != chosen_share_type):
                    cleaned_data[error] = 'Not set'
                    self.errors.pop(error, None)

            cleaned_data['share_network'] = cleaned_data.get(
                self.sn_field_name_prefix + cleaned_data.get('share_type'))

        return cleaned_data

    def handle(self, request, data):
        try:
            snapshot_id = None
            source_type = data.get('share_source_type', None)
            share_network = data.get('share_network', None)
            if (data.get("snapshot", None) and
                    source_type in [None, 'snapshot']):
                # Create from Snapshot
                snapshot = self.get_snapshot(request,
                                             data["snapshot"])
                snapshot_id = snapshot.id
                if (data['size'] < snapshot.size):
                    error_message = _('The share size cannot be less than the '
                                      'snapshot size (%sGiB)') % snapshot.size
                    raise ValidationError(error_message)
            else:
                data['size'] = int(data['size'])

            metadata = {}
            try:
                set_dict, unset_list = utils.parse_str_meta(data['metadata'])
                if unset_list:
                    msg = _("Expected only pairs of key=value.")
                    raise ValidationError(message=msg)
                metadata = set_dict
            except ValidationError as e:
                self.api_error(e.messages[0])
                return False
            is_public = self.enable_public_shares and data['is_public']
            share = manila.share_create(
                request,
                size=data['size'],
                name=data['name'],
                description=data['description'],
                proto=data['share_proto'],
                share_network=share_network,
                snapshot_id=snapshot_id,
                share_type=utils.transform_dashed_name(data['share_type']),
                is_public=is_public,
                metadata=metadata,
                availability_zone=data.get('availability_zone') or None,
                share_group_id=data.get('sg') or None,
            )
            message = _('Creating share "%s"') % data['name']
            messages.success(request, message)
            return share
        except ValidationError as e:
            self.api_error(e.messages[0])
        except m_exceptions.BadRequest as e:
            self.api_error(_("Unable to create share. %s") % e.message)
        except Exception:
            exceptions.handle(request, ignore=True)
            self.api_error(_("Unable to create share."))
        return False

    @memoized
    def get_snapshot(self, request, id):
        return manila.share_snapshot_get(request, id)


class UpdateForm(forms.SelfHandlingForm):
    name = forms.CharField(max_length="255", label=_("Share Name"))
    description = forms.CharField(widget=forms.Textarea,
                                  label=_("Description"), required=False)
    is_public = forms.BooleanField(
        label=_("Make visible to users from all projects"),
        required=False)

    def __init__(self, request, *args, **kwargs):
        super(UpdateForm, self).__init__(request, *args, **kwargs)
        manila_features = getattr(settings, 'OPENSTACK_MANILA_FEATURES', {})
        self.enable_public_shares = manila_features.get(
            'enable_public_shares', True)
        if not self.enable_public_shares:
            self.fields.pop('is_public')

    def handle(self, request, data):
        share_id = self.initial['share_id']
        is_public = data['is_public'] if self.enable_public_shares else False
        try:
            share = manila.share_get(self.request, share_id)
            manila.share_update(
                request, share, data['name'], data['description'],
                is_public=is_public)
            message = _('Updating share "%s"') % data['name']
            messages.success(request, message)
            return True
        except Exception:
            redirect = reverse("horizon:project:shares:index")
            exceptions.handle(request,
                              _('Unable to update share.'),
                              redirect=redirect)


class UpdateMetadataForm(forms.SelfHandlingForm):
    metadata = forms.CharField(widget=forms.Textarea,
                               label=_("Metadata"), required=False)

    def __init__(self, *args, **kwargs):
        super(UpdateMetadataForm, self).__init__(*args, **kwargs)
        meta_str = ""
        for k, v in self.initial["metadata"].items():
            meta_str += "%s=%s\r\n" % (k, v)
        self.initial["metadata"] = meta_str

    def handle(self, request, data):
        share_id = self.initial['share_id']
        try:
            share = manila.share_get(self.request, share_id)
            set_dict, unset_list = utils.parse_str_meta(data['metadata'])
            if set_dict:
                manila.share_set_metadata(request, share, set_dict)
            if unset_list:
                manila.share_delete_metadata(request, share, unset_list)
            message = _('Updating share metadata "%s"') % share.name
            messages.success(request, message)
            return True
        except ValidationError as e:
            self.api_error(e.messages[0])
            return False
        except Exception:
            redirect = reverse("horizon:project:shares:index")
            exceptions.handle(request,
                              _('Unable to update share metadata.'),
                              redirect=redirect)


class AddRule(forms.SelfHandlingForm):
    access_type = forms.ChoiceField(
        label=_("Access Type"), required=True,
        choices=(('ip', 'ip'), ('user', 'user'), ('cert', 'cert'),
                 ('cephx', 'cephx')))
    access_level = forms.ChoiceField(
        label=_("Access Level"), required=True,
        choices=(('rw', 'read-write'), ('ro', 'read-only'),))
    access_to = forms.CharField(
        label=_("Access To"), max_length="255", required=True)
    metadata = forms.CharField(
        label=_("Metadata"), required=False,
        widget=forms.Textarea(attrs={'rows': 4}))

    def handle(self, request, data):
        share_id = self.initial['share_id']
        metadata = {}
        try:
            set_dict, unset_list = utils.parse_str_meta(data['metadata'])
            if unset_list:
                msg = _("Expected only pairs of key=value.")
                raise ValidationError(message=msg)
            metadata = set_dict
        except ValidationError as e:
            self.api_error(e.messages[0])
            return False
        try:
            manila.share_allow(
                request, share_id,
                access_to=data['access_to'],
                access_type=data['access_type'],
                access_level=data['access_level'],
                metadata=metadata)
            message = _('Creating rule for "%s"') % data['access_to']
            messages.success(request, message)
            return True
        except Exception:
            redirect = reverse("horizon:project:shares:manage_rules",
                               args=[self.initial['share_id']])
            exceptions.handle(
                request, _('Unable to add rule.'), redirect=redirect)


class UpdateRuleMetadataForm(forms.SelfHandlingForm):
    metadata = forms.CharField(widget=forms.Textarea,
                               label=_("Metadata"), required=False)

    def __init__(self, *args, **kwargs):
        super(UpdateRuleMetadataForm, self).__init__(*args, **kwargs)
        rule_metadata = utils.metadata_to_str(
            self.initial["metadata"]
        ).replace('<br/>', '\r\n')
        self.initial["metadata"] = rule_metadata

    def handle(self, request, data):
        rule_id = self.initial['rule_id']
        try:
            rule = manila.share_rule_get(self.request, rule_id)
            set_dict, unset_list = utils.parse_str_meta(data['metadata'])
            if set_dict:
                manila.share_rule_set_metadata(request, rule, set_dict)
            if unset_list:
                manila.share_rule_unset_metadata(request, rule, unset_list)
            message = _('Updating share access rule metadata ')
            messages.success(request, message)
            return True
        except ValidationError as e:
            self.api_error(e.messages[0])
            return False
        except Exception:
            redirect = reverse("horizon:project:shares:manage_rules",
                               args=[self.initial['share_id']])
            exceptions.handle(request,
                              _('Unable to update rule metadata.'),
                              redirect=redirect)


class ResizeForm(forms.SelfHandlingForm):
    name = forms.CharField(
        max_length="255", label=_("Share Name"),
        widget=forms.TextInput(attrs={'readonly': 'readonly'}),
        required=False,
    )

    orig_size = forms.IntegerField(
        label=_("Current Size (GiB)"),
        widget=forms.TextInput(attrs={'readonly': 'readonly'}),
        required=False,
    )

    new_size = forms.IntegerField(
        label=_("New Size (GiB)")
    )

    def clean(self):
        cleaned_data = super(ResizeForm, self).clean()
        new_size = cleaned_data.get('new_size')
        orig_size = self.initial['orig_size']

        if new_size == orig_size:
            message = _("New size must be different than the existing size")
            self._errors["new_size"] = self.error_class([message])
            return cleaned_data

        if new_size <= 0:
            message = _("New size should not be less than or equal to zero")
            self._errors["new_size"] = self.error_class([message])
            return cleaned_data

        usages = manila.tenant_absolute_limits(self.request)
        availableGB = (usages['maxTotalShareGigabytes'] -
                       usages['totalShareGigabytesUsed'])
        if availableGB < (new_size - orig_size):
            message = _('Share cannot be extended to %(req)iGiB as '
                        'you only have %(avail)iGiB of your quota '
                        'available.')
            params = {'req': new_size, 'avail': availableGB + orig_size}
            self._errors["new_size"] = self.error_class([message % params])
        return cleaned_data

    def handle(self, request, data):
        share_id = self.initial['share_id']
        new_size = data['new_size']
        orig_size = data['orig_size']
        try:
            manila.share_resize(request, share_id, new_size, orig_size)
            return self.check_size(request, share_id, new_size)
        except Exception:
            redirect = reverse("horizon:project:shares:index")
            exceptions.handle(request, _(
                'Unable to resize share.'), redirect=redirect)

    def check_size(self, request, share_id, new_size):
        share = manila.share_get(request, share_id)
        timeout = 30
        interval = 0.35
        time_elapsed = 0
        while share.status != 'available':
            time.sleep(interval)
            time_elapsed += interval
            share = manila.share_get(request, share_id)
            if time_elapsed > timeout:
                raise exceptions.WorkflowError(_(
                    "The operation timed out while resizing. "
                    "Please try again."))

        if share.size == new_size:
            message = _('Resized share "%s"') % share.name
            messages.success(request, message)
            return True
        raise exceptions.WorkflowError(_(
            "Unable to resize share. "))


class RevertForm(forms.SelfHandlingForm):
    """Form for reverting a share to a snapshot."""

    snapshot = forms.ChoiceField(
        label=_("Snapshot"),
        required=True,
        widget=forms.Select(
            attrs={'class': 'switchable', 'data-slug': 'share_snapshot'}))

    def __init__(self, req, *args, **kwargs):
        super(self.__class__, self).__init__(req, *args, **kwargs)
        # NOTE(vponomaryov): manila client does not allow to filter snapshots
        # using "created_at" field, so, we need to get all snapshots of a share
        # and do filtering here.
        search_opts = {'share_id': self.initial['share_id']}
        snapshots = manila.share_snapshot_list(req, search_opts=search_opts)
        amount_of_snapshots = len(snapshots)
        if amount_of_snapshots < 1:
            self.fields['snapshot'].choices = [("", "")]
        else:
            snapshot = snapshots[0]
            if amount_of_snapshots > 1:
                for s in snapshots[1:]:
                    if s.created_at > snapshot.created_at:
                        snapshot = s
            self.fields['snapshot'].choices = [
                (snapshot.id, snapshot.name or snapshot.id)]

    def handle(self, request, data):
        share_id = self.initial['share_id']
        snapshot_id = data['snapshot']
        try:
            manila.share_revert(request, share_id, snapshot_id)
            message = _('Share "%(s)s" has been reverted to "%(ss)s" snapshot '
                        'successfully') % {'s': share_id, 'ss': snapshot_id}
            messages.success(request, message)
            return True
        except Exception:
            redirect = reverse("horizon:project:shares:index")
            exceptions.handle(
                request,
                _('Unable to revert share to the snapshot.'),
                redirect=redirect)
