#!/usr/bin/env python3
from logs.TailFile import TailFile
from mail.InboundMailLogHandler import InboundMailLogHandler
from logs.ReadPositionStoreInFile import ReadPositionStoreInFile
from db.SqliteConnFactory import SqliteConnFactory
from db.SqliteEventStore import SqliteEventStore
from db.Pruner import Pruner
from util.env import load_env_vars_from_file

import time
import logging, logging.handlers
import json
import re
import os
import sys
import signal

log = logging.getLogger(__name__)

if os.path.exists('/etc/mailinabox.conf'):
    env = load_env_vars_from_file('/etc/mailinabox.conf')
else:
    env = { 'STORAGE_ROOT': os.environ.get('STORAGE_ROOT', os.getcwd()) }

CAPTURE_STORAGE_ROOT = os.path.join(env['STORAGE_ROOT'], 'reporting')


# default configuration, if not specified or
# CAPTURE_STORAGE_ROOT/config.json does not exist
config_default = {
    'default_config': True,
    'capture': True,
    'prune_policy': {
        'frequency_min': 2400,
        'older_than_days': 30
    },
    'drop_disposition': {
        'failed_login_attempt': True,
        'suspected_scanner': False,
        'reject': True
    }
}

config_default_file = os.path.join(CAPTURE_STORAGE_ROOT, 'config.json')


# options
    
options = {
    'daemon': True,
    'log_level': logging.WARNING,
    'log_file': "/var/log/mail.log",
    'pos_file': "/var/lib/mailinabox/capture-pos.json",
    'sqlite_file': os.path.join(CAPTURE_STORAGE_ROOT, 'capture.sqlite'),
    'working_dir': "/var/run/mailinabox",
    'config': config_default,
    '_config_file': False,  # absolute path if "config" was from a file
    '_runtime_config_file': "runtime_config.json" # relative to working_dir
}


def read_config_file(file, throw=False):
    try:
        with open(file) as fp:
            newconfig = json.loads(fp.read())
        newconfig['from'] = { 'type':'file', 'location':file }
        return newconfig
    except FileNotFoundError as e:
        if not throw:
            return False
        raise e

def write_config(tofile, config):
    d = os.path.dirname(tofile)
    if d and not os.path.exists(d):
        os.mkdir(d, mode=0o770)
    with open(tofile, "w") as fp:
        fp.write(json.dumps(config))

def usage():
    print('usage: %s [options]' % sys.argv[0])
    sys.exit(1)

def process_cmdline(options):
    argi = 1
    marg = 0
    while argi < len(sys.argv):
        arg = sys.argv[argi]
        have_next = ( argi+1 < len(sys.argv) )
        
        if arg=='-d':
            options['daemon'] = False
                
        elif arg=='-loglevel' and have_next:
            argi += 1
            arg = sys.argv[argi].lower()
            if arg=='info':
                options['log_level'] = logging.INFO
            elif arg=='warning':
                options['log_level'] = logging.WARNING
            elif arg=='debug':
                options['log_level'] = logging.DEBUG
            elif arg=='error':
                options['log_level'] = logging.ERROR
            else:
                sys.stderr.write('unknown log level "%s"\n' % sys.argv[argi])
                
        elif arg=='-config' and have_next:
            argi += 1
            arg = sys.argv[argi]
            try:
                if arg.startswith('{'):
                    options['config'] = json.loads(arg)
                else:
                    newconfig = read_config_file(arg, throw=True)
                    options['config'] = newconfig
                    options['_config_file'] = arg
            except Exception as e:
                if options['daemon']: log.exception(e)
                raise e

        elif arg=='-logfile' and have_next:
            argi+=1
            options['log_file'] = sys.argv[argi]
            
        elif arg=='-posfile' and have_next:
            argi+=1
            options['pos_file'] = sys.argv[argi]
            
        elif arg=='-sqlitefile' and have_next:
            argi+=1
            options['sqlite_file'] = sys.argv[argi]            

        elif arg.startswith('-'):
            usage()

        else:
            if marg==0:
                options['log_file'] = arg
            elif marg==1:
                options['pos_file'] = arg
            elif marg==2:
                options['sqlite_file'] = arg
            else:
                usage()
            marg += 1
        argi += 1


