# 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 json

from heatclient import client as heat_client
from oslo.config import cfg

from sahara import context
from sahara import exceptions as ex
from sahara.i18n import _
from sahara.openstack.common import log as logging
from sahara.utils import files as f
from sahara.utils import general as g
from sahara.utils.openstack import base


CONF = cfg.CONF
LOG = logging.getLogger(__name__)

SSH_PORT = 22


def client():
    ctx = context.current()
    heat_url = base.url_for(ctx.service_catalog, 'orchestration')
    return heat_client.Client('1', heat_url, token=ctx.token)


def get_stack(stack_name):
    heat = client()
    for stack in heat.stacks.list(filters={'stack_name': stack_name}):
        return stack

    raise ex.NotFoundException(_('Failed to find stack %(stack)s')
                               % {'stack': stack_name})


def wait_stack_completion(stack):
    # NOTE: expected empty status because status of stack
    # maybe is not set in heat database
    while stack.status in ['IN_PROGRESS', '']:
        context.sleep(1)
        stack.get()

    if stack.status != 'COMPLETE':
        raise ex.HeatStackException(stack.stack_status)


def _get_inst_name(cluster_name, ng_name, index):
    return g.generate_instance_name(cluster_name, ng_name, index + 1)


def _get_aa_group_name(cluster_name):
    return g.generate_aa_group_name(cluster_name)


def _get_port_name(inst_name):
    return '%s-port' % inst_name


def _get_floating_name(inst_name):
    return '%s-floating' % inst_name


def _get_floating_assoc_name(inst_name):
    return '%s-floating-assoc' % inst_name


def _get_volume_name(inst_name, volume_idx):
    return '%s-volume-%i' % (inst_name, volume_idx)


def _get_volume_attach_name(inst_name, volume_idx):
    return '%s-volume-attachment-%i' % (inst_name, volume_idx)


def _load_template(template_name, fields):
    template_file = f.get_file_text('resources/%s' % template_name)
    return template_file.rstrip() % fields


def _prepare_userdata(userdata):
    """Converts userdata as a text into format consumable by heat template."""

    userdata = userdata.replace('"', '\\"')

    lines = userdata.splitlines()
    return '"' + '",\n"'.join(lines) + '"'


