#!/usr/bin/env python
# -*- coding: utf-8 -*-

# TODO: USE SCRIPT ENTRY PATH AS BASE FOR ALL RELATIVE PATH CALCULATIONS

import eslib  # For logging
import eslib.service
from eslib.service import HttpService, status
import yaml, logging, argparse, json
import sys, os, imp, inspect, pwd, daemon, signal
from setproctitle import setproctitle

if sys.platform == 'darwin':
    # On my Mac under Yosemite and Python 2.7.9, I got a segmentation fault in
    # /Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload/_scproxy.so
    # Therefore, I brutally force a bypass of it thus:
    import urllib
    def proxy_bypass_macosx_sysconf(host):
        return True
    urllib.proxy_bypass_macosx_sysconf = proxy_bypass_macosx_sysconf

class ServiceRunner(object):
    def __init__(self, service, is_daemon):
        self._service = service
        self._reload = False
        self._terminate = False
        # Use service log here too
        self.log = service.log
        # Set up signal handlers
        signal.signal(signal.SIGABRT, self._sighandler_ABRT)  # abort and terminate service
        signal.signal(signal.SIGHUP , self._sighandler_HUP)   # reload service with new config
        signal.signal(signal.SIGINT , self._sighandler_INT)   # interrupt/stop processing (or map to SIGTERM)
        signal.signal(signal.SIGTERM, self._sighandler_TERM)  # terminate service and exit (normally)
        # When running as daemon, grab suspend/resume signals as well:
        if is_daemon:
            signal.signal(signal.SIGTSTP, self._sighandler_TSTP)  # suspend
            signal.signal(signal.SIGCONT, self._sighandler_CONT)  # resume

    def _sighandler_ABRT(self, sig, frame):
        self.log.info("Service wrapper received SIGABRT. Aborting any processing and terminating service.")
        self._service.processing_abort()
        self._service.processing_wait()
        self._service.shutdown()  # Not waiting
        self._reload = False
        self._terminate = True

    def _sighandler_HUP(self, sig, frame):
        info = None
        s = self._service.status
        self.log.info("Service wrapper received SIGHUP. Shutting down and reloading service.")
        self._service.shutdown()  # Not waiting
        self._reload = True

    def _sighandler_INT(self, sig, frame):
        self.log.info("Service wrapper received SIGINT. Shutting down and terminating.")
        self._service.shutdown()  # Not waiting
        self._terminate = True

    def _sighandler_TERM(self, sig, frame):
        self.log.info("Service wrapper received SIGTERM. Shutting down and terminating.")
        self._service.shutdown()  # Not waiting
        self._terminate = True

    def _sighandler_TSTP(self, sig, frame):
        s = self._service.status
        info = None
        doit = False
        if s == status.PROCESSING:
            info = "Suspending processing."
            doit = True
        elif s == status.SUSPENDED:
            info = "Processing is already suspended; no need to suspend."
        else:
            info = "Unable to suspend from state '%s'." % s
        self.log.info("Service wrapper received SIGTSTP. " + info)
        if doit:
            self._service.processing_resume()

    def _sighandler_CONT(self, sig, frame):
        s = self._service.status
        info = None
        doit = False
        if s == status.SUSPENDED:
            info = "Resuming processing."
            doit = True
        elif s == status.PROCESSING:
            info = "Already processing; no need to resume."
        else:
            info = "Unable to resume from state '%s'." % s
        self.log.info("Service wrapper received SIGCONT. " + info)
        if doit:
            self._service.processing_resume()

    def run(self, auto_start):
        self._service.log.status("--- STARTING SERVICE ---")
        ok = self._service.run()
        if ok and auto_start:
            ok = self._service.processing_start()
        self._service.wait()
        self._service.log.status("--- SERVICE STOPPED ---")
        reload = False if self._terminate else self._reload
        if reload:
            self._service.log.info("Reloading service.")
        return reload


def list_services(run_dir=None, service_package_path=None, service_file_path=None):
    if not run_dir:
        run_dir = os.environ.get("ESLIB_SERVICE_DIR")

    tops = _get_tops(run_dir, service_package_path, service_file_path)
    for top in tops:
        services = eslib.unique(_list_services(top))
        for service in services:
            print service

def _list_services(mod, prefix="", ll=None, seen=None):
    if ll is None:
        ll = []
    if seen is None:
        seen = []
    for name, cls in inspect.getmembers(mod):
        if inspect.isclass(cls) and issubclass(cls, HttpService) and not issubclass(HttpService, cls):
            ll.append(prefix + name)
        elif inspect.ismodule(cls) and not cls in seen:
            seen.append(cls)
            _list_services(cls, "%s%s." % (prefix, name), ll, seen)
    return ll

