# Copyright 2016 Red Hat, 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.
import json
import logging
import yaml

from heatclient import exc as heatexceptions
from mistral.workflow import utils as mistral_workflow_utils
from mistralclient.api import base as mistralclient_base
import six
from swiftclient import exceptions as swiftexceptions

from tripleo_common.actions import base
from tripleo_common import constants
from tripleo_common import exception
from tripleo_common.utils import swift as swiftutils
from tripleo_common.utils.validations import pattern_validator


LOG = logging.getLogger(__name__)

default_container_headers = {
    constants.TRIPLEO_META_USAGE_KEY: 'plan'
}


class CreateContainerAction(base.TripleOAction):
    """Creates an object container

    This action creates an object container for a given name.  If a container
    with the same name already exists an exception is raised.
    """

    def __init__(self, container):
        super(CreateContainerAction, self).__init__()
        self.container = container

    def run(self):
        oc = self.get_object_client()

        # checks to see if a container has a valid name
        if not pattern_validator(constants.PLAN_NAME_PATTERN, self.container):
            message = ("Unable to create plan. The plan name must "
                       "only contain letters, numbers or dashes")
            return mistral_workflow_utils.Result(error=message)

        # checks to see if a container with that name exists
        if self.container in [container["name"] for container in
                              oc.get_account()[1]]:
            result_string = ("A container with the name %s already"
                             " exists.") % self.container
            return mistral_workflow_utils.Result(error=result_string)
        oc.put_container(self.container, headers=default_container_headers)


class CreatePlanAction(base.TripleOAction):
    """Creates a plan

    Given a container, creates a Mistral environment with the same name,
    parses the capabilities map file and sets initial plan template and
    environment files.
    """

    def __init__(self, container):
        super(CreatePlanAction, self).__init__()
        self.container = container

    def run(self):
        oc = self.get_object_client()
        env_data = {
            'name': self.container,
        }
        env_vars = {}
        error_text = None

        if not pattern_validator(constants.PLAN_NAME_PATTERN, self.container):
            message = ("Unable to create plan. The plan name must "
                       "only contain letters, numbers or dashes")
            return mistral_workflow_utils.Result(error=message)

        # Check to see if an environment with that name already exists
        try:
            self.get_workflow_client().environments.get(self.container)
        except mistralclient_base.APIException:
            # The environment doesn't exist, as expected. Proceed.
            pass
        else:
            message = ("Unable to create plan. The Mistral environment "
                       "already exists")
            return mistral_workflow_utils.Result(error=message)

        try:
            # parses capabilities to get root_template, root_environment
            mapfile = yaml.safe_load(
                oc.get_object(self.container, 'capabilities-map.yaml')[1])

            if mapfile['root_template']:
                env_vars['template'] = mapfile['root_template']
            if mapfile['root_environment']:
                env_vars['environments'] = [
                    {'path': mapfile['root_environment']}]

            env_data['variables'] = json.dumps(env_vars, sort_keys=True,)
            # creates environment
            self.get_workflow_client().environments.create(**env_data)
        except yaml.YAMLError as yaml_err:
            error_text = "Error parsing the yaml file: %s" % yaml_err
        except swiftexceptions.ClientException as obj_err:
            error_text = "File missing from container: %s" % obj_err
        except KeyError as key_err:
            error_text = ("capabilities-map.yaml missing key: "
                          "%s" % key_err)
        except Exception as err:
            error_text = "Error occurred creating plan: %s" % err

        if error_text:
            return mistral_workflow_utils.Result(error=error_text)


