# -*- coding: utf-8 -*-
"""hooks for vcsfile content types

:organization: Logilab
:copyright: 2007-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
from __future__ import with_statement

__docformat__ = "restructuredtext en"

try:
    from cubicweb import _
except ImportError:
    _ = unicode

import os
import os.path as osp
import errno
import shutil

from logilab.common.textutils import unormalize
from cubicweb import QueryError, ValidationError
from cubicweb.server import hook, session, Service
from cubicweb.predicates import is_instance, score_entity, match_user_groups

from cubes.vcsfile import bridge

def repo_cache_dir(config):
    """Return the directory to use for repo cache

    This function create directory if necessary."""
    directory = config['local-repo-cache-root']
    if not osp.isabs(directory):
        directory = osp.join(config.appdatahome, directory)
    config.local_repo_cache_root = directory
    if not osp.exists(directory):
        try:
            os.makedirs(directory)
        except Exception:
            config.critical('could not find local repo cache directory; check '
                            'local-repo-cache-root option whose present value '
                            'is %r)', config['local-repo-cache-root'])
            raise
    return directory

def url_to_relative_path(url):
    scheme, url = url.split('://', 1)
    if scheme == 'file':
        return url.rstrip('/').rsplit('/', 1)[-1]
    else:
        try:
            return url.rstrip('/').rsplit('/', 1)[1]
        except IndexError:
            return '/'

def set_local_cache(vcsrepo):
    if vcsrepo.local_cache:
        return
    cachedir = osp.join(repo_cache_dir(vcsrepo._cw.vreg.config),
                        unicode(vcsrepo.eid))
    if not osp.exists(cachedir):
        try:
            os.makedirs(cachedir)
        except OSError:
            vcsrepo.exception('cant create repo cache directory %s', cachedir)
            return
    if vcsrepo.source_url:
        reponame = vcsrepo.source_url.rstrip('/').rsplit('/', 1)[-1]
    elif vcsrepo.title:
        reponame = unormalize(vcsrepo.title).replace(' ', '_')
    else:
        reponame = 'repository'
    vcsrepo.cw_edited['local_cache'] = osp.join(unicode(vcsrepo.eid), reponame)

def clone_to_local_cache(vcsrepo):
    handler = vcsrepo.cw_adapt_to('VCSRepo')
    url = vcsrepo.source_url
    try:
        handler.pull()
    except Exception:
        handler.exception('while trying to clone repo %s', url)
        msg = _('can not clone the repo from %(source)s, '
                'please check the source url')
        raise ValidationError(vcsrepo.eid, {'source_url': msg}, {'source': url})


def missing_relation_error(entity, rtype):
    # use __ since msgid recorded in cw, we don't want to translate it in
    # this cube
    __ = entity._cw._
    msg = __('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)')
    errors = {'from_repository': msg % {'rtype': __(rtype),
                                        'etype': __(entity.__regid__),
                                        'eid': entity.eid}}
    return ValidationError(entity.eid, errors)

def missing_attribute_error(entity, attrs):
    msg = _('at least one attribute of %s must be set on a Repository')
    errors = {}
    for attr in attrs:
        errors[attr] = msg % ', '.join(attrs)
    return ValidationError(entity.eid, errors)


# initialization hooks #########################################################


class VCSFileInitHook(hook.Hook):
    """install attribute map on the system source sql generator and init
    configuration
    """
    __regid__ = 'vcsfile.hook.init'
    events = ('server_startup', 'server_maintenance')

    def __call__(self):
        cacheroot = repo_cache_dir(self.repo.config)
        if not os.access(cacheroot, os.W_OK):
            raise ValueError('directory %r is not writable (check "local-repo-'
                             'cache-root" option)' % cacheroot)
        hgrcpath = self.repo.config['hgrc-path']
        if hgrcpath is not None:
            os.environ['HGRCPATH'] = hgrcpath



class VCSFileStartHook(hook.Hook):
    """register task to regularly pull/import vcs repositories content"""
    __regid__ = 'vcsfile.hook.start'
    events = ('server_startup',)

    def __call__(self):
        # closure to avoid error on code reloading
        interval = self.repo.config.get('check-revision-interval')
        if interval < 0:
            return
        repo = self.repo
        def refresh_vcsrepos():
            from cubes.vcsfile.hooks import _refresh_repos as refresh
            return refresh(repo)
        self.repo.looping_task(interval, refresh_vcsrepos)

# internals to be able to create new vcs repo revision using rql queries #######

### Other hooks
###############################################################################

class SetRepoCacheBeforeAddHook(hook.Hook):
    """clone just after repo creation

    mostly helps the tests
    """
    __regid__ = 'clone_repo_after_add'
    __select__ = (hook.Hook.__select__ & is_instance('Repository'))
    events = ('before_add_entity',)
    category = 'vcsfile.cache'
    order = 2

    def __call__(self):
        repo = self.entity
        if repo.type == 'mercurial':
            set_local_cache(repo)


class UpdateRepositoryHook(hook.Hook):
    """add repository eid to vcs bridge cache

    - Check that the type attribute is immutable
    - refresh the cache on url changes

    XXX stuff claiming to be cache but actually necessary are EVIL"""
    __regid__ = 'vcsfile.update_repository_hook'
    __select__ = hook.Hook.__select__ & is_instance('Repository')
    events = ('before_update_entity', )
    # XXX no category, contains integrity, mandatory and vcsfile.cache behaviour

    def __call__(self):
        vcsrepo = self.entity
        if 'type' in vcsrepo.cw_edited:
            msg = self._cw._('updating type attribute of a repository isn\'t '
                             'supported. Delete it and add a new one.')
            raise ValidationError(vcsrepo.eid, {'type': msg})
        if vcsrepo.type == 'mercurial' and not vcsrepo.local_cache:
            # XXX rmdir on rollback
            set_local_cache(vcsrepo)
            clone_to_local_cache(vcsrepo)


class DeleteDirsOp(hook.DataOperationMixIn, hook.Operation):

    def postcommit_event(self):
        for cachedir in self.get_data():
            try:
                try:
                    shutil.rmtree(cachedir)
                    self.info('deleted repository cache at %s', cachedir)
                except OSError, exc:
                    if (exc.errno != errno.ENOENT
                        or getattr(exc, 'filename', None) != cachedir):
                        raise
            except Exception:
                self.exception('cannot delete repository cache at %s', cachedir)

class DeleteRepositoryHook(hook.Hook):
    __regid__ = 'vcsfile.add_update_repository_hook'
    __select__ = (hook.Hook.__select__ &
                  is_instance('Repository') &
                  score_entity(lambda x: x.local_cache))
    events = ('before_delete_entity',)
    # XXX mandatory hook?

    def __call__(self):
        if self.entity.local_cache:
            cachedir = osp.dirname(osp.join(
                self._cw.vreg.config.local_repo_cache_root,
                self.entity.local_cache)).encode('ascii')
            DeleteDirsOp.get_instance(self._cw).add_data(cachedir)


# safety belts #################################################################

def _check_in_transaction(vf_or_rev):
    """check that a newly added versioned file or revision entity is done in
    a vcs repository transaction.
    """
    try:
        vcsrepo = vf_or_rev.from_repository[0]
    except IndexError:
        raise missing_relation_error(vf_or_rev, 'from_repository')
    try:
        transactions = vcsrepo._cw.transaction_data['vctransactions']
        transaction = transactions[vcsrepo.eid]
    except KeyError:
        raise QueryError('no transaction in progress for repository %s'
                         % vcsrepo.eid)
    return transaction


class AddUpdateRepositoryHook(hook.Hook):
    __regid__ = 'vcsfile.add_update_repository_hook'
    __select__ = hook.Hook.__select__ & is_instance('Repository')
    events = ('after_add_entity', 'after_update_entity')
    category = 'integrity'
    required_attrs = ('source_url',)
    order = 1

    def __call__(self):
        entity = self.entity
        if entity.source_url:
            try:
                url_to_relative_path(entity.source_url)
            except Exception:
                msg = self._cw._('url must be of the form protocol://path/to/stuff')
                raise ValidationError(entity.eid, {'source_url': msg})
        # ensure local cache exists
        entity.cw_adapt_to('VCSRepo')

def _refresh_repos(repo, eids=None, bridge=bridge, raise_on_error=False):
    '''
    Refresh vcs repos designated by the `eids` parameter, if refreshable:

      * None for all vcs repos
      * a list of integers for specific vcs repos
    '''
    with repo.internal_cnx() as cnx:
        config = cnx.vreg.config
        rql = ('Any R, RT, RE, RLC, RSU WHERE R is Repository, '
               'NOT (R local_cache NULL),'
               'R type RT, R encoding RE, R local_cache RLC, R source_url RSU')
        if not config['cache-external-repositories']:
            rql += ', R cw_source S, S name "system"'
        kwargs = None
        if eids is not None:
            if isinstance(eids, int):
                rql += ', R eid %(x)s'
                kwargs = {'x': eids}
            else:
                rql += ', R eid IN (%s)' % ','.join(str(eid) for eid in eids)
        reposrset = cnx.execute(rql, kwargs)
        for vcsrepo in reposrset.entities():
            # watch for repository being shut-down, our thread may:
            # * not receive the KeyboardInterrupt or shutdown signal
            # * loop a while without accessing the session (actually
            #   until some repository is actually not up-to-date, while
            #   this access to the session would abort the task)
            try:
                repohdlr = vcsrepo.cw_adapt_to('VCSRepo')
            except bridge.VCSException, ex:
                if raise_on_error:
                    raise
                cnx.error(str(ex))
                continue
            except Exception:
                if raise_on_error:
                    raise
                cnx.exception('error while retrieving handler for %s', vcsrepo.eid)
                continue
            if vcsrepo.local_cache is not None:
                try:
                    repohdlr.pull()
                except bridge.VCSException, ex:
                    if raise_on_error:
                        raise
                    repohdlr.error(str(ex))
                except Exception:
                    if raise_on_error:
                        raise
                    repohdlr.exception(
                        'error while updating local cache of repository %s',
                        vcsrepo.eid)
            if (config['repository-import']
                and vcsrepo.cw_metainformation()['source']['uri'] == 'system'):
                bridge.import_vcsrepo_content(
                    cnx, repohdlr, raise_on_error=raise_on_error,
                    commitevery=config.get('check-revision-commit-every', 1))


class RefreshRepoService(Service):
    __regid__ = 'refresh_repository'
    __select__ = Service.__select__ & match_user_groups('managers')

    def call(self, eids=None, raise_on_error=False):
        _refresh_repos(self._cw.repo, eids, raise_on_error=raise_on_error)


class VCSRepoNotifHook(hook.Hook):
    events = ('server_startup',)
    __regid__ = 'vcsfile_repo_notif_startup'
    order = 10 # make sure it is loaded "late" (in fact, it must be
               # loaded after ZMQStartHook but we lack a dependency
               # system here, see http://www.cubicweb.org/ticket/2900987)
    def __call__(self):
        def callback(msg):
            self.debug('received notification: %s', ' '.join(msg))
            msg = msg[-1]
            with self.repo.internal_cnx() as cnx:
                for suffix in ('', '/'):
                    rset = cnx.execute('Any R WHERE R source_url U, '
                                            'R source_url LIKE %(u)s',
                                            {'u': '%/'+msg+suffix})
                    if rset:
                        break
                if rset:
                    assert len(rset) == 1, 'found multiple repos for "%s"'%msg
                    eid = rset[0][0]
                    self.debug('Calling refresh service for repo %s', eid)
                    cnx.call_service('refresh_repository', eids=[eid])
                else:
                    self.debug('No repo for %s', msg)

        self.repo.app_instances_bus.add_subscription('vcsfile-repo-notif', callback)
