"""
A module for zkg's notion of "user variables": named values required
by packages that the user can provide in a variety of ways, including
responses to zkg's input prompting.
"""

import configparser
import os
import re
import readline


def slugify(string: str) -> str:
    """Returns file-system-safe, lower-case version of the input string.

    Any character sequence outside of ``[a-zA-Z0-9_]+`` gets replaced by a
    single underscore. If the variable has no value or the value is an
    empty string, returns the given default.
    """
    return re.sub(r"[^\w]+", "_", string, flags=re.ASCII).lower()


def _rlinput(prompt: str, prefill: str = "") -> str:
    """Variation of input() that supports pre-filling a value."""
    readline.set_startup_hook(lambda: readline.insert_text(prefill))
    try:
        return input(prompt)
    finally:
        readline.set_startup_hook()


class UserVar:
    """A class representing a single user variable.

    User variables have a name and an optional description. They
    resolve to a value using a cascade of mechanisms, including
    command-line arguments (via --user-var), environment variables,
    cached previous values, and user input. They may come with a
    default value.
    """

    def __init__(
        self,
        name: str,
        val: str | None = None,
        default: str | None = None,
        desc: str | None = None,
    ) -> None:
        self._name = name
        self._desc = desc or ""
        self._val = val
        self._default = default if default is not None else val

    def name(self) -> str:
        return self._name

    def desc(self) -> str:
        return self._desc

    def set(self, val: str) -> None:
        self._val = val

    def val(self, fallback: str | None = None) -> str | None:
        return self._val if self._val is not None else fallback

    def default(self) -> str | None:
        return self._default

    def resolve(
        self,
        name: str,
        config: configparser.ConfigParser,
        user_var_args: list["UserVar"] | None = None,
        force: bool = False,
    ) -> str:
        """Populates user variables with updated values and returns them.

        This function resolves the variable in the following order:

        (1) Use any value provided on the command line via --user-var
        (2) If force is not used, prompt the user for input
        (3) use an environment variables of the same name,
        (4) retrieve from the provided config parser's "user_vars" section,
        (5) use the default value of the user variable.

        The resolved value is stored with the instance (to be
        retrieved via .val() in the future) and also returned.

        Args:
            name (str): the requesting entity, e.g. a package name

            config (configparser.ConfigParser): the zkg configuration

            user_var_args (list of UserVar): user-var instances
                provided via command line

            force (bool): whether to skip prompting for input

        Returns:
            str: the resulting variable value

        Raises:
            ValueError: when we couldn't produce a value for the variable
        """
        val: str | None = None
        source = None

        if user_var_args:
            for uvar in user_var_args:
                if uvar.name() == self._name:
                    val = uvar.val()
                    source = "command line"
                    break

        if val is None:
            val = os.environ.get(self._name)
            if val:
                source = "environment"

        if source:
            print(
                f'"{name}" will use value of "{self._name}" ({self._desc}) from {source}: {val}',
            )
            self._val = val
            assert val is not None
            return val

        if val is None:
            # Try to re-use a cached value in the subsequent prompt
            val = config.get("user_vars", self._name, fallback=self._default)

        if force:
            if val is None:
                raise ValueError(self._name)
            self._val = val
            return val

        desc = " (" + self._desc + ")" if self._desc else ""
        print(f'"{name}" requires a "{self._name}" value{desc}: ')
        self._val = _rlinput(self._name + ": ", val if val else "")

        return self._val

    @staticmethod
    def parse_arg(arg: str) -> "UserVar":
        """Parser for NAME=VAL format string used in command-line args."""
        try:
            name, val = arg.split("=", 1)
            return UserVar(name, val=val)
        except ValueError as error:
            raise ValueError(
                f'invalid user var argument "{arg}", must be NAME=VAR',
            ) from error

    @staticmethod
    def parse_dict(metadata_dict: dict[str, str]) -> list["UserVar"] | None:
        """Returns list of UserVars from the metadata's 'user_vars' field.

        Args:
            metadata_dict (dict of str->str): the input metadata, e.g. from
                a configparser entry value.

        Returns:
            list of UserVar. If the 'user_vars' field is not present,
            an empty list is returned.  If malformed, returns None.
        """
        text = metadata_dict.get("user_vars")

        if not text:
            return []

        rval = []

        text = text.strip()
        # Example: LIBRDKAFKA_ROOT [/usr] "Path to librdkafka installation"

        entries = re.split(r'(\w+\s+\[.*\]\s+".*")\s+', text)
        entries = list(filter(None, entries))

        for entry in entries:
            mob = re.match(r'(\w+)\s+\[(.*)\]\s+"(.*)"', entry)

            if not mob:
                return None

            groups = mob.groups()

            if len(groups) != 3:
                return None

            rval.append(UserVar(groups[0], val=groups[1], desc=groups[2]))

        return rval
