#!/usr/bin/env python3
"""
    Library functions for cij_extractor
"""
import os
import importlib.util
from typing import List, Optional, cast
import dataclasses

import cij.runner
from cij.runner import TestRun, TestCase
from cij.errors import CIJError, InitializationError
from cij.util import rehome, list_dir

# pylint:disable=unsubscriptable-object

_EXTRACTORS_IMPORT_PATH = "cijoe_extractors"


class Extractor:
    """ Type declaration for extractors. Used to help mypy. """

    # pylint: disable=too-few-public-methods

    def extract_metrics(self, tcase: TestCase) -> List[dict]:
        """
        Extracts metrics from relevant tcase logs in tcase.aux_root and dumps
        to tcase.analysis_log_fpath in yaml format.

        Returns the list of metrics.
        """
        raise NotImplementedError


def extract_metrics(trun: TestRun, extractors: List[Extractor]) -> int:
    """ Run the given extractors on all TestCases in the given trun. """
    if len(extractors) > 1:
        cij.warn(
            "NOTICE: "
            "multiple extractors requested. If any of your testcases contain "
            "measurements supported by multiple extractors, only extractions "
            "by the last extractor will be persisted to metrics.yml"
        )

    for tplan in trun.testplans:
        for tsuite in tplan.testsuites:
            for tcase in tsuite.testcases:
                for extractor in extractors:
                    extractor.extract_metrics(tcase)

    return 0


def _get_extractor(fpath: str, name: str) -> Optional[Extractor]:
    """
    Import an extractor by name or filepath
    """
    try:
        spec = importlib.util.find_spec(f"{_EXTRACTORS_IMPORT_PATH}.{name}")
    except ModuleNotFoundError:
        spec = None

    if spec is None:
        spec = importlib.util.spec_from_file_location("", fpath)
    if spec is None and not fpath.endswith(".py"):
        spec = importlib.util.spec_from_file_location("", "%s.py" % fpath)
    if not spec or not spec.loader:
        cij.err(f"Cannot find: {name} ({fpath})")
        return None

    try:
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
    except FileNotFoundError as ex:
        cij.err(f"Cannot find: {name}")
        raise InitializationError(f"extractor '{name}' not recognized") from ex

    if not getattr(module, 'extract_metrics', None):
        raise InitializationError(f"extractor '{name} doesn't have the "
                                  "required 'extract_metrics' method")

    return cast(Extractor, module)


@dataclasses.dataclass
class ExtractorMeta:
    """ Container for extractor name and docstring """
    name: str
    docs: str


def find_extractors() -> List[ExtractorMeta]:
    """ Search cijoe_extractors.* for available extractors and return
    a list of their names and docstrings.
    """

    try:
        spec = importlib.util.find_spec(_EXTRACTORS_IMPORT_PATH)
    except ModuleNotFoundError:
        return []

    if spec is None:
        return []

    extractor_fpaths = []
    assert spec.submodule_search_locations
    for dir_ in spec.submodule_search_locations:
        extractor_fpaths += list(list_dir(dir_, recursive=True, ext=".py"))

    emeta = []
    for fpath in extractor_fpaths:
        name = os.path.splitext(os.path.basename(fpath))[0]
        try:
            extractor = _get_extractor(fpath, name)
        except InitializationError:
            continue

        emeta.append(ExtractorMeta(
            name=name,
            docs=getattr(extractor, "__doc__", "").strip(),
        ))

    return emeta


def main(args):
    """
    Run cij extractor.

    """

    trun = cij.runner.trun_from_file(args.trun_fpath)

    # pylint: disable=no-member
    rehome(trun.args.output, args.output, trun)

    err = 0
    try:
        extractors = [_get_extractor("", ename) for ename in args.extractor]
        err += extract_metrics(trun, extractors)
        cij.info("Metrics extracted")
    except CIJError as ex:
        cij.err(f"main:FAILED to run data extraction: {ex}")
        err += 1

    return err
