#!/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 sys

import lib.args
import lib.base
import lib.cache
import lib.feedparser
import lib.human
import lib.icinga
import lib.time
from lib.globals import STATE_OK, STATE_UNKNOWN, STATE_WARN

__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
__version__ = '2026040801'

DESCRIPTION = """Monitors an RSS or Atom feed for new entries and alerts when new items appear within
a configurable time window (default: 3 days). If Icinga callback is enabled, the
alert is automatically cleared once the corresponding service is acknowledged in
Icinga. After the time window expires, the alert clears regardless of acknowledgement
status."""

DEFAULT_FEED_URL = 'https://www.heise.de/security/rss/alert-news-atom.xml'
DEFAULT_ICINGA_CALLBACK = False
DEFAULT_INSECURE = False
DEFAULT_LATEST = False
DEFAULT_NO_PROXY = False
DEFAULT_NO_SUMMARY = False
DEFAULT_TIMEOUT = 5
DEFAULT_WARN = 4320  # = minutes (3 days)


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(
        '--icinga-service-name',
        help='Unique name of the service using this check within Icinga, '
        'taken from the `__name` service attribute. '
        'Example: `icinga-server!my-service-name`.',
        dest='ICINGA_SERVICE_NAME',
    )

    parser.add_argument(
        '--icinga-password',
        help='Password for the Icinga API.',
        dest='ICINGA_PASSWORD',
    )

    parser.add_argument(
        '--icinga-url',
        help='Icinga API URL. Example: `https://icinga-server:5665`.',
        dest='ICINGA_URL',
    )

    parser.add_argument(
        '--icinga-username',
        help='Username for the Icinga API.',
        dest='ICINGA_USERNAME',
    )

    parser.add_argument(
        '--icinga-callback',
        help='Query Icinga for the service acknowledgement state and auto-clear alerts on ack. '
        'Default: %(default)s',
        dest='ICINGA_CALLBACK',
        action='store_true',
        default=DEFAULT_ICINGA_CALLBACK,  # False
    )

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

    parser.add_argument(
        '--latest',
        help='Return the newest feed item, even if its timestamp is in the future.',
        dest='LATEST',
        action='store_true',
        default=False,
    )

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

    parser.add_argument(
        '--no-summary',
        help='Suppress the feed item summary in the output. Default: %(default)s',
        dest='NO_SUMMARY',
        action='store_true',
        default=DEFAULT_NO_SUMMARY,  # False
    )

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

    parser.add_argument(
        '--url',
        help='RSS or Atom feed URL. Default: %(default)s',
        dest='FEED_URL',
        default=DEFAULT_FEED_URL,
    )

    parser.add_argument(
        '-w',
        '--warning',
        help='Time window in minutes during which new feed entries trigger a warning. '
        'Default: %(default)s',
        dest='WARN',
        type=int,
        default=DEFAULT_WARN,
    )

    args, _ = parser.parse_known_args()
    return args


def fetch_feed_latest_item(feed_url, insecure=False, no_proxy=False, timeout=5):
    success, myfeed = lib.feedparser.parse(
        feed_url,
        insecure=insecure,
        no_proxy=no_proxy,
        timeout=timeout,
    )
    if not success:
        return (False, myfeed)
    if len(myfeed['entries']) == 0:
        return (True, [])
    return (True, myfeed['entries'][0])


def fetch_feed_todays_item(feed_url, insecure=False, no_proxy=False, timeout=5):
    """Fetch the newest feed item starting from now/today, or older."""
    success, myfeed = lib.feedparser.parse(
        feed_url,
        insecure=insecure,
        no_proxy=no_proxy,
        timeout=timeout,
    )
    if not success:
        return (False, myfeed)
    if len(myfeed['entries']) == 0:
        return (True, [])
    prev_delta = None
    idx = 0
    for i, item in enumerate(myfeed['entries']):
        delta = lib.time.now(as_type='datetime') - item.get(
            'updated_parsed', lib.time.now(as_type='datetime')
        )
        if delta.total_seconds() < 0:
            continue
        if prev_delta is None or delta < prev_delta:
            prev_delta = delta
            idx = i
    return (True, myfeed['entries'][idx])


