#!/usr/bin/python3
# -*- coding: utf-8 -*-

import json
import logging
import os
from pathlib import Path
from typing import Generator

from sun.blacklist import load_blacklist
from sun.configs import configs_load

logger = logging.getLogger(__name__)

_INSTALLED_DIR: Path = Path('/var/log/packages')


def _parse_pkg_name(filename: str) -> str:
    """Extract package name from a Slackware package filename.

    Slackware filenames follow name-version-arch-build[tag] format.
    Splits from the right to isolate the name component.

    Args:
        filename (str): Installed package filename (e.g. 'vim-9.0-x86_64-1').

    Returns:
        str: Package name (e.g. 'vim').
    """
    parts = filename.rsplit('-', 3)
    return parts[0] if len(parts) == 4 else filename


def _installed() -> dict[str, str]:
    """Returns a mapping of package name to installed filename.

    Returns:
        dict[str, str]: {name: filename} for each entry in /var/log/packages.
    """
    result: dict[str, str] = {}
    try:
        for entry in os.scandir(_INSTALLED_DIR):
            if entry.is_file():
                result[_parse_pkg_name(entry.name)] = entry.name
    except OSError as e:
        logger.warning('Cannot read installed packages from %s: %s', _INSTALLED_DIR, e)
    return result


def _pending_slpkg(installed: dict[str, str], data_json: Path) -> Generator[str, None, None]:
    """Yields package filenames available in the slpkg repo at a newer version than installed.

    Only packages whose name exists as a key in data.json are checked; third-party
    packages (SBo, compat32, etc.) absent from the repo index are skipped automatically.

    Args:
        installed (dict[str, str]): Mapping of name to installed filename.
        data_json (Path): Path to the slpkg data.json file.

    Yields:
        str: Package filename (with .txz) pending upgrade.
    """
    try:
        data: dict[str, dict[str, str]] = json.loads(
            data_json.read_text(encoding='utf-8')
        )
    except (OSError, json.JSONDecodeError) as e:
        logger.warning('Cannot read slpkg data.json from %s: %s', data_json, e)
        return

    for name, info in data.items():
        available = info.get('package', '')
        if not available:
            continue
        current = installed.get(name)
        if current is not None and current + '.txz' != available:
            yield f'{available} (installed: {current})'


def _pending_slackpkg(installed: dict[str, str], packages_txt: Path) -> Generator[str, None, None]:
    """Yields package descriptions from PACKAGES.TXT that are newer than installed versions.

    Args:
        installed (dict[str, str]): Mapping of name to installed filename.
        packages_txt (Path): Path to the slackpkg PACKAGES.TXT file.

    Yields:
        str: 'name-available_ver (installed: current_ver)' for each pending package.
    """
    try:
        content = packages_txt.read_text(encoding='utf-8', errors='replace')
    except OSError as e:
        logger.warning('Cannot read PACKAGES.TXT from %s: %s', packages_txt, e)
        return

    pkg_name = ''
    for line in content.splitlines():
        if line.startswith('PACKAGE NAME:'):
            pkg_name = line.split(':', 1)[1].strip()
        elif line.startswith('PACKAGE VERSION:') and pkg_name:
            available_ver = line.split(':', 1)[1].strip()
            current_file = installed.get(pkg_name)
            if current_file:
                parts = current_file.rsplit('-', 3)
                installed_ver = parts[1] if len(parts) == 4 else ''
                if installed_ver and installed_ver != available_ver:
                    yield f'{pkg_name}-{available_ver} (installed: {installed_ver})'
            pkg_name = ''


class Pending:  # pylint: disable=too-few-public-methods
    """Detects installed Slackware packages that have a newer version available in the repo.

    The package manager is auto-detected from LOG_PATH in sun.conf:
    - If LOG_PATH contains data.json  → slpkg
    - If LOG_PATH contains PACKAGES.TXT → slackpkg
    """

    def __init__(self) -> None:
        self.configs = configs_load
        self.data_file: str = self._detect_data_file()

    def _detect_data_file(self) -> str:
        """Detects the package manager data file path from LOG_PATH.

        Checks for data.json (slpkg) first, then PACKAGES.TXT (slackpkg).

        Returns:
            str: Absolute path to the data file, or empty string if not found.
        """
        log_path = Path(self.configs.repo_log_path)
        for candidate in ('data.json', 'PACKAGES.TXT'):
            path = log_path / candidate
            if path.is_file():
                return str(path)
        logger.warning('No package manager data found in %s', log_path)
        return ''

    def upgradable(self) -> list[str]:
        """Returns the list of packages with a newer version available but not yet installed.

        Returns:
            list[str]: Package identifiers for packages pending upgrade.
        """
        if not self.data_file:
            return []

        blacklist = load_blacklist()
        installed = _installed()
        data_path = Path(self.data_file)

        if data_path.name == 'data.json':
            results = list(_pending_slpkg(installed, data_path))
        else:
            results = list(_pending_slackpkg(installed, data_path))

        if not blacklist:
            return results
        return [r for r in results if _parse_pkg_name(r.split(' ')[0].replace('.txz', '')) not in blacklist]
