#!/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.db_mysql
import lib.db_sqlite
import lib.human
from lib.globals import STATE_OK, STATE_UNKNOWN, STATE_WARN

SQLITE_DB = 'linuxfabrik-monitoring-plugins-mysql-aria.db'

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

DESCRIPTION = """Checks metrics of the Aria storage engine in MariaDB, including the page cache size
relative to the total Aria index size and the page cache hit rate. Aria is the crash-safe,
non-transactional storage engine MariaDB uses for its internal temporary tables; it is not
shipped with MySQL or Percona Server. Alerts when the page cache is smaller than the Aria
indexes and its hit rate is below 95% at the same time."""

DEFAULT_DEFAULTS_FILE = '/var/spool/icinga2/.my.cnf'
DEFAULT_DEFAULTS_GROUP = 'client'
DEFAULT_TIMEOUT = 3


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(
        '--defaults-file',
        help='MySQL/MariaDB cnf file to read user, host and password from. '
        'Example: `--defaults-file=/var/spool/icinga2/.my.cnf`. '
        'Default: %(default)s',
        dest='DEFAULTS_FILE',
        default=DEFAULT_DEFAULTS_FILE,
    )

    parser.add_argument(
        '--defaults-group',
        help=lib.args.help('--defaults-group') + ' Default: %(default)s',
        dest='DEFAULTS_GROUP',
        default=DEFAULT_DEFAULTS_GROUP,
    )

    parser.add_argument(
        '--no-perfdata',
        help=lib.args.help('--no-perfdata'),
        dest='NO_PERFDATA',
        action='store_true',
        default=False,
    )

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

    args, _ = parser.parse_known_args()
    return args


def get_vars(conn):
    # Do not implement `get_all_vars()`, just fetch the ones we need for this check.
    # Without the GLOBAL modifier, SHOW VARIABLES displays the values that are used for
    # the current connection to MariaDB.
    sql = """
        show global variables
        where variable_name like 'aria_pagecache_buffer_size';
          """
    return lib.base.coe(lib.db_mysql.select(conn, sql))


