# -*- coding: utf-8 -*-
'''
Clone a remote SVN repository and use the filesystem as a Pillar source

This external Pillar source can be configured in the master config file like
so:

.. code-block:: yaml

    ext_pillar:
      - svn: trunk svn://svnserver/repo root=subdirectory

The `root=` parameter is optional and used to set the subdirectory from where
to look for Pillar files (such as ``top.sls``).

.. versionchanged:: 2014.7.0
    The optional ``root`` parameter will be added.

Note that this is not the same thing as configuring pillar data using the
:conf_master:`pillar_roots` parameter. The branch referenced in the
:conf_master:`ext_pillar` entry above (``master``), would evaluate to the
``base`` environment, so this branch needs to contain a ``top.sls`` with a
``base`` section in it, like this:

.. code-block:: yaml

    base:
      '*':
        - foo

To use other environments from the same SVN repo as svn_pillar sources, just
add additional lines, like so:

.. code-block:: yaml

    ext_pillar:
      - svn: trunk svn://svnserver/repo
      - svn: dev svn://svnserver/repo

In this case, the ``dev`` branch would need its own ``top.sls`` with a ``dev``
section in it, like this:

.. code-block:: yaml

    dev:
      '*':
        - bar
'''
from __future__ import absolute_import

# Import python libs
from copy import deepcopy
import logging
import os
import hashlib

# Import third party libs
HAS_SVN = False
try:
    import pysvn
    HAS_SVN = True
    CLIENT = pysvn.Client()
except ImportError:
    pass

# Import salt libs
from salt.pillar import Pillar

# Set up logging
log = logging.getLogger(__name__)

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


def __virtual__():
    ext_pillar_sources = [x for x in __opts__.get('ext_pillar', [])]
    if not any(['svn' in x for x in ext_pillar_sources]):
        return False
    if not HAS_SVN:
        log.error('SVN-based ext_pillar is enabled in configuration but '
                  'could not be loaded, is pysvn installed?')
        return False
    return __virtualname__


class SvnPillar(object):
    '''
    Deal with the remote SVN repository for Pillar
    '''

    def __init__(self, branch, repo_location, root, opts):
        '''
        Try to initialize the SVN repo object
        '''
        repo_hash = hashlib.md5(repo_location).hexdigest()
        repo_dir = os.path.join(opts['cachedir'], 'pillar_svnfs', repo_hash)

        self.branch = branch
        self.root = root
        self.repo_dir = repo_dir
        self.repo_location = repo_location

        if not os.path.isdir(repo_dir):
            os.makedirs(repo_dir)
            log.debug('Checking out fileserver for svn_pillar module')
            try:
                CLIENT.checkout(repo_location, repo_dir)
            except pysvn.ClientError:
                log.error('Failed to initialize svn_pillar {0} {1}'.format(repo_location, repo_dir))

    def update(self):
        try:
            log.debug('Updating fileserver for svn_pillar module')
            CLIENT.update(self.repo_dir)
        except pysvn.ClientError as exc:
            log.error('Unable to fetch the latest changes from remote '
                      '{0}: {1}'.format(self.repo_location, exc))

    def pillar_dir(self):
        '''
        Returns the directory of the pillars (repo cache + branch + root)
        '''
        repo_dir = self.repo_dir
        root = self.root
        branch = self.branch
        if branch == 'trunk' or branch == 'base':
            working_dir = os.path.join(repo_dir, 'trunk', root)
            if not os.path.isdir(working_dir):
                log.error('Could not find {0}/trunk/{1}'.format(self.repo_location, root))
            else:
                return os.path.normpath(working_dir)
        working_dir = os.path.join(repo_dir, 'branches', branch, root)
        if os.path.isdir(working_dir):
            return os.path.normpath(working_dir)
        working_dir = os.path.join(working_dir, 'tags', branch, root)
        if os.path.isdir(working_dir):
            return os.path.normpath(working_dir)
        log.error('Could not find {0}/branches/{1}/{2}'.format(self.repo_location, branch, root))
        return repo_dir


def _extract_key_val(kv, delimiter='='):
    '''Extract key and value from key=val string.

    Example:
    >>> _extract_key_val('foo=bar')
    ('foo', 'bar')
    '''
    pieces = kv.split(delimiter)
    key = pieces[0]
    val = delimiter.join(pieces[1:])
    return key, val


def ext_pillar(minion_id,
               pillar,  # pylint: disable=W0613
               repo_string):
    '''
    Execute a command and read the output as YAML
    '''
    # split the branch, repo name and optional extra (key=val) parameters.
    options = repo_string.strip().split()
    branch = options[0]
    repo_location = options[1]
    root = ''

    for extraopt in options[2:]:
        # Support multiple key=val attributes as custom parameters.
        DELIM = '='
        if DELIM not in extraopt:
            log.error('Incorrectly formatted extra parameter. '
                      'Missing {0!r}: {1}'.format(DELIM, extraopt))
        key, val = _extract_key_val(extraopt, DELIM)
        if key == 'root':
            root = val
        else:
            log.warning('Unrecognized extra parameter: {0}'.format(key))

    svnpil = SvnPillar(branch, repo_location, root, __opts__)

    # environment is "different" from the branch
    branch = (branch == 'trunk' and 'base' or branch)

    pillar_dir = svnpil.pillar_dir()
    log.debug("[pillar_roots][{0}] = {1}".format(branch, pillar_dir))

    # Don't recurse forever-- the Pillar object will re-call the ext_pillar
    # function
    if __opts__['pillar_roots'].get(branch, []) == [pillar_dir]:
        return {}
    svnpil.update()
    opts = deepcopy(__opts__)
    opts['pillar_roots'][branch] = [pillar_dir]
    pil = Pillar(opts, __grains__, minion_id, branch)
    return pil.compile_pillar()
