import errno
import fcntl
import logging
import os
import signal
import sys
import time

import __main__

logger = logging.getLogger(__name__)


class Daemon(object):

    def __init__(self,
                 pidfile=None,
                 stdin='/dev/null',
                 stdout='/dev/null',
                 stderr='/dev/null',
                 close_fds=False):

        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr
        self.pidfile = pidfile or _default_pid_file()
        # NOTE: We need to open another separate file to avoid the file
        #       being reopened again.
        #       In which case, process loses file lock.
        #
        # From "man fcntl":
        # As well as being removed by an explicit F_UNLCK, record locks are
        # automatically released when the process terminates or if it
        # closes any file descriptor referring to a file on which locks
        # are held. This is bad: it means that a process can lose the locks
        # on a file like /etc/passwd or /etc/mtab when for some reason a
        # library function decides to open, read and close it.
        self.lockfile = self.pidfile + ".lock"
        self.lockfp = None
        self.close_fds = close_fds

    def daemonize(self):
        """
        do the UNIX double-fork magic, see Stevens' "Advanced
        Programming in the UNIX Environment" for details (ISBN 0201563177)
        http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
        """

        try:

            pid = os.fork()
            if pid > 0:
                # exit first parent
                _close_std_io()
                sys.exit(0)

        except OSError as e:
            logger.error("fork #1 failed: " + repr(e))
            sys.exit(1)

        # decouple from parent environment
        os.setsid()
        os.umask(0)

        # do second fork
        try:

            pid = os.fork()
            if pid > 0:
                # exit from second parent
                _close_std_io()
                sys.exit(0)

        except OSError as e:
            logger.error("fork #2 failed: " + repr(e))
            sys.exit(1)

        if self.close_fds:
            _close_fds()

        # redirect standard file descriptors
        sys.stdout.flush()
        sys.stderr.flush()
        si = file(self.stdin, 'r')
        so = file(self.stdout, 'a+')
        se = file(self.stderr, 'a+', 0)
        os.dup2(si.fileno(), sys.stdin.fileno())
        os.dup2(so.fileno(), sys.stdout.fileno())
        os.dup2(se.fileno(), sys.stderr.fileno())

        logger.info("OK daemonized")

    def trylock_or_exit(self, timeout=10):

        interval = 0.1
        n = int(timeout / interval) + 1
        flag = fcntl.LOCK_EX | fcntl.LOCK_NB

        for ii in range(n):

            fd = os.open(self.lockfile, os.O_RDWR | os.O_CREAT)

            fcntl.fcntl(fd, fcntl.F_SETFD,
                        fcntl.fcntl(fd, fcntl.F_GETFD, 0)
                        | fcntl.FD_CLOEXEC)

            try:
                fcntl.lockf(fd, flag)

                self.lockfp = os.fdopen(fd, 'w+r')
                break

            except IOError as e:
                os.close(fd)
                if e[0] == errno.EAGAIN:
                    time.sleep(interval)
                else:
                    raise

        else:
            logger.info("Failure acquiring lock %s" % (self.lockfile, ))
            sys.exit(1)

        logger.info("OK acquired lock %s" % (self.lockfile))

    def unlock(self):

        if self.lockfp is None:
            return

        fd = self.lockfp.fileno()
        fcntl.lockf(fd, fcntl.LOCK_UN)
        self.lockfp.close()
        self.lockfp = None

    def start(self):

        self.daemonize()
        self.init_proc()

    def init_proc(self):
        self.trylock_or_exit()
        self.write_pid_or_exit()

    def write_pid_or_exit(self):

        self.pf = open(self.pidfile, 'w+r')
        pf = self.pf

        fd = pf.fileno()
        fcntl.fcntl(fd, fcntl.F_SETFD,
                    fcntl.fcntl(fd, fcntl.F_GETFD, 0)
                    | fcntl.FD_CLOEXEC)

        try:
            pid = os.getpid()
            logger.debug('write pid:' + str(pid))

            pf.truncate(0)
            pf.write(str(pid))
            pf.flush()
        except Exception as e:
            logger.exception('write pid failed.' + repr(e))
            sys.exit(0)

    def stop(self):

        pid = None

        if not os.path.exists(self.pidfile):

            logger.debug('pidfile not exist:' + self.pidfile)
            return

        try:
            pid = _read_file(self.pidfile)
            pid = int(pid)
            os.kill(pid, signal.SIGTERM)
            return

        except Exception as e:
            logger.warn('{e} while get and kill pid={pid}'.format(
                e=repr(e), pid=pid))


def _read_file(fn):
    with open(fn, 'r') as f:
        return f.read()


def _close_std_io():
    os.close(0)
    os.close(1)
    os.close(2)


def _close_fds():

    try:
        max_fd = os.sysconf("SC_OPEN_MAX")
    except ValueError as e:
        logger.warn(repr(e) + ' while get max fds of a process')
        max_fd = 65536

    for i in xrange(3, max_fd):
        try:
            os.close(i)
        except OSError:
            pass


def _default_pid_file():

    if hasattr(__main__, '__file__'):
        name = __main__.__file__
        name = os.path.basename(name)
        if name == '<stdin>':
            name = '__stdin__'
        return '/var/run/' + name.rsplit('.', 1)[0]
    else:
        return '/var/run/pykit.daemonize'


def daemonize_cli(run_func, pidfn, close_fds=False):

    logging.basicConfig(stream=sys.stderr)
    logging.getLogger(__name__).setLevel(logging.DEBUG)

    d = Daemon(pidfile=pidfn, close_fds=close_fds)

    logger.info("sys.argv: " + repr(sys.argv))

    try:
        if len(sys.argv) == 1:
            d.init_proc()
            run_func()

        elif len(sys.argv) == 2:

            if 'start' == sys.argv[1]:
                d.start()
                run_func()

            elif 'stop' == sys.argv[1]:
                d.stop()

            elif 'restart' == sys.argv[1]:
                d.stop()
                d.start()
                run_func()

            else:
                logger.error("Unknown command: %s" % (sys.argv[1]))
                print "Unknown command"
                sys.exit(2)

            sys.exit(0)
        else:
            print "usage: %s start|stop|restart" % sys.argv[0]
            sys.exit(2)

    except Exception as e:
        logger.exception(repr(e))


standard_daemonize = daemonize_cli