#!/usr/bin/env python

#   Copyright 2009-2018 Oli Schacher
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
# main startup file

from __future__ import print_function

try:
    import configparser
except ImportError:
    import ConfigParser as configparser

from fuglu import FUGLU_VERSION
from fuglu.daemon import DaemonStuff
import logging
import logging.config
import fuglu.funkyconsole
from fuglu import loghandlers
import sys
from fuglu.core import MainController
import signal
import os
import pwd
import grp
import optparse
import fuglu.logtools
import multiprocessing
import time
import hashlib

controller = None

theconfigfile = '/etc/fuglu/fuglu.conf'
dconfdir = '/etc/fuglu/conf.d'
theloggingfile = '/etc/fuglu/logging.conf'
thepidfile = '/var/run/fuglu.pid'

# register custom log handlers
logging.custom_handlers = loghandlers


def reloadconfig():
    """reload configuration file"""

    logger = logging.getLogger('fuglu')
    if controller:
        if controller.threadpool is None and controller.procpool is None:
            logger.error("""No process/threadpool -> This is not the main process!
                          \nNo changes will be applied...!\nSend SIGHUP to the main process!
                          \nCurrent controller object : %s, %s""" % (id(controller),controller.__repr__()))
            return
    else:
        logger.error("No controller -> This is not the main process!\n"
                     "No changes will be applied...!\nSend SIGHUP to the main process!")
        return

    if controller.logProcessFacQueue is None:
        logger.error("No log process in controller -> This is not the main process!\n"
                     "No changes will be applied...!\nSend SIGHUP to the main process!")
        return

    assert controller.logConfigFileUpdates is not None
    assert controller.configFileUpdates is not None

    configFileUpdates    = getConfigFileUpdatesDict(theconfigfile,dconfdir)
    logConfigFileUpdates = getLogConfigFileUpdatesDict(theloggingfile)

    logger.info("Log config has changes: %s" % str(logConfigFileUpdates != controller.logConfigFileUpdates))
    logger.info("Main config has changes: %s" % str(configFileUpdates != controller.configFileUpdates))

    logger.info('Number of messages in logging queue: %u'%controller.logQueue.qsize())

    if logConfigFileUpdates != controller.logConfigFileUpdates:
        # save back log config file dict for later use
        controller.logConfigFileUpdates = logConfigFileUpdates

        logger.info("Create new log process with new configuration")
        logProcessFacQueue = controller.logProcessFacQueue
        newLogConfigure = fuglu.logtools.logConfig(logConfigFile=theloggingfile)
        logProcessFacQueue.put(newLogConfigure)


    if configFileUpdates != controller.configFileUpdates:
        logger.info('Reloading configuration')

        # save back config file dict for later use
        controller.configFileUpdates = configFileUpdates
        newconfig = configparser.RawConfigParser()
        with open(theconfigfile) as fp:
            newconfig.readfp(fp)

        identifier = "no identifier given"
        if newconfig.has_option('main', 'identifier'):
            identifier = newconfig.get('main', 'identifier')

        # load conf.d
        if os.path.isdir(dconfdir):
            filelist = os.listdir(dconfdir)
            configfiles = [
                dconfdir + '/' + c for c in filelist if c.endswith('.conf')]
            logger.debug('Conffiles in %s: %s' % (dconfdir, configfiles))
            readfiles = newconfig.read(configfiles)
            logger.debug('Read additional files: %s' % (readfiles))

        logger.info('Reload config complete. Current configuration:%s' %
                    identifier)
        controller.config = newconfig
        controller.propagate_core_defaults()

        logger.info('Reloading plugins...')
        ok = controller.load_plugins()
        if ok:
            logger.info('Plugin reload completed')
        else:
            logger.error('Plugin reload failed')

        controller.propagate_plugin_defaults()

        controller.reload()


def sighup(signum, frame):
    """handle sighup to reload config"""
    reloadconfig()

def getConfigFileUpdatesDict(configfilename,dconfigFileDir):
    configfiles = [configfilename]
    # load conf.d
    if dconfigFileDir and os.path.isdir(dconfigFileDir):
        filelist = os.listdir(dconfigFileDir)
        configfiles.extend([dconfigFileDir + '/' + c for c in filelist if c.endswith('.conf')])

    hashlist = createMD5(configfiles)
    configFileUpdates = dict(zip(configfiles, hashlist))
    return configFileUpdates

def getLogConfigFileUpdatesDict(logConfFile):
    logConfigFileUpdates = {}
    logConfigFileUpdates[logConfFile] = createMD5([logConfFile])[0]
    return logConfigFileUpdates


def hash_bytestr_iter(bytesiter, hasher, ashexstr=False):
    for block in bytesiter:
        hasher.update(block)
    return (hasher.hexdigest() if ashexstr else hasher.digest())

def file_as_blockiter(afile, blocksize=65536):
    with afile:
        block = afile.read(blocksize)
        while len(block) > 0:
            yield block
            block = afile.read(blocksize)


def createMD5(fnamelst):
    return [(fname, hash_bytestr_iter(file_as_blockiter(open(fname, 'rb')), hashlib.md5()))
        for fname in fnamelst]

lint = False
debugmsg = False
console = False


parser = optparse.OptionParser(version=FUGLU_VERSION)
parser.add_option("--lint", action="store_true", dest="lint",
                  default=False, help="Check configuration and exit")