class ClusterTemplate(object):
    def __init__(self, cluster):
        self.cluster = cluster
        self.node_groups_extra = {}

    def add_node_group_extra(self, node_group_id, node_count,
                             gen_userdata_func):
        self.node_groups_extra[node_group_id] = {
            'node_count': node_count,
            'gen_userdata_func': gen_userdata_func
        }

    # Consider using a single Jinja template for all this
    def instantiate(self, update_existing, disable_rollback=True):
        main_tmpl = _load_template('main.heat',
                                   {'resources': self._serialize_resources()})

        heat = client()

        kwargs = {
            'stack_name': self.cluster.name,
            'timeout_mins': 180,
            'disable_rollback': disable_rollback,
            'parameters': {},
            'template': json.loads(main_tmpl)}

        if not update_existing:
            heat.stacks.create(**kwargs)
        else:
            for stack in heat.stacks.list():
                if stack.stack_name == self.cluster.name:
                    stack.update(**kwargs)
                    break

        return ClusterStack(self, get_stack(self.cluster.name))

    def _need_aa_server_group(self, node_group):
        for node_process in node_group.node_processes:
            if node_process in self.cluster.anti_affinity:
                return True
        return False

    def _get_anti_affinity_scheduler_hints(self, node_group):
        if not self._need_aa_server_group(node_group):
            return ''

        return ('"scheduler_hints" : %s,' %
                json.dumps({"group": {"Ref": _get_aa_group_name(
                    self.cluster.name)}}))

    def _serialize_resources(self):
        resources = []

        if self.cluster.anti_affinity:
            resources.extend(self._serialize_aa_server_group())

        for ng in self.cluster.node_groups:
            if ng.auto_security_group:
                resources.extend(self._serialize_auto_security_group(ng))
            for idx in range(0, self.node_groups_extra[ng.id]['node_count']):
                resources.extend(self._serialize_instance(ng, idx))

        return ',\n'.join(resources)

    def _serialize_auto_security_group(self, ng):
        fields = {
            'security_group_name': g.generate_auto_security_group_name(
                ng.cluster.name, ng.name),
            'security_group_description':
            "Auto security group created by Sahara for Node Group "
            "'%s' of cluster '%s'." % (ng.name, ng.cluster.name),
            'rules': self._serialize_auto_security_group_rules(ng)}

        yield _load_template('security_group.heat', fields)

    def _serialize_auto_security_group_rules(self, ng):
        rules = []
        for port in ng.open_ports:
            rules.append({"remote_ip_prefix": "0.0.0.0/0", "protocol": "tcp",
                          "port_range_min": port, "port_range_max": port})

        rules.append({"remote_ip_prefix": "0.0.0.0/0", "protocol": "tcp",
                      "port_range_min": SSH_PORT, "port_range_max": SSH_PORT})

        return json.dumps(rules)

    def _serialize_instance(self, ng, idx):
        inst_name = _get_inst_name(self.cluster.name, ng.name, idx)

        nets = ''
        security_groups = ''
        if CONF.use_neutron:
            port_name = _get_port_name(inst_name)
            yield self._serialize_port(port_name,
                                       self.cluster.neutron_management_network,
                                       self._get_security_groups(ng))

            nets = '"networks" : [{ "port" : { "Ref" : "%s" }}],' % port_name

            if ng.floating_ip_pool:
                yield self._serialize_neutron_floating(inst_name, port_name,
                                                       ng.floating_ip_pool)
        else:
            if ng.floating_ip_pool:
                yield self._serialize_nova_floating(inst_name,
                                                    ng.floating_ip_pool)

            if ng.security_groups:
                security_groups = (
                    '"security_groups": %s,' % json.dumps(
                        self._get_security_groups(ng)))

        # Check if cluster contains user key-pair and include it to template.
        key_name = ''
        if self.cluster.user_keypair_id:
            key_name = '"key_name" : "%s",' % self.cluster.user_keypair_id

        gen_userdata_func = self.node_groups_extra[ng.id]['gen_userdata_func']
        userdata = gen_userdata_func(ng, inst_name)

        fields = {'instance_name': inst_name,
                  'flavor_id': ng.flavor_id,
                  'image_id': ng.get_image_id(),
                  'image_username': ng.image_username,
                  'network_interfaces': nets,
                  'key_name': key_name,
                  'userdata': _prepare_userdata(userdata),
                  'scheduler_hints':
                  self._get_anti_affinity_scheduler_hints(ng),
                  'security_groups': security_groups}

        yield _load_template('instance.heat', fields)

        for idx in range(0, ng.volumes_per_node):
            yield self._serialize_volume(inst_name, idx, ng.volumes_size)

    def _serialize_port(self, port_name, fixed_net_id, security_groups):
        fields = {'port_name': port_name,
                  'fixed_net_id': fixed_net_id,
                  'security_groups': ('"security_groups": %s,' % json.dumps(
                      security_groups) if security_groups else '')}

        return _load_template('neutron-port.heat', fields)

    def _serialize_neutron_floating(self, inst_name, port_name,
                                    floating_net_id):
        fields = {'floating_ip_name': _get_floating_name(inst_name),
                  'floating_net_id': floating_net_id,
                  'port_name': port_name}

        return _load_template('neutron-floating.heat', fields)

    def _serialize_nova_floating(self, inst_name, floating_pool_name):
        fields = {
            'floating_ip_name': _get_floating_name(inst_name),
            'floating_ip_assoc_name': _get_floating_assoc_name(inst_name),
            'instance_name': inst_name,
            'pool': floating_pool_name
        }

        return _load_template('nova-floating.heat', fields)

    def _serialize_volume(self, inst_name, volume_idx, volumes_size):
        fields = {'volume_name': _get_volume_name(inst_name, volume_idx),
                  'volumes_size': volumes_size,
                  'volume_attach_name': _get_volume_attach_name(inst_name,
                                                                volume_idx),
                  'instance_name': inst_name}

        return _load_template('volume.heat', fields)

    def _get_security_groups(self, node_group):
        if not node_group.auto_security_group:
            return node_group.security_groups

        return (list(node_group.security_groups or []) +
                [{"Ref": g.generate_auto_security_group_name(
                    node_group.cluster.name, node_group.name)}])

    def _serialize_aa_server_group(self):
        fields = {'server_group_name': _get_aa_group_name(self.cluster.name)}

        yield _load_template('aa_server_group.heat', fields)


class ClusterStack(object):
    def __init__(self, tmpl, heat_stack):
        self.tmpl = tmpl
        self.heat_stack = heat_stack

    def get_node_group_instances(self, node_group):
        insts = []

        count = self.tmpl.node_groups_extra[node_group.id]['node_count']

        heat = client()
        for i in range(0, count):
            name = _get_inst_name(self.tmpl.cluster.name, node_group.name, i)
            res = heat.resources.get(self.heat_stack.id, name)
            insts.append((name, res.physical_resource_id))

        return insts
