#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4
#
# Copyright (C) 2010, 2011, 2012, 2013 Intel, Inc.
#
#    This program is free software; you can redistribute it and/or
#    modify it under the terms of the GNU General Public License
#    as published by the Free Software Foundation;version 2 of the License.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
"""
Access to backend data stored in Redis

Example of usage:
 dbobj = BackendDB()
 # Get repositories as a dict-like object
 repos = dbobj.get_repos()
 # Use it
 print repo['Release'], repo['latest_snapshot']
 # Change latest snapshot
 repo['latest_snapshot'] = 'product.123456.7'
 dbobj.set_repo('Project-Main', repo)
"""

import os
import redis
import yaml
import json
from copy import deepcopy

class EntityError(Exception):
    """Entity custom exception."""
    pass

class BackendDBError(Exception):
    """BackendDB custom exception."""
    pass

class Entity(object):
    """
    Generic dict-like access to any type of
    entities: repos, mappings, etc.

    Usage example:
        bdb = BackendDB()
        repos = bdb.get_repos() # get entitiy object
        if 'Myrepo' in repos:
            print repos['Myrepo'], repos['Myrepo']['latest_snapthot']
            repos.delete('Myrepo')
        else:
            repos['Myrepo'] = {'name': 'Myreponame', 'latest_snapshot': '123.4'}
    """

    def __init__(self, dbobj, prefix, jsoned=None):
        """__init__
           Args:
               dbobj - Redis database object
               prefix - Uniq prefix for this entity
               jsoned - list of jsoned keys(for dicts)
                        or indexes (for iterables) or bool (for strings).
                        It's used to convert data to json
                        format when storing in db and convert it back when
                        it's retrived.
        """
        self._db = dbobj
        self._prefix = prefix
        self.jsoned = jsoned or []

    def keys(self):
        """Get set of keys started with the self._prefix."""
        return set([key.split(self._prefix)[1] \
                        for key in self._db.keys("%s*" % self._prefix)])

    def __contains__(self, key):
        return self._db.exists('%s%s' % (self._prefix, key))

    def __getitem__(self, key):
        key = "%s%s" % (self._prefix, key)
        if self._db.type(key) == 'hash':
            value = deepcopy(self._db.hgetall(key))
            for field in self.jsoned:
                value[field] = json.loads(value[field])
        elif self._db.type(key) == 'list':
            value = self._db.lrange(key, 0, -1)
            for i in self.jsoned:
                value[i] = json.loads(value[i])
        elif self._db.type(key) == 'string':
            value = self._db.get(key)
            if self.jsoned:
                value = json.loads(value)
        elif self._db.type(key) == 'none':
            raise EntityError("[Error] key %s don't exist in redis" % key)
        else:
            raise EntityError("[Error] don't support \"%s\" data in redis" \
                                 % self._db.type(key))

        return value

    def __setitem__(self, key, value):
        key = "%s%s" % (self._prefix, key)
        if isinstance(value, dict):
            mvalue = deepcopy(value)
            # delete fields which are not present in new value
            for field in self._db.hgetall(key).keys():
                if field not in mvalue:
                    self._db.hdel(key, field)
            # dump jsoned fields
            for field in self.jsoned:
                mvalue[field] = json.dumps(value[field])
            self._db.hmset(key, mvalue)
        elif isinstance(value, list):
            mvalue = deepcopy(value)
            for i in self.jsoned:
                mvalue[i] = json.dumps(mvalue[i])
            for item in mvalue:
                self._db.rpush(key, item)
        elif isinstance(value, str):
            self._db.set(key, value)
        else:
            raise EntityError("[Error] don't support \"%s\" data in redis" \
                                 % type(value))

    def delete(self, key):
        """Remove key starting with the prefix from the db."""
        self._db.delete("%s%s" % (self._prefix, key))

    def __iter__(self):
        return iter(self.keys())

    def __len__(self):
        return len(self.keys())

class BackendDB():
    """
    Access and maintain backend data stored in Redis.

    Data structure:
       'repo:<name>' hash contains attributes of the repository and their values

    # Future plans: develop similar set of APIs for git->obs mappings and other
                    backend-related data
       for example, for git->obs mappings it would look like this:
       'git_obs_mapping:<name>' hash contains attributes of the mapping
                                and their values
       read_gitobs_mappings - read git->obs mappings from the file
       get_gitobs_mappings - get mapings as a dict-like object
       the rest is the same as for repos as it's implemented in Entity API
    """

    def __init__(self, host='localhost', port=6379):
        try:
            self._redis = redis.Redis(host=host, port=port)
            # try to do a simple query to confirm connection
            self._redis.exists('anything')
        except redis.RedisError, ex:
            raise BackendDBError('[Error] cannot connect to redis server: %s' \
                                 % str(ex))

    def get_repos(self):
        """Return repos entity object."""
        return Entity(self._redis, "repo:", ["Targets"])

    def read_repos_dryrun(self, yamlobj, extra_files=[]):
        try:
            repos = yaml.load(yamlobj)["Repositories"]
            if extra_files is not None and isinstance(extra_files, list):
                for df in extra_files:
                    print 'Appending %s' % os.path.basename(df)
                    repos.append(yaml.load(file(df, 'r').read()))
            for repo in repos:
                if repo.has_key('Target') or not repo.has_key('Targets'):
                    continue
                repo.pop('Name')
                repo['Project']
        except Exception as err:
            return '\n\n%s' % repr(err)
        return ''

    def read_repos(self, yamlobj, extra_files=[]):
        """
        Read repos from repos.yaml.
        Args:
            yamlobj (str or file object): Content of repos.yaml
                                          or its file object
            extra_files: Path list of extra files to append

        Raises: BackendDBError when can't load yaml

        """
        try:
            repos = yaml.load(yamlobj)["Repositories"]
            if extra_files is not None and isinstance(extra_files, list):
                for df in extra_files:
                    print 'Appending %s' % os.path.basename(df)
                    repos.append(yaml.load(file(df, 'r').read()))
        except (yaml.YAMLError, TypeError), err:
            raise BackendDBError("Error loading yaml: %s" % err)

        db_repos = self.get_repos()
        db_obsrepomap = self.get_obs_repo_map()

        # save set of repos and mappings before loading new repos
        db_list = db_repos.keys()
        db_map_list = db_obsrepomap.keys()

        names = set()
        mapkeys = set()
        for repo in repos:
            # Only support new format with keyword 'Targets'. Ignore origin
            # format with keyword 'Target'.
            if repo.has_key('Target') or not repo.has_key('Targets'):
                continue
            name = repo.pop('Name')
            names.add(name)
            db_repos[name] = repo

            mapkey = "%s" % repo['Project']
            mapkeys.add(mapkey)
            db_obsrepomap[mapkey] = name

        # Cleanup old entries from repos
        for name in db_list.difference(names):
            db_repos.delete(name)
        # and from obsrepomaps
        for mapping in db_map_list.difference(mapkeys):
            db_obsrepomap.delete(mapping)


    def get_release_ids(self):
        """Return release id entity object."""
        return Entity(self._redis, "releaseid:")

    def get_obs_repo_map(self):
        """Return obs project->repo mapping entity"""
        return Entity(self._redis, "obsrepomap:")

    def get_releases(self):
        """Return releases entity"""
        return Entity(self._redis, "release:")
