# Copyright 2014-2015 Canonical Limited.
#
# 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 six

from charmhelpers.core.hookenv import relation_id as current_relation_id
from charmhelpers.core.hookenv import (
    is_relation_made,
    relation_ids,
    relation_get as _relation_get,
    local_unit,
    relation_set as _relation_set,
    leader_get as _leader_get,
    leader_set,
    is_leader,
)


"""
This helper provides functions to support use of a peer relation
for basic key/value storage, with the added benefit that all storage
can be replicated across peer units.

Requirement to use:

To use this, the "peer_echo()" method has to be called form the peer
relation's relation-changed hook:

@hooks.hook("cluster-relation-changed") # Adapt the to your peer relation name
def cluster_relation_changed():
    peer_echo()

Once this is done, you can use peer storage from anywhere:

@hooks.hook("some-hook")
def some_hook():
    # You can store and retrieve key/values this way:
    if is_relation_made("cluster"):  # from charmhelpers.core.hookenv
        # There are peers available so we can work with peer storage
        peer_store("mykey", "myvalue")
        value = peer_retrieve("mykey")
        print value
    else:
        print "No peers joind the relation, cannot share key/values :("
"""


def leader_get(attribute=None, rid=None):
    """Wrapper to ensure that settings are migrated from the peer relation.

    This is to support upgrading an environment that does not support
    Juju leadership election to one that does.

    If a setting is not extant in the leader-get but is on the relation-get
    peer rel, it is migrated and marked as such so that it is not re-migrated.
    """
    migration_key = '__leader_get_migrated_settings__'
    if not is_leader():
        return _leader_get(attribute=attribute)

    settings_migrated = False
    leader_settings = _leader_get(attribute=attribute)
    previously_migrated = _leader_get(attribute=migration_key)

    if previously_migrated:
        migrated = set(json.loads(previously_migrated))
    else:
        migrated = set([])

    try:
        if migration_key in leader_settings:
            del leader_settings[migration_key]
    except TypeError:
        pass

    if attribute:
        if attribute in migrated:
            return leader_settings

        # If attribute not present in leader db, check if this unit has set
        # the attribute in the peer relation
        if not leader_settings:
            peer_setting = _relation_get(attribute=attribute, unit=local_unit(),
                                         rid=rid)
            if peer_setting:
                leader_set(settings={attribute: peer_setting})
                leader_settings = peer_setting

        if leader_settings:
            settings_migrated = True
            migrated.add(attribute)
    else:
        r_settings = _relation_get(unit=local_unit(), rid=rid)
        if r_settings:
            for key in set(r_settings.keys()).difference(migrated):
                # Leader setting wins
                if not leader_settings.get(key):
                    leader_settings[key] = r_settings[key]

                settings_migrated = True
                migrated.add(key)

            if settings_migrated:
                leader_set(**leader_settings)

    if migrated and settings_migrated:
        migrated = json.dumps(list(migrated))
        leader_set(settings={migration_key: migrated})

    return leader_settings


def relation_set(relation_id=None, relation_settings=None, **kwargs):
    """Attempt to use leader-set if supported in the current version of Juju,
    otherwise falls back on relation-set.

    Note that we only attempt to use leader-set if the provided relation_id is
    a peer relation id or no relation id is provided (in which case we assume
    we are within the peer relation context).
    """
    try:
        if relation_id in relation_ids('cluster'):
            return leader_set(settings=relation_settings, **kwargs)
        else:
            raise NotImplementedError
    except NotImplementedError:
        return _relation_set(relation_id=relation_id,
                             relation_settings=relation_settings, **kwargs)


def relation_get(attribute=None, unit=None, rid=None):
    """Attempt to use leader-get if supported in the current version of Juju,
    otherwise falls back on relation-get.

    Note that we only attempt to use leader-get if the provided rid is a peer
    relation id or no relation id is provided (in which case we assume we are
    within the peer relation context).
    """
    try:
        if rid in relation_ids('cluster'):
            return leader_get(attribute, rid)
        else:
            raise NotImplementedError
    except NotImplementedError:
        return _relation_get(attribute=attribute, rid=rid, unit=unit)


