# Copyright (c) 2013 Mirantis, 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 yaml

from keystoneclient import exceptions as ks_exceptions

from murano.common import auth_utils
from murano.common import config
from murano.common import uuidutils
from murano.db import models
from murano.db.services import sessions
from murano.db import session as db_session
from murano.openstack.common import log as logging
from murano.services import states


LOG = logging.getLogger(__name__)
DEFAULT_NETWORK_TYPES = {
    "nova": 'io.murano.resources.NovaNetwork',
    "neutron": 'io.murano.resources.NeutronNetwork'
}


class EnvironmentServices(object):
    @staticmethod
    def get_environments_by(filters):
        """Returns list of environments
           :param filters: property filters
           :return: Returns list of environments
        """
        unit = db_session.get_session()
        environments = unit.query(models.Environment). \
            filter_by(**filters).all()

        for env in environments:
            env['status'] = EnvironmentServices.get_status(env['id'])

        return environments

    @staticmethod
    def get_status(environment_id):
        """Environment can have one of the following statuses:

         - deploying: there is ongoing deployment for environment
         - deleting: environment is currently being deleted
         - deploy failure: last deployment session has failed
         - delete failure: last delete session has failed
         - pending: there is at least one session with status `open` and no
            errors in previous sessions
         - ready: there are no sessions for environment

        :param environment_id: Id of environment for which we checking status.
        :return: Environment status
        """
        # Deploying: there is at least one valid session with status deploying
        session_list = sessions.SessionServices.get_sessions(environment_id)
        has_opened = False
        for session in session_list:
            if session.state == states.SessionState.DEPLOYING:
                return states.EnvironmentStatus.DEPLOYING
            elif session.state == states.SessionState.DELETING:
                return states.EnvironmentStatus.DELETING
            elif session.state == states.SessionState.DEPLOY_FAILURE:
                return states.EnvironmentStatus.DEPLOY_FAILURE
            elif session.state == states.SessionState.DELETE_FAILURE:
                return states.EnvironmentStatus.DELETE_FAILURE
            elif session.state == states.SessionState.OPENED:
                has_opened = True
            elif session.state == states.SessionState.DEPLOYED:
                break
        if has_opened:
            return states.EnvironmentStatus.PENDING

        return states.EnvironmentStatus.READY

    @staticmethod
    def create(environment_params, context):
        # tagging environment by tenant_id for later checks
        """Creates environment with specified params, in particular - name

           :param environment_params: Dict, e.g. {'name': 'env-name'}
           :param context: request context to get the tenant id and the token
           :return: Created Environment
        """
        objects = {'?': {
            'id': uuidutils.generate_uuid(),
        }}
        network_driver = EnvironmentServices.get_network_driver(context)
        objects.update(environment_params)
        if not objects.get('defaultNetworks'):
            objects['defaultNetworks'] = \
                EnvironmentServices.generate_default_networks(objects['name'],
                                                              network_driver)
        objects['?']['type'] = 'io.murano.Environment'
        environment_params['tenant_id'] = context.tenant

        data = {
            'Objects': objects,
            'Attributes': []
        }

        environment = models.Environment()
        environment.update(environment_params)

        unit = db_session.get_session()
        with unit.begin():
            unit.add(environment)

        # saving environment as Json to itself
        environment.update({'description': data})
        environment.save(unit)

        return environment

    @staticmethod
    def delete(environment_id, session_id):
        """Deletes environment and notify orchestration engine about deletion

           :param environment_id: Environment that is going to be deleted
           :param token: OpenStack auth token
        """

        env_description = EnvironmentServices.get_environment_description(
            environment_id, session_id, False)
        env_description['Objects'] = None
        EnvironmentServices.save_environment_description(
            session_id, env_description, False)

    @staticmethod
    def remove(environment_id):
        unit = db_session.get_session()
        environment = unit.query(models.Environment).get(environment_id)
        if environment:
            with unit.begin():
                unit.delete(environment)

    @staticmethod
    def get_environment_description(environment_id, session_id=None,
                                    inner=True):
        """Returns environment description for specified environment.

           If session is specified and not in deploying state function
           returns modified environment description,
           otherwise returns actual environment desc.

           :param environment_id: Environment Id
           :param session_id: Session Id
           :param inner: return contents of environment rather than whole
            Object Model structure
           :return: Environment Description Object
        """
        unit = db_session.get_session()

        if session_id:
            session = unit.query(models.Session).get(session_id)
            if sessions.SessionServices.validate(session):
                if session.state != states.SessionState.DEPLOYED:
                    env_description = session.description
                else:
                    env = unit.query(models.Environment) \
                        .get(session.environment_id)
                    env_description = env.description
            else:
                env = unit.query(models.Environment) \
                    .get(session.environment_id)
                env_description = env.description
        else:
            env = (unit.query(models.Environment).get(environment_id))
            env_description = env.description

        if not inner:
            return env_description
        else:
            return env_description['Objects']

    @staticmethod
    def save_environment_description(session_id, environment, inner=True):
        """Saves environment description to specified session.

           :param session_id: Session Id
           :param environment: Environment Description
           :param inner: save modifications to only content of environment
            rather than whole Object Model structure
        """
        unit = db_session.get_session()
        session = unit.query(models.Session).get(session_id)
        if inner:
            data = session.description.copy()
            data['Objects'] = environment
            session.description = data
        else:
            session.description = environment
        session.save(unit)

    @staticmethod
    def generate_default_networks(env_name, network_driver):
        net_config = config.CONF.find_file(
            config.CONF.networking.network_config_file)
        if net_config:
            LOG.debug("Loading network configuration from file")
            with open(net_config) as f:
                data = yaml.safe_load(f)
                return EnvironmentServices._objectify(data, {
                    'ENV': env_name
                })

        network_type = DEFAULT_NETWORK_TYPES[network_driver]
        LOG.debug("Setting '{0}' as environment's "
                  "default network".format(network_type))
        return {
            'environment': {
                '?': {
                    'id': uuidutils.generate_uuid(),
                    'type': network_type
                },
                'name': env_name + '-network'
            },
            'flat': None
        }

    @staticmethod
    def deploy(session, unit, token):
        environment = unit.query(models.Environment).get(
            session.environment_id)

        if (session.description['Objects'] is None and
                'ObjectsCopy' not in session.description):
            EnvironmentServices.remove(session.environment_id)
        else:
            sessions.SessionServices.deploy(session, environment, unit, token)

    @staticmethod
    def _objectify(data, replacements):
        if isinstance(data, dict):
            if isinstance(data.get('?'), dict):
                data['?']['id'] = uuidutils.generate_uuid()
            result = {}
            for key, value in data.iteritems():
                result[key] = EnvironmentServices._objectify(
                    value, replacements)
            return result
        elif isinstance(data, list):
            return [EnvironmentServices._objectify(v, replacements)
                    for v in data]
        elif isinstance(data, (str, unicode)):
            for key, value in replacements.iteritems():
                data = data.replace('%' + key + '%', value)
        return data

    @staticmethod
    def get_network_driver(context):
        ks = auth_utils.get_client(context.auth_token, context.tenant)
        try:
            ks.service_catalog.url_for(service_type='network')
        except ks_exceptions.EndpointNotFound:
            LOG.debug("Will use NovaNetwork as a network driver")
            return "nova"
        else:
            LOG.debug("Will use Neutron as a network driver")
            return "neutron"