def get_feed_state(feed_id, delta, args):
    """age of feed-item within warning period?
    yes:
        is item in cache (is it the same as from the last fetch)?
        yes:
            value from cache == already acked?
            yes:
                return STATE_OK
            is it acknowledged in icinga? (ack could have been removed in the meantime)
            yes:
                save feed-item-id to cache, value = acked, do not change expire time
                return STATE_OK
            return STATE_WARN
        no:
            save feed-item-id to cache, value = '', expire time = now + args.WARN minutes
            return STATE_WARN
    no:
        return STATE_OK (feed item is not of interest any more)
    """

    if delta < args.WARN * 60:
        if not args.ICINGA_CALLBACK:
            return STATE_WARN
        cached_item = lib.cache.get(
            f'feed-{feed_id}',
            as_dict=True,
            filename='linuxfabrik-monitoring-plugins-feed.db',
        )
        if cached_item:
            if cached_item['value'] == 'acked':
                return STATE_OK
            success, icinga = lib.icinga.get_service(
                args.ICINGA_URL,
                args.ICINGA_USERNAME,
                args.ICINGA_PASSWORD,
                servicename=args.ICINGA_SERVICE_NAME,
                attrs='state,acknowledgement',
                insecure=args.INSECURE,
                no_proxy=args.NO_PROXY,
                timeout=args.TIMEOUT,
            )
            if not success:
                return STATE_WARN
            if icinga['results'][0]['attrs']['acknowledgement']:
                lib.cache.set(
                    f'feed-{feed_id}',
                    'acked',
                    cached_item['timestamp'],
                    filename='linuxfabrik-monitoring-plugins-feed.db',
                )
                return STATE_OK
            return STATE_WARN
        else:
            lib.cache.set(
                f'feed-{feed_id}',
                'unacked',
                lib.time.now() + args.WARN * 60,
                filename='linuxfabrik-monitoring-plugins-feed.db',
            )
            return STATE_WARN
    else:
        return STATE_OK


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)

    if args.ICINGA_CALLBACK and (
        not args.ICINGA_URL
        or not args.ICINGA_PASSWORD
        or not args.ICINGA_USERNAME
        or not args.ICINGA_SERVICE_NAME
    ):
        lib.base.cu(
            '--icinga-callback requires --icinga-url, --icinga-password, --icinga-username and --icinga-service-name'
        )

    if args.LATEST:
        # get the newest/latest feed item, even if it is in the future
        feed_item = lib.base.coe(
            fetch_feed_latest_item(
                args.FEED_URL,
                args.INSECURE,
                args.NO_PROXY,
                args.TIMEOUT,
            )
        )
    else:
        # get the latest feed item starting from today
        feed_item = lib.base.coe(
            fetch_feed_todays_item(
                args.FEED_URL,
                args.INSECURE,
                args.NO_PROXY,
                args.TIMEOUT,
            )
        )

    # feed with 0 entries
    if not feed_item:
        lib.base.oao('No news are good news.', STATE_OK)

    delta = abs(
        lib.time.now(as_type='datetime')
        - feed_item.get('updated_parsed', lib.time.now(as_type='datetime'))
    )
    delta = delta.total_seconds()  # this is the age of the newest feed item, in seconds

    state = get_feed_state(feed_item['id'], delta, args)

    if args.NO_SUMMARY:
        msg = f'{feed_item.get("title", "No title").strip()} ({lib.human.seconds2human(delta)} ago)'
    else:
        msg = (
            f'{feed_item.get("title", "No title").strip()}'
            f'. {feed_item.get("summary", "No summary").strip()}'
            f' ({lib.human.seconds2human(delta)} ago)'
        )

    # over and out
    lib.base.oao(msg, state, always_ok=args.ALWAYS_OK)


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