#!/usr/bin/env python3
"""Icinga/Nagios monitoring plugin for DokuWiki core and extensions via native CLI"""

import argparse
import logging
import os
import re
import subprocess  # nosec
import sys

import requests

VERSION = "2026.9.1"

EXIT_CODES = {
    "UNKNOWN": 3,
    "CRITICAL": 2,
    "WARNING": 1,
    "OK": 0,
}

FLAGS = {
    "installed": "i",
    "disabled": "d",
    "update": "u",
    "security": "S",
    "bundled": "b",
    "error": "E",
    "warning": "W",
    "notice": "I",
    "git": "g",
}


class DokuWikiChecker:
    """DokuWiki installation checker"""

    def __init__(
        self,
        dokuwiki_path,
        php_bin_path=None,
        timeout=None,
        skip_extensions=False,
        ignore_extensions_warnings=None,
    ):
        self.dokuwiki_path = dokuwiki_path
        self.php_bin_path = php_bin_path or "php"
        self.timeout = timeout or 30
        self.skip_extensions = skip_extensions
        self.ignore_extensions_warnings = ignore_extensions_warnings or []

        self.errors = []
        self.messages = []
        self.extra_warnings = []
        self.extensions = {"plugin": {}, "template": {}}
        self.performance_data = {}

    def add_error(self, message, severity="WARNING"):
        """
        Record an error or warning issue.
        :param message: Detailed description of the issue.
        :param severity: Severity level ('CRITICAL', 'WARNING', 'UNKNOWN').
        """
        severity = severity.upper()
        self.errors.append(
            {
                "message": message,
                "severity": severity,
            }
        )
        logging.error("[%s] %s", severity, message)

    def add_info(self, message):
        """Record a positive status or informative context message."""
        self.messages.append(message)
        logging.info(message)

    def add_extra_warnings(self, message):
        """Add extra warning messages to display on top of output extra lines"""
        self.extra_warnings.append(message)
        logging.warning(message)

    @property
    def exit_code(self):
        """Monitoring plugin exit code"""
        severities = {item["severity"] for item in self.errors}
        for severity, exit_code in EXIT_CODES.items():
            if severity in severities:
                return exit_code
        return EXIT_CODES["OK"]

    @property
    def status_name(self):
        """Monitoring plugin exit status"""
        statuses = {exit_code: severity for severity, exit_code in EXIT_CODES.items()}
        return statuses[self.exit_code]

    def build_output(self):
        """Build output."""

        messages = [
            *[
                error["message"]
                for error in sorted(self.errors, key=lambda x: EXIT_CODES[x["severity"]])
            ],
            *self.messages,
        ]

        def format_extension(ext):
            """Format one extension one line output"""
            tags = [
                (
                    "bundled"
                    if "bundled" in ext["flags"]
                    else "installed via git" if "git" in ext["flags"] else ext["date"]
                ),
                *ext["tags"],
            ]
            return f"{ext['name']} ({', '.join(tags)})"

        return "\n".join(
            [
                f"{self.status_name} - {', '.join(messages)} | {self.format_performance_data()}",
                *((self.extra_warnings + [""]) if self.extra_warnings else []),
                *(
                    [
                        "[PLUGINS]",
                        *[
                            format_extension(plugin)
                            for plugin in self.extensions["plugin"].values()
                        ],
                        "",
                    ]
                    if self.extensions["plugin"]
                    else []
                ),
                *(
                    [
                        "[TEMPLATES]",
                        *[format_extension(tpl) for tpl in self.extensions["template"].values()],
                        "",
                    ]
                    if self.extensions["template"]
                    else []
                ),
            ]
        )

    def add_performance_data(
        self,
        label,
        value,
        unit=None,
        warning=None,
        critical=None,
        minimum=None,
        maximum=None,
    ):
        """Add performance data"""
        self.performance_data[label] = {
            "value": value,
            "unit": unit,
            "warning": warning,
            "critical": critical,
            "minimum": minimum if minimum else "0" if unit == "%" else None,
            "maximum": maximum if maximum else "0" if unit == "%" else None,
        }

    def format_performance_data(self):
        """Format registered performance data as string"""
        perf_data = []
        for label, data in self.performance_data.items():
            if " " in label:
                label = f"'{label}'"
            parts = [f"{label}={data['value']}{data['unit'] if data['unit'] else ''}"]
            if data["warning"] or data["critical"] or data["minimum"] or data["maximum"]:
                parts.append(str(data["warning"]) if data["warning"] else "")
                if data["critical"] or data["minimum"] or data["maximum"]:
                    parts.append(
                        str(data["critical"])
                        if data["critical"]
                        else str(data["maximum"]) if data["maximum"] else ""
                    )
                    if data["minimum"] or data["maximum"]:
                        parts.append(str(data["minimum"]) if data["minimum"] else "")
                        if data["maximum"]:
                            parts.append(str(data["maximum"]))
            perf_data.append(";".join(parts))
        return " ".join(perf_data)

    def get_installed_core(self):
        """Get installed core version"""
        version_file = os.path.join(self.dokuwiki_path, "VERSION")
        logging.debug("Reading core version from '%s'", version_file)
        if not os.path.isfile(version_file):
            self.add_error("DokuWiki VERSION file not found", "UNKNOWN")
            return None
        try:
            with open(version_file, encoding="utf-8") as f:
                content = f.read().strip()
                match = re.search(r"(\d{4}-\d{2}-\d{2}[a-z]?)", content)
                return match.group(1) if match else content
        except Exception as err:  # pylint: disable=broad-exception-caught
            self.add_error(f"Failed to read DokuWiki VERSION file ({err})", "UNKNOWN")
            return None

    def get_latest_core(self):
        """Get the latest core version available"""
        url = "https://api.github.com/repos/dokuwiki/dokuwiki/releases/latest"
        logging.debug("Fetching latest DokuWiki core release from GitHub API (%s)", url)
        headers = {
            "User-Agent": "Icinga-DokuWiki-Check",
            "Accept": "application/vnd.github+json",
        }
        try:
            response = requests.get(url, headers=headers, timeout=self.timeout)
            response.raise_for_status()
            data = response.json()
            tag = data.get("tag_name", "")
            return tag.replace("release-", "").replace("release_", "")
        except Exception as err:  # pylint: disable=broad-exception-caught
            self.add_error(f"Failed to retrieve latest core version from GitHub ({err})", "UNKNOWN")
            return None

    def check_core(self):
        """Check DokuWiki core version is up-to-date"""
        installed_version = self.get_installed_core()
        if not installed_version:
            return
        latest_version = self.get_latest_core()
        if not latest_version:
            return
        if latest_version == installed_version:
            self.add_info(f"DokuWiki is up to date ({installed_version})")
        else:
            self.add_error(f"New version of DokuWiki is available ({latest_version})")

    def get_extensions(self):
        """Execute DokuWiki CLI plugin script to fetch extensions list."""
        plugin_script = os.path.join(self.dokuwiki_path, "bin", "plugin.php")
        if not os.path.isfile(plugin_script):
            self.add_error("DokuWiki CLI plugin script not found", "UNKNOWN")
            return None

        cmd = [self.php_bin_path, plugin_script, "extension", "list"]
        logging.debug(
            "Executing DokuWiki CLI plugin script to fetch extensions list (%s)", " ".join(cmd)
        )

        try:
            res = subprocess.run(  # nosec
                cmd,
                cwd=self.dokuwiki_path,
                capture_output=True,
                text=True,
                timeout=self.timeout,
                check=True,
            )
            lines = res.stdout.splitlines()
        except Exception as err:  # pylint: disable=broad-exception-caught
            self.add_error(
                f"Error occurred running DokuWiki CLI plugin script to list extensions ({err})",
                "UNKNOWN",
            )
            return None

        extensions = []
        # Pattern: id  flags  date/bundled  description
        pattern = re.compile(r"^\s*(\S+)\s+(\S+)\s+(\S+)\s*(.*)$")

        for line in lines:
            line_str = line.strip()
            if not line_str or line_str.startswith("#"):
                continue

            match = pattern.match(line_str)
            if match:
                ext_id, flags, date_val, desc = match.groups()
                is_template = ext_id.startswith("template:")
                clean_name = ext_id.replace("template:", "")

                extensions.append(
                    {
                        "id": ext_id,
                        "name": clean_name,
                        "is_template": is_template,
                        "flags": flags,
                        "date": date_val,
                        "description": desc,
                    }
                )

        return extensions

    def check_extensions(self):
        """Analyze extension flags and populate plugin/template line lists."""
        extensions = self.get_extensions()
        if extensions is None:
            return

        pending_security_updates = 0
        pending_updates = 0
        extensions_with_errors = 0
        extensions_with_warnings = 0

        for ext in extensions:
            name = ext["name"]
            # Ignore "testing" plugin (used to test the test framework)
            if name == "testing":
                continue
            ext_type = "template" if ext["is_template"] else "plugin"
            flags = {flag for flag, letter in FLAGS.items() if letter in ext["flags"]}
            tags = []

            if "security" in flags:
                pending_security_updates += 1
                self.add_extra_warnings(
                    f"Security update required for {ext_type} '{name}'",
                )
                tags.append("security issue")

            if "error" in flags:
                extensions_with_errors += 1
                self.add_extra_warnings(
                    f"Error detected in {ext_type} '{name}'",
                )
                tags.append("error detected")

            if "warning" in flags and name not in self.ignore_extensions_warnings:
                extensions_with_warnings += 1
                self.add_extra_warnings(
                    f"Warning detected in {ext_type} '{name}'",
                )
                tags.append("warning detected")

            if "update" in flags and "security" not in flags:
                pending_updates += 1
                self.add_extra_warnings(
                    f"Update available for {ext_type} '{name}'",
                )
                tags.append("update available")

            if "disabled" in flags:
                tags.append("disabled")
            if "notice" in flags:
                tags.append("have notice information")

            if not flags.intersection({"security", "error", "warning", "update"}):
                tags.append("up to date")

            self.extensions[ext_type][name] = {
                "name": name,
                "date": ext["date"] if "bundled" not in flags else None,
                "flags": flags,
                "tags": tags,
                "description": ext["description"],
            }

        for ext_type, installed in self.extensions.items():
            self.add_performance_data(f"{ext_type}s_installed", len(installed))
        self.add_performance_data(
            "pending_security_updates",
            pending_security_updates,
            maximum=len(extensions),
            critical=1,
        )
        self.add_performance_data(
            "pending_updates", pending_updates, maximum=len(extensions), warning=1
        )
        self.add_performance_data(
            "extensions_with_errors", extensions_with_errors, maximum=len(extensions), critical=1
        )
        self.add_performance_data(
            "extensions_with_warnings", extensions_with_warnings, maximum=len(extensions), warning=1
        )

        if pending_security_updates:
            self.add_error(
                f"{pending_security_updates} pending extension security update(s)", "CRITICAL"
            )
        if extensions_with_errors:
            self.add_error(f"{extensions_with_errors} extension(s) with errors", "CRITICAL")
        if pending_updates:
            self.add_error(f"{pending_updates} pending extension update(s)", "WARNING")
        if extensions_with_warnings:
            self.add_error(f"{extensions_with_warnings} extension(s) with warnings", "WARNING")

        if extensions and not (
            pending_security_updates
            or extensions_with_errors
            or pending_updates
            or extensions_with_warnings
        ):
            self.add_info("All extensions are up to date and healthy")

    def check(self):
        """Check DokuWiki installation"""
        if not os.path.isdir(self.dokuwiki_path):
            self.add_error("DokuWiki directory not found", "UNKNOWN")
            return

        self.check_core()
        if not self.skip_extensions:
            self.check_extensions()


