#!/usr/bin/env python

import sys
import os
import argparse
import logging
import time
import daemonocle
import psutil
import signal
import json
import pwd
from datetime import datetime
from yachain import Config

from oanda_trading_environment.daemon.plugin import PluginManager
from oanda_trading_environment.stream import CandleFactory, StreamRecord
from oanda_trading_environment import stream

# initial exponential backoff
EBO = 1
MAX_FORK_STREAMPARSER_SLAVE = 5

# in case we have real_prefix this is in a virtual env
PREFIX = "/" if not hasattr(sys, 'real_prefix') else sys.prefix

CONFIG_FILE = os.path.join(PREFIX, "etc/OANDA/config/OANDAd.cfg")

parser = argparse.ArgumentParser(prog='OANDAd')

verbose = argparse.ArgumentParser(add_help=False)
verbose.add_argument('--verbose', '-v', action="count", help='verbosity level')
verbose.add_argument('--config', '-c', default=CONFIG_FILE,
                     help="config file, default: " + CONFIG_FILE)
verbose.add_argument('--foreground', '-f', action='store_true', default=False,
                     help="run in foreground, default is daemonize")

sp = parser.add_subparsers()
sp_command = sp.add_parser('start', parents=[verbose],
                           help="Starts %(prog)s daemon")
sp_stop = sp.add_parser('stop', parents=[verbose],
                        help="Stops %(prog)s daemon")
sp_status = sp.add_parser('status', parents=[verbose],
                          help="Status %(prog)s daemon")

verbose.set_defaults(verbose=None)
sp_command.set_defaults(command='start')
sp_status.set_defaults(verbose=0, foreground=None,
                       config=CONFIG_FILE, command='status')
sp_stop.set_defaults(verbose=0, foreground=None,
                     config=CONFIG_FILE, command='stop')

args = parser.parse_args()

config = Config(prefix=PREFIX)
config.load(args.config)
try:
    int(config['account_id'])
except ValueError:
    from oanda_trading_environment.stream.parserV20 import V20Streamer as Streamer
else:
    from oanda_trading_environment.stream.parser import Streamer

# globals
factories = {}


def cb_shutdown(message, code):
    logging.error('Daemon is stopping %d', os.getpid())

    def on_terminate(proc):
        logging.error("process {} terminated with exit code {}".format(
                      proc, proc.returncode))

    procs = psutil.Process().children()
    for p in procs:
        p.terminate()
        gone, still_alive = psutil.wait_procs(procs,
                                              timeout=3,
                                              callback=on_terminate)
    for p in still_alive:
        p.kill()

    logging.debug(message)


class TickProcessor(object):

    def __init__(self, plugin_manager):
        self.pm = plugin_manager

    def on_success(self, s):
        if isinstance(s, stream.StreamRecord) and s.recordtype() == stream.TICK:
            try:
                for factory in factories[s['instrument']]:
                    r = factory.processTick(s)
                    if r:
                        logging.info("record completed: " + json.dumps(r))
                        self.pm.execute(r)
            except KeyError:
                logging.error("no factory for: %s", s['instrument'])

            except Exception as e:
                logging.error("TickProcessor: unknown exception: %s", e)

# -------------------------------------------------


class Main(object):

    def __init__(self, config, daemon):
        self._config = config
        self._daemon = daemon
        self.pid = None
        processingMode = "atEndOfTimeFrame"
        # --
        logging.basicConfig(
            filename=config['daemon::logfile'],
            level=logging.DEBUG,
            format='%(asctime)s [%(levelname)s] %(name)s : %(message)s',
        )

        try:
            logging.info("Setting loglevel to: %s", config['daemon::loglevel'])
            logging.getLogger().setLevel(getattr(logging, config['daemon::loglevel']))
        except:
            logging.error("failure changing loglevel, check configfile")
        # --

        if "fabricate" in config['streamer']:
            processingMode = config['streamer::fabricate']

        logging.info("Daemon is starting (%d)", os.getpid())
        for instrument, details in config['instruments'].iteritems():
            factories.update({instrument: []})
            logging.info("setting up candle factories for: %s", instrument)
            for timeframe in details['timeframes']:
                factories[instrument].append(CandleFactory(instrument,
                                             timeframe,
                                             processingMode=processingMode))
                logging.info("  created: factory %s, timeframe: %s",
                             instrument, timeframe)

    def main(self):
        ebo = EBO
        forks = 0
        while True:
            logging.info("forking slave streamparser")
            pid = None
            referenceEpoch = 0
            try:
                pid = os.fork()
                referenceEpoch = int(time.time())
            except OSError as e:
                logging.error('%s', e)
                time.sleep(2)
                forks += 1
                if forks <= MAX_FORK_STREAMPARSER_SLAVE:
                    continue
                else:
                    logging.error("Exiting: max forks reached")
                    exit(2)

            if pid:
                childExitStatus, text = os.wait()

                childExitCode = childExitStatus >> 8
                logging.info('slave %d died: exit: %d', pid, childExitCode)
                # check how/ how soon it died
                now = int(time.time())
                if now - referenceEpoch < 1:
                    logging.error("slave %d died to soon ... respawn with "
                                  "exp. backoff %d seconds", pid, ebo)
                    ebo *= 2
                    time.sleep(ebo)
                else:
                    logging.error("slave %d died ... after %d seconds",
                                  pid, now - referenceEpoch)
            elif pid == 0:
                # start the streamer
                # if it times out the outer while-True will get us a new one

                # prevent cleanup of the pidfile when child process exits
                self._daemon.shutdown_callback = None
                self._daemon.pidfile = None
                logging.info('starting slave streamparser: %d %d',
                             os.getpid(), os.getppid())
                streamLog = None
                if "logfile" in config["streamer"]:
                    try:
                        streamLogName = config['streamer::logfile']
                        extFormat = ""
                        if 'date_extension_format' in config["streamer"]:
                            extFormat = config['streamer::date_extension_format']
                        ext = datetime.now().strftime(extFormat)
                        streamLogName = "%s.%s" % (streamLogName, ext)
                        streamLog = open(streamLogName, "a")
                    except:
                        logging.error('failure opening streamlog: %',
                                      config["streamer::logfile"])

                try:
                    pm = None
                    # initialize the PluginManager with partial Config
                    pm = PluginManager(
                           config=Config(configdata=self._config['plugins'],
                                         prefix=config.prefix))
                    pm.load()
                    tp = TickProcessor(pm)
                    s = Streamer(on_success=tp.on_success,
                                 streamLog=streamLog,
                                 environment="practice",
                                 access_token=config['access_token'])
                    s.start(accountId=config['account_id'],
                            instruments=",".join(factories.keys()))
                except Exception as e:
                    logging.error("%s", e)
                    exit(2)


if __name__ == '__main__':
    uid, gid = None, None
    try:
        pwrec = pwd.getpwnam(config["daemon::user"])
        uid = pwrec.pw_uid
        gid = pwrec.pw_gid
    except Exception as e:
        logging.error("%s", e)
        exit(0)

    if args.command in ["start", "stop", "status"]:
        detach = False if args.foreground else True
        daemon = daemonocle.Daemon(
            shutdown_callback=cb_shutdown,
            workdir=PREFIX,
            detach=detach,
            uid=uid,
            gid=gid,
            pidfile=config['daemon::pidfile'],
        )
        try:
            parser = Main(config=config, daemon=daemon)
        except Exception as e:
            logging.error("%s", e)
        else:
            daemon.worker = parser.main
            daemon.do_action(args.command)
