# -*- coding: utf-8 -*-
'''
Return data to a redis server

To enable this returner the minion will need the python client for redis
installed and the following values configured in the minion or master
config, these are the defaults:

.. code-block:: yaml

    redis.db: '0'
    redis.host: 'salt'
    redis.port: 6379

Alternative configuration values can be used by prefacing the configuration.
Any values not found in the alternative configuration will be pulled from
the default location:

.. code-block:: yaml

    alternative.redis.db: '0'
    alternative.redis.host: 'salt'
    alternative.redis.port: 6379

To use the redis returner, append '--return redis' to the salt command.

.. code-block:: bash

    salt '*' test.ping --return redis

To use the alternative configuration, append '--return_config alternative' to the salt command.

.. versionadded:: 2015.5.0

.. code-block:: bash

    salt '*' test.ping --return redis --return_config alternative

To override individual configuration items, append --return_kwargs '{"key:": "value"}' to the salt command.

.. versionadded:: 2016.3.0

.. code-block:: bash

    salt '*' test.ping --return redis --return_kwargs '{"db": "another-salt"}'

'''

# Import python libs
from __future__ import absolute_import
import json

# Import Salt libs
import salt.ext.six as six
import salt.utils
import salt.utils.jid
import salt.returners

# Import third party libs
try:
    import redis
    HAS_REDIS = True
except ImportError:
    HAS_REDIS = False

# Define the module's virtual name
__virtualname__ = 'redis'


def __virtual__():
    if not HAS_REDIS:
        return False, 'Could not import redis returner; ' \
                      'redis python client is not installed.'
    return __virtualname__


def _get_options(ret=None):
    '''
    Get the redis options from salt.
    '''
    attrs = {'host': 'host',
             'port': 'port',
             'db': 'db'}

    if salt.utils.is_proxy():
        return {
            'host': __opts__.get('redis.host', 'salt'),
            'port': __opts__.get('redis.port', 6379),
            'db': __opts__.get('redis.db', '0')
        }

    _options = salt.returners.get_returner_options(__virtualname__,
                                                   ret,
                                                   attrs,
                                                   __salt__=__salt__,
                                                   __opts__=__opts__)
    return _options


def _get_serv(ret=None):
    '''
    Return a redis server object
    '''
    _options = _get_options(ret)
    host = _options.get('host')
    port = _options.get('port')
    db = _options.get('db')

    return redis.Redis(
            host=host,
            port=port,
            db=db)


def _get_ttl():
    return __opts__['keep_jobs'] * 3600


def returner(ret):
    '''
    Return data to a redis data store
    '''
    serv = _get_serv(ret)
    pipeline = serv.pipeline(transaction=False)
    minion, jid = ret['id'], ret['jid']
    pipeline.hset('ret:{0}'.format(jid), minion, json.dumps(ret))
    pipeline.expire('ret:{0}'.format(jid), _get_ttl())
    pipeline.set('{0}:{1}'.format(minion, ret['fun']), jid)
    pipeline.sadd('minions', minion)
    pipeline.execute()


def save_load(jid, load, minions=None):
    '''
    Save the load to the specified jid
    '''
    serv = _get_serv(ret=None)
    serv.setex('load:{0}'.format(jid), json.dumps(load), _get_ttl())


def save_minions(jid, minions, syndic_id=None):  # pylint: disable=unused-argument
    '''
    Included for API consistency
    '''
    pass


def get_load(jid):
    '''
    Return the load data that marks a specified jid
    '''
    serv = _get_serv(ret=None)
    data = serv.get('load:{0}'.format(jid))
    if data:
        return json.loads(data)
    return {}


def get_jid(jid):
    '''
    Return the information returned when the specified job id was executed
    '''
    serv = _get_serv(ret=None)
    ret = {}
    for minion, data in six.iteritems(serv.hgetall('ret:{0}'.format(jid))):
        if data:
            ret[minion] = json.loads(data)
    return ret


def get_fun(fun):
    '''
    Return a dict of the last function called for all minions
    '''
    serv = _get_serv(ret=None)
    ret = {}
    for minion in serv.smembers('minions'):
        ind_str = '{0}:{1}'.format(minion, fun)
        try:
            jid = serv.get(ind_str)
        except Exception:
            continue
        if not jid:
            continue
        data = serv.get('{0}:{1}'.format(minion, jid))
        if data:
            ret[minion] = json.loads(data)
    return ret


def get_jids():
    '''
    Return a dict mapping all job ids to job information
    '''
    serv = _get_serv(ret=None)
    ret = {}
    for s in serv.mget(serv.keys('load:*')):
        if s is None:
            continue
        load = json.loads(s)
        jid = load['jid']
        ret[jid] = salt.utils.jid.format_jid_instance(jid, load)
    return ret


def get_minions():
    '''
    Return a list of minions
    '''
    serv = _get_serv(ret=None)
    return list(serv.smembers('minions'))


def clean_old_jobs():
    '''
    Clean out minions's return data for old jobs.

    Normally, hset 'ret:<jid>' are saved with a TTL, and will eventually
    get cleaned by redis.But for jobs with some very late minion return, the
    corresponding hset's TTL will be refreshed to a too late timestamp, we'll
    do manually cleaning here.
    '''
    serv = _get_serv(ret=None)
    living_jids = set(serv.keys('load:*'))
    to_remove = []
    for ret_key in serv.keys('ret:*'):
        load_key = ret_key.replace('ret:', 'load:', 1)
        if load_key not in living_jids:
            to_remove.append(ret_key)
    if len(to_remove) != 0:
        serv.delete(*to_remove)


def prep_jid(nocache=False, passed_jid=None):  # pylint: disable=unused-argument
    '''
    Do any work necessary to prepare a JID, including sending a custom id
    '''
    return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid()
