# Copyright 2017 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 time
import uuid

from heatclient.common import template_utils
from heatclient import exc as heat_exc
from swiftclient import exceptions as swiftexceptions

from tripleo_common import constants
from tripleo_common import update
from tripleo_common.utils import plan as plan_utils
from tripleo_common.utils import template as templates

LOG = logging.getLogger(__name__)


def stack_update(swift, heat, timeout,
                 container=constants.DEFAULT_CONTAINER_NAME):
    # get the stack. Error if doesn't exist
    try:
        stack = heat.stacks.get(container)
    except heat_exc.HTTPNotFound:
        msg = "Error retrieving stack: %s" % container
        LOG.exception(msg)
        raise RuntimeError(msg)

    try:
        env = plan_utils.get_env(swift, container)
    except swiftexceptions.ClientException as err:
        err_msg = ("Error retrieving environment for plan %s: %s" % (
            container, err))
        LOG.exception(err_msg)
        raise RuntimeError(err_msg)

    update_env = {
        'parameter_defaults': {
            'DeployIdentifier': int(time.time()),
        },
    }

    noop_env = {
        'resource_registry': {
            'OS::TripleO::DeploymentSteps': 'OS::Heat::None',
        },
    }

    for output in stack.to_dict().get('outputs', {}):
        if output['output_key'] == 'RoleData':
            for role in output['output_value']:
                role_env = {
                    "OS::TripleO::Tasks::%sPreConfig" % role:
                    'OS::Heat::None',
                    "OS::TripleO::Tasks::%sPostConfig" % role:
                    'OS::Heat::None',
                }
                noop_env['resource_registry'].update(role_env)
    update_env.update(noop_env)
    template_utils.deep_update(env, update_env)
    try:
        plan_utils.put_env(swift, env)
    except swiftexceptions.ClientException as err:
        err_msg = ("Error updating environment for plan %s: %s" % (
            container, err))
        LOG.exception(err_msg)
        raise RuntimeError(err_msg)

    # process all plan files and create or update a stack
    processed_data = templates.process_templates(
        swift, heat, container
    )
    stack_args = processed_data.copy()
    stack_args['timeout_mins'] = timeout

    LOG.info("Performing Heat stack update")
    LOG.info('updating stack: %s', stack.stack_name)
    return heat.stacks.update(stack.id, **stack_args)


def _process_params(flattened, params):
    for item in params:
        if item not in flattened['parameters']:
            param_obj = {}
            for key, value in params.get(item).items():
                camel_case_key = key[0].lower() + key[1:]
                param_obj[camel_case_key] = value
            param_obj['name'] = item
            flattened['parameters'][item] = param_obj
    return list(params)


def _flat_it(flattened, name, data):
    key = str(uuid.uuid4())
    value = {}
    value.update({
        'name': name,
        'id': key
    })
    if 'Type' in data:
        value['type'] = data['Type']
    if 'Description' in data:
        value['description'] = data['Description']
    if 'Parameters' in data:
        value['parameters'] = _process_params(flattened,
                                              data['Parameters'])
    if 'ParameterGroups' in data:
        value['parameter_groups'] = data['ParameterGroups']
    if 'NestedParameters' in data:
        nested = data['NestedParameters']
        nested_ids = []
        for nested_key in nested.keys():
            nested_data = _flat_it(flattened, nested_key,
                                   nested.get(nested_key))
            # nested_data will always have one key (and only one)
            nested_ids.append(list(nested_data)[0])

        value['resources'] = nested_ids

    flattened['resources'][key] = value
    return {key: value}


def validate_stack_and_flatten_parameters(heat, processed_data, env):
    params = env.get('parameter_defaults')
    fields = {
        'template': processed_data['template'],
        'files': processed_data['files'],
        'environment': processed_data['environment'],
        'show_nested': True
    }

    processed_data = {
        'heat_resource_tree': heat.stacks.validate(**fields),
        'environment_parameters': params,
    }

    if processed_data['heat_resource_tree']:
        flattened = {'resources': {}, 'parameters': {}}
        _flat_it(flattened, 'Root',
                 processed_data['heat_resource_tree'])
        processed_data['heat_resource_tree'] = flattened
    return processed_data


