# Copyright 2012 Nebula, Inc.
#
#    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
from importlib import import_module
import inspect
import logging

from django.conf import settings
from django import forms
from django.forms.forms import NON_FIELD_ERRORS
from django import template
from django.template.defaultfilters import linebreaks
from django.template.defaultfilters import safe
from django.template.defaultfilters import slugify
from django import urls
from django.utils.encoding import force_text
from django.utils import module_loading
from django.utils.translation import ugettext_lazy as _
from openstack_auth import policy
import six

from horizon import base
from horizon import exceptions
from horizon.templatetags.horizon import has_permissions
from horizon.utils import html


LOG = logging.getLogger(__name__)


class WorkflowContext(dict):
    def __init__(self, workflow, *args, **kwargs):
        super(WorkflowContext, self).__init__(*args, **kwargs)
        self._workflow = workflow

    def __setitem__(self, key, val):
        super(WorkflowContext, self).__setitem__(key, val)
        return self._workflow._trigger_handlers(key)

    def __delitem__(self, key):
        return self.__setitem__(key, None)

    def set(self, key, val):
        return self.__setitem__(key, val)

    def unset(self, key):
        return self.__delitem__(key)


class ActionMetaclass(forms.forms.DeclarativeFieldsMetaclass):
    def __new__(mcs, name, bases, attrs):
        # Pop Meta for later processing
        opts = attrs.pop("Meta", None)
        # Create our new class
        cls = super(ActionMetaclass, mcs).__new__(mcs, name, bases, attrs)
        # Process options from Meta
        cls.name = getattr(opts, "name", name)
        cls.slug = getattr(opts, "slug", slugify(name))
        cls.permissions = getattr(opts, "permissions", ())
        cls.policy_rules = getattr(opts, "policy_rules", ())
        cls.progress_message = getattr(opts,
                                       "progress_message",
                                       _("Processing..."))
        cls.help_text = getattr(opts, "help_text", "")
        cls.help_text_template = getattr(opts, "help_text_template", None)
        return cls


@six.python_2_unicode_compatible
@six.add_metaclass(ActionMetaclass)
class Action(forms.Form):
    """An ``Action`` represents an atomic logical interaction with the system.

    This is easier to understand with a conceptual example: in the context of
    a "launch instance" workflow, actions would include "naming the instance",
    "selecting an image", and ultimately "launching the instance".

    Because ``Actions`` are always interactive, they always provide form
    controls, and thus inherit from Django's ``Form`` class. However, they
    have some additional intelligence added to them:

    * ``Actions`` are aware of the permissions required to complete them.

    * ``Actions`` have a meta-level concept of "help text" which is meant to be
      displayed in such a way as to give context to the action regardless of
      where the action is presented in a site or workflow.

    * ``Actions`` understand how to handle their inputs and produce outputs,
      much like :class:`~horizon.forms.SelfHandlingForm` does now.

    ``Action`` classes may define the following attributes in a ``Meta``
    class within them:

    .. attribute:: name

       The verbose name for this action. Defaults to the name of the class.

    .. attribute:: slug

       A semi-unique slug for this action. Defaults to the "slugified" name
       of the class.

    .. attribute:: permissions

       A list of permission names which this action requires in order to be
       completed. Defaults to an empty list (``[]``).

    .. attribute:: policy_rules

       list of scope and rule tuples to do policy checks on, the
       composition of which is (scope, rule)

       * scope: service type managing the policy for action
       * rule: string representing the action to be checked

       for a policy that requires a single rule check::

           policy_rules should look like
               "(("compute", "compute:create_instance"),)"

       for a policy that requires multiple rule checks::

           rules should look like
               "(("identity", "identity:list_users"),
                 ("identity", "identity:list_roles"))"

       where two service-rule clauses are OR-ed.

    .. attribute:: help_text

       A string of simple help text to be displayed alongside the Action's
       fields.

    .. attribute:: help_text_template

       A path to a template which contains more complex help text to be
       displayed alongside the Action's fields. In conjunction with
       :meth:`~horizon.workflows.Action.get_help_text` method you can
       customize your help text template to display practically anything.

    """

    def __init__(self, request, context, *args, **kwargs):
        if request.method == "POST":
            super(Action, self).__init__(request.POST, initial=context)
        else:
            super(Action, self).__init__(initial=context)

        if not hasattr(self, "handle"):
            raise AttributeError("The action %s must define a handle method."
                                 % self.__class__.__name__)
        self.request = request
        self._populate_choices(request, context)
        self.required_css_class = 'required'

    def __str__(self):
        return force_text(self.name)

    def __repr__(self):
        return "<%s: %s>" % (self.__class__.__name__, self.slug)

    def _populate_choices(self, request, context):
        for field_name, bound_field in self.fields.items():
            meth = getattr(self, "populate_%s_choices" % field_name, None)
            if meth is not None and callable(meth):
                bound_field.choices = meth(request, context)

    def get_help_text(self, extra_context=None):
        """Returns the help text for this step."""
        text = ""
        extra_context = extra_context or {}
        if self.help_text_template: