#!/usr/bin/env python

"""The core of trond is a hacked-up version of twistd. It mainly exists to set
up command line-related logging options and invoke the MasterControlProgram.
This file can probably be left alone as long as we continue to use Twisted.
"""

from __future__ import with_statement

import errno
import logging
import optparse
import os
from pkg_resources import resource_string
import signal
import sys

from twisted.internet import reactor, defer
from twisted.python import log
from twisted.web import server

# Annoying that we have to import twisted internal module here, but we don't
# really want to use 'twistd' to run our daemon.
from twisted.scripts import _twistd_unix

import tron
from tron import commands
from tron import mcp
from tron import www
from tron.utils import log_handler

logger = logging.getLogger('bin.trond')


def create_default_config(config_path):
    """Create a default empty configuration for first time installs"""
    default_config = resource_string('tron', 'default_config.yaml')
    with open(config_path, "w") as config_file:
        config_file.write(default_config)


def parse_options():
    parser = optparse.OptionParser(version="%%prog %s" % tron.__version__)

    parser.add_option("--working-dir", action="store", dest="working_dir",
                      help="Directory where tron's state and output is stored"
                           " (default %default)",
                      default="/var/lib/tron/")
    parser.add_option("--log-file", "-l", action="store", dest="log_file",
                      help="Where the logs are stored (default %default)",
                      default="/var/log/tron/tron.log")
    parser.add_option("--config-file", "-c", action="store",
                      dest="config_file", default=None,
                      help="Configuration file to load (default in working"
                           " dir)")

    parser.add_option("--verbose", "-v", action="count", dest="verbose",
                      help="Verbose logging", default=0)
    parser.add_option("--debug", action="store_true", dest="debug",
                      help="Debug mode, extra error reporting, no daemonizing")

    parser.add_option("--nodaemon", action="store_true", dest="nodaemon",
                      help="Indicates we should not fork and daemonize the"
                           " process (default %default)",
                      default=False)
    parser.add_option("--pid-file", action="store", dest="pidfile",
                      help="Where to store pid of the executing process"
                           " (default %default)",
                      default="/var/run/tron.pid")

    parser.add_option("--port", "-P", action="store", dest="listen_port",
                      help="What port to listen on, defaults %default",
                      default=commands.DEFAULT_PORT, type=int)
    parser.add_option("--host", "-H", action="store", dest="listen_host",
                      help="What host to listen on defaults to %default",
                      default=commands.DEFAULT_HOST, type=str)

    (options, args) = parser.parse_args(sys.argv)

    if not options.working_dir:
        parser.error("Bad working-dir option")

    if options.config_file is None:
        options.config_file = os.path.join(options.working_dir,
                                           "tron_config.yaml")

    return options


def setup_logging(options):
    level = logging.WARNING
    twist_level = logging.WARNING

    if options.verbose > 0:
        level = logging.INFO
        twist_level = logging.WARNING
    if options.verbose > 1:
        level = logging.DEBUG
        twist_level = logging.INFO
    if options.verbose > 2:
        twist_level = logging.DEBUG

    root = logging.getLogger('')

    try:
        handler = log_handler.ReOpeningFileHandler(options.log_file)
    except IOError, e:
        print >>sys.stderr, e
        sys.exit()

    fmt_str = '%(asctime)s %(name)s %(levelname)s %(message)s'
    formatter = logging.Formatter(fmt_str)
    handler.setFormatter(formatter)

    root.addHandler(handler)
    root.setLevel(level)
    logging.getLogger('twisted').setLevel(twist_level)

    # Hookup twisted to standard logging
    observer = log.PythonLoggingObserver()
    observer.start()

    # Show stack traces for errors in twisted deferreds.
    if options.debug:
        defer.setDebugging(True)

    return handler


# This is rather crazy thing to do, but I don't really like 'twistd', but it
# has all kinds of useful functions in it for daemonizing stuff. So rather than
# use twisted and build a tac file and all that crap, we're going to hack out
# the parts we like.
class FakeConfig(dict):
    """Wrapper class to make a options object look like dictionary for twistd
    stuff
    """

    def __init__(self, options):
        self.options = options

    def __getitem__(self, key):
        return getattr(self.options, key)


class TronApplicationRunner(_twistd_unix.UnixApplicationRunner):

    def __init__(self, options):
        # We arn't supporting all the options that twistd has, so add some
        # default values here.
        options.profile = None
        options.chroot = None
        options.rundir = '.'
        options.umask = 022

        self.options = options

        self.config = FakeConfig(options)

        # The ApplicationRunner is suppose to have an actual application it
        # runs, but that abstraction just confused me.
        self.application = None
        self.profiler = None

    def preApplication(self):
        _twistd_unix.UnixApplicationRunner.preApplication(self)

        # Setup our environment
        try:
            os.makedirs(self.options.working_dir)
        except OSError, e:
            if e.errno != errno.EEXIST:
                raise

        if (not os.path.isdir(self.options.working_dir) or
            not os.access(self.options.working_dir,
                          os.R_OK | os.W_OK | os.X_OK)):
            print >>sys.stderr, "Error, working directory %s invalid" % (
                self.options.working_dir)
            sys.exit(1)

        # See if we can access or create the config file
        if not os.path.exists(self.options.config_file):
            try:
                create_default_config(self.options.config_file)
            except OSError, create_e:
                print >>sys.stderr, ("Error creating default configuration at"
                                     " %s: %r" % (self.options.config_file,
                                                  create_e))
                sys.exit(1)

        if not os.access(self.options.config_file, os.R_OK | os.W_OK):
            print >>sys.stderr, ("Error opening configuration %s:"
                                 " Permissions" % (self.options.config_file))
            sys.exit(1)

    def run(self):
        logger.debug("init: preApplication")
        self.preApplication()

        logger.debug("init: setup_logging")
        self.log_handler = setup_logging(self.options)

        logger.debug("init: postApplication")
        self.postApplication()

    def startApplication(self, application):
        logger.debug("init: about to setup environment")

        try:
            self.setupEnvironment(self.config['chroot'],
                              self.config['rundir'],
                              self.config['nodaemon'],
                              self.config['umask'],
                              self.config['pidfile'])
        except Exception, e:
            # We may have already forked/daemonized at this point, so lets hope
            # that logging was setup properly otherwise, we may never know...
            logger.exception("Error setting up environment")
            print >>sys.stderr, "Error setting up environment: %r" % e
            sys.exit(1)

        # Build and configure the mcp
        master_control = mcp.MasterControlProgram(self.options.working_dir,
                                                  self.options.config_file)
        try:
            master_control.load_config()
        except Exception, e:
            logger.exception("Error in configuration file %s" %
                             self.options.config_file)
            sys.exit(1)

        # Try to load up previous state
        logger.debug("attempt restore")
        master_control.try_restore()

        master_control.state_handler.writing_enabled = True

        # Setup our reconfiguration signal handler
        def sighup_handler(signum, frame):
            logger.info("SIGHUP Caught!")
            reactor.callLater(0, master_control.live_reconfig)
            reactor.callLater(0, self.log_handler.reopen)

        signal.signal(signal.SIGHUP, sighup_handler)

        # Start up our web management interface
        logger.debug("listenTCP")
        reactor.listenTCP(self.options.listen_port, server.Site(
            www.RootResource(master_control)),
            interface=self.options.listen_host)

        # Setup the mcp timed callbacks
        reactor.callLater(0, master_control.run_jobs)


def main():
    options = parse_options()

    app_runner = TronApplicationRunner(options)
    app_runner.run()

if __name__ == '__main__':
    main()
