#!/usr/bin/env python3
# -*- coding: utf-8; py-indent-offset: 4 -*-
#
# Author:  Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
#          https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.

# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.md

"""See the check's README for more details."""

import argparse
import base64
import sys

import lxml.etree as ET

import lib.args
import lib.base
import lib.txt
import lib.url
from lib.globals import STATE_OK, STATE_UNKNOWN, STATE_WARN

__author__ = """Linuxfabrik GmbH, Zurich/Switzerland;
                originally written by Simon Wunderlin,
                adapted by Dominik Riva, Universitätsspital Basel/Switzerland"""
__version__ = '2026040801'


DESCRIPTION = """Fetches an XML document via HTTP(S) and checks for a matching string using XPath
expressions. Supports namespace prefixes and HTTP Basic Authentication. Alerts when
the expected value is not found or does not match."""

DEFAULT_INSECURE = False
DEFAULT_NO_PROXY = False
DEFAULT_TIMEOUT = 7


def parse_args():
    """Parse command line arguments using argparse."""
    parser = argparse.ArgumentParser(description=DESCRIPTION)

    parser.add_argument(
        '-V',
        '--version',
        action='version',
        version=f'%(prog)s: v{__version__} by {__author__}',
    )

    parser.add_argument(
        '--always-ok',
        help=lib.args.help('--always-ok'),
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--expect',
        help='Expected string at the XPath location. '
        'If omitted, just checks whether the XPath exists.',
        dest='EXPECT',
    )

    parser.add_argument(
        '--insecure',
        help=lib.args.help('--insecure'),
        dest='INSECURE',
        action='store_true',
        default=DEFAULT_INSECURE,
    )

    parser.add_argument(
        '--namespace',
        help='Namespace prefix-to-URI mapping for XPath expressions. '
        'Can be specified multiple times. '
        'Example: `--namespace="prefix1:https://schemas.xmlsoap.org/prefix1/"`.',
        dest='NAMESPACES',
        action='append',
    )

    parser.add_argument(
        '--no-proxy',
        help=lib.args.help('--no-proxy'),
        dest='NO_PROXY',
        action='store_true',
        default=DEFAULT_NO_PROXY,
    )

    parser.add_argument(
        '--password',
        help='HTTP Basic Auth password.',
        dest='PASSWORD',
    )

    parser.add_argument(
        '--timeout',
        help=lib.args.help('--timeout') + ' Default: %(default)s (seconds)',
        dest='TIMEOUT',
        type=int,
        default=DEFAULT_TIMEOUT,
    )

    parser.add_argument(
        '--url',
        help='XML endpoint URL.',
        dest='URL',
        required=True,
    )

    parser.add_argument(
        '--username',
        help='HTTP Basic Auth username.',
        dest='USERNAME',
    )

    parser.add_argument(
        '--xpath',
        help='XPath expression to query. Must point to a single value (attribute or node content). '
        'Lists/arrays are not supported.',
        dest='XPATH',
        required=True,
    )

    args, _ = parser.parse_known_args()
    return args


def main():
    """The main function. This is where the magic happens."""

    # parse the command line
    try:
        args = parse_args()
    except SystemExit:
        sys.exit(STATE_UNKNOWN)

    header = {
        'Accept': 'application/xml',
        'Connection': 'keep-alive',
        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
        'Chrome/100.0.4896.127 Safari/537.36',
    }

    # Authorization (if needed)
    if args.USERNAME and args.PASSWORD:
        auth = f'{args.USERNAME}:{args.PASSWORD}'
        encoded_auth = lib.txt.to_text(base64.b64encode(lib.txt.to_bytes(auth)))
        header['Authorization'] = f'Basic {encoded_auth}'

    # fetch data
    result = lib.base.coe(
        lib.url.fetch(
            args.URL,
            header=header,
            insecure=args.INSECURE,
            no_proxy=args.NO_PROXY,
            timeout=args.TIMEOUT,
        )
    )

    # analyze data
    try:
        doc = ET.fromstring(lib.txt.to_bytes(result))
    except Exception:
        lib.base.cu('Failed to parse XML.')

    # https://lxml.de/xpathxslt.html
    if args.NAMESPACES:
        namespaces = {}
        for ns in args.NAMESPACES:
            try:
                namespaces[ns.split(':', 1)[0]] = ns.split(':', 1)[1]
            except Exception:
                lib.base.cu(f'Wrong namespace mapping syntax for "{ns}".')
        try:
            r = doc.xpath(args.XPATH, namespaces=namespaces)
        except Exception:
            lib.base.cu('An XML xpath error occurred.')
    else:
        try:
            r = doc.xpath(args.XPATH)
        except Exception:
            lib.base.cu('An XML xpath error occurred.')

    # over and out
    if args.EXPECT is None:
        # just prove that there is a valid answer
        if r:
            lib.base.oao('Everything is ok.', STATE_OK)
        else:
            state = STATE_WARN
            lib.base.oao(
                f'Empty result{lib.base.state2str(state, prefix=" ")}.',
                state,
                always_ok=args.ALWAYS_OK,
            )
    else:
        # search for a string
        try:
            result = r[0].text
        except Exception:
            lib.base.cu('Response types other than string are not supported.')
        if args.EXPECT in result:
            lib.base.oao(
                f'Everything is ok, "{args.EXPECT}" found in result "{result}".',
                STATE_OK,
            )
        else:
            state = STATE_WARN
            lib.base.oao(
                (
                    f'"{args.EXPECT}" not found'
                    f' in "{result}"'
                    f'{lib.base.state2str(state, prefix=" ")}.'
                ),
                state,
                always_ok=args.ALWAYS_OK,
            )


if __name__ == '__main__':
    try:
        sys.exit(main())
    except Exception:
        lib.base.cu()
