# -*- coding: utf-8 -*-

import filecmp
import glob
import logging
import os
import pywintypes
import shutil
import subprocess
import sys
import win32api
import win32event
import win32service
import win32serviceutil

from elevate import elevate
from logging.handlers import RotatingFileHandler
from pathlib import Path
from ruamel.yaml import YAML
from win32com.shell import shell


class ServiceInfo:
    """
    Represents the persistent configuration of the service.

    Accessible both during installation and service runtime
    using pythonservice.exe as it's bootstrapper.
    """
    fws_dir = Path(__file__).parent
    binaries_dir = fws_dir.parent
    # Static configuration helper fields
    yaml_config_file = fws_dir / 'forcomwebservice.yaml'

    def __init__(self):
        if not self.yaml_config_file.exists():
            raise Exception(
                f'Missing configuration file: {self.yaml_config_file}')
        with open(self.yaml_config_file) as stream:
            yaml = YAML().load(stream)
        # Fetch service container parameters
        configuration = yaml['Configuration']
        # Get services.msc fields
        self.service_name = configuration['service_name']
        self.service_display_name = configuration['service_display_name']
        self.service_description = configuration['service_description']
        # Get container configuration fields
        self.fws_heartbeat_millisec = configuration['fws_heartbeat_millisec']
        self.fws_logging_dir = configuration['fws_logging_dir']
        self.fws_logging_level = configuration['fws_logging_level']
        # Get jvm64 fields
        self.jvm64_executable = configuration['jvm64_executable']
        # Fetch and format fws command lines parameters
        fws_parameters = []
        for param, value in yaml['ForcomWebService'].items():
            if value:
                fws_parameters.append('-{}={}'.format(param, value))
        self.fws_parameters = fws_parameters
        self.fws_webservice_address = (
            yaml['ForcomWebService']['webserviceaddress'])
        # Setup logging
        fmt = logging.Formatter(
            '%(levelname)-5s %(asctime)s,%(msecs)03d %(message)s',
            datefmt='%Y/%m/%d %H:%M:%S')
        self.logger = logging.getLogger("fws.logger")
        self.logger.setLevel(logging.getLevelName(self.fws_logging_level))
        log_file = Path(self.fws_logging_dir) / (self.service_name + '.log')
        handler = logging.handlers.RotatingFileHandler(str(log_file))
        handler.setFormatter(fmt)
        self.logger.addHandler(handler)

    def get_fws_command_line(self):
        return [
            self.jvm64_executable,
            '-cp',
            'bin/ImporterBusiness.jar;config/centrala/logging',
            'pl.forcom.transfer.webservices.imports.ForcomWebService',
        ] + self.fws_parameters


def copy_file(logger, pattern, src_dir, dst_dir):
    src_pattern = src_dir / pattern
    for file in glob.glob(str(src_pattern)):
        file_name = Path(file).name
        dst_file = dst_dir / file_name
        if dst_file.exists() and filecmp.cmp(file, dst_file, shallow=False):
            continue
        logger.info(f'Copying {file} to {dst_file}')
        shutil.copy(file, dst_file)


def patch_pywin32_installation(logger):
    if all(i not in ('install', 'update') for i in sys.argv):
        return
    logger.info("Patching pywin32 installation")
    # Static runtime-only helper fields.
    win32_dir = Path(win32service.__file__).parent
    pywin32_dir = win32_dir.parent / 'pywin32_system32'
    executable_dir = Path(sys.executable).parent
    logger.info("Executable dir %s", executable_dir)
    # Wymagane, ponieważ pythonservice.exe musi załadować swoje zależności,
    # a sam znajduje się w innym katalogu niż python.exe
    copy_file(logger, 'python3*.dll', executable_dir, win32_dir)
    copy_file(logger, 'vcruntime*.dll', executable_dir, win32_dir)
    copy_file(logger, 'pywintypes*.dll', pywin32_dir, win32_dir)


class ServiceContainer(win32serviceutil.ServiceFramework):
    def __init__(self, args):
        self._logger.info(
            'Starting service (service_name: %s)',
            self._svc_name_)
        win32serviceutil.ServiceFramework.__init__(self, args)
        self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)

    def web_service_start(self):
        self._logger.info(
            'Starting web service java subprocess on %s',
            self._service_info.fws_webservice_address)
        cmd = self._service_info.get_fws_command_line()
        self._logger.debug(cmd)
        fws_subprocess = subprocess.Popen(
            cmd, stderr=subprocess.PIPE,
            cwd=str(self._service_info.binaries_dir))
        self.ReportServiceStatus(win32service.SERVICE_RUNNING)
        return fws_subprocess

    def web_service_heartbeat(self, fws_subprocess):
        rc = None
        while rc != win32event.WAIT_OBJECT_0:
            poll = fws_subprocess.poll()
            if poll is not None:
                self._logger.info('Java subprocess exited with code: %s', poll)
                for line in iter(fws_subprocess.stderr.readline, b''):
                    self._logger.error(
                        line.rstrip().decode('windows-1250', errors="ignore"))
                return
            self._logger.debug('Heartbeat')
            rc = win32event.WaitForSingleObject(
                self.hWaitStop, self._service_info.fws_heartbeat_millisec)

    def ensure_web_service_stopped(self, fws_subprocess):
        if fws_subprocess and fws_subprocess.poll() is None:
            self._logger.info('Stopping web service java virtual machine')
            fws_subprocess.terminate()
            fws_subprocess.wait()

    def SvcDoRun(self):
        try:
            fws_subprocess = self.web_service_start()
            self.web_service_heartbeat(fws_subprocess)
        except Exception as e:
            self._logger.exception(e)
        self._logger.info('Shutting down service')
        self.ensure_web_service_stopped(fws_subprocess)

    def SvcStop(self):
        self._logger.info('Stop signal received')
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        win32event.SetEvent(self.hWaitStop)


# Must be defined here, because pythonservice.exe calls do not enter main()
service_info = ServiceInfo()
ServiceContainer._service_info = service_info
ServiceContainer._logger = service_info.logger
ServiceContainer._svc_name_ = service_info.service_name
ServiceContainer._svc_display_name_ = service_info.service_display_name
ServiceContainer._svc_description_ = service_info.service_description


def main():
    logger = service_info.logger

    try:
        if not shell.IsUserAnAdmin():
            elevate(show_console=True)

        patch_pywin32_installation(logger)

        win32serviceutil.HandleCommandLine(ServiceContainer)
    except Exception as e:
        logger.exception(e)
    input("Service has been shut down. Press ENTER to exit...")


if __name__ == "__main__":
    main()