def _get_tops(run_dir, service_package_path, service_file_path):
    tops = []
    if service_file_path:
        mod = imp.load_source("service_module", service_file_path)
        if mod:
            tops.append(mod)
    if service_package_path:
        pkg = imp.load_package("service_package", service_package_path)
        if pkg:
            tops.append(pkg)
    if run_dir:
        pkg = imp.load_package("service_package", "/".join([run_dir, "source"]))
        if pkg:
            tops.append(pkg)
    tops.append(eslib.service)
    return tops

def find_type(service_type_name, run_dir=None, service_package_path=None, service_file_path=None):
    tops = _get_tops(run_dir, service_package_path, service_file_path)
    for top in tops:
        found = _find_type(top, service_type_name)
        if found:
            return found
    return None

def _find_type(mod, service_type_name, seen=None):
    if seen is None:
        seen = []
    for name, cls in inspect.getmembers(mod):
        if inspect.isclass(cls) and issubclass(cls, HttpService) and not issubclass(HttpService, cls) and name == service_type_name:
            return cls
        elif inspect.ismodule(cls) and not cls in seen:
            seen.append(cls)
            hit = _find_type(cls, service_type_name, seen)
            if hit:
                return hit
    return None

def run_service(
        run_dir=False, service_package_path=None, service_file_path=None,
        service_name=None, service_type_name=None,
        stdin_dict=None,
        config_file_path=None, config_section=None,
        console_log_level=None,
        daemon_mode=None, run_as_user=None,
        manager_endpoint=None, endpoint=None,
        auto_start=False):

    console_mode = not daemon_mode

    if not config_section:
        config_section = service_name

    if not run_dir:
        run_dir = os.environ.get("ESLIB_SERVICE_DIR")
    if not run_dir:
        print >> sys.stderr, "CRITICAL: Missing run_dir; cannot continue."
        sys.exit(1)

    config_path = None
    if config_file_path:
        if config_file_path == os.path.basename(config_file_path):
            config_path  = "/".join([run_dir, "config", config_file_path])
        else:
            config_path = config_file_path
    else:
        config_path  = "/".join([run_dir, "config", "services.yaml"])
    credentials_path = "/".join([run_dir, "config", "credentials.yaml"])
    log_config_path  = "/".join([run_dir, "config", "logging.yaml"])
    log_console_path = "/".join([run_dir, "config", "logging-console.yaml"])
    log_dir          = "/".join([run_dir, "log"   , service_name])

    # Change executing user (if privileged to do so)
    if run_as_user:
        pw = pwd.getpwnam(run_as_user)
        uid = pw.pw_uid
        gid = pw.pw_gid
        os.setgid(gid)
        os.setuid(uid)

    # Change output dir to log_dir
    os.system('mkdir -p %s' % log_dir)
    os.chdir(log_dir)

    if daemon_mode:
        print "Daemonizing... redirecting output to directory %s" % log_dir
        daemon_context = daemon.DaemonContext(
            stdout=open(os.path.join(log_dir, "daemon.stdout"), "wb"),
            stderr=open(os.path.join(log_dir, "daemon.stderr"), "wb"),
            files_preserve=None,
            umask=022,
            prevent_core=True,
            detach_process=True,
            #pidfile=,
            working_directory = log_dir
        )
        if run_as_user:
            daemon_context.uid = uid
            daemon_context.gid = gid
        daemon_context.open()
        # From here on we are in a forked daemon process, and the original parent process is dead

    # Write pid
    with open("pid", "wt") as pidfile:
        print >> pidfile, os.getpid()

    # Change process name
    setproctitle("%s %s" % (os.path.basename(sys.argv[0]), service_name))

    reload = True
    while reload:
        # We do all of the below per reload iteration, since either source code or configs can have changed meanwhile.
        # Only exceptions and 'reload' set to false will get us out of here

        credentials = {}
        global_config = {}
        log_config = {}

        # Load config files
        if stdin_dict:
            credentials = stdin_dict.get("credentials") or {}
            global_config = stdin_dict.get("config") or {}
        else:
            if os.path.isfile(credentials_path):
                with open(credentials_path, "r") as f:
                    credentials = yaml.load(f) or {}
            if os.path.isfile(config_path):
                with open(config_path, "r") as f:
                    global_config = yaml.load(f) or {}
        # Using local log config files as of yet..
        try:
            with open(log_console_path if console_mode else log_config_path) as f:
                log_config = yaml.load(f) or {}
        except:
            pass

        # Unless explicitly overridden, look for the service_type_name in the config
        if not service_type_name:
            section = global_config.get(config_section)
            if section:
                config_stn = section.get("type")
                if config_stn:
                    service_type_name =config_stn
        if not service_type_name:
            # No service, no logger, so we have to print this to stderr:
            print >> sys.stderr, "Missing service type name; not in config and not explicitly overridden."
            exit(1)

        # Find service type from name
        service_type = find_type(service_type_name, run_dir, service_package_path, service_file_path)
        if not service_type:
            # No service, no logger, so we have to print this to stderr:
            print >> sys.stderr, "Failed to evaluate service type '%s'." % service_type_name
            exit(1)

        # Initialize logging
        if log_config:
            logging.config.dictConfig(config=log_config)
        else:
            logging.basicConfig()

        if console_mode and console_log_level:
            for n in ["servicelog", "proclog", "doclog"]:
                logging.getLogger(n).setLevel(console_log_level or logging.INFO)

        # Instantiate and set up
        service = service_type(name=service_name)
        service.config_file = config_file_path  # The manager needs to know this so it can spawn processes based on the same config
        service.config_key = config_section
        service.configure(credentials, global_config.get(config_section) or {}, global_config)
        if manager_endpoint:
            service.config.manager_endpoint = manager_endpoint
        if endpoint:
            service.config.management_endpoint = endpoint

        reload = ServiceRunner(service, daemon_mode).run(auto_start)

    os.remove("pid")

    if daemon_mode:
        daemon_context.close()

