#!/usr/bin/python3
"""Validate slpkg.toml without opening an editor."""

from __future__ import annotations

import logging
import sys
from pathlib import Path
from typing import Any

import tomlkit
from tomlkit import exceptions

from slpkg.config import config_load

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Schema: section → key → rule
#   type    : expected Python type(s) — tuple passed to isinstance()
#   choices : allowed string values (optional)
#   min     : minimum numeric value (optional)
# ---------------------------------------------------------------------------

_BOOL = (bool,)
_STR = (str,)
_INT = (int,)
_FLOAT = (int, float)
_LIST = (list,)

SCHEMA: dict[str, dict[str, dict[str, Any]]] = {
    "logging": {
        "LOGGING_LEVEL": {
            "type": _STR,
            "choices": {"CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"},
        },
    },
    "ui": {
        "COLORS": {"type": _BOOL},
        "PAGER": {"type": _STR},
        "EDITOR": {"type": _STR},
        "DIALOG": {"type": _BOOL},
        "TERMINAL_SELECTOR": {"type": _BOOL},
        "QUIET": {"type": _BOOL},
        "PROGRESS_SPINNER": {
            "type": _STR,
            "choices": {"spinner", "pie", "moon", "line", "pixel", "ball", "clock"},
        },
        "SPINNER_COLOR": {
            "type": _STR,
            "choices": {"white", "green", "yellow", "cyan", "grey", "red"},
        },
    },
    "packages": {
        "PACKAGE_TYPE": {"type": _LIST},
        "FILE_LIST_SUFFIX": {"type": _STR},
        "PACKAGE_METHOD": {"type": _BOOL},
        "DOWNGRADE_PACKAGES": {"type": _BOOL},
        "VIEW_MISSING_DEPS": {"type": _BOOL},
        "KERNEL_VERSION": {"type": _BOOL},
        "BOOTLOADER_COMMAND": {"type": _STR},
    },
    "build": {
        "MAKEFLAGS": {"type": _STR},
        "DELETE_SOURCES": {"type": _BOOL},
    },
    "verification": {
        "GPG_VERIFICATION": {"type": _BOOL},
        "CHECKSUM_MD5": {"type": _BOOL},
        "VERIFY_SKIP_VIRTUAL": {"type": _LIST},
        "VERIFY_SKIP_PREFIXES": {"type": _LIST},
        "VERIFY_SKIP_SUFFIXES": {"type": _LIST},
    },
    "download": {
        "DOWNLOADER": {
            "type": _STR,
            "choices": {"wget", "wget2", "curl", "aria2c", "lftp"},
        },
        "DOWNLOAD_ONLY_PATH": {"type": _STR},
        "PARALLEL_DOWNLOADS": {"type": _BOOL},
        "MAXIMUM_PARALLEL": {"type": _INT, "min": 1},
        "WGET_OPTIONS": {"type": _STR},
        "CURL_OPTIONS": {"type": _STR},
        "ARIA2_OPTIONS": {"type": _STR},
        "LFTP_GET_OPTIONS": {"type": _STR},
        "LFTP_MIRROR_OPTIONS": {"type": _STR},
        "GIT_CLONE": {"type": _STR},
    },
    "commands": {
        "INSTALLPKG": {"type": _STR},
        "LOCAL_INSTALLPKG": {"type": _STR},
        "REINSTALL": {"type": _STR},
        "REMOVEPKG": {"type": _STR},
    },
    "behavior": {
        "ASK_QUESTION": {"type": _BOOL},
        "ALL_REPOS_CONFIRM": {
            "type": _STR,
            "choices": {"once", "per_repo"},
        },
        "PROCESS_LOG": {"type": _BOOL},
    },
    "network": {
        "URLLIB_RETRIES": {"type": _BOOL},
        "URLLIB_REDIRECT": {"type": _BOOL},
        "URLLIB_TIMEOUT": {"type": _FLOAT, "min": 0},
    },
    "proxy": {
        "PROXY_ADDRESS": {"type": _STR},
        "PROXY_USERNAME": {"type": _STR},
        "PROXY_PASSWORD": {"type": _STR},
    },
}