class UpdatePlanAction(base.TripleOAction):
    """Update a plan

    Given a container, update the Mistral environment with the same name,
    parses the capabilities map file and sets the initial plan template
    and environment files if they have changed since the plan was originally
    created.
    """

    def __init__(self, container):
        super(UpdatePlanAction, self).__init__()
        self.container = container

    def run(self):
        swift = self.get_object_client()
        mistral = self.get_workflow_client()

        error_text = None

        try:
            mapobject = swift.get_object(self.container,
                                         'capabilities-map.yaml')[1]
            mapfile = yaml.safe_load(mapobject)

            mistral_env = mistral.environments.get(self.container)

            # We always want the root template to match whatever is in the
            # capabilities map, so update that regardless.
            mistral_env.variables['root_template'] = mapfile['root_template']

            # Check to see if the root environment is already listed, if it
            # isn't - add it.
            # TODO(d0ugal): Users could get into a situation where they have
            #               an old root environment still added and they
            #               then have two after the new one is added.
            root_env = {'path': mapfile['root_environment']}
            if root_env not in mistral_env.variables['environments']:
                mistral_env.variables['environments'].insert(0, root_env)

            mistral.environments.update(
                name=self.container,
                variables=mistral_env.variables,
            )
        except yaml.YAMLError as yaml_err:
            error_text = "Error parsing the yaml file: %s" % yaml_err
        except swiftexceptions.ClientException as obj_err:
            error_text = "File missing from container: %s" % obj_err
        except KeyError as key_err:
            error_text = ("capabilities-map.yaml missing key: "
                          "%s" % key_err)
        if error_text:
            return mistral_workflow_utils.Result(error=error_text)


class ListPlansAction(base.TripleOAction):
    """Lists deployment plans

    This action lists all deployment plans residing in the undercloud.  A
    deployment plan consists of a container marked with metadata
    'x-container-meta-usage-tripleo' and a mistral environment with the same
    name as the container.
    """

    def run(self):
        # plans consist of a container object and mistral environment
        # with the same name.  The container is marked with metadata
        # to ensure it isn't confused with another container
        plan_list = []
        oc = self.get_object_client()
        mc = self.get_workflow_client()
        for item in oc.get_account()[1]:
            container = oc.get_container(item['name'])[0]
            if constants.TRIPLEO_META_USAGE_KEY in container.keys():
                plan_list.append(item['name'])
        return list(set(plan_list).intersection(
            [env.name for env in mc.environments.list()]))


class DeletePlanAction(base.TripleOAction):
    """Deletes a plan and associated files

    Deletes a plan by deleting the container matching plan_name. It
    will not delete the plan if a stack exists with the same name.

    Raises StackInUseError if a stack with the same name as plan_name
    exists.
    """

    def __init__(self, container):
        super(DeletePlanAction, self).__init__()
        self.container = container

    def run(self):
        error_text = None
        # heat throws HTTPNotFound if the stack is not found
        try:
            stack = self.get_orchestration_client().stacks.get(self.container)
        except heatexceptions.HTTPNotFound:
            pass
        else:
            if stack is not None:
                raise exception.StackInUseError(name=self.container)

        try:
            swift = self.get_object_client()
            swiftutils.delete_container(swift, self.container)

            # if mistral environment exists, delete it too
            mistral = self.get_workflow_client()
            if self.container in [env.name for env in
                                  mistral.environments.list()]:
                # deletes environment
                mistral.environments.delete(self.container)
        except swiftexceptions.ClientException as ce:
            LOG.exception("Swift error deleting plan.")
            error_text = ce.msg
        except Exception as err:
            LOG.exception("Error deleting plan.")
            error_text = six.text_type(err)

        if error_text:
            return mistral_workflow_utils.Result(error=error_text)


class ListRolesAction(base.TripleOAction):
    """Returns a deployment plan's roles

    Parses overcloud.yaml and returns the Heat resources where
    type = OS::Heat::ResourceGroup

    :param container: name of the Swift container / plan name
    :return: list of roles in the container's deployment plan
    """

    def __init__(self, container=constants.DEFAULT_CONTAINER_NAME):
        super(ListRolesAction, self).__init__()
        self.container = container

    def run(self):
        try:
            mc = self.get_workflow_client()
            mistral_env = mc.environments.get(self.container)
            template_name = mistral_env.variables['template']

            oc = self.get_object_client()
            resources = yaml.safe_load(
                oc.get_object(self.container, template_name)[1])['resources']
        except Exception as mistral_err:
            err_msg = ("Error retrieving deployment plan: %s"
                       % mistral_err)
            LOG.exception(err_msg)
            return mistral_workflow_utils.Result(error=err_msg)

        roles = []
        for resource, details in resources.items():
            if details['type'] == constants.RESOURCE_GROUP_TYPE:
                roles.append(resource)
        return roles