#================================

def main():
    usage = \
"""
Runner for eslib document processing services.

Usage:
  %(prog)s list [*options]
  %(prog)s [options] <service_name>

Options:
  [-d <run_dir>]            * Home directory for service config and output.
  [-f <config_file>]          Use this config file instead of the default. Searches config dir first.
  [-t <service_type>]         Normally this is gotten from the config file, but can be overridden.
  [-c <config_section>]       Name of config section for service within
                              ./config/services.yaml.
  [-l <log_level>]            Log level, for console mode logging only.
  [-u <run_as_user>]          Run as this user.
  [-m <manager_endpoint>]     "host:port" of service manager.
  [-e <endpoint>]             "host:port" where this service listens for commands. Port is optional.
  [--start]                   Start processing at once.
  [--daemon]                  Detach and run in background.
                              (Otherwise logging to console, etc..)
  [--service-package <dir>] * Override path to service package. Default=./service.
  [--service-file <file>]   * Load types directly from module file instead of package.

Run dir expects directories with content in

    config/
        credentials.yaml
        logging.yaml
        logging-console.yaml
        services.yaml
    source/
        <python package with services>

and writes pid and log output to log/.

Default run_dir can be set with environment variable ESLIB_SERVICE_DIR.
"""
    parser_desc = "Runner for eslib document processing services."
    parser = argparse.ArgumentParser(description=parser_desc)
    parser._actions[0].help = argparse.SUPPRESS

    if len(sys.argv) == 1:
        print usage.strip() % {"prog": parser.prog}
        sys.exit(1)

    if len(sys.argv) > 1 and sys.argv[1] == "list":
        sys.argv.remove("list")
        # Use a different parser instead
        desc = "List available service types."
        parser = argparse.ArgumentParser(description=desc)
        parser._actions[0].help = argparse.SUPPRESS
        parser.add_argument("-d", "--run-dir"  , dest="run_dir"        , type=str, required=False)
        parser.add_argument("--service-package", dest="service_package", type=str, required=False)
        parser.add_argument("--service-file"   , dest="service_file"   , type=str, required=False)

        args = parser.parse_args()

        list_services(args.run_dir, args.service_package, args.service_file,)
    else:
        parser.add_argument("name", type=str, nargs=1)
        parser.add_argument("-d", "--run-dir"    , dest="run_dir"     , type=str, required=False)
        parser.add_argument("-f", "--config-file", dest="config_file" , type=str, required=False)
        parser.add_argument("-c", dest="config_section" , type=str, required=False)
        parser.add_argument("-t", dest="type"           , type=str, required=False)
        parser.add_argument("-l", dest="loglevel"       , type=str, required=False)
        parser.add_argument("-u", dest="user"           , type=str, required=False)
        parser.add_argument("-m", dest="mgr_endpoint"   , type=str, required=False)
        parser.add_argument("-e", dest="mgmt_endpoint"  , type=str, required=False)
        parser.add_argument("--daemon" , action="store_true")
        parser.add_argument("--start"  , action="store_true")
        parser.add_argument("--service-package", dest="service_package", type=str, required=False)
        parser.add_argument("--service-file"   , dest="service_file"   , type=str, required=False)
        parser.add_argument("--stdincfg", action="store_true")


        args = parser.parse_args()

        # Get config from stdin if 'stdincfg' was specified.
        stdin_dict = None
        if args.stdincfg:
            stdin_dict = json.load(sys.stdin)

        run_service(
            args.run_dir, args.service_package, args.service_file,
            args.name[0], args.type,
            stdin_dict,
            args.config_file, args.config_section,
            args.loglevel,
            args.daemon, args.user,
            args.mgr_endpoint, args.mgmt_endpoint,
            args.start)

if __name__ == '__main__':
    main()
