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

"""
firebat.launcher
~~~~~~~~~~~~~~~

Start new POSIX process for Phantom supervisor and monitor it.
"""

import os
import sys
import time
import signal
import socket
import getpass
import logging
import datetime
import cPickle
import base64
from string import maketrans
from subprocess import Popen, PIPE

import simplejson as json
from simplejson.decoder import JSONDecodeError

PHANTOM_CMD = ['phantom', 'run', 'phantom.conf']


class FireStatus(object):
    """Supervisor and Phantom status information singleton.

    Attributes:
        state: str, human readable status.
        started_at: datetime obj, time when work was started.
        duration: int, time delta in seconds from start.
        uid: str, process owner.
        owner: str, responsible person
        total_dur: int, theoretical test self.duration in seconds
        wd: str, working directory if process.
        pid: int, supervisor PID.
        phantom_pid: int, Phantom process PID.
        answ_mtime: str, time when answ.txt was last time writen by Phantom.
        answ_mago: int, number of seconds since answ_mtime moment.
        retcode: int, Phantom process exit code.
        stdout: str, Phantom process STDOUT.
        stderr: str, Phantom process STDERR.
        eggs: An integer count of the eggs we have laid.
    """
    def __init__(self, opts, state='Starting', logger=None):
        if logger:
            self.logger = logger
        else:
            self.logger = get_logger()

        self.pid = os.getpid()
        self.state = state
        self.started_at = datetime.datetime.now()
        self.duration = 0
        self.uid = getpass.getuser()
        self.owner = opts.get('owner', 'uid')
        self.total_dur = opts.get('total_dur', 0)
        self.wd = os.getcwd()
        self.phantom_pid = None
        self.answ_mtime = None
        self.answ_mago = None
        self.retcode = None
        self.stdout = None
        self.stderr = None

    def duration_update(self):
        delta = datetime.datetime.now() - self.started_at
        self.duration = delta.seconds

    def log_mtime_update(self):
        '''Check when Phantom write one of his log files last time,
        helps to check, is Phantom alive or not.'''
        log_path = './answ.txt'
        try:
            mtime = os.path.getmtime(log_path)
            self.answ_mtime = time.ctime(mtime)
            delta = datetime.datetime.now() -\
                datetime.datetime.fromtimestamp(int(mtime))
            self.answ_mago = delta.seconds

        except OSError:
            self.logger.error('Can\'t read mtime of file: %s' % log_path)

    def jsonify(self):
        '''Represent collected data in human readable format'''
        result = {
            'state': self.state,
            'wd': self.wd,
            'pid': self.pid,
            'uid': self.uid,
            'owner': self.owner,
            'phantom_pid': self.phantom_pid,
            'duration': self.duration,
            'total_dur': self.total_dur,
            'answ_mtime': self.answ_mtime,
            'answ_mago': self.answ_mago,
            'retcode': self.retcode,
            'stdout': self.stdout,
            'stderr': self.stderr,
        }
        return json.dumps(result, sort_keys=True, indent=4 * ' ')

    def put(self):
        '''React on USR1 signal, put collected data on file system.'''
        file_path = '/tmp/%s.fire' % self.pid
        file(file_path, 'w').write(str(self.jsonify()))
        return file_path


def drop_pid(name, pid, pid_dir='/var/run/', stamp=False):
    if stamp:
        if not os.path.exists(pid_dir):
            os.makedirs(pid_dir)
        time_str = "%.6f" % time.time()
        time_str = time_str.translate(maketrans('.', '_'))
        pid_file = pid_dir + name + '_%s.pid' % time_str
    else:
        pid_file = './' + name + '.pid'

    with open(pid_file, 'w') as pid_fh:
        pid_fh.write(pid)
    return pid_file


def get_logger(log_path='./fire.log', log_lvl=logging.DEBUG):
    '''Create logger object.
    Args:
        log_path: str, path to new log file.
        log_lvl: int

    Returns:
        logger obj
    '''

    logger = logging.getLogger('firebat.launcher')
    logger.setLevel(logging.DEBUG)
    fh = logging.FileHandler(log_path)
    fh.setLevel(log_lvl)
    formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
    fh.setFormatter(formatter)
    logger.addHandler(fh)
    return logger


def update_fire(status, json_path='.fire.json', logger=None):
    '''Update fire dict according to status object.
    Args:
        status: obj, current status object.
        json_path: str, file path fire dict shud be readed from.
        logger: logger object.

    Returns:
        put new JSON file to fire working directory.
    '''

    new_path = '.fire_up.json'
    try:
        with open(json_path, "r") as fire_fh:
            fire = json.loads(fire_fh.read())
    except IOError, e:
        logger.error('Could not read "%s": %s\n' % (json_path, e))
    except JSONDecodeError, e:
        logger.error('Could not parse fire config file: %s\n%s' % (json_path,
                                                                   e))
    fire['owner'] = status.owner
    fire['uid'] = status.uid
    fire['total_dur'] = status.total_dur
    fire['started_at'] = status.started_at.strftime('%s')
    fire['src_host'] = socket.getfqdn()
    new_cfg = json.dumps(fire, indent=4 * ' ')
    try:
        with open(new_path, "w+") as fire_fh:
            fire_fh.write(new_cfg)
    except IOError, e:
        logger.error('Could not write "%s": %s\n' % (new_path, e))


def run_phantom(status, sleep_time=1, logger=None):
    '''Create new POSIX process and wait until exit.
    Args:
        status: obj, FireStatus instance to collect monitorring data.
        sleep_time: int, time interval between iterations.
        logger: obj, logger instance.
    '''

    if not logger:
        logger = get_logger()
    logger.debug('Phantom launch')
    job = Popen(PHANTOM_CMD, stdout=PIPE, stderr=PIPE)
    status.state = 'phantom launched'
    status.phantom_pid = job.pid
    status.state = 'phantom working'
    drop_pid('phantom', str(os.getpid()), pid_dir='./')
    update_fire(status, json_path='.fire.json', logger=logger)
    while 1:
        status.duration_update()
        status.log_mtime_update()
        retcode = job.poll()
        if retcode is not None:  # Process finished.
            status.retcode = retcode
            status.stdout = job.stdout.read()
            status.stderr = job.stderr.read()
            status.state = 'phantom exited'
            logger.debug('phantom exited with retcode: %s' % retcode)
            break
        else:  # No process is done, wait a bit and check again.
            time.sleep(sleep_time)


def run_supervisor():
    '''Shud be called in new daemon process to run and check phantom tool
    Args:
        log_path: str, path to new log file.
        log_lvl: int
    '''
    # put PID file two times, one for debug
    pid_p = drop_pid('fire',
                     str(os.getpid()),
                     pid_dir='/tmp/fire/',
                     stamp=True)
    drop_pid('fire', str(os.getpid()), pid_dir='./')
    logger = get_logger()

    opts = None
    if (len(sys.argv) > 1):
        opts = cPickle.loads(base64.b64decode(sys.argv[1]))

    status = FireStatus(opts, logger=logger)
    logger.info('PID file: %s' % pid_p)
    logger.info('PID: %s' % os.getpid())

    logger.info('Starting')

    def sig_usr1_handler(signum, frame):
        # TODO: Need to improve SIG handler.
        # see http://lcamtuf.coredump.cx/signals.txt
        file_path = status.put()
        logger.debug('USR1 signal catched. See info in: %s' % file_path)
        return

    signal.signal(signal.SIGUSR1, sig_usr1_handler)
    run_phantom(status, logger=logger)

    logger.debug(str(status.jsonify()))
    os.unlink(pid_p)
    logger.info('Exiting')

if __name__ == '__main__':
    run_supervisor()
