#!/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  # pylint: disable=C0413
import sys  # pylint: disable=C0413

import lib.base  # pylint: disable=C0413
import lib.cache  # pylint: disable=C0413
import lib.feedparser  # pylint: disable=C0413
import lib.human  # pylint: disable=C0413
import lib.icinga  # pylint: disable=C0413
import lib.time  # pylint: disable=C0413
from lib.globals import (STATE_OK, STATE_UNKNOWN,  # pylint: disable=C0413
                          STATE_WARN)

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

DESCRIPTION = '''Warns on new feed items of an RSS or Atom feed. Does not warn any more
    if you acknowledge the warning in Icingaweb2, and/or if a given amount of time is over.'''

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():
    parser = argparse.ArgumentParser(description=DESCRIPTION)

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

    parser.add_argument(
        '--always-ok',
        help='Always returns 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. '
             'Take it from the `__name` service attribute, for example '
             '`icinga-server!my-service-name`.',
        dest='ICINGA_SERVICE_NAME',
    )

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

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

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

    parser.add_argument(
        '--icinga-callback',
        help='Get the service acknowledgement from Icinga. '
             'Default: %(default)s',
        dest='ICINGA_CALLBACK',
        action='store_true',
        default=DEFAULT_ICINGA_CALLBACK,   # False
    )

    parser.add_argument(
        '--insecure',
        help='This option explicitly allows to perform "insecure" SSL connections. '
             'Default: %(default)s',
        dest='INSECURE',
        action='store_true',
        default=DEFAULT_INSECURE,
    )

    parser.add_argument(
        '--latest',
        help='Return the newest/latest feed item (may be in the future).',
        dest='LATEST',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--no-proxy',
        help='Do not use a proxy. '
             'Default: %(default)s',
        dest='NO_PROXY',
        action='store_true',
        default=DEFAULT_NO_PROXY,
    )

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

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

    parser.add_argument(
        '--url',
        help='The Feed URL. '
             'Default: %(default)s',
        dest='FEED_URL',
        default=DEFAULT_FEED_URL,
    )

    parser.add_argument(
        '-w', '--warning',
        help='How long should this check return a warning on new entries? '
             'Default: %(default)s (minutes)',
        dest='WARN',
        type=int,
        default=DEFAULT_WARN,
    )

    return parser.parse_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(
            'feed-{}'.format(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(
                    'feed-{}'.format(feed_id),
                    'acked',
                    cached_item['timestamp'],
                    filename='linuxfabrik-monitoring-plugins-feed.db',
                )
                return STATE_OK
            return STATE_WARN
        else:
            lib.cache.set(
                'feed-{}'.format(feed_id),
                'unacked',
                lib.time.now() + args.WARN*60,
                filename='linuxfabrik-monitoring-plugins-feed.db',
            )
            return STATE_WARN
    else:
        return STATE_OK


def main():
    # parse the command line, exit with UNKNOWN if it fails
    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 = '{} ({} ago)'.format(
            feed_item.get('title', 'No title').strip(),
            lib.human.seconds2human(delta)
        )
    else:
        msg = '{}. {} ({} ago)'.format(
            feed_item.get('title', 'No title').strip(),
            feed_item.get('summary', 'No summary').strip(),
            lib.human.seconds2human(delta)
        )

    lib.base.oao(msg, state, always_ok=args.ALWAYS_OK)


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