#    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 os
import tempfile
import urlparse

from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from heatclient.common import template_utils
from heatclient.exc import HTTPNotFound
from horizon.utils import memoized
from openstack_dashboard.api import base
from openstack_dashboard.api import heat
from openstack_dashboard.api import keystone

from tuskar_ui.api import node
from tuskar_ui.api import tuskar
from tuskar_ui.cached_property import cached_property  # noqa
from tuskar_ui.handle_errors import handle_errors  # noqa
from tuskar_ui.utils import utils


LOG = logging.getLogger(__name__)


@memoized.memoized
def overcloud_keystoneclient(request, endpoint, password):
    """Returns a client connected to the Keystone backend.

    Several forms of authentication are supported:

        * Username + password -> Unscoped authentication
        * Username + password + tenant id -> Scoped authentication
        * Unscoped token -> Unscoped authentication
        * Unscoped token + tenant id -> Scoped authentication
        * Scoped token -> Scoped authentication

    Available services and data from the backend will vary depending on
    whether the authentication was scoped or unscoped.

    Lazy authentication if an ``endpoint`` parameter is provided.

    Calls requiring the admin endpoint should have ``admin=True`` passed in
    as a keyword argument.

    The client is cached so that subsequent API calls during the same
    request/response cycle don't have to be re-authenticated.
    """
    api_version = keystone.VERSIONS.get_active_version()

    # TODO(lsmola) add support of certificates and secured http and rest of
    # parameters according to horizon and add configuration to local settings
    # (somehow plugin based, we should not maintain a copy of settings)
    LOG.debug("Creating a new keystoneclient connection to %s." % endpoint)

    # TODO(lsmola) we should create tripleo-admin user for this purpose
    # this needs to be done first on tripleo side
    conn = api_version['client'].Client(username="admin",
                                        password=password,
                                        tenant_name="admin",
                                        auth_url=endpoint)

    return conn


def _save_templates(templates):
    """Saves templates into tmpdir on server

    This should go away and get replaced by libutils.save_templates from
    tripleo-common https://github.com/openstack/tripleo-common/
    """
    output_dir = tempfile.mkdtemp()

    for template_name, template_content in templates.items():

        # It's possible to organize the role templates and their dependent
        # files into directories, in which case the template_name will carry
        # the directory information. If that's the case, first create the
        # directory structure (if it hasn't already been created by another
        # file in the templates list).
        template_dir = os.path.dirname(template_name)
        output_template_dir = os.path.join(output_dir, template_dir)
        if template_dir and not os.path.exists(output_template_dir):
            os.makedirs(output_template_dir)

        filename = os.path.join(output_dir, template_name)
        with open(filename, 'w+') as template_file:
            template_file.write(template_content)
    return output_dir


def _process_templates(templates):
    """Process templates

    Due to bug in heat api
    https://bugzilla.redhat.com/show_bug.cgi?id=1212740, we need to
    save the templates in tmpdir, reprocess them with template_utils
    from heatclient and then we can use them in creating/updating stack.

    This should be replaced by the same code that is in tripleo-common and
    eventually it will not be needed at all.
    """

    tpl_dir = _save_templates(templates)

    tpl_files, template = template_utils.get_template_contents(
        template_file=os.path.join(tpl_dir, tuskar.MASTER_TEMPLATE_NAME))
    env_files, env = (
        template_utils.process_multiple_environments_and_files(
            env_paths=[os.path.join(tpl_dir, tuskar.ENVIRONMENT_NAME)]))

    files = dict(list(tpl_files.items()) + list(env_files.items()))

    return template, env, files


class Stack(base.APIResourceWrapper):
    _attrs = ('id', 'stack_name', 'outputs', 'stack_status', 'parameters')

    def __init__(self, apiresource, request=None):
        super(Stack, self).__init__(apiresource)
        self._request = request

    @classmethod
    def create(cls, request, stack_name, templates):
        template, environment, files = _process_templates(templates)

        fields = {
            'stack_name': stack_name,
            'template': template,
            'environment': environment,
            'files': files,
            'timeout_mins': 240,
        }
        password = getattr(settings, 'UNDERCLOUD_ADMIN_PASSWORD', None)
        stack = heat.stack_create(request, password, **fields)
        return cls(stack, request=request)

    def update(self, request, stack_name, templates):
        template, environment, files = _process_templates(templates)

        fields = {
            'stack_name': stack_name,
            'template': template,
            'environment': environment,
            'files': files,
        }
        password = getattr(settings, 'UNDERCLOUD_ADMIN_PASSWORD', None)
        heat.stack_update(request, self.id, password, **fields)

    @classmethod
    @handle_errors(_("Unable to retrieve heat stacks"), [])
    def list(cls, request):
        """Return a list of stacks in Heat

        :param request: request object
        :type  request: django.http.HttpRequest

        :return: list of Heat stacks, or an empty list if there
                 are none
        :rtype:  list of tuskar_ui.api.heat.Stack
        """
        stacks, has_more_data, has_prev_data = heat.stacks_list(request)
        return [cls(stack, request=request) for stack in stacks]

    @classmethod
    @handle_errors(_("Unable to retrieve stack"))
    def get(cls, request, stack_id):
        """Return the Heat Stack associated with this Overcloud

        :return: Heat Stack associated with the stack_id; or None
                 if no Stack is associated, or no Stack can be
                 found
        :rtype:  tuskar_ui.api.heat.Stack or None
        """
        return cls(heat.stack_get(request, stack_id), request=request)

    @classmethod
    @handle_errors(_("Unable to retrieve stack"))
    def get_by_plan(cls, request, plan):
        """Return the Heat Stack associated with a Plan

        :return: Heat Stack associated with the plan; or None
                 if no Stack is associated, or no Stack can be
                 found
        :rtype:  tuskar_ui.api.heat.Stack or None
        """
        # TODO(lsmola) until we have working deployment through Tuskar-API,
        # this will not work
        # for stack in Stack.list(request):
        #     if stack.plan and (stack.plan.id == plan.id):
        #         return stack
        try:
            stack = Stack.list(request)[0]
        except IndexError:
            return None
        # TODO(lsmola) stack list actually does not contain all the detail
        # info, there should be call for that, investigate
        return Stack.get(request, stack.id)

    @classmethod
    @handle_errors(_("Unable to delete Heat stack"), [])
    def delete(cls, request, stack_id):
        heat.stack_delete(request, stack_id)

    @memoized.memoized
    def resources(self, with_joins=True, role=None):
        """Return list of OS::Nova::Server Resources

        Return list of OS::Nova::Server Resources associated with the Stack
        and which are associated with a Role

        :param with_joins: should we also retrieve objects associated with each
                           retrieved Resource?
        :type  with_joins: bool

        :return: list of all Resources or an empty list if there are none
        :rtype:  list of tuskar_ui.api.heat.Resource
        """

        if role:
            roles = [role]
        else:
            roles = self.plan.role_list
        resource_dicts = []

        # A provider resource is deployed as a nested stack, so we have to
        # drill down and retrieve those that match a tuskar role
        for role in roles:
            resource_group_name = role.name
            try:
                resource_group = heat.resource_get(self._request,
                                                   self.id,
                                                   resource_group_name)

                group_resources = heat.resources_list(
                    self._request, resource_group.physical_resource_id)
                for group_resource in group_resources:
                    if not group_resource.physical_resource_id:
                        # Skip groups who has no physical resource.
                        continue
                    nova_resources = heat.resources_list(
                        self._request,
                        group_resource.physical_resource_id)
                    resource_dicts.extend([{"resource": resource,
                                            "role": role}
                                           for resource in nova_resources])

            except HTTPNotFound:
                pass

        if not with_joins:
            return [Resource(rd['resource'], request=self._request,
                             stack=self, role=rd['role'])
                    for rd in resource_dicts]

        nodes_dict = utils.list_to_dict(node.Node.list(self._request,
                                                       associated=True),
                                        key_attribute='instance_uuid')
        joined_resources = []
        for rd in resource_dicts:
            resource = rd['resource']
            joined_resources.append(
                Resource(resource,
                         node=nodes_dict.get(resource.physical_resource_id,
                                             None),
                         request=self._request, stack=self, role=rd['role']))
        # TODO(lsmola) I want just resources with nova instance
        # this could be probably filtered a better way, investigate
        return [r for r in joined_resources if r.node is not None]

    @memoized.memoized
    def resources_count(self, overcloud_role=None):
        """Return count of associated Resources

        :param overcloud_role: role of resources to be counted; None means all
        :type  overcloud_role: tuskar_ui.api.tuskar.Role

        :return: Number of matching resources
        :rtype:  int
        """
        # TODO(dtantsur): there should be better way to do it, rather than
        # fetching and calling len()
        # FIXME(dtantsur): should also be able to use with_joins=False
        # but unable due to bug #1289505
        if overcloud_role is None:
            resources = self.resources()
        else:
            resources = self.resources(role=overcloud_role)
        return len(resources)

    @cached_property
    def plan(self):
        """return associated Plan if a plan_id exists within stack parameters.

        :return: associated Plan if plan_id exists and a matching plan
                 exists as well; None otherwise
        :rtype:  tuskar_ui.api.tuskar.Plan
        """
        # TODO(lsmola) replace this by actual reference, I am pretty sure
        # the relation won't be stored in parameters, that would mean putting
        # that into template, which doesn't make sense
        # if 'plan_id' in self.parameters:
        #     return tuskar.Plan.get(self._request,
        #                                     self.parameters['plan_id'])
        try:
            plan = tuskar.Plan.list(self._request)[0]
        except IndexError:
            return None
        return plan

    @cached_property
    def is_initialized(self):
        """Check if this Stack is successfully initialized.

        :return: True if this Stack is successfully initialized, False
                 otherwise
        :rtype:  bool
        """
        return len(self.dashboard_urls) > 0

    @cached_property
    def is_deployed(self):
        """Check if this Stack is successfully deployed.

        :return: True if this Stack is successfully deployed, False otherwise
        :rtype:  bool
        """
        return self.stack_status in ('CREATE_COMPLETE',
                                     'UPDATE_COMPLETE')

    @cached_property
    def is_deploying(self):
        """Check if this Stack is currently deploying.

        :return: True if deployment is in progress, False otherwise.
        :rtype: bool
        """
        return self.stack_status in ('CREATE_IN_PROGRESS',)

    @cached_property
    def is_updating(self):
        """Check if this Stack is currently updating.

        :return: True if updating is in progress, False otherwise.
        :rtype: bool
        """
        return self.stack_status in ('UPDATE_IN_PROGRESS',)

    @cached_property
    def is_failed(self):
        """Check if this Stack failed to update or deploy.

        :return: True if deployment there was an error, False otherwise.
        :rtype: bool
        """
        return self.stack_status in ('CREATE_FAILED',
                                     'UPDATE_FAILED',)

    @cached_property
    def is_deleting(self):
        """Check if this Stack is deleting.

        :return: True if Stack is deleting, False otherwise.
        :rtype: bool
        """
        return self.stack_status in ('DELETE_IN_PROGRESS', )

    @cached_property
    def is_delete_failed(self):
        """Check if Stack deleting has failed.

        :return: True if Stack deleting has failed, False otherwise.
        :rtype: bool
        """
        return self.stack_status in ('DELETE_FAILED', )

    @cached_property
    def events(self):
        """Return the Heat Events associated with this Stack

        :return: list of Heat Events associated with this Stack;
                 or an empty list if there is no Stack associated with
                 this Stack, or there are no Events
        :rtype:  list of heatclient.v1.events.Event
        """
        return heat.events_list(self._request,
                                self.stack_name)

    @property
    def stack_outputs(self):
        return getattr(self, 'outputs', [])

    @cached_property
    def keystone_auth_url(self):
        for output in self.stack_outputs:
            if output['output_key'] == 'KeystoneURL':
                return output['output_value']

    @cached_property
    def keystone_ip(self):
        if self.keystone_auth_url:
            return urlparse.urlparse(self.keystone_auth_url).hostname

    @cached_property
    def overcloud_keystone(self):
        try:
            return overcloud_keystoneclient(
                self._request,
                self.keystone_auth_url,
                self.plan.parameter_value('Controller-1::AdminPassword'))
        except Exception:
            LOG.debug('Unable to connect to overcloud keystone.')
            return None

    @cached_property
    def dashboard_urls(self):
        client = self.overcloud_keystone
        if not client:
            return []

        try:
            services = client.services.list()
            for service in services:
                if service.name == 'horizon':
                    break
            else:
                return []
        except Exception:
            return []

        admin_urls = [endpoint.adminurl for endpoint
                      in client.endpoints.list()
                      if endpoint.service_id == service.id]

        return admin_urls


class Resource(base.APIResourceWrapper):
    _attrs = ('resource_name', 'resource_type', 'resource_status',
              'physical_resource_id')

    def __init__(self, apiresource, request=None, **kwargs):
        """Initialize a resource

        :param apiresource: apiresource we want to wrap
        :type  apiresource: heatclient.v1.resources.Resource

        :param request: request
        :type  request: django.core.handlers.wsgi.WSGIRequest

        :param node: node relation we want to cache
        :type  node: tuskar_ui.api.node.Node

        :return: Resource object
        :rtype:  Resource
        """
        super(Resource, self).__init__(apiresource)
        self._request = request
        if 'node' in kwargs:
            self._node = kwargs['node']
        if 'stack' in kwargs:
            self._stack = kwargs['stack']
        if 'role' in kwargs:
            self._role = kwargs['role']

    @classmethod
    @memoized.memoized
    def _resources_by_nodes(cls, request):
        return {resource.physical_resource_id: resource
                for resource in cls.list_all_resources(request)}

    @classmethod
    def get_by_node(cls, request, node):
        """Return the specified Heat Resource given a Node

        :param request: request object
        :type  request: django.http.HttpRequest

        :param node: node to match
        :type  node: tuskar_ui.api.node.Node

        :return: matching Resource, or raises LookupError if no
                 resource matches the node
        :rtype:  tuskar_ui.api.heat.Resource
        """
        return cls._resources_by_nodes(request)[node.instance_uuid]

    @classmethod
    def list_all_resources(cls, request):
        """Iterate through all the stacks and return all relevant resources

        :param request: request object
        :type  request: django.http.HttpRequest

        :return: list of resources
        :rtype:  list of tuskar_ui.api.heat.Resource
        """
        all_resources = []
        for stack in Stack.list(request):
            all_resources.extend(stack.resources(with_joins=False))
        return all_resources

    @cached_property
    def role(self):
        """Return the Role associated with this Resource

        :return: Role associated with this Resource, or None if no
                 Role is associated
        :rtype:  tuskar_ui.api.tuskar.Role
        """
        if hasattr(self, '_role'):
            return self._role

    @cached_property
    def node(self):
        """Return the Ironic Node associated with this Resource

        :return: Ironic Node associated with this Resource, or None if no
                 Node is associated
        :rtype:  tuskar_ui.api.node.Node

        :raises: ironicclient.exc.HTTPNotFound if there is no Node with the
                 matching instance UUID
        """
        if hasattr(self, '_node'):
            return self._node
        if self.physical_resource_id:
            return node.Node.get_by_instance_uuid(self._request,
                                                  self.physical_resource_id)
        return None

    @cached_property
    def stack(self):
        """Return the Stack associated with this Resource

        :return: Stack associated with this Resource, or None if no
                 Stack is associated
        :rtype:  tuskar_ui.api.heat.Stack
        """
        if hasattr(self, '_stack'):
            return self._stack
