#!/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__ = '2026083001'

DESCRIPTION = """Monitors an RSS or Atom feed for new entries and alerts when new items appear within
a configurable time window (default: 3 days). With the callback enabled, the alert is
automatically cleared once the service running this check is acknowledged on the
monitoring server. 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,
        epilog=lib.args.epilog(__file__),
        formatter_class=lib.args.HelpFormatter,
    )

    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-callback',
        help=lib.args.help('--icinga-callback'),
        dest='ICINGA_CALLBACK',
        action='store_true',
        default=DEFAULT_ICINGA_CALLBACK,  # False
    )
    parser.add_argument(
        '--icinga-password',
        help=lib.args.help('--icinga-password'),
        dest='ICINGA_PASSWORD',
    )
    parser.add_argument(
        '--icinga-service-name',
        help=lib.args.help('--icinga-service-name'),
        dest='ICINGA_SERVICE_NAME',
    )
    parser.add_argument(
        '--icinga-url',
        help=lib.args.help('--icinga-url'),
        dest='ICINGA_URL',
    )
    parser.add_argument(
        '--icinga-username',
        help=lib.args.help('--icinga-username'),
        dest='ICINGA_USERNAME',
    )
    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.',
        dest='NO_SUMMARY',
        action='store_true',
        default=DEFAULT_NO_SUMMARY,  # False
    )
    parser.add_argument(
        '--proxy',
        help=lib.args.help('--proxy'),
        dest='PROXY',
        default=None,
    )

    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, proxy=None, timeout=5
):
    success, myfeed = lib.feedparser.parse(
        feed_url,
        insecure=insecure,
        no_proxy=no_proxy,
        proxy=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, proxy=None, 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,
        proxy=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,
                proxy=args.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.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.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()
