# 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 openstack.orchestration.v1 import resource as _resource
from openstack.orchestration.v1 import software_config as _sc
from openstack.orchestration.v1 import software_deployment as _sd
from openstack.orchestration.v1 import stack as _stack
from openstack.orchestration.v1 import stack_environment as _stack_environment
from openstack.orchestration.v1 import stack_files as _stack_files
from openstack.orchestration.v1 import stack_template as _stack_template
from openstack.orchestration.v1 import template as _template
from openstack.orchestration.util import template_utils
from openstack import exceptions
from openstack import proxy
from openstack import resource


class Proxy(proxy.Proxy):

    def _extract_name_consume_url_parts(self, url_parts):
        if (len(url_parts) == 3 and url_parts[0] == 'software_deployments'
                and url_parts[1] == 'metadata'):
            # Another nice example of totally different URL naming scheme,
            # which we need to repair /software_deployment/metadata/server_id -
            # just replace server_id with metadata to keep further logic
            return ['software_deployment', 'metadata']
        if (url_parts[0] == 'stacks' and len(url_parts) > 2
                and not url_parts[2] in ['preview', 'resources']):
            # orchestrate introduce having stack name and id part of the URL
            # (/stacks/name/id/everything_else), so if on third position we
            # have not a known part - discard it, not to brake further logic
            del url_parts[2]
        return super(Proxy, self)._extract_name_consume_url_parts(url_parts)

    def read_env_and_templates(self, template_file=None, template_url=None,
                               template_object=None, files=None,
                               environment_files=None):
        """Read templates and environment content and prepares
        corresponding stack attributes

        :param string template_file: Path to the template.
        :param string template_url: URL of template.
        :param string template_object: URL to retrieve template object.
        :param dict files: dict of additional file content to include.
        :param environment_files: Paths to environment files to apply.

        :returns: Attributes dict to be set on the
            :class:`~openstack.orchestration.v1.stack.Stack`
        :rtype: dict
        """
        stack_attrs = dict()
        envfiles = dict()
        tpl_files = None
        if environment_files:
            envfiles, env = \
                template_utils.process_multiple_environments_and_files(
                    env_paths=environment_files)
            stack_attrs['environment'] = env
        if template_file or template_url or template_object:
            tpl_files, template = template_utils.get_template_contents(
                template_file=template_file,
                template_url=template_url,
                template_object=template_object,
                files=files)
            stack_attrs['template'] = template
            if tpl_files or envfiles:
                stack_attrs['files'] = dict(
                    list(tpl_files.items()) + list(envfiles.items())
                )
        return stack_attrs

    def create_stack(self, preview=False, **attrs):
        """Create a new stack from attributes

        :param bool preview: When ``True``, a preview endpoint will be used to
            verify the template
            *Default: ``False``*
        :param dict attrs: Keyword arguments which will be used to create
            a :class:`~openstack.orchestration.v1.stack.Stack`,
            comprised of the properties on the Stack class.

        :returns: The results of stack creation
        :rtype: :class:`~openstack.orchestration.v1.stack.Stack`
        """

        base_path = None if not preview else '/stacks/preview'
        return self._create(_stack.Stack, base_path=base_path, **attrs)

    def find_stack(self, name_or_id,
                   ignore_missing=True, resolve_outputs=True):
        """Find a single stack

        :param name_or_id: The name or ID of a stack.
        :param bool ignore_missing: When set to ``False``
                    :class:`~openstack.exceptions.ResourceNotFound` will be
                    raised when the resource does not exist.
                    When set to ``True``, None will be returned when
                    attempting to find a nonexistent resource.
        :returns: One :class:`~openstack.orchestration.v1.stack.Stack` or None
        """
        return self._find(_stack.Stack, name_or_id,
                          ignore_missing=ignore_missing,
                          resolve_outputs=resolve_outputs)

    def stacks(self, **query):
        """Return a generator of stacks

        :param kwargs query: Optional query parameters to be sent to limit
                                 the resources being returned.

        :returns: A generator of stack objects
        :rtype: :class:`~openstack.orchestration.v1.stack.Stack`
        """
        return self._list(_stack.Stack, **query)

    def get_stack(self, stack, resolve_outputs=True):
        """Get a single stack

        :param stack: The value can be the ID of a stack or a
               :class:`~openstack.orchestration.v1.stack.Stack` instance.
        :param resolve_outputs: Whether stack should contain outputs resolved.

        :returns: One :class:`~openstack.orchestration.v1.stack.Stack`
        :raises: :class:`~openstack.exceptions.ResourceNotFound`
                 when no resource can be found.
        """
        return self._get(_stack.Stack, stack, resolve_outputs=resolve_outputs)

    def update_stack(self, stack, preview=False, **attrs):
        """Update a stack

        :param stack: The value can be the ID of a stack or a
               :class:`~openstack.orchestration.v1.stack.Stack` instance.
        :param kwargs attrs: The attributes to update on the stack
                                 represented by ``value``.

        :returns: The updated stack
        :rtype: :class:`~openstack.orchestration.v1.stack.Stack`
        :raises: :class:`~openstack.exceptions.ResourceNotFound`
                 when no resource can be found.
        """
        res = self._get_resource(_stack.Stack, stack, **attrs)
        return res.update(self, preview)

    def delete_stack(self, stack, ignore_missing=True):
        """Delete a stack

        :param stack: The value can be either the ID of a stack or a
                      :class:`~openstack.orchestration.v1.stack.Stack`
                      instance.
        :param bool ignore_missing: When set to ``False``
                    :class:`~openstack.exceptions.ResourceNotFound` will be
                    raised when the stack does not exist.
                    When set to ``True``, no exception will be set when
                    attempting to delete a nonexistent stack.

        :returns: ``None``
        """
        self._delete(_stack.Stack, stack, ignore_missing=ignore_missing)

    def check_stack(self, stack):
        """Check a stack's status

        Since this is an asynchronous action, the only way to check the result
        is to track the stack's status.

        :param stack: The value can be either the ID of a stack or an instance
                      of :class:`~openstack.orchestration.v1.stack.Stack`.
        :returns: ``None``
        """
        if isinstance(stack, _stack.Stack):
            stk_obj = stack
        else:
            stk_obj = _stack.Stack.existing(id=stack)

        stk_obj.check(self)

    def abandon_stack(self, stack):
        """Abandon a stack's without deleting it's resources

        :param stack: The value can be either the ID of a stack or an instance
                      of :class:`~openstack.orchestration.v1.stack.Stack`.
        :returns: ``None``
        """
        res = self._get_resource(_stack.Stack, stack)
        return res.abandon(self)

    def get_stack_template(self, stack):
        """Get template used by a stack

        :param stack: The value can be the ID of a stack or an instance of
            :class:`~openstack.orchestration.v1.stack.Stack`

        :returns: One object of
            :class:`~openstack.orchestration.v1.stack_template.StackTemplate`
        :raises: :class:`~openstack.exceptions.ResourceNotFound`
                 when no resource can be found.
        """
        if isinstance(stack, _stack.Stack):
            obj = stack
        else:
            obj = self._find(_stack.Stack, stack, ignore_missing=False)

        return self._get(_stack_template.StackTemplate, requires_id=False,
                         stack_name=obj.name, stack_id=obj.id)

    def get_stack_environment(self, stack):
        """Get environment used by a stack

        :param stack: The value can be the ID of a stack or an instance of
            :class:`~openstack.orchestration.v1.stack.Stack`

        :returns: One object of
                  :class:`~openstack.orchestration.v1.stack_environment.\
                  StackEnvironment`
        :raises: :class:`~openstack.exceptions.ResourceNotFound` when no
                 resource can be found.
        """
        if isinstance(stack, _stack.Stack):
            obj = stack
        else:
            obj = self._find(_stack.Stack, stack, ignore_missing=False)

        return self._get(_stack_environment.StackEnvironment,
                         requires_id=False, stack_name=obj.name,
                         stack_id=obj.id)

    def get_stack_files(self, stack):
        """Get files used by a stack

        :param stack: The value can be the ID of a stack or an instance of
            :class:`~openstack.orchestration.v1.stack.Stack`

        :returns: A dictionary containing the names and contents of all files
                  used by the stack.
        :raises: :class:`~openstack.exceptions.ResourceNotFound`
                 when the stack cannot be found.
        """
        if isinstance(stack, _stack.Stack):
            stk = stack
        else:
            stk = self._find(_stack.Stack, stack, ignore_missing=False)

        obj = _stack_files.StackFiles(stack_name=stk.name, stack_id=stk.id)
        return obj.fetch(self)

    def resources(self, stack, **query):
        """Return a generator of resources

        :param stack: This can be a stack object, or the name of a stack
                      for which the resources are to be listed.
        :param kwargs query: Optional query parameters to be sent to limit
                                 the resources being returned.

        :returns: A generator of resource objects if the stack exists and
                  there are resources in it. If the stack cannot be found,
                  an exception is thrown.
        :rtype: A generator of
            :class:`~openstack.orchestration.v1.resource.Resource`
        :raises: :class:`~openstack.exceptions.ResourceNotFound`
                 when the stack cannot be found.
        """
        # first try treat the value as a stack object or an ID
        if isinstance(stack, _stack.Stack):
            obj = stack
        else:
            obj = self._find(_stack.Stack, stack, ignore_missing=False)

        return self._list(_resource.Resource, stack_name=obj.name,
                          stack_id=obj.id, **query)

    def create_software_config(self, **attrs):
        """Create a new software config from attributes

        :param dict attrs: Keyword arguments which will be used to create a
            :class:`~openstack.orchestration.v1.software_config.SoftwareConfig`,
            comprised of the properties on the SoftwareConfig class.

        :returns: The results of software config creation
        :rtype:
            :class:`~openstack.orchestration.v1.software_config.SoftwareConfig`
        """
        return self._create(_sc.SoftwareConfig, **attrs)

    def software_configs(self, **query):
        """Returns a generator of software configs

        :param dict query: Optional query parameters to be sent to limit the
                           software configs returned.
        :returns: A generator of software config objects.
        :rtype: :class:`~openstack.orchestration.v1.software_config.\
                SoftwareConfig`
        """
        return self._list(_sc.SoftwareConfig, **query)

    def get_software_config(self, software_config):
        """Get details about a specific software config.

        :param software_config: The value can be the ID of a software config
            or a instace of
            :class:`~openstack.orchestration.v1.software_config.SoftwareConfig`,

        :returns: An object of type
            :class:`~openstack.orchestration.v1.software_config.SoftwareConfig`
        """
        return self._get(_sc.SoftwareConfig, software_config)

    def delete_software_config(self, software_config, ignore_missing=True):
        """Delete a software config

        :param software_config: The value can be either the ID of a software
            config or an instance of
            :class:`~openstack.orchestration.v1.software_config.SoftwareConfig`
        :param bool ignore_missing: When set to ``False``
                    :class:`~openstack.exceptions.ResourceNotFound` will be
                    raised when the software config does not exist.
                    When set to ``True``, no exception will be set when
                    attempting to delete a nonexistent software config.
        :returns: ``None``
        """
        self._delete(_sc.SoftwareConfig, software_config,
                     ignore_missing=ignore_missing)

    def create_software_deployment(self, **attrs):
        """Create a new software deployment from attributes

        :param dict attrs: Keyword arguments which will be used to create a
            :class:`~openstack.orchestration.v1.software_deployment.SoftwareDeployment`,
            comprised of the properties on the SoftwareDeployment class.

        :returns: The results of software deployment creation
        :rtype:
            :class:`~openstack.orchestration.v1.software_deployment.SoftwareDeployment`
        """
        return self._create(_sd.SoftwareDeployment, **attrs)

    def software_deployments(self, **query):
        """Returns a generator of software deployments

        :param dict query: Optional query parameters to be sent to limit the
                           software deployments returned.
        :returns: A generator of software deployment objects.
        :rtype: :class:`~openstack.orchestration.v1.software_deployment.\
                SoftwareDeployment`
        """
        return self._list(_sd.SoftwareDeployment, **query)

    def get_software_deployment(self, software_deployment):
        """Get details about a specific software deployment resource

        :param software_deployment: The value can be the ID of a software
            deployment or an instace of
            :class:`~openstack.orchestration.v1.software_deployment.SoftwareDeployment`,

        :returns: An object of type
            :class:`~openstack.orchestration.v1.software_deployment.SoftwareDeployment`
        """
        return self._get(_sd.SoftwareDeployment, software_deployment)

    def delete_software_deployment(self, software_deployment,
                                   ignore_missing=True):
        """Delete a software deployment

        :param software_deployment: The value can be either the ID of a
            software deployment or an instance of
            :class:`~openstack.orchestration.v1.software_deployment.SoftwareDeployment`
        :param bool ignore_missing: When set to ``False``
                    :class:`~openstack.exceptions.ResourceNotFound` will be
                    raised when the software deployment does not exist.
                    When set to ``True``, no exception will be set when
                    attempting to delete a nonexistent software deployment.
        :returns: ``None``
        """
        self._delete(_sd.SoftwareDeployment, software_deployment,
                     ignore_missing=ignore_missing)

    def update_software_deployment(self, software_deployment, **attrs):
        """Update a software deployment

        :param server: Either the ID of a software deployment or an instance of
            :class:`~openstack.orchestration.v1.software_deployment.SoftwareDeployment`
        :param dict attrs: The attributes to update on the software deployment
                           represented by ``software_deployment``.

        :returns: The updated software deployment
        :rtype: :class:`~openstack.orchestration.v1.software_deployment.\
                SoftwareDeployment`
        """
        return self._update(_sd.SoftwareDeployment, software_deployment,
                            **attrs)

    def validate_template(self, template, environment=None, template_url=None,
                          ignore_errors=None):
        """Validates a template.

        :param template: The stack template on which the validation is
                         performed.
        :param environment: A JSON environment for the stack, if provided.
        :param template_url: A URI to the location containing the stack
                             template for validation. This parameter is only
                             required if the ``template`` parameter is None.
                             This parameter is ignored if ``template`` is
                             specified.
        :param ignore_errors: A string containing comma separated error codes
                              to ignore. Currently the only valid error code
                              is '99001'.
        :returns: The result of template validation.
        :raises: :class:`~openstack.exceptions.InvalidRequest` if neither
                 `template` not `template_url` is provided.
        :raises: :class:`~openstack.exceptions.HttpException` if the template
                 fails the validation.
        """
        if template is None and template_url is None:
            raise exceptions.InvalidRequest(
                "'template_url' must be specified when template is None")

        tmpl = _template.Template.new()
        return tmpl.validate(self, template, environment=environment,
                             template_url=template_url,
                             ignore_errors=ignore_errors)

    def wait_for_status(self, res, status='ACTIVE', failures=None,
                        interval=2, wait=120):
        """Wait for a resource to be in a particular status.

        :param res: The resource to wait on to reach the specified status.
                    The resource must have a ``status`` attribute.
        :type resource: A :class:`~openstack.resource.Resource` object.
        :param status: Desired status.
        :param failures: Statuses that would be interpreted as failures.
        :type failures: :py:class:`list`
        :param interval: Number of seconds to wait before to consecutive
                         checks. Default to 2.
        :param wait: Maximum number of seconds to wait before the change.
                     Default to 120.
        :returns: The resource is returned on success.
        :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition
                 to the desired status failed to occur in specified seconds.
        :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource
                 has transited to one of the failure statuses.
        :raises: :class:`~AttributeError` if the resource does not have a
                ``status`` attribute.
        """
        failures = [] if failures is None else failures
        return resource.wait_for_status(
            self, res, status, failures, interval, wait)

    def wait_for_delete(self, res, interval=2, wait=120):
        """Wait for a resource to be deleted.

        :param res: The resource to wait on to be deleted.
        :type resource: A :class:`~openstack.resource.Resource` object.
        :param interval: Number of seconds to wait before to consecutive
                         checks. Default to 2.
        :param wait: Maximum number of seconds to wait before the change.
                     Default to 120.
        :returns: The resource is returned on success.
        :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition
                 to delete failed to occur in the specified seconds.
        """
        return resource.wait_for_delete(self, res, interval, wait)

    def get_template_contents(
            self, template_file=None, template_url=None,
            template_object=None, files=None):
        try:
            return template_utils.get_template_contents(
                template_file=template_file, template_url=template_url,
                template_object=template_object, files=files)
        except Exception as e:
            raise exceptions.SDKException(
                "Error in processing template files: %s" % str(e))