def set_working_dir(working_dir):
    try:
        if not os.path.exists(working_dir):
            os.mkdir(working_dir, mode=0o770)
        os.chdir(working_dir)
    except Exception as e:
        log.exception(e)
        raise e

def close_stdio():
    sys.stdout.close()
    sys.stderr.close()
    sys.stdin.close()

    
# if config.json exists in the default location start with that
# instead of `config_default`. config can still be changed with the
# command line argument "-config"

newconfig = read_config_file(config_default_file, throw=False)
if newconfig:
    options['config'] = newconfig
    options['_config_file'] = config_default_file
    
# process command line
process_cmdline(options)


# init logging & set working directory
if options['daemon']:
    logging.basicConfig(
        level = options['log_level'],
        handlers= [
            logging.handlers.SysLogHandler(address='/dev/log')
        ],
        format = 'miabldap/capture %(message)s'
    )
    log.warning('MIAB-LDAP capture/SEM daemon starting: wd=%s; log_file=%s; pos_file=%s; db=%s',
                options['working_dir'],
                options['log_file'],
                options['pos_file'],
                options['sqlite_file']
    )
    close_stdio()
    set_working_dir(options['working_dir'])
    
else:
    logging.basicConfig(level=options['log_level'])
    log.info('starting: log_file=%s; pos_file=%s; db=%s',
             options['log_file'],
             options['pos_file'],
             options['sqlite_file']
    )


# save runtime config
write_config(options['_runtime_config_file'], options['config'])


# start modules
log.info('config: %s', options['config'])
try:
    db_conn_factory = SqliteConnFactory(
        options['sqlite_file']
    )
    event_store = SqliteEventStore(
        db_conn_factory
    )
    position_store = ReadPositionStoreInFile(
        options['pos_file']
    )
    mail_tail = TailFile(
        options['log_file'],
        position_store
    )
    inbound_mail_handler = InboundMailLogHandler(
        event_store,
        capture_enabled = options['config'].get('capture',True),
        drop_disposition = options['config'].get('drop_disposition')
    )
    mail_tail.add_handler(inbound_mail_handler)
    pruner = Pruner(
        db_conn_factory,
        policy=options['config']['prune_policy']
    )
    pruner.add_prunable(event_store)
    
except Exception as e:
    if options['daemon']: log.exception(e)
    raise e


# termination handler for graceful shutdowns
def terminate(sig, stack):
    if sig == signal.SIGTERM:
        log.warning("shutting down due to SIGTERM")
    log.debug("stopping mail_tail")
    mail_tail.stop()
    log.debug("stopping pruner")
    pruner.stop()
    log.debug("stopping position_store")
    position_store.stop()
    log.debug("stopping event_store")
    event_store.stop()
    try:
        os.remove(options['_runtime_config_file'])
    except Exception:
        pass
    log.info("stopped")

# reload settings handler
def reload(sig, stack):
    # if the default config (`config_default`) is in use, check to see
    # if a default config.json (`config_default_file`) now exists, and
    # if so, use that
    if options['config'].get('default_config', False) and os.path.exists(config_default_file):
        options['config']['default_config'] = False
        options['_config_file'] = config_default_file

    log.info('%s records are in-progress',
             inbound_mail_handler.get_inprogress_count())

    if options['_config_file']:
        log.info('reloading %s', options['_config_file'])
        try:
            newconfig = read_config_file(options['_config_file'], throw=True)
            pruner.set_policy(
                newconfig['prune_policy']
            )
            inbound_mail_handler.set_capture_enabled(
                newconfig.get('capture', True)
            )
            inbound_mail_handler.update_drop_disposition(
                newconfig.get('drop_disposition', {})
            )
            write_config(options['_runtime_config_file'], newconfig)
        except Exception as e:
            if options['daemon']:
                log.exception(e)
            else:
                raise e

signal.signal(signal.SIGTERM, terminate)
signal.signal(signal.SIGINT, terminate)
signal.signal(signal.SIGHUP, reload)


# monitor and capture
mail_tail.start()
mail_tail.join()