"""Core module of the dbis pipline."""

from collections.abc import Iterable
import importlib.util
import platform
import socket
import time

import git

from . import store
from .base import Loader
from .evaluators import Evaluator
from .storage_handlers import PickleBackupHandler
from .storage_handlers import PostGresOutputHandler
from .storage_handlers import StorageHandler
from .utils import LOGGER
from .utils import check_serializability
from .utils import get_backup_dir
from .utils import get_job_id
from .utils import notify_error


class Core:
    """Represents the core functionality of the DBIS pipeline."""

    def __init__(self, plan, dryrun=False, mail=None):
        """
        Intitializes the pipeline.

        Args:
            plan: either the path to or the contents of the file containing
                information about the steps that should be performed. This must
                include a valid dataloader, pipeline and evaluator object.

            dryrun: if true, this run won't be stored to the database

            mail: one of [None, 'total', 'runs'] - defines how verbose the mail
                notification system should be. None: no mails are sent. total:
                one mail is sent at the end of the entire run. runs: one mail
                is sent for every configuration generated by the dataloader.
        """
        self.dryrun = dryrun
        self.mail = mail
        self.plan_path = None
        self.plan_content = None
        self.git_info = self._get_git_info()

        if type(plan) == str:
            with open(plan, 'r') as plan_file:
                self.plan_content = plan_file.read()
            self.plan_path = plan
            plan = load_plan(plan)

        store['plan_path'] = self.plan_path
        self.dataloader = plan.dataloader
        self.pipeline = plan.pipeline
        self.evaluator = plan.evaluator
        self.storage_handlers = plan.storage_handlers
        if not dryrun:
            self.storage_handlers.insert(
                0,
                PickleBackupHandler(directory=get_backup_dir()),
            )
            self.storage_handlers.append(PostGresOutputHandler())
        if 'result_handlers' in dir(plan):
            self.result_handlers = plan.result_handlers
        else:
            self.result_handlers = []
        self.times = {}

    def run(self):
        """Runs the evaluation specified by the pipeline."""
        self.times['global_start'] = time.perf_counter()
        try:
            data = self.dataloader.load()
            loader_configs = self.dataloader.configuration
        except Exception as e:
            LOGGER.error('Error during data loading:')
            LOGGER.exception(e)
            # this is not recoverable.
            return

        if not self.dataloader.is_multiloader:
            data = [data]
            loader_configs = [loader_configs]
        for i, (run_data,
                loader_config) in enumerate(zip(data, loader_configs)):
            try:
                result = self.evaluator.evaluate(self.pipeline, run_data)
            except Exception as e:
                LOGGER.exception('Error during evaluation: %s', e)
                # this is not recoverable.
                return
            for handler in self.result_handlers:
                self._run_result_handler(result, handler, i, loader_config)
            self._store(i, result, loader_config)

    def _run_result_handler(self, result, handler, run_number, loader_config):
        try:
            if callable(handler):
                handler(result)
            else:
                function, kwargs = handler
                function(result, **kwargs)
        except Exception as e:
            if hasattr(handler, '__name__'):
                LOGGER.error('Result handler %s failed:', handler.__name__)
                LOGGER.exception(e)
            else:
                LOGGER.error('Result handler %s failed:', handler)
                LOGGER.exception(e)
            if self.mail:
                notify_error(self.plan_path,
                             'result handling',
                             e,
                             run=run_number,
                             loader_config=loader_config)

    def _get_pipeline_configuration(self, pipeline):
        clazz = pipeline.__class__.__name__

        # for custom models with detailed information
        if hasattr(pipeline, 'configuration'):
            return pipeline.configuration

        # for pipeline objects
        if hasattr(pipeline, 'named_steps'):
            steps = pipeline.named_steps.items()
            ret = {
                name: self._get_pipeline_configuration(transformer)
                for name, transformer in steps
            }
            ret['TYPE'] = clazz
            return ret

        # for featureunion, transformerpicker objects
        if hasattr(pipeline, 'transformer_list'):
            transformers = pipeline.transformer_list
            ret = {
                transformer[0]:
                    self._get_pipeline_configuration(transformer[1])
                for transformer in list(transformers)
            }
            ret['TYPE'] = clazz
            return ret

        return clazz

    def _get_git_info(self):
        sha_commit_id = None
        remote_url = None
        is_dirty = None
        try:
            repo = git.Repo(search_parent_directories=True)
            is_dirty = repo.is_dirty()
            sha_commit_id = repo.head.object.hexsha
            if repo.remotes and hasattr(repo.remotes, 'origin'):
                remote_url = repo.remotes.origin.url
            return sha_commit_id, remote_url, is_dirty
        except git.GitError:
            return None, None, None

    def _get_platform_info(self):
        info = {}
        info['arch'] = platform.architecture()
        info['machine'] = platform.machine()
        info['node'] = socket.getfqdn()
        info['platform'] = platform.platform()
        info['processor'] = platform.processor()
        info['system'] = platform.system()
        info['job_id'] = get_job_id()

        return info

    def _store(self, run_number, result, dl_conf):
        """Stores the results of the pipeline."""
        try:
            if self.dataloader.is_multiloader and 'run_number' not in dl_conf:
                dl_conf['run_number'] = run_number
            if 'class' not in dl_conf:
                dl_conf['class'] = self.dataloader.__class__.__name__

            ev_conf = self.evaluator.configuration
            if type(ev_conf) == dict:
                ev_conf['class'] = self.evaluator.__class__.__name__

            pipeline_conf = self._get_pipeline_configuration(self.pipeline)

            config = {
                'git_info': self.git_info,
                'sourcefile': self.plan_path,
                'plan_content': self.plan_content,
                'dataloader': dl_conf,
                'pipeline': pipeline_conf,
                'evaluator': ev_conf,
                'outcome': result,
                'durations': self.times,
                'platform': self._get_platform_info(),
            }

            config = check_serializability(config)

            for storage_handler in self.storage_handlers:
                try:
                    storage_handler.handle_result(config)
                except Exception as e:
                    LOGGER.error('storage handler %s failed:', storage_handler)
                    LOGGER.exception(e)
        except Exception as e:
            LOGGER.error(f'Error during storing: {e}')
            if self.mail:
                notify_error(self.plan_path,
                             'storing',
                             e,
                             run=run_number,
                             loader_config=dl_conf)