parser.add_option("--console", action="store_true", dest="console",
                  default=False, help="start an interactive console after fuglu startup")
parser.add_option("-f", "--foreground", action="store_true", dest="foreground", default=False,
                  help="start fuglu in the foreground, even if daemonize is enabled in the config")
parser.add_option("--pidfile", action="store", dest="pidfile",
                  help="use a different pidfile than /var/run/fuglu.pid")
parser.add_option("-c", "--config", action="store", dest="configfile",
                  help="use a different config file and disable reading from /etc/fuglu/conf.d")

(opts, args) = parser.parse_args()
if len(args) > 0:
    print("Unknown option(s): %s" % args)
    print("")
    parser.print_help()
    sys.exit(1)

lint = opts.lint
console = opts.console

if opts.pidfile:
    thepidfile = opts.pidfile

if opts.configfile:
    theconfigfile = opts.configfile
    theloggingfile = os.path.join(
        os.path.split(theconfigfile)[0], os.path.split(theloggingfile)[1])
    dconfdir = None

config = configparser.RawConfigParser()
if not os.path.exists(theconfigfile):
    print("""Configfile (%s) not found. Please create it by renaming the .dist file and modifying it to your needs""" % theconfigfile)
    sys.exit(1)
with open(theconfigfile) as fp:
    readconfig = config.readfp(fp)
# load conf.d
if dconfdir and os.path.isdir(dconfdir):
    filelist = os.listdir(dconfdir)
    configfiles = [dconfdir + '/' + c for c in filelist if c.endswith('.conf')]
    readfiles = config.read(configfiles)


daemon = DaemonStuff(thepidfile)
# we could have an empty config file
# no daemon for lint&console mode
if not lint and not console and not opts.foreground:
    if config.has_option('main', 'daemonize'):
        if config.getboolean('main', 'daemonize'):
            daemon.createDaemon()
    else:  # option not specified -> default to run daemon
        daemon.createDaemon()


# drop privileges
try:
    running_user = config.get('main', 'user')
    running_group = config.get('main', 'group')
except:
    running_user = 'nobody'
    running_group = 'nobody'

priv_drop_ok = False
try:
    daemon.drop_privs(running_user, running_group)
    priv_drop_ok = True
except:
    err = sys.exc_info()[1]
    print("Could not drop privileges to %s/%s : %s" %
          (running_user, running_group, str(err)))


#--
# set up logging
#--

# all threads/processes write to the logQueue which
# will be handled by a separate process
logQueue   = multiprocessing.Queue(-1)
logFactoryQueue = multiprocessing.Queue(-1)

# setup a process which handles logging messages
if lint:
    fc = fuglu.funkyconsole.FunkyConsole()
    print(fc.strcolor("Fuglu", "yellow"), end=' ')
    print(fc.strcolor(FUGLU_VERSION, "green"))
    print("----------", fc.strcolor("LINT MODE",
                                    (fc.MODE["blink"], fc.FG["magenta"])), "----------")

    logConfigure = fuglu.logtools.logConfig(lint=True)

else:
    logConfigure = fuglu.logtools.logConfig(logConfigFile=theloggingfile)

#--
# start process handling logging queue
#--
logProcessFactory = multiprocessing.Process(target=fuglu.logtools.logFactoryProcess,
                                     args=(logFactoryQueue,logQueue))
#logProcessFactory.daemon = True
logProcessFactory.start()

# now create a log - listener process
logFactoryQueue.put(logConfigure)

# setup this main thread to send messages to the log queue 
# (which is handled by the logger process created by the logProcessFactory)
fuglu.logtools.client_configurer(logQueue)

#===                      ===#
#= Now logging is available =#
#===                      ===#
baselogger = logging.getLogger()
baselogger.info("FuGLU Version %s starting up" % FUGLU_VERSION)

# instantiate the MainController and load default configuration
controller = MainController(config,logQueue,logFactoryQueue)
controller.configFileUpdates = getConfigFileUpdatesDict(theconfigfile,dconfdir)
controller.logConfigFileUpdates = getLogConfigFileUpdatesDict(theloggingfile)
controller.propagate_core_defaults()


if lint:
    controller.lint()
    # the controller doesn't know about the logging.conf, so we lint this here
    print("")
    if priv_drop_ok:
        print("Checking logging configuration....")
        try:
            logging.config.fileConfig(theloggingfile)
            logging.info("fuglu --lint log configuration test")
            print(fc.strcolor("OK", "green"))
        except Exception as e:
            print("Logging configuration check failed: %s " % str(e))
            print("This may prevent the daemon from starting up.")
            print(
                "Make sure the log directory exists and is writable by user '%s' " % running_user)
            print(fc.strcolor("NOT OK", "red"))
    else:
        print(fc.strcolor("WARNING:", "yellow"))
        print("Skipping logging configuration check because I could not switch to user '%s' earlier." % running_user)
        print("please re-run fuglu --lint as privileged user")
        print("(problems in the logging configuration could prevent the fuglu daemon from starting up)")

else:
    signal.signal(signal.SIGHUP, sighup)
    if console:
        controller.debugconsole = True
    controller.startup()


#---
# stop logger factory & process
#---
try:
    logFactoryQueue.put_nowait(None)
    logProcessFactory.join(120)
except Exception as e:
    logProcessFactory.terminate()
