"""
OpenBSD Pkg Package Manager Provider
"""

import re

from fabric import Connection

from exosphere.data import Update
from exosphere.errors import DataRefreshError
from exosphere.providers.api import PkgManager


class PkgAdd(PkgManager):
    """
    Package manager for OpenBSD using pkg_add

    Limitations:
        - Does not check security state at all if system is tracking -current,
          as it then behaves like a rolling release and there is no way to tell
        - On stable/release, all updates are assumed to be security updates,
          as OpenBSD only ever updates packages for security issues.
        - Thoroughly untested on more exotic architectures where syspatch
          may not be available or fail in different ways. Bug reports welcome!
    """

    def __init__(self) -> None:
        """
        Initialize the PkgOpenBSD package manager.
        """
        super().__init__()
        self.logger.debug("Initializing OpenBSD pkg_add package manager")
        self.line_pattern: re.Pattern | None = None

    def reposync(self, cx: Connection) -> bool:
        """
        Synchronize the package repository

        By default, OpenBSD does not require a separate step for this
        and queries the pkg mirrors directly.

        This step is essentially a no-op.

        :param cx: Fabric Connection object
        :return: True if synchronization is successful, False otherwise.
        """

        self.logger.debug("OpenBSD pkg_add does not require repository sync.")
        return True

    def get_updates(self, cx: Connection) -> list[Update]:
        """
        Retrieve the list of available updates.

        This method retrieves the list of available package updates for OpenBSD
        using the 'pkg_add' command.

        :param cx: Fabric Connection object
        :return: List of Update objects
        """

        updates: list[Update] = []
        is_current = False  # Whether or not the system is tracking -current or -beta

        # First, perform version query
        version_query = cx.run("/usr/sbin/syspatch -l", hide=True, warn=True)

        # Syspatch returns non-zero exit code if the release is unsupported,
        # and we use this to determine if we can track security status.
        if version_query.failed:
            if "unsupported release" in version_query.stderr.casefold():
                self.logger.warning(
                    "Host is running unsupported OpenBSD release. "
                    "Security status will not be tracked.",
                )
                is_current = True
            else:
                # Call to syspatch failed unexpectedly
                raise DataRefreshError(
                    f"Failed to query OpenBSD version: {version_query.stderr}"
                )

        # Query for available package updates
        packages_query = cx.run(
            "/usr/sbin/pkg_add -u -v -x -n | grep -e '^Update candidate'",
            hide=True,
            warn=True,
        )

        # grep will exit with 1 if there are no candidates at all
        # This is expected if packages are up to date AND if there are
        # no updated versions of any package available at all.
        if packages_query.failed:
            if (
                packages_query.return_code == 1
                and not packages_query.stdout
                and packages_query.stderr == "pkg_add should be run as root\n"
            ):
                self.logger.debug("No OpenBSD packages with updates.")
                return updates

            # Call to pkg_add failed unexpectedly
            raise DataRefreshError(
                f"Failed to query OpenBSD pkg_add updates: {packages_query.stderr}"
            )

        # Parse output and filter out packages that don't actually change version
        for line in packages_query.stdout.splitlines():
            line = line.strip()

            # Skip blanks
            if not line:
                continue

            update = self._parse_line(line, is_current=is_current)

            if update is None:
                continue

            updates.append(update)

        self.logger.debug(
            "Found %d updates for OpenBSD packages: %s",
            len(updates),
            ", ".join([u.name for u in updates]),
        )

        return updates

    def get_reboot_status(self, cx: Connection) -> bool | None:
        """
        Determine whether the host requires a reboot.

        OpenBSD does not offer a reliable mechanism for this out of the
        box, and we only track packages, not the base system.

        This method is essentially a no-op and always returns None.

        :param cx: Fabric Connection object
        :return: None (reboot state is not tracked on OpenBSD).
        """
        self.logger.debug("OpenBSD reboot status is not tracked, reporting unknown.")
        return None

    def _parse_line(self, line: str, is_current: bool = False) -> Update | None:
        """
        Parse a line of pkg_add output to extract update information.

        We do this fairly clumsily and just return None if either:

        - We can't parse that at all
        - The current and new version are the same (no actual update)

        :param line: A line from pkg_add output
        :return: An Update object or None if not an update
        """

        # Compile the regex pattern on first use
        # Follows OpenBSD::PackageName parsing rules for package names.
        # Special care needs to be taken to avoid splitting on the
        # flavor part, if present.
        if self.line_pattern is None:
            self.line_pattern = re.compile(
                r"^Update candidates: "
                r"(?P<name>.*?)-(?P<version>\d[^\s-]*)(?:-\S+)?"  # name-version[-flavor]
                r" -> "
                r"(?P<new_name>.*?)-(?P<new_version>\d[^\s-]*)(?:-\S+)?"  # new of the same
                r"$"
            )

        match = self.line_pattern.match(line)

        if not match:
            self.logger.debug("Could not parse: %s", line)
            return None

        package_name = match["name"]
        current_version = match["version"]
        new_name = match["new_name"]
        new_version = match["new_version"]

        # If the package name has changed, given the strict regex
        # above, this is likely a quirks-based package rename, and we
        # should report it as a new install, especially on -current.
        if package_name != new_name:
            self.logger.debug(
                "Package %s changed name to %s, reporting as new install.",
                package_name,
                new_name,
            )
            return Update(
                name=new_name,
                current_version=None,
                new_version=new_version,
                security=not is_current,  # All updates are security unless on -current
                source="Packages Mirror",
            )

        if current_version == new_version:
            self.logger.debug(
                "No version change for %s: %s -> %s , ignoring.",
                package_name,
                current_version,
                new_version,
            )
            return None

        self.logger.debug(
            "Parsed update: %s from %s to %s",
            package_name,
            current_version,
            new_version,
        )

        return Update(
            name=package_name,
            current_version=current_version,
            new_version=new_version,
            security=not is_current,  # All updates are security unless on -current
            source="Packages Mirror",
        )