def deploy_stack(swift, heat, container, skip_deploy_identifier=False,
                 timeout_mins=240):
    try:
        stack = heat.stacks.get(container, resolve_outputs=False)
    except heat_exc.HTTPNotFound:
        stack = None

    stack_is_new = stack is None

    # update StackAction, DeployIdentifier

    parameters = dict()
    if not skip_deploy_identifier:
        parameters['DeployIdentifier'] = int(time.time())
    else:
        parameters['DeployIdentifier'] = ''

    parameters['StackAction'] = 'CREATE' if stack_is_new else 'UPDATE'

    try:
        env = plan_utils.get_env(swift, container)
    except swiftexceptions.ClientException as err:
        err_msg = ("Error retrieving environment for plan %s: %s" % (
            container, err))
        LOG.exception(err_msg)
        raise RuntimeError(err_msg)

    set_tls_parameters(parameters, env)
    try:
        plan_utils.update_in_env(swift, env, 'parameter_defaults',
                                 parameters)
    except swiftexceptions.ClientException as err:
        err_msg = ("Error updating environment for plan %s: %s" % (
            container, err))
        LOG.exception(err_msg)
        raise RuntimeError(err_msg)

    if not stack_is_new:
        try:
            LOG.debug('Checking for compatible neutron mechanism drivers')
            msg = update.check_neutron_mechanism_drivers(env, stack,
                                                         swift,
                                                         container)
            if msg:
                raise RuntimeError(msg)
        except swiftexceptions.ClientException as err:
            err_msg = ("Error getting template %s: %s" % (
                container, err))
            LOG.exception(err_msg)
            raise RuntimeError(err_msg)

    # process all plan files and create or update a stack
    processed_data = templates.process_templates(
        swift, heat, container=container,
        prune_services=True
    )
    stack_args = processed_data.copy()
    stack_args['timeout_mins'] = timeout_mins

    if stack_is_new:
        try:
            swift.copy_object(
                "%s-swift-rings" % container, "swift-rings.tar.gz",
                "%s-swift-rings/%s-%d" % (
                    container, "swift-rings.tar.gz", time.time()))
            swift.delete_object(
                "%s-swift-rings" % container, "swift-rings.tar.gz")
        except swiftexceptions.ClientException:
            pass
        LOG.info("Perfoming Heat stack create")
        try:
            return heat.stacks.create(**stack_args)
        except heat_exc.HTTPException as err:
            err_msg = "Error during stack creation: %s" % (err,)
            LOG.exception(err_msg)
            raise RuntimeError(err_msg)

    LOG.info("Performing Heat stack update")
    stack_args['existing'] = 'true'
    try:
        return heat.stacks.update(stack.id, **stack_args)
    except heat_exc.HTTPException as err:
        err_msg = "Error during stack update: %s" % (err,)
        LOG.exception(err_msg)
        raise RuntimeError(err_msg)


def set_tls_parameters(parameters, env,
                       local_ca_path=constants.LOCAL_CACERT_PATH):

    def get_camap():
        return env['parameter_defaults'].get('CAMap', {})

    def get_updated_camap_entry(entry_name, cacert, orig_camap):
        ca_map_entry = {
            entry_name: {
                'content': cacert
            }
        }
        orig_camap.update(ca_map_entry)
        return orig_camap

    cacert_string = get_local_cacert(local_ca_path)
    if cacert_string:
        parameters['CAMap'] = get_updated_camap_entry(
            'undercloud-ca', cacert_string, get_camap())


def get_local_cacert(local_ca_path):
    # Since the undercloud has TLS by default, we'll add the undercloud's
    # CA to be trusted by the overcloud.
    try:
        with open(local_ca_path, 'rb') as ca_file:
            return ca_file.read().decode('utf-8')
    except IOError:
        # If the file wasn't found it means that the undercloud's TLS
        # was explicitly disabled or another CA is being used. So we'll
        # let the user handle this.
        return None
    except Exception:
        raise


def preview_stack_and_network_configs(heat, processed_data,
                                      container, role_name):
    # stacks.preview method raises validation message if stack is
    # already deployed. here renaming container to get preview data.
    container_temp = container + "-TEMP"
    fields = {
        'template': processed_data['template'],
        'files': processed_data['files'],
        'environment': processed_data['environment'],
        'stack_name': container_temp,
    }
    preview_data = heat.stacks.preview(**fields)

    try:
        stack = heat.stacks.get(container_temp)
    except heat_exc.HTTPNotFound:
        msg = "Error retrieving stack: %s" % container_temp
        LOG.exception(msg)
        raise RuntimeError(msg)

    return get_network_config(preview_data, stack.stack_name, role_name)


def get_network_config(preview_data, stack_name, role_name):
    result = None
    if preview_data:
        for res in preview_data.resources:
            net_script = process_preview_list(res, stack_name,
                                              role_name)
            if net_script:
                ns_len = len(net_script)
                start_index = (net_script.find(
                    "echo '{\"network_config\"", 0, ns_len) + 6)
                # In file network/scripts/run-os-net-config.sh
                end_str = "' > /etc/os-net-config/config.json"
                end_index = net_script.find(end_str, start_index, ns_len)
                if (end_index > start_index):
                    net_config = net_script[start_index:end_index]
                    if net_config:
                        result = json.loads(net_config)
                break
    if not result:
        err_msg = ("Unable to determine network config for role '%s'."
                   % role_name)
        LOG.exception(err_msg)
        raise RuntimeError(err_msg)
    return result


def process_preview_list(res, stack_name, role_name):
    if type(res) == list:
        for item in res:
            out = process_preview_list(item, stack_name, role_name)
            if out:
                return out
    elif type(res) == dict:
        res_stack_name = stack_name + '-' + role_name
        if res['resource_name'] == "OsNetConfigImpl" and \
            res['resource_identity'] and \
            res_stack_name in res['resource_identity']['stack_name']:
            return res['properties']['config']
    return None
