"""All POSIX Type client support for Bcfg2."""

import os
import re
import sys
import shutil
from datetime import datetime
import Bcfg2.Client.Tools
from Bcfg2.Compat import walk_packages
from Bcfg2.Client.Tools.POSIX.base import POSIXTool


class POSIX(Bcfg2.Client.Tools.Tool):
    """POSIX File support code."""
    name = 'POSIX'

    def __init__(self, logger, setup, config):
        Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config)
        self.ppath = setup['ppath']
        self.max_copies = setup['max_copies']
        self._handlers = self._load_handlers()
        self.logger.debug("POSIX: Handlers loaded: %s" %
                          (", ".join(self._handlers.keys())))
        self.__req__ = dict(Path=dict())
        for etype, hdlr in self._handlers.items():
            self.__req__['Path'][etype] = hdlr.__req__
            self.__handles__.append(('Path', etype))
        # Tool.__init__() sets up the list of handled entries, but we
        # need to do it again after __handles__ has been populated. we
        # can't populate __handles__ when the class is created because
        # _load_handlers() _must_ be called at run-time, not at
        # compile-time.  This also has to _extend_ self.handled, not
        # set it, because self.handled has some really crazy
        # semi-global thing going that, frankly, scares the crap out
        # of me.
        for struct in config:
            self.handled.extend([e for e in struct
                                 if (e not in self.handled and
                                     self.handlesEntry(e))])

    def _load_handlers(self):
        """ load available POSIX tool handlers.  this must be called
        at run-time, not at compile-time, or we get wierd circular
        import issues. """
        rv = dict()
        for submodule in walk_packages(path=__path__, prefix=__name__ + "."):
            mname = submodule[1].rsplit('.', 1)[-1]
            if mname == 'base':
                continue
            try:
                module = getattr(__import__(submodule[1]).Client.Tools.POSIX,
                                 mname)
            except ImportError:
                continue
            hdlr = getattr(module, "POSIX" + mname)
            if POSIXTool in hdlr.__mro__:
                # figure out what entry type this handler handles
                etype = hdlr.__name__[5:].lower()
                rv[etype] = hdlr(self.logger, self.setup, self.config)
        return rv

    def canVerify(self, entry):
        if not Bcfg2.Client.Tools.Tool.canVerify(self, entry):
            return False
        if not self._handlers[entry.get("type")].fully_specified(entry):
            self.logger.error('POSIX: Cannot verify incomplete entry %s. '
                              'Try running bcfg2-lint.' %
                              entry.get('name'))
            return False
        return True

    def canInstall(self, entry):
        """Check if entry is complete for installation."""
        if not Bcfg2.Client.Tools.Tool.canInstall(self, entry):
            return False
        if not self._handlers[entry.get("type")].fully_specified(entry):
            self.logger.error('POSIX: Cannot install incomplete entry %s. '
                              'Try running bcfg2-lint.' %
                              entry.get('name'))
            return False
        return True

    def InstallPath(self, entry):
        """Dispatch install to the proper method according to type"""
        self.logger.debug("POSIX: Installing entry %s:%s:%s" %
                          (entry.tag, entry.get("type"), entry.get("name")))
        self._paranoid_backup(entry)
        return self._handlers[entry.get("type")].install(entry)

    def VerifyPath(self, entry, modlist):
        """Dispatch verify to the proper method according to type"""
        self.logger.debug("POSIX: Verifying entry %s:%s:%s" %
                          (entry.tag, entry.get("type"), entry.get("name")))
        ret = self._handlers[entry.get("type")].verify(entry, modlist)
        if self.setup['interactive'] and not ret:
            entry.set('qtext',
                      '%s\nInstall %s %s: (y/N) ' %
                      (entry.get('qtext', ''),
                       entry.get('type'), entry.get('name')))
        return ret

    def _prune_old_backups(self, entry):
        """ Remove old paranoid backup files """
        bkupnam = entry.get('name').replace('/', '_')
        bkup_re = re.compile(
            bkupnam + r'_\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{6}$')
        # current list of backups for this file
        try:
            bkuplist = [f for f in os.listdir(self.ppath) if
                        bkup_re.match(f)]
        except OSError:
            err = sys.exc_info()[1]
            self.logger.error("POSIX: Failed to create backup list in %s: %s" %
                              (self.ppath, err))
            return
        bkuplist.sort()
        while len(bkuplist) >= int(self.max_copies):
            # remove the oldest backup available
            oldest = bkuplist.pop(0)
            self.logger.info("POSIX: Removing old backup %s" % oldest)
            try:
                os.remove(os.path.join(self.ppath, oldest))
            except OSError:
                err = sys.exc_info()[1]
                self.logger.error("POSIX: Failed to remove old backup %s: %s" %
                                  (os.path.join(self.ppath, oldest), err))

    def _paranoid_backup(self, entry):
        """ Take a backup of the specified entry for paranoid mode """
        if (entry.get("paranoid", 'false').lower() == 'true' and
            self.setup.get("paranoid", False) and
            entry.get('current_exists', 'true') == 'true' and
            not os.path.isdir(entry.get("name"))):
            self._prune_old_backups(entry)
            bkupnam = "%s_%s" % (entry.get('name').replace('/', '_'),
                                 datetime.isoformat(datetime.now()))
            bfile = os.path.join(self.ppath, bkupnam)
            try:
                shutil.copy(entry.get('name'), bfile)
                self.logger.info("POSIX: Backup of %s saved to %s" %
                                 (entry.get('name'), bfile))
            except IOError:
                err = sys.exc_info()[1]
                self.logger.error("POSIX: Failed to create backup file for "
                                  "%s: %s" % (entry.get('name'), err))