def get_status(conn):
    # Do not implement `get_all_vars()`, just fetch the ones we need for this check.
    # Without the GLOBAL modifier, SHOW STATUS displays the values that are used for
    # the current connection to MariaDB.
    sql = """
        show global status
        where variable_name like 'Aria_pagecache_reads'
            or variable_name like 'Aria_pagecache_read_requests';
          """
    return lib.base.coe(lib.db_mysql.select(conn, sql))


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

    # logic taken from mysqltuner.pl:mariadb_aria(), verified in sync with
    # MySQLTuner (have_aria gate, aria_pagecache_buffer_size vs
    # total_aria_indexes comparison and the pct_aria_keys_from_mem hit-rate
    # formula are unchanged upstream since the original port). The standalone
    # "hit rate < 95%" WARN that mysqltuner emits is intentionally not
    # raised here: an admin cannot fix a low Aria hit rate without growing
    # aria_pagecache_buffer_size, which we already cover via the buffer-vs-
    # index check above.

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

    mysql_connection = {
        'defaults_file': args.DEFAULTS_FILE,
        'defaults_group': args.DEFAULTS_GROUP,
        'timeout': args.TIMEOUT,
    }
    conn = lib.base.coe(lib.db_mysql.connect(mysql_connection))
    lib.base.coe(lib.db_mysql.check_privileges(conn, 'SELECT'))
    myvar = lib.db_mysql.lod2dict(get_vars(conn))
    engines = lib.db_mysql.get_engines(conn)

    if not engines.get('have_aria', ''):
        lib.base.oao('Aria Storage Engine not available.', STATE_OK)
    if engines['have_aria'] != 'YES':
        lib.base.oao('Aria Storage Engine is disabled.', STATE_OK)

    # Aria pagecache
    sql = """
        select ifnull(sum(index_length), 0) as total_aria_indexes
        from information_schema.tables
        where table_schema not in ('information_schema')
            and engine = 'aria';
          """
    result = lib.base.coe(lib.db_mysql.select(conn, sql))
    # IFNULL(SUM(...), 0) in the SQL above ensures a numeric result even when no Aria tables
    # are present (or the user has no SELECT privileges on them); 0 is the legitimate value
    # for "no Aria tables to size against" and downstream comparisons handle it correctly.
    total_aria_indexes = int(result[0]['total_aria_indexes'])

    mystat = lib.db_mysql.lod2dict(get_status(conn))
    lib.db_mysql.close(conn)

    # init some vars
    msg = ''
    state = STATE_OK
    perfdata = ''

    # calculations
    if (
        mystat.get('Aria_pagecache_read_requests', 0)
        and int(mystat['Aria_pagecache_read_requests']) > 0
    ):
        pct_aria_keys_from_mem = 100 - round(
            int(mystat['Aria_pagecache_reads'])
            / int(mystat['Aria_pagecache_read_requests'])
            * 100,
            1,
        )
    else:
        pct_aria_keys_from_mem = 0

    # total_aria_indexes == 0 if there are no aria tables or user has no SELECT privileges on them
    if (
        int(myvar['aria_pagecache_buffer_size']) < total_aria_indexes
        and pct_aria_keys_from_mem < 95
    ):
        aria_pagecache_buffer_size_state = STATE_WARN
        state = lib.base.get_worst(state, aria_pagecache_buffer_size_state)
        aria_state_str = lib.base.state2str(
            aria_pagecache_buffer_size_state,
            prefix=' ',
        )

        # build the message
        msg += (
            f' (adjust `aria_pagecache_buffer_size`'
            f' > {total_aria_indexes}){aria_state_str}'
        )
    else:
        aria_pagecache_buffer_size_state = STATE_OK
    aria_idx = lib.human.bytes2human(int(total_aria_indexes))
    aria_cache = lib.human.bytes2human(
        int(myvar['aria_pagecache_buffer_size']),
    )
    msg = f'Total Aria indexes: {aria_idx}, Aria pagecache size: {aria_cache}{msg}; '
    perfdata += lib.base.get_perfdata(
        'mysql_aria_pagecache_buffer_size',
        myvar['aria_pagecache_buffer_size'],
        uom='B',
        _min=0,
    )
    perfdata += lib.base.get_perfdata(
        'mysql_total_aria_indexes',
        total_aria_indexes,
        _min=0,
    )

    aria_read_requests = int(mystat.get('Aria_pagecache_read_requests', 0))
    aria_reads = int(mystat.get('Aria_pagecache_reads', 0))

    # Per-CONTRIBUTING: emit the cumulative Aria pagecache counters as per-second
    # deltas (computed in-plugin against a local SQLite cache) instead of cumulative
    # `c` counters that force Grafana to do non_negative_difference() per panel.
    rates = lib.db_sqlite.per_second_deltas(
        SQLITE_DB,
        'mysql-aria',
        {
            'aria_pagecache_read_requests': aria_read_requests,
            'aria_pagecache_reads': aria_reads,
        },
    )
    req_per_s = (
        int(rates['aria_pagecache_read_requests']) if rates is not None else None
    )
    reads_per_s = int(rates['aria_pagecache_reads']) if rates is not None else None
    if req_per_s is not None:
        perfdata += lib.base.get_perfdata(
            'mysql_aria_pagecache_read_requests_per_second',
            req_per_s,
            _min=0,
        )
        perfdata += lib.base.get_perfdata(
            'mysql_aria_pagecache_reads_per_second',
            reads_per_s,
            _min=0,
        )

    if aria_read_requests > 0:
        cached = lib.human.number2human(aria_read_requests)
        reads_human = lib.human.number2human(aria_reads)
        msg += (
            f'{pct_aria_keys_from_mem}%'
            f' Aria pagecache hit rate'
            f' ({cached} cached / {reads_human} reads)'
        )
        # Standalone WARN at pct_aria_keys_from_mem < 95% (which mysqltuner emits)
        # is intentionally not raised here: the only fix is growing
        # aria_pagecache_buffer_size, which the buffer-vs-index check above
        # already covers. When the hit rate is sub-95% but the buffer is
        # already larger than the total Aria index size, surface a short note
        # so the admin understands why we stayed OK rather than alerting.
        if (
            pct_aria_keys_from_mem < 95
            and int(myvar['aria_pagecache_buffer_size']) >= total_aria_indexes
        ):
            msg += (
                f'. Note: `aria_pagecache_buffer_size` ({aria_cache}) already'
                f' exceeds the sum of all Aria index sizes ({aria_idx}), so'
                f' growing `aria_pagecache_buffer_size` further would not help'
                f' the sub-95% hit rate.'
            )
        perfdata += lib.base.get_perfdata(
            'mysql_pct_aria_keys_from_mem',
            pct_aria_keys_from_mem,
            uom='%',
            warn=95,
            _min=0,
            _max=100,
        )

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


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