class ConfigValidator:  # pylint: disable=[R0903]
    """Validate slpkg.toml and print a human-readable report."""

    def __init__(self) -> None:
        self.config_file: Path = config_load.etc_path / f"{config_load.prog_name}.toml"
        self.bold: str = config_load.bold
        self.green: str = config_load.green
        self.red: str = config_load.red
        self.yellow: str = config_load.yellow
        self.grey: str = config_load.grey
        self.endc: str = config_load.endc

    # ------------------------------------------------------------------
    # Public entry point
    # ------------------------------------------------------------------

    def run(self) -> None:
        """Validate the config file and exit with 0 (OK) or 1 (errors)."""
        errors, warnings = self._validate()
        self._print_report(errors, warnings)
        sys.exit(1 if errors else 0)

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _validate(self) -> tuple[list[str], list[str]]:
        """Return (errors, warnings) after checking the TOML file."""
        errors: list[str] = []
        warnings: list[str] = []

        if not self.config_file.exists():
            errors.append(f"Configuration file not found: {self.config_file}")
            return errors, warnings

        # --- 1. TOML syntax ---
        try:
            doc: dict[str, Any] = tomlkit.parse(self.config_file.read_text(encoding="utf-8"))
        except exceptions.TOMLKitError as exc:
            errors.append(f"TOML syntax error: {exc}")
            return errors, warnings

        if not doc:
            warnings.append("Configuration file is empty — defaults will be used.")
            return errors, warnings

        # --- 2. Unknown sections ---
        for section in doc:
            if section not in SCHEMA:
                warnings.append(f"Unknown section [{section}] — will be ignored.")

        # --- 3. Per-section key / value checks ---
        for section, schema_keys in SCHEMA.items():
            section_data = doc.get(section, {})
            if not isinstance(section_data, dict):
                errors.append(f"[{section}] must be a TOML table, got {type(section_data).__name__}.")
                continue

            for key in section_data:
                if key not in schema_keys:
                    warnings.append(f"[{section}] unknown key '{key}' — will be ignored.")

            for key, rule in schema_keys.items():
                if key not in section_data:
                    warnings.append(f"[{section}] key '{key}' is missing — default value will be used.")
                    continue
                self._validate_key(section, key, section_data[key], rule, errors)

        return errors, warnings

    @staticmethod
    def _unwrap(value: Any) -> Any:
        """Unwrap tomlkit proxy objects to get the raw Python value."""
        if hasattr(value, "unwrap"):
            try:
                return value.unwrap()
            except Exception as e:  # pylint: disable=[broad-except]
                logger.debug("tomlkit unwrap() failed for %r, using raw value: %s", value, e)
        return value

    def _validate_key(  # pylint: disable=[R0913,R0917]
        self,
        section: str,
        key: str,
        value: Any,
        rule: dict[str, Any],
        errors: list[str],
    ) -> None:
        """Validate a single key's type and value constraints."""
        raw = self._unwrap(value)
        expected_types: tuple[type, ...] = rule["type"]

        # Type check — bool must come before int (bool is a subclass of int)
        if bool in expected_types:
            if not isinstance(raw, bool):
                errors.append(
                    f"[{section}] '{key}': expected bool (true/false), "
                    f"got {type(raw).__name__} = {raw!r}."
                )
                return
        elif not isinstance(raw, expected_types):
            type_names = " | ".join(t.__name__ for t in expected_types)
            errors.append(
                f"[{section}] '{key}': expected {type_names}, "
                f"got {type(raw).__name__} = {raw!r}."
            )
            return

        # Enum choices
        if "choices" in rule and isinstance(raw, str) and raw not in rule["choices"]:
            allowed = ", ".join(sorted(rule["choices"]))
            errors.append(
                f"[{section}] '{key}': invalid value {raw!r}. "
                f"Allowed: {allowed}."
            )

        # Minimum numeric value
        if "min" in rule and isinstance(raw, (int, float)) and raw < rule["min"]:
            errors.append(
                f"[{section}] '{key}': value {raw!r} is below minimum ({rule['min']})."
            )

    def _print_report(self, errors: list[str], warnings: list[str]) -> None:
        """Print a human-readable validation report."""
        print(f"Validating {self.config_file} ...\n")

        for msg in warnings:
            print(f"  {self.yellow}WARN{self.endc}  {msg}")

        for msg in errors:
            print(f"  {self.red}ERR {self.endc}  {msg}")

        if not errors and not warnings:
            print(f"  {self.green}OK{self.endc}  Configuration is valid.\n")
        elif not errors:
            print(f"  {self.green}OK{self.endc}  {len(warnings)} warning(s), no errors. Configuration is usable.\n")
        else:
            print(
                f"{self.red}FAIL{self.endc}  {len(errors)} error(s)"
                + (f", {len(warnings)} warning(s)" if warnings else "")
                + " found.\n"
            )
