#!/usr/bin/env python
from __future__ import with_statement

import datetime
import errno
import gc
import logging
import optparse
import os
import re
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 cmd
from tron import config
from tron import emailer
from tron import job
from tron import mcp
from tron import monitor
from tron import node
from tron import resource
from tron import scheduler
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"""
    with open(config_path, "w") as config_file:
        config_file.write(config.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", help="Configuration file to load (default in working dir)", default=None)

    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=cmd.DEFAULT_PORT, type=int)
    parser.add_option("--host", "-H", action="store", dest="listen_host", help="What host to listen on defaults to %default", default=cmd.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

    # TODO: Some stderr logging during development might be a good idea
    #stream = sys.stderr
    if options.verbose > 0:
        stream = sys.stdout
        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()

    formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s')
    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()
