#!/usr/bin/python3

from argparse import ArgumentParser
import fcntl
import heapq
import logging
import logging.handlers
import os
import pwd
import pyinotify
import shutil
import signal
import sys

from contextlib import contextmanager
from urllib.parse import quote_plus

import atexit
import time

from git_slug.gitconst import EMPTYSHA1, REFREPO, REFFILE
from git_slug.gitrepo import GitRepo

LOCKFILE = 'slug_watch.lock'
PROJECTS_LIST = 'projects.list'
PROJECTS_LIST_NEW = PROJECTS_LIST + '.new'
PROJECTS_LIST_HEAD = PROJECTS_LIST + '.head'
PROJECTS_LIST_GITWEB = PROJECTS_LIST + ".gitweb"
REFFILE_NEW = REFFILE + '.new'
REFREPO_WDIR = 'Refs'


def sigtermhandler(no, stack):
    raise SystemExit

@contextmanager
def lock(path=LOCKFILE):
    signal.signal(signal.SIGTERM, sigtermhandler)
    f = open(path, 'a')
    try:
        fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except IOError:
        raise SystemExit('Already running: file {} locked'.format(path))
    else:
        try:
            yield
        finally:
            f.close()
            os.remove(path)

def convertstream(stream):
    for line in stream:
        (sha1, ref, repo) = line.decode('utf-8').split()
        yield (repo, ref, 1, sha1)

def processnewfile(stream):
    repo = stream.readline().strip()
    for line in stream:
        (sha1old, sha1, ref) = line.split()
        if ref.startswith('refs/heads/'):
            yield (repo, ref, 0, sha1)


def process_file(pathname):
    if not os.path.isfile(pathname):
        print('{} is not an ordinary file'.format(pathname))
        return

    if os.path.isfile(PROJECTS_LIST_HEAD):
        try:
            shutil.copyfile(PROJECTS_LIST_HEAD, PROJECTS_LIST_NEW)
        except (OSError, shutil.Error):
            logger.exception('Cannot write {}'.format(PROJECTS_LIST_NEW))

    with open(os.path.join(REFREPO_WDIR, REFFILE),'w') as headfile_new, open(pathname, 'r') as newfile, \
            open(PROJECTS_LIST_NEW,'a') as projects:
        committer = newfile.readline().strip()
        oldtuple = (None, None)
        refrepo = GitRepo(git_dir=REFREPO_GDIR)
        process = refrepo.showfile(REFFILE, 'master')
        headfile = process.stdout
        try:
            for (repo, ref, number, sha1) in heapq.merge(sorted(processnewfile(newfile)), convertstream(headfile)):
                if (repo, ref) == oldtuple:
                    continue
                if sha1 != EMPTYSHA1:
                    print(sha1, ref, repo, file=headfile_new)
                    if repo != oldtuple[0]:
                        print('packages/'+repo+'.git', file=projects)
                oldtuple = (repo, ref)
        except (ValueError, OSError):
            logger.exception("Problem with file: {}".format(pathname))
            return
        process.wait()

    os.rename(PROJECTS_LIST_NEW, PROJECTS_LIST)
    with open(PROJECTS_LIST, 'r') as projects, open(PROJECTS_LIST_GITWEB, 'w') as output:
        for line in projects:
            print(quote_plus(line, safe='/\n'), end='', file=output)

    headrepo = GitRepo(REFREPO_WDIR, REFREPO_GDIR)
    headrepo.commitfile(REFFILE, 'Changes by {}'.format(committer))
    os.remove(pathname)

class EventHandler(pyinotify.ProcessEvent):
    def process_IN_CLOSE_WRITE(self, event):
        process_file(event.pathname)

def runwatch(user=None):
    logger.info("Starting")
    try:
        if user is not None:
            uid = pwd.getpwnam(user).pw_uid
            gid = pwd.getpwnam(user).pw_gid
            os.setgid(gid)
            os.setuid(uid)
            os.putenv('HOME', pwd.getpwnam(user).pw_dir)

        os.chdir(pwd.getpwuid(os.getuid()).pw_dir)
        for directory in (WATCHDIR, REFREPO_WDIR):
            if not os.path.isdir(directory):
                logger.info('Creating {}'.format(directory))
                os.mkdir(directory)

        refrepo = GitRepo(git_dir=REFREPO_GDIR)
        if not os.path.isdir(REFREPO_GDIR):
            refrepo.init_gitdir()
        refrepo.commandexc(['config', 'daemon.uploadarch', 'true'])


        with lock(LOCKFILE):
            wm = pyinotify.WatchManager()  # Watch Manager
            mask = pyinotify.IN_CLOSE_WRITE # watched events
            notifier = pyinotify.Notifier(wm, EventHandler())
            wm.add_watch(WATCHDIR, mask, rec=False)
            for filename in sorted(os.listdir(WATCHDIR), key=lambda f: os.stat(os.path.join(WATCHDIR, f)).st_mtime):
                process_file(os.path.join(WATCHDIR, filename))
            notifier.loop()
    except SystemExit:
        logger.info("Stopped")
    except:
        logger.exception('Got exception')
        raise


parser = ArgumentParser(description='daemon to register changes in PLD repositories')
parser.add_argument('-d', '--daemon', nargs='?', choices=['start', 'stop'], default=None, const='start')
parser.add_argument('-m', '--maillogs', action='append', default=None)
parser.add_argument('-u', '--user')
parser.add_argument('-r', '--refrepodir', required=True)
parser.add_argument('-s', '--sender')
parser.add_argument('-w', '--watchdir', required=True)
options = parser.parse_args()

REFREPO_GDIR = os.path.join(options.refrepodir, REFREPO+'.git')
WATCHDIR = options.watchdir

logger = logging.getLogger('slug_watch')
logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(name)s: %(levelname)s %(message)s')
if options.maillogs is not None:
    if options.sender is None:
        parser.error("Sender of logs is required with -m/--maillogs option")
    handler_email = logging.handlers.SMTPHandler("localhost", options.sender, options.maillogs, "slug_watch status")
    handler_email.setFormatter(formatter)
    logger.addHandler(handler_email)
if options.daemon  is not None:
    handler = logging.handlers.SysLogHandler(address="/dev/log", facility="daemon")
else:
    handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(name)s: %(levelname)s %(message)s'))
logger.addHandler(handler)

class Daemon:
    """Generic UNIX daemon using double-fork mechanism.

    Subclass and override run().  Use start()/stop() to control.
    """
    def __init__(self, pidfile):
        self.pidfile = pidfile

    def daemonize(self):
        """Daemonize via UNIX double fork."""
        try:
            if os.fork() > 0:
                sys.exit(0)
        except OSError as err:
            sys.stderr.write('fork #1 failed: {}\n'.format(err))
            sys.exit(1)

        os.chdir('/')
        os.setsid()
        os.umask(0o033)

        try:
            if os.fork() > 0:
                sys.exit(0)
        except OSError as err:
            sys.stderr.write('fork #2 failed: {}\n'.format(err))
            sys.exit(1)

        sys.stdout.flush()
        sys.stderr.flush()
        si = open(os.devnull, 'r')
        so = open(os.devnull, 'a+')
        se = open(os.devnull, 'a+')
        os.dup2(si.fileno(), sys.stdin.fileno())
        os.dup2(so.fileno(), sys.stdout.fileno())
        os.dup2(se.fileno(), sys.stderr.fileno())

        atexit.register(self._delpid)
        with open(self.pidfile, 'w+') as f:
            f.write(str(os.getpid()) + '\n')

    def _delpid(self):
        os.remove(self.pidfile)

    def start(self):
        """Start the daemon."""
        try:
            with open(self.pidfile, 'r') as pf:
                pid = int(pf.read().strip())
        except IOError:
            pid = None
        if pid:
            sys.stderr.write('pidfile {} already exists. Daemon already running?\n'.format(
                self.pidfile))
            sys.exit(1)
        self.daemonize()
        self.run()

    def stop(self):
        """Stop the daemon."""
        try:
            with open(self.pidfile, 'r') as pf:
                pid = int(pf.read().strip())
        except IOError:
            pid = None
        if not pid:
            sys.stderr.write('pidfile {} does not exist. Daemon not running?\n'.format(
                self.pidfile))
            return
        try:
            while True:
                os.kill(pid, signal.SIGTERM)
                time.sleep(0.1)
        except OSError as err:
            if 'No such process' in str(err.args):
                if os.path.exists(self.pidfile):
                    os.remove(self.pidfile)
            else:
                sys.stderr.write(str(err.args) + '\n')
                sys.exit(1)

    def restart(self):
        """Restart the daemon."""
        self.stop()
        self.start()

    def run(self):
        """Override in subclass."""


class SlugWatch(Daemon):
    def __init__(self, user, pidfile):
        super().__init__(pidfile)
        self.user = user
    def run(self):
        runwatch(self.user)

if options.daemon is not None:
    daemon = SlugWatch(options.user, "/var/run/slug_watch.pid")
    getattr(daemon, options.daemon)()
else:
    runwatch(options.user)