def main():
    """Entrypoint"""
    parser = argparse.ArgumentParser(description=f"{__doc__} (version: {VERSION})")
    parser.add_argument(
        "-p", "--path", required=True, help="Absolute path to DokuWiki root directory"
    )
    parser.add_argument("--php-bin", default="php", help="Path to PHP binary (default: php)")
    parser.add_argument("--timeout", type=int, help="Timeout on external actions")
    parser.add_argument(
        "--skip-extensions", action="store_true", help="Skip checking DokuWiki extensions"
    )
    parser.add_argument(
        "-i",
        "--ignore-extensions-warnings",
        action="extend",
        nargs="+",
        help="Ignore warning about the specified extension(s)",
    )
    parser.add_argument("-d", "--debug", action="store_true", help="Enable debug mode")
    args = parser.parse_args()

    # Configure logging module
    logging.basicConfig(
        level=logging.DEBUG if args.debug else logging.WARNING,
        format="[%(asctime)s] [%(levelname)s] %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",  # cspell:words datefmt
    )

    checker = DokuWikiChecker(
        args.path,
        php_bin_path=args.php_bin,
        timeout=args.timeout,
        skip_extensions=args.skip_extensions,
        ignore_extensions_warnings=args.ignore_extensions_warnings,
    )
    checker.check()
    print(checker.build_output())
    sys.exit(checker.exit_code)


if __name__ == "__main__":
    main()
