#!/usr/bin/python3

import argparse
import importlib.util
import logging
import logging.handlers
import os
import platform
import sys
from pathlib import Path
from typing import Any

import tomlkit
from tomlkit import exceptions

from slpkg.toml_errors import TomlErrors

DIALOG_AVAILABLE = importlib.util.find_spec("dialog") is not None


def _supports_unicode() -> bool:
    """Return True if stdout encoding supports Unicode (UTF-8/16/32)."""
    codec = (getattr(sys.stdout, "encoding", "ascii") or "ascii").lower().replace("-", "")  # pylint: disable=no-member
    return codec in ("utf8", "utf16", "utf32")


config_logger = logging.getLogger("slpkg.config_internal")
config_logger.setLevel(logging.DEBUG)
config_logger.propagate = False

MAX_LOG_SIZE_BYTES = 1 * 1024 * 1024  # 1 MB
BACKUP_LOG_COUNT = 3

if os.geteuid() == 0:
    config_log_file: Path = Path("/var/log/slpkg/config.log")
    config_log_file.parent.mkdir(parents=True, exist_ok=True)
    file_handler: logging.Handler = logging.handlers.RotatingFileHandler(
        config_log_file, maxBytes=MAX_LOG_SIZE_BYTES, backupCount=BACKUP_LOG_COUNT, encoding="utf-8"
    )
    file_handler.setLevel(logging.DEBUG)
    formatter = logging.Formatter("%(levelname)s: %(asctime)s - %(name)s - %(funcName)s - %(message)s")
    file_handler.setFormatter(formatter)
else:
    file_handler = logging.NullHandler()

config_logger.addHandler(file_handler)


class Config:  # pylint: disable=[R0902, R0903]
    """Loads and holds configurations."""

    # Maps each TOML key to (attribute_name, expected_types).
    _TOML_CONFIG: dict[str, tuple[str, tuple[type, ...]]] = {
        "LOGGING_LEVEL": ("logging_level", (str,)),
        "FILE_LIST_SUFFIX": ("file_list_suffix", (str,)),
        "PACKAGE_TYPE": ("package_type", (list,)),
        "INSTALLPKG": ("installpkg", (str,)),
        "LOCAL_INSTALLPKG": ("local_installpkg", (str,)),
        "REINSTALL": ("reinstall", (str,)),
        "REMOVEPKG": ("removepkg", (str,)),
        "KERNEL_VERSION": ("kernel_version", (bool,)),
        "BOOTLOADER_COMMAND": ("bootloader_command", (str,)),
        "COLORS": ("colors", (bool,)),
        "MAKEFLAGS": ("makeflags", (str,)),
        "GPG_VERIFICATION": ("gpg_verification", (bool,)),
        "CHECKSUM_MD5": ("checksum_md5", (bool,)),
        "VERIFY_SKIP_VIRTUAL": ("verify_skip_virtual", (list,)),
        "VERIFY_SKIP_PREFIXES": ("verify_skip_prefixes", (list,)),
        "VERIFY_SKIP_SUFFIXES": ("verify_skip_suffixes", (list,)),
        "DIALOG": ("dialog", (bool,)),
        "PAGER": ("pager", (str,)),
        "EDITOR": ("editor", (str,)),
        "TERMINAL_SELECTOR": ("terminal_selector", (bool,)),
        "VIEW_MISSING_DEPS": ("view_missing_deps", (bool,)),
        "PACKAGE_METHOD": ("package_method", (bool,)),
        "DOWNGRADE_PACKAGES": ("downgrade_packages", (bool,)),
        "DELETE_SOURCES": ("delete_sources", (bool,)),
        "DOWNLOADER": ("downloader", (str,)),
        "WGET_OPTIONS": ("wget_options", (str,)),
        "CURL_OPTIONS": ("curl_options", (str,)),
        "ARIA2_OPTIONS": ("aria2_options", (str,)),
        "LFTP_GET_OPTIONS": ("lftp_get_options", (str,)),
        "LFTP_MIRROR_OPTIONS": ("lftp_mirror_options", (str,)),
        "GIT_CLONE": ("git_clone", (str,)),
        "DOWNLOAD_ONLY_PATH": ("download_only_path", (str, Path)),
        "ASK_QUESTION": ("ask_question", (bool,)),
        "ALL_REPOS_CONFIRM": ("all_repos_confirm", (str,)),
        "MIRROR_VERSION_CHECK": ("mirror_version_check", (bool,)),
        "PARALLEL_DOWNLOADS": ("parallel_downloads", (bool,)),
        "MAXIMUM_PARALLEL": ("maximum_parallel", (int,)),
        "QUIET": ("quiet", (bool,)),
        "PROGRESS_SPINNER": ("progress_spinner", (str,)),
        "SPINNER_COLOR": ("spinner_color", (str,)),
        "PROCESS_LOG": ("process_log", (bool,)),
        "URLLIB_RETRIES": ("urllib_retries", (bool,)),
        "URLLIB_REDIRECT": ("urllib_redirect", (bool,)),
        "URLLIB_TIMEOUT": ("urllib_timeout", (int, float)),
        "PROXY_ADDRESS": ("proxy_address", (str,)),
        "PROXY_USERNAME": ("proxy_username", (str,)),
        "PROXY_PASSWORD": ("proxy_password", (str,)),
    }

    def __init__(self) -> None:  # pylint: disable=[R0915]
        # Initialize variables for error handling and architecture detection.
        config_logger.debug("Initialization of the Config module.")
        self.toml_errors = TomlErrors()
        self.cpu_arch: str = platform.machine()
        self.os_arch: str = platform.architecture()[0]
        config_logger.debug("Detected CPU architecture: %s, OS architecture: %s", self.cpu_arch, self.os_arch)

        # Detect Unicode support once at startup.
        self.unicode: bool = _supports_unicode()
        self.sep_line: str = "-"
        config_logger.debug("Unicode support detected: %s. Separator character: '%s'.", self.unicode, self.sep_line)

        # Program name.
        self.prog_name: str = "slpkg"

        # Slpkg default utility paths.
        self.tmp_path: Path = Path("/tmp")
        self.tmp_slpkg: Path = Path(self.tmp_path, self.prog_name)
        self.build_path: Path = Path(self.tmp_path, self.prog_name, "build")
        self.etc_path: Path = Path("/etc", self.prog_name)
        self.lib_path: Path = Path("/var/lib", self.prog_name)
        self.log_path: Path = Path("/var/log/", self.prog_name)
        self.log_packages: Path = Path("/var", "log", "packages")
        self.lock_file: Path = Path("/var/run", f"{self.prog_name}.lock")

        # Fallback to tmp if /var/run doesn't exist.
        if not self.lock_file.parent.exists():
            self.lock_file = Path(self.tmp_slpkg, f"{self.prog_name}.lock")

        config_logger.debug(
            "Default paths initialized: tmp_path=%s, etc_path=%s, lib_path=%s, log_path=%s, lock_file=%s",
            self.tmp_path,
            self.etc_path,
            self.lib_path,
            self.log_path,
            self.lock_file,
        )

        # Slpkg log files.
        self.deps_log_file: Path = Path(self.log_path, "deps.log")
        self.marks_log_file: Path = Path(self.log_path, "marks.log")
        self.pins_log_file: Path = Path(self.log_path, "pins.log")
        self.process_log_file: Path = Path(self.log_path, "process.log")
        self.history_log_file: Path = Path(self.log_path, "history.log")
        self.slpkg_log_file: Path = Path(self.log_path, "slpkg.log")
        config_logger.debug(
            "Log files paths initialized: deps_log_file=%s, marks_log_file=%s, pins_log_file=%s, process_log_file=%s, history_log_file=%s, slpkg_log_file=%s",
            self.deps_log_file,
            self.marks_log_file,
            self.pins_log_file,
            self.process_log_file,
            self.history_log_file,
            self.slpkg_log_file,
        )

        # Answer yes always is False except user changes via argparse module.
        self.answer_yes: bool = False

        # Default configurations for slpkg.toml.
        # These are "fallback" values if not found in the TOML file or not set by argparse.
        self.logging_level: str = "INFO"
        self.file_list_suffix: str = ".pkgs"
        self.package_type: list[str] = [".txz", ".tgz", ".tlz", ".tbz"]
        self.installpkg: str = "upgradepkg --install-new"
        self.local_installpkg: str = "installpkg"
        self.reinstall: str = "upgradepkg --reinstall"
        self.removepkg: str = "removepkg"
        self.kernel_version: bool = True
        self.bootloader_command: str = ""
        self.colors: bool = True
        self.makeflags: str = "-j4"
        self.gpg_verification: bool = False
        self.checksum_md5: bool = True
        self.verify_skip_virtual: list[str] = ["dev/", "proc/", "sys/", "run/"]
        self.verify_skip_prefixes: list[str] = ["install/"]
        self.verify_skip_suffixes: list[str] = [".new", ".sample"]
        self.dialog: bool = False
        self.pager: str = "most"
        self.editor: str = "vim"
        self.terminal_selector: bool = True
        self.view_missing_deps: bool = False
        self.package_method: bool = True
        self.downgrade_packages: bool = False
        self.delete_sources: bool = False
        self.downloader: str = "wget"
        self.wget_options: str = "-c -q --progress=bar:force:noscroll --show-progress"
        self.curl_options: str = ""
        self.aria2_options: str = "-c"
        self.lftp_get_options: str = "-c get -e"
        self.lftp_mirror_options: str = "-c mirror --parallel=100 --only-newer --delete"
        self.git_clone: str = "git clone --depth 1"
        self.download_only_path: Path = self.tmp_slpkg
        self.ask_question: bool = True
        self.all_repos_confirm: str = "once"
        self.mirror_version_check: bool = True
        self.parallel_downloads: bool = False
        self.maximum_parallel: int = 5
        self.quiet: bool = True
        self.progress_spinner: str = "spinner"
        self.spinner_color: str = "white"
        self.process_log: bool = True
        self.urllib_retries: bool = False
        self.urllib_redirect: bool = False
        self.urllib_timeout: float = 3.0
        self.proxy_address: str = ""
        self.proxy_username: str = ""
        self.proxy_password: str = ""
        config_logger.debug("Default configuration values initialized.")

        self.config: dict[str, Any] = {}

        # Load configurations from the TOML file (before potential argparse override)
        self._load_config()
        # Apply static checks/adjustments based on loaded settings
        self._set_colors()  # Will apply colors based on self.colors (from TOML or default)
        self._create_paths()
        config_logger.debug("Config module initialization complete.")

    def _read_toml_file(self, path: Path) -> dict[str, Any]:
        """Read and parse the TOML config file.

        Args:
            path (Path): Path to the TOML configuration file.

        Returns:
            dict: Parsed TOML content, or empty dict if the file is absent.
        """
        if path.exists():
            config_logger.debug("Configuration file found at: %s", path)
            parsed = tomlkit.parse(path.read_text(encoding="utf-8"))
            config_logger.info("Successfully parsed configuration file: %s", path)
            return parsed
        config_logger.warning("Configuration file not found at: %s. Using default settings.", path)
        return {}

    @staticmethod
    def _flatten_toml(conf: dict[str, Any]) -> dict[str, Any]:
        """Flatten all TOML sections into a single key→value dict.

        Args:
            conf (dict): Parsed TOML document (may contain nested sections).

        Returns:
            dict: All keys from every section merged into one flat dict.
        """
        flat: dict[str, Any] = {}
        for section_data in conf.values():
            if isinstance(section_data, dict):
                flat.update(section_data)
        return flat

    def _apply_toml_values(self) -> None:
        """Validate and apply every key from the flattened TOML config.

        Iterates over _TOML_TO_ATTR, type-checks each value against
        _CONFIG_TYPES, and sets the corresponding instance attribute.
        Prints user-facing messages for missing keys or type errors.
        """
        error_type = False
        toml_setting_name: str = ""
        keys_not_found: list[str] = []

        for toml_key, (attr_name, expected_types) in self._TOML_CONFIG.items():
            if toml_key not in self.config:
                keys_not_found.append(toml_key)
                continue

            value = self.config[toml_key]

            # Special handling for DOWNLOAD_ONLY_PATH: convert string to Path.
            if toml_key == "DOWNLOAD_ONLY_PATH" and isinstance(value, str):
                value = Path(value)
                config_logger.debug("Converted DOWNLOAD_ONLY_PATH from string to Path: %s", value)

            if not isinstance(value, expected_types):
                error_type = True
                toml_setting_name = toml_key
                config_logger.error(
                    "Type mismatch for TOML setting '%s'. Expected %s, got %s. Value: %s",
                    toml_key,
                    expected_types,
                    type(value),
                    value,
                )
                break

            setattr(self, attr_name, value)
            config_logger.debug("Set config '%s' to value '%s' (from TOML).", attr_name, value)

        if keys_not_found:
            config_logger.warning(
                "TOML settings '%s' not found in config file. Using default values.", ", ".join(keys_not_found)
            )
            print(
                f"Important notification: Unable to find '{', '.join(keys_not_found)}' settings in your current configurations. "
                f"This issue may arise after an '{self.prog_name}' upgrade. To resolve this issue, please run:\n"
                f"\n{'':>4}$ slpkg_new-configs\n"
                "\nDefault settings are currently in use.\n"
            )

        if error_type:
            print(
                f"{self.prog_name}: Error: Setting '{toml_setting_name}' in configurations contain wrong type.\n"
                f"Default configurations are used.\n"
            )
            config_logger.error(
                "Configuration loading aborted due to type error in setting '%s'. Using default values for remaining settings.",
                toml_setting_name,
            )
        else:
            config_logger.info("All configurations from TOML file processed successfully.")

    def _check_dialog_available(self) -> None:
        """Force self.dialog to False when the python-dialog module is absent."""
        if not DIALOG_AVAILABLE:
            if self.dialog:
                config_logger.warning("Python 'dialog' module is not available. Forcing self.dialog to False.")
            self.dialog = False
        config_logger.debug("Dialog status after check: %s", self.dialog)

    def _load_config(self) -> None:
        """Load configuration from the TOML file and apply values to instance attributes."""
        config_logger.debug("Loading configuration from TOML file.")
        config_path_file: Path = Path(self.etc_path, f"{self.prog_name}.toml")
        try:
            conf = self._read_toml_file(config_path_file)
            if conf:
                self.config = self._flatten_toml(conf)
                config_logger.debug("Loaded and flattened %d keys from TOML sections.", len(self.config))
                self._apply_toml_values()
            else:
                config_logger.warning(
                    "Configuration file %s is empty or unparseable. Using default settings.", config_path_file
                )
        except (KeyError, exceptions.TOMLKitError) as e:
            config_logger.critical(
                "Failed to parse or access required key in %s. Error: %s", config_path_file, e, exc_info=True
            )
            self.toml_errors.raise_toml_error_message(str(e), config_path_file)
            print("The default configurations are used.\n")
        self._check_dialog_available()

    def update_from_args(self, args: argparse.Namespace) -> None:
        """
        Updates configuration settings based on parsed argparse arguments.
        This method should be called from main.py after arguments are parsed.
        """
        config_logger.debug("Updating configurations from argparse arguments: %s", args)
        if (
            hasattr(args, "option_color") and args.option_color is not None
        ):  # Check for None to allow explicit --color=off.
            # argparse might pass 'on', 'off', True, False. Normalize to boolean.
            self.colors = str(args.option_color).lower() == "on" or args.option_color is True
            self._set_colors()  # Reapply color settings if changed.
            config_logger.info("Colors updated from args to: %s", self.colors)

        if getattr(args, "option_dialog", False):
            self.dialog = True
            if not DIALOG_AVAILABLE:
                config_logger.warning(
                    "Attempted to enable dialog via -D, but 'dialog' module is not available. Forcing self.dialog to False."
                )
                self.dialog = False
            config_logger.info("Dialog updated from args to: %s", self.dialog)

        if getattr(args, "option_parallel", False):
            self.parallel_downloads = True
            config_logger.info("Parallel downloads enabled from args.")

        if getattr(args, "option_quiet", False):
            self.quiet = True
            config_logger.info("Quiet mode enabled from args.")

        if getattr(args, "option_yes", False):
            self.answer_yes = True
            config_logger.info("Answer 'yes' enabled from args.")
        config_logger.debug("Configuration update from argparse complete.")

    def _set_colors(self) -> None:
        config_logger.debug("Setting terminal color codes.")
        self.clear_screen: str = "\033[2J\033[H"  # Always set — not color-dependent
        # Reset color codes
        self.back_white: str = ""
        self.back_grey: str = ""
        self.bold: str = ""
        self.white: str = ""
        self.black: str = ""
        self.red: str = ""
        self.green: str = ""
        self.yellow: str = ""
        self.cyan: str = ""
        self.grey: str = ""
        self.endc: str = ""

        # Apply colors only if self.colors is True
        if self.colors:
            self.back_white = "\x1b[107m"
            self.back_grey = "\x1b[48;5;234m"
            self.bold = "\x1b[1m"
            self.white = "\x1b[37m"
            self.black = "\x1b[30m"
            self.red = "\x1b[91m"
            self.green = "\x1b[32m"
            self.yellow = "\x1b[93m"
            self.cyan = "\x1b[96m"
            self.grey = "\x1b[90m"
            self.endc = "\x1b[0m"
            config_logger.debug("Terminal colors enabled and set.")
        else:
            config_logger.debug("Terminal colors disabled. Color codes are empty strings.")

    def _create_paths(self) -> None:
        config_logger.debug("Creating necessary application paths.")
        paths = [
            self.lib_path,
            self.etc_path,
            self.build_path,
            self.tmp_slpkg,
            self.log_path,
            self.download_only_path,
        ]
        for path in paths:
            try:
                if not path.is_dir():
                    path.mkdir(parents=True, exist_ok=True)
                    config_logger.debug("Created directory: %s", path)
                else:
                    config_logger.debug("Directory already exists: %s", path)
            except OSError as e:  # noqa: PERF203
                config_logger.error("Failed to create directory '%s': %s", path, e)
        config_logger.debug("All necessary paths checked/created.")

        # Initialize log files with empty defaults if they don't exist.
        log_files_defaults: dict[Path, str] = {
            self.deps_log_file: "{}",
            self.marks_log_file: "[]",
            self.pins_log_file: "[]",
            self.history_log_file: "",
        }
        for log_file, default_content in log_files_defaults.items():
            if not log_file.is_file():
                try:
                    log_file.write_text(default_content, encoding="utf-8")
                    config_logger.debug("Created empty log file: %s", log_file)
                except OSError as e:
                    config_logger.error("Failed to create log file '%s': %s", log_file, e)

    def is_64bit(self) -> bool:
        """Determines the CPU and the OS architecture.

        Returns:
            bool: True if the system is 64-bit, False otherwise.
        """
        is_64 = self.cpu_arch in {"x86_64", "amd64", "aarch64", "arm64", "ia64"} and self.os_arch == "64bit"
        config_logger.debug(
            "System architecture check: CPU='%s', OS='%s'. Is 64-bit: %s", self.cpu_arch, self.os_arch, is_64
        )
        return is_64


# Creating a unique instance of the Config class.
# This instance will be loaded by any module that imports 'config'.
# It contains default settings and settings from TOML (if config.toml exists in /etc/slpkg).
config_load = Config()
