"""Abstract base class and loader for probe plugins for jobs."""

import os
import re
from configparser import ConfigParser
from logging import Logger
from typing import Callable, Dict, TextIO, List, Optional, Union
from arcnagios import substitution
from arcnagios.arcutils import ArcClient, JobState
from arcnagios.nagutils import NagiosReport, ServiceUnknown
from arcnagios.reputation import ReputationTracker
from arcnagios.utils import Alpha, ident, unspecified, Unspecified

def _bool_of_str(s):
    s_lc = s.lower()
    if s_lc in ['0', 'false', 'no', 'off']:
        return False
    if s_lc in ['1', 'true', 'yes', 'on']:
        return True
    raise ValueError('invalid literal for boolean: %r' % s)

class StagingSpec:

    def __init__(
            self, filename: str, url: Optional[str], urloptions: List[str]):
        self.filename = filename
        self.url = url
        self.urloptions = urloptions

    @classmethod
    def parse(cls, spec: str, workdir: str): # TODO(python-3.11): -> Self
        if isinstance(spec, tuple):
            filename, spec, urloptions = spec
        else:
            if ';' in spec:
                xs = spec.split(';')
                spec, urloptions = xs[0], xs[1:]
            else:
                urloptions = []
            filename = os.path.basename(spec)
        if spec is None or ':/' in spec:
            url = spec
        elif os.path.isabs(spec):
            url = 'file:' + spec
        else:
            url = 'file:' + os.path.join(workdir, spec)
        return cls(filename, url, urloptions)

    def __iter__(self):
        # For template compatibility.
        return iter((self.filename, self.url, self.urloptions))

_interp_re = re.compile(r'%\(([a-zA-Z0-9_]+)\)')

class JobPlugin:
    """A base-class for tests to run within a job script.  Implementations
    provide commands to run, and how to extract the result.  Optionally it may
    specify staging and cleanup."""

    name: str
    config: ConfigParser
    config_section: str
    reputation_tracker: ReputationTracker
    logger: Logger
    arcclient: ArcClient
    environment: Dict[str, str]

    def __init__(
            self, name: str,
            config: ConfigParser, config_section: str,
            reputation_tracker: ReputationTracker,
            log: Logger,
            arcclient: ArcClient,
            env: Optional[Dict[str, str]] = None):
        self.name = name
        self.config = config
        self.config_section = config_section
        self.reputation_tracker = reputation_tracker
        self.test_name = config_section[6:] # Strip "arcce."
        self.log = log
        self.arcclient = arcclient
        self.environment = env or {}

    def _import_interpolations(self, var: str) -> None:
        if not var in self.environment \
                and self.config.has_option(self.config_section, var):
            raw_value = self.config.get(self.config_section, var, raw = True)
            for mo in re.finditer(_interp_re, raw_value):
                v = mo.group(1)
                if not v in self.environment \
                        and self.config.has_section('variable.' + v):
                    substitution.import_variable(
                            self.config, v, self.reputation_tracker,
                            self.environment)

    def _update_subst(self, subst: Optional[Dict[str, str]]) -> Dict[str, str]:
        if subst is None:
            return self.environment
        subst.update(self.environment)
        return subst

    def hasconf(self, var: str) -> bool:
        return self.config.has_option(self.config_section, var)

    def _getconf(
            self, var: str, conv: Callable[[str], Alpha],
            default: Union[Alpha, Unspecified], subst: Optional[Dict[str, str]]
        ) -> Alpha:
        if not isinstance(default, Unspecified) and not self.hasconf(var):
            return default
        self._import_interpolations(var)
        subst = self._update_subst(subst)
        try:
            return conv(self.config.get(self.config_section, var, vars=subst))
        except ValueError as exn:
            raise ServiceUnknown(
                    'Bad value for configuration parameter %s in section %s: %s'
                    % (var, self.config_section, exn)) from exn

    def getconf_str(
            self, var: str, default: Union[str, Unspecified] = unspecified,
            subst: Optional[Dict[str, str]] = None) -> str:
        return self._getconf(var, ident, default, subst)

    def getconf_int(
            self, var: str, default: Union[int, Unspecified] = unspecified,
            subst: Optional[Dict[str, str]] = None) -> int:
        return self._getconf(var, int, default, subst)

    def getconf_bool(
            self, var: str, default: Union[bool, Unspecified] = unspecified,
            subst: Optional[Dict[str, str]] = None) -> bool:
        return self._getconf(var, _bool_of_str, default, subst)

    def getconf_float(
            self, var: str, default: Union[float, Unspecified] = unspecified,
            subst: Optional[Dict[str, str]] = None) -> float:
        return self._getconf(var, float, default, subst)

    def getconf_strlist(
            self, var: str,
            default: Union[List[str], Unspecified] = unspecified,
            sep: Optional[str] = None,
            subst: Optional[Dict[str, str]] = None) -> List[str]:
        if not isinstance(default, Unspecified) and not self.hasconf(var):
            return default
        self._import_interpolations(var)
        subst = self._update_subst(subst)
        raw = self.config.get(self.config_section, var, vars=subst)
        return [s.strip() for s in raw.split(sep)]

    @property
    def service_description(self) -> Optional[str]:
        if self.hasconf('service_description'):
            return self.getconf_str('service_description')
        return None

    def staged_inputs(self, workdir: str) -> List[StagingSpec]:
        # pylint: disable=W0613
        """Override this method to specify files used by the script."""
        return []

    def staged_outputs(self, workdir: str) -> List[StagingSpec]:
        # pylint: disable=W0613
        """Override this method to specify files produced by the script, which
        are needed by `extract_result`."""
        return []

    def runtime_environments(self) -> List[str]:
        rtes = self.getconf_str('runtime_environments', '')
        return [x for x in map(str.strip, rtes.split(',')) if x]

    def write_script(self, fh: TextIO, workdir: str) -> None:
        """This method may write commands to run in the job script.  The
        commands are run in the standard shell (/bin/sh), and must not call
        exit or otherwise disrupt the control flow of the script, since other
        commands are run in the same script."""

    def check(self, report: NagiosReport, jobdir: str, stored_urls: List[str]):
        """This method is run to check the output of a job."""
        raise NotImplementedError('extract_result')

    def cleanup(self, job_state: JobState):
        pass
