#!/usr/bin/python3


from __future__ import annotations

import logging
import re
from pathlib import Path

from slpkg.config import config_load
from slpkg.utilities import Utilities
from slpkg.views.process import ViewProcess

logger = logging.getLogger(__name__)


class VerifyFiles:  # pylint: disable=[R0902,R0903]
    """Verify installed packages for missing files."""

    def __init__(self, options: dict[str, bool], packages: list[str]) -> None:
        logger.debug("Initializing VerifyFiles with packages: %s, options: %s", packages, options)
        self.packages = packages

        self.grey = config_load.grey
        self.green = config_load.green
        self.yellow = config_load.yellow
        self.red = config_load.red
        self.endc = config_load.endc
        self.log_packages = config_load.log_packages

        self.option_quiet: bool = options.get("option_quiet", False)

        self.virtual_prefixes: tuple[str, ...] = tuple(config_load.verify_skip_virtual)
        self.skip_prefixes: tuple[str, ...] = tuple(config_load.verify_skip_prefixes)
        self.skip_suffixes: tuple[str, ...] = tuple(config_load.verify_skip_suffixes)

        self.utils = Utilities()
        self.view_process = ViewProcess()
        self.missing: dict[str, list[str]] = {}
        self.checked_count: int = 0

    def check(self) -> list[str]:
        """Verify installed packages for missing files.

        Returns:
            list[str]: Short package names that have missing files.
        """
        all_installed: dict[str, str] = self.utils.all_installed()

        if self.packages:
            full_names: list[str] = [self.utils.is_package_installed(name) for name in self.packages]
        else:
            full_names = list(all_installed.values())

        self.checked_count = len(full_names)
        logger.info("Checking %d installed packages for missing files.", self.checked_count)

        self.view_process.message(f"Checking {self.checked_count} installed package(s) for missing files")

        for full_name in full_names:
            pkg_file: Path = self.log_packages / full_name
            missing_paths: list[str] = self._check_package_files(pkg_file)
            if missing_paths:
                self.missing[full_name] = missing_paths
                logger.info("Package '%s' has %d missing file(s).", full_name, len(missing_paths))

        self.view_process.done()

        self._display_results()
        return [self.utils.split_package(full_name)["name"] for full_name in self.missing]

    def _check_package_files(self, pkg_file: Path) -> list[str]:
        """Read the FILE LIST section and return paths that no longer exist.

        Args:
            pkg_file (Path): Path to the package log file.

        Returns:
            list[str]: Missing absolute file paths.
        """
        try:
            lines: list[str] = pkg_file.read_text(encoding="utf-8", errors="ignore").splitlines()
        except OSError as e:
            logger.warning("Could not read package file '%s': %s", pkg_file, e)
            return []

        try:
            idx: int = lines.index("FILE LIST:")
        except ValueError:
            logger.debug("No 'FILE LIST:' section found in '%s'.", pkg_file)
            return []

        missing: list[str] = []
        for entry in lines[idx + 1:]:
            if (
                not entry
                or entry.endswith("/")
                or entry.startswith(self.skip_prefixes)
                or entry.endswith(self.skip_suffixes)
            ):
                continue
            if entry.startswith(self.virtual_prefixes):
                continue
            abs_path: Path = Path("/", self._decode_octal_escapes(entry))
            try:
                if not abs_path.parent.exists():
                    logger.debug("Skipping '%s': parent directory does not exist (staging/cleanup artifact).", abs_path)
                    continue
                if not abs_path.exists():
                    missing.append(str(abs_path))
                    logger.debug("Missing file: '%s' (from package '%s')", abs_path, pkg_file.name)
            except PermissionError:
                logger.debug("Skipping '%s': permission denied.", abs_path)

        return missing

    @staticmethod
    def _decode_octal_escapes(entry: str) -> str:
        """Decode octal escape sequences in file paths from the Slackware FILE LIST.

        The Slackware package log stores non-ASCII filenames using octal escapes
        (e.g. \\303\\236 → 0xC3 0x9E → UTF-8 → Þ).

        Args:
            entry (str): A raw path entry from the FILE LIST section.

        Returns:
            str: The path with octal sequences replaced by the actual characters.
        """

        def replace_match(m: re.Match) -> str:  # type: ignore[type-arg]
            octals = [part for part in m.group(0).split("\\") if part]
            return bytes([int(o, 8) for o in octals]).decode("utf-8", errors="replace")

        return re.sub(r"(?:\\[0-7]{3})+", replace_match, entry)

    def _display_results(self) -> None:
        """Print the integrity check results."""
        total_missing: int = sum(len(paths) for paths in self.missing.values())

        if self.option_quiet:
            for full_name in sorted(self.missing):
                print(full_name)
            logger.info(
                "Integrity check complete (quiet). Issues: %d, Missing files: %d",
                len(self.missing),
                total_missing,
            )
            return

        if self.missing:
            for full_name, paths in sorted(self.missing.items()):
                count: int = len(paths)
                print(f"{self.red}{full_name}{self.endc}  [{count} missing file(s)]")
                for path in paths:
                    print(f"  {self.yellow}{path}{self.endc}")
                print()

            print(
                f"{self.grey}Checked: {self.checked_count}   "
                f"Issues: {len(self.missing)}   "
                f"Missing files: {total_missing}{self.endc}"
            )
            logger.info(
                "Integrity check complete. Checked: %d, Issues: %d, Missing files: %d",
                self.checked_count,
                len(self.missing),
                total_missing,
            )
        else:
            print(f"{self.green}All {self.checked_count} package(s) passed integrity check.{self.endc}")
            logger.info("All %d packages passed integrity check.", self.checked_count)