def peer_retrieve(key, relation_name='cluster'):
    """Retrieve a named key from peer relation `relation_name`."""
    cluster_rels = relation_ids(relation_name)
    if len(cluster_rels) > 0:
        cluster_rid = cluster_rels[0]
        return relation_get(attribute=key, rid=cluster_rid,
                            unit=local_unit())
    else:
        raise ValueError('Unable to detect'
                         'peer relation {}'.format(relation_name))


def peer_retrieve_by_prefix(prefix, relation_name='cluster', delimiter='_',
                            inc_list=None, exc_list=None):
    """ Retrieve k/v pairs given a prefix and filter using {inc,exc}_list """
    inc_list = inc_list if inc_list else []
    exc_list = exc_list if exc_list else []
    peerdb_settings = peer_retrieve('-', relation_name=relation_name)
    matched = {}
    if peerdb_settings is None:
        return matched
    for k, v in peerdb_settings.items():
        full_prefix = prefix + delimiter
        if k.startswith(full_prefix):
            new_key = k.replace(full_prefix, '')
            if new_key in exc_list:
                continue
            if new_key in inc_list or len(inc_list) == 0:
                matched[new_key] = v
    return matched


def peer_store(key, value, relation_name='cluster'):
    """Store the key/value pair on the named peer relation `relation_name`."""
    cluster_rels = relation_ids(relation_name)
    if len(cluster_rels) > 0:
        cluster_rid = cluster_rels[0]
        relation_set(relation_id=cluster_rid,
                     relation_settings={key: value})
    else:
        raise ValueError('Unable to detect '
                         'peer relation {}'.format(relation_name))


def peer_echo(includes=None, force=False):
    """Echo filtered attributes back onto the same relation for storage.

    This is a requirement to use the peerstorage module - it needs to be called
    from the peer relation's changed hook.

    If Juju leader support exists this will be a noop unless force is True.
    """
    try:
        is_leader()
    except NotImplementedError:
        pass
    else:
        if not force:
            return  # NOOP if leader-election is supported

    # Use original non-leader calls
    relation_get = _relation_get
    relation_set = _relation_set

    rdata = relation_get()
    echo_data = {}
    if includes is None:
        echo_data = rdata.copy()
        for ex in ['private-address', 'public-address']:
            if ex in echo_data:
                echo_data.pop(ex)
    else:
        for attribute, value in six.iteritems(rdata):
            for include in includes:
                if include in attribute:
                    echo_data[attribute] = value
    if len(echo_data) > 0:
        relation_set(relation_settings=echo_data)


def peer_store_and_set(relation_id=None, peer_relation_name='cluster',
                       peer_store_fatal=False, relation_settings=None,
                       delimiter='_', **kwargs):
    """Store passed-in arguments both in argument relation and in peer storage.

    It functions like doing relation_set() and peer_store() at the same time,
    with the same data.

    @param relation_id: the id of the relation to store the data on. Defaults
                        to the current relation.
    @param peer_store_fatal: Set to True, the function will raise an exception
                             should the peer storage not be available."""

    relation_settings = relation_settings if relation_settings else {}
    relation_set(relation_id=relation_id,
                 relation_settings=relation_settings,
                 **kwargs)
    if is_relation_made(peer_relation_name):
        for key, value in six.iteritems(dict(list(kwargs.items()) +
                                             list(relation_settings.items()))):
            key_prefix = relation_id or current_relation_id()
            peer_store(key_prefix + delimiter + key,
                       value,
                       relation_name=peer_relation_name)
    else:
        if peer_store_fatal:
            raise ValueError('Unable to detect '
                             'peer relation {}'.format(peer_relation_name))