def load_plan(file_path):
    """
    Loads the plan module from the given python file.

    It loads the module and checks whether it is a valid plan module.

    Args:
        file_path: The path to the plan module.
    """
    spec = importlib.util.spec_from_file_location('plan', file_path)
    pipeline_plan = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(pipeline_plan)

    missing_members = []
    wrong_members = []
    if 'dataloader' not in dir(pipeline_plan):
        missing_members.append('dataloader')
    elif not isinstance(pipeline_plan.dataloader, Loader):
        wrong_members.append('dataloader')

    if 'pipeline' not in dir(pipeline_plan):
        missing_members.append('pipeline')

    if 'evaluator' not in dir(pipeline_plan):
        missing_members.append('evaluator')
    elif not isinstance(pipeline_plan.evaluator, Evaluator):
        wrong_members.append('evaluator')

    if 'storage_handlers' in dir(pipeline_plan):
        if isinstance(pipeline_plan.storage_handlers, Iterable):
            for handler in pipeline_plan.storage_handlers:
                if not isinstance(handler, StorageHandler):
                    wrong_members.append(f'handler: {handler}')
        elif isinstance(pipeline_plan.storage_handlers, StorageHandler):
            pipeline_plan.storage_handlers = [pipeline_plan.storage_handlers]
        else:
            wrong_members.append('storage_handler')
    else:
        pipeline_plan.storage_handlers = []

    if len(missing_members) > 0:
        raise ValueError(
            f'Module {file_path} is not a valid plan module.'
            f"The following members are missing: {','.join(missing_members)}.")

    if len(wrong_members) > 0:
        raise ValueError(f'Module {file_path} includes invalid members:'
                         f"{','.join(wrong_members)}.")

    return pipeline_plan
