import argparse
import logging
import os
import signal
import subprocess
import sys
import time

from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer

from .bootstrap import Bootstrap
from .config import get_config, get_path


class ProcessWatchdog(FileSystemEventHandler):
    ''' Handle watchdog events by restarting a subprocess. '''

    def __init__(self):
        ''' Constructor. '''

        self._logger = logging.getLogger('watchdog')
        self._process = None

    def dispatch(self, event):
        ''' Restart the subprocess if a source/config file changed. '''

        path = event.src_path
        file = os.path.basename(path)
        descr = '{} was {}'.format(event.src_path, event.event_type)

        if (file.endswith('.py') and not file.startswith('test_')) or \
            file.endswith('.ini'):
            self._logger.info('%s (Reloading)', descr)
            self.terminate_process()
            self.start_process()

    def join(self):
        ''' Wait for subprocess to exit. '''
        if self._process is not None:
            self._process.wait()

    def start_process(self):
        ''' Start the subprocess. '''

        if self._process is not None:
            msg = 'Cannot start subprocess if it is already running.'
            raise RuntimeError(msg)

        time.sleep(1)
        args = [sys.executable, '-m', __package__] + sys.argv[1:]
        new_env = dict(os.environ)
        new_env['WATCHDOG_RUNNING'] = '1'
        self._process = subprocess.Popen(args, env=new_env)

    def terminate_process(self):
        ''' Terminate the subprocess. '''
        if self._process is not None:
            try:
                self._process.send_signal(signal.SIGTERM)
                self._process.wait()
                self._process = None
            except ProcessLookupError:
                pass # The process already died.


class Reloader:
    ''' Reloads the subprocess when a source file is modified. '''
    def __init__(self):
        ''' Constructor. '''
        self._logger = logging.getLogger('reloader')
        self._observer = None
        self._running = False
        self._watchdog = None

    def run(self):
        ''' Run the reloader. '''

        self._logger.info('Running with reloader...')
        self._watchdog = ProcessWatchdog()
        self._watchdog.start_process()

        self._observer = Observer()
        self._observer.schedule(
            self._watchdog, str(get_path('starbelly')), recursive=True)
        self._observer.start()

        while True:
            time.sleep(1)

    def shutdown(self, signum, _):
        ''' Exit the reloader. '''
        signame = signal.Signals(signum).name
        self._logger.info('Caught %s (shutting down)', signame)
        self._watchdog.terminate_process()
        self._observer.stop()
        self._observer.join()
        sys.exit(0)


def configure_logging(log_level, error_log):
    ''' Set default format and output stream for logging. '''
    log_format = '%(asctime)s [%(name)s] %(levelname)s: %(message)s'
    log_date_format = '%Y-%m-%d %H:%M:%S'
    log_formatter = logging.Formatter(log_format, log_date_format)
    log_level = getattr(logging, log_level.upper())
    log_handler = logging.StreamHandler(sys.stderr)
    log_handler.setFormatter(log_formatter)
    log_handler.setLevel(log_level)
    logger = logging.getLogger()
    logger.addHandler(log_handler)
    logger.setLevel(log_level)
    if log_level < logging.INFO:
        logging.getLogger('watchdog').setLevel(logging.INFO)
        logging.getLogger('trio-websocket').setLevel(logging.INFO)

    if error_log is not None:
        exc_handler = logging.FileHandler(error_log)
        exc_handler.setFormatter(log_formatter)
        exc_handler.setLevel(logging.ERROR)
        logger.addHandler(exc_handler)


def get_args():
    ''' Parse command line arguments. '''
    arg_parser = argparse.ArgumentParser(description='Starbelly')
    arg_parser.add_argument(
        '--log-level',
        default='warning',
        metavar='LEVEL',
        choices=['debug', 'info', 'warning', 'error', 'critical'],
        help='Set logging verbosity (default: warning)'
    )
    arg_parser.add_argument(
        '--ip',
        default='127.0.0.1',
        help='The IP address to bind to (default: 127.0.0.1)'
    )
    arg_parser.add_argument(
        '--port',
        type=int,
        default=8000,
        help='The TCP port to bind to (default: 8000)'
    )
    arg_parser.add_argument(
        '--reload',
        action='store_true',
        help='Auto-reload when code or static assets are modified.'
    )
    arg_parser.add_argument(
        '--error-log',
        help='Copy error logs to the specified file.'
    )
    return arg_parser.parse_args()


def main():
    ''' Set up watchdog or run starbelly. '''
    args = get_args()
    configure_logging(args.log_level, args.error_log)
    config = get_config()

    if args.reload and os.getenv('WATCHDOG_RUNNING') is None:
        reloader = Reloader()
        signal.signal(signal.SIGINT, reloader.shutdown)
        signal.signal(signal.SIGTERM, reloader.shutdown)
        reloader.run()
    else:
        bootstrap = Bootstrap(config, args)
        bootstrap.run()


if __name__ == '__main__':
    main()