#!/usr/bin/env perl
# mysqltuner.pl - Version 2.8.45
# High Performance MySQL Tuning Script
# Copyright (C) 2015-2026 Jean-Marie Renouard - jmrenouard@gmail.com
# Copyright (C) 2006-2026 Major Hayden - major@mhtx.net

# For the latest updates, please visit http://mysqltuner.pl/
# Git repository available at https://github.com/jmrenouard/MySQLTuner-perl/
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# This project would not be possible without help from:
#   Matthew Montgomery     Paul Kehrer          Dave Burgess
#   Jonathan Hinds         Mike Jackson         Nils Breunese
#   Shawn Ashlee           Luuk Vosslamber      Ville Skytta
#   Trent Hornibrook       Jason Gill           Mark Imbriaco
#   Greg Eden              Aubin Galinotti      Giovanni Bechis
#   Bill Bradford          Ryan Novosielski     Michael Scheidell
#   Blair Christensen      Hans du Plooy        Victor Trac
#   Everett Barnes         Tom Krouper          Gary Barrueto
#   Simon Greenaway        Adam Stein           Isart Montane
#   Baptiste M.            Cole Turner          Major Hayden
#   Joe Ashcraft           Jean-Marie Renouard  Christian Loos
#   Julien Francoz         Daniel Black         Long Radix
#   Daniel Lewart          Brian Devendorf      Fabio Pedretti
#   Shai Magal             Baptiste Fontaine
# Inspired by Matthew Montgomery's tuning-primer.sh script:
# http://www.day32.com/MySQL/
#
package main;

use 5.005;
use strict;
use warnings;

use POSIX;
use File::Spec;
use File::Temp;
use Getopt::Long;
use Pod::Usage;
use Sys::Hostname;
use File::Basename;
use Cwd 'abs_path';

# Subroutine declarations
sub show_help;
sub subheaderprint;
sub execute_system_command;

#use Data::Dumper;
#$Data::Dumper::Pair = " : ";

# for which()
#use Env;

our $is_win = $^O eq 'MSWin32';

# Set up a few variables for use in the script
our $tunerversion = "2.8.45";
our ( @adjvars, @generalrec, @modeling, @sysrec, @secrec );
our ( %result, %myvar, %real_vars, %mystat, %mycalc, %myrepl, %myreplicas,
    $dummyselect );
our %exported_manifest;
our $failed_connection_attempts = 0;
our $previous_failed_attempts   = 0;

# Set defaults
# Central metadata for CLI options
# Categories: CONNECTION, PERFORMANCE, OUTPUT, CLOUD, MISC
our %CLI_METADATA = (

    # Connection and Authentication
    'host' => {
        type        => '=s',
        default     => undef,
        desc        => 'Connect to a remote host to perform tests',
        placeholder => '<host>',
        cat         => 'CONNECTION'
    },
    'socket' => {
        type        => '=s',
        default     => undef,
        desc        => 'Use a different socket for a local connection',
        placeholder => '<path>',
        cat         => 'CONNECTION'
    },
    'pipe' => {
        type    => '!',
        default => 0,
        desc    => 'Connect to a local Windows database using named pipes',
        cat     => 'CONNECTION'
    },
    'pipe_name' => {
        type        => '=s',
        default     => undef,
        desc        => 'Use a different pipe name for a local connection',
        placeholder => '<name>',
        cat         => 'CONNECTION'
    },
    'port' => {
        type        => '=i',
        default     => 3306,
        desc        => 'Port to use for connection',
        placeholder => '<port>',
        cat         => 'CONNECTION',
        validate    => qr/^\d+$/
    },
    'user|u' => {
        type        => '=s',
        default     => undef,
        desc        => 'Username to use for authentication',
        placeholder => '<user>',
        cat         => 'CONNECTION'
    },
    'pass|p|password' => {
        type        => '=s',
        default     => undef,
        desc        => 'Password to use for authentication',
        placeholder => '<pass>',
        cat         => 'CONNECTION'
    },
    'userenv' => {
        type        => '=s',
        default     => undef,
        desc        => 'Env variable name for username',
        placeholder => '<envvar>',
        cat         => 'CONNECTION'
    },
    'passenv' => {
        type        => '=s',
        default     => undef,
        desc        => 'Env variable name for password',
        placeholder => '<envvar>',
        cat         => 'CONNECTION'
    },
    'ssl-ca' => {
        type        => '=s',
        default     => undef,
        desc        => 'Path to public key (SSL CA)',
        placeholder => '<path>',
        cat         => 'CONNECTION'
    },
    'mysqladmin' => {
        type        => '=s',
        default     => undef,
        desc        => 'Path to a custom mysqladmin executable',
        placeholder => '<path>',
        cat         => 'CONNECTION'
    },
    'mysqlcmd' => {
        type        => '=s',
        default     => undef,
        desc        => 'Path to a custom mysql executable',
        placeholder => '<path>',
        cat         => 'CONNECTION'
    },
    'defaults-file' => {
        type        => '=s',
        default     => undef,
        desc        => 'Path to a custom .my.cnf',
        placeholder => '<path>',
        cat         => 'CONNECTION'
    },
    'defaults-extra-file' => {
        type        => '=s',
        default     => undef,
        desc        => 'Path to an extra custom config file',
        placeholder => '<path>',
        cat         => 'CONNECTION'
    },
    'login-path' => {
        type        => '=s',
        default     => undef,
        desc        => 'Read options from the specified login path',
        placeholder => '<path>',
        cat         => 'CONNECTION'
    },
    'protocol' => {
        type        => '=s',
        default     => undef,
        desc        => 'Force TCP connection instead of socket',
        placeholder => 'tcp',
        cat         => 'CONNECTION'
    },
    'server-log' => {
        type        => '=s',
        default     => undef,
        desc        => 'Path to explicit log file (error_log)',
        placeholder => '<path>',
        cat         => 'CONNECTION'
    },

    # Performance and Reporting
    'skipsize' => {
        type    => '!',
        default => 0,
        desc    => "Don't enumerate tables and their sizes",
        cat     => 'PERFORMANCE'
    },
    'checkversion' => {
        type    => '!',
        default => 0,
        desc    => 'Check for updates to MySQLTuner',
        cat     => 'PERFORMANCE'
    },
    'updateversion' => {
        type    => '!',
        default => 0,
        desc    => 'Update MySQLTuner if newer version is available',
        cat     => 'PERFORMANCE',
        implies => { checkversion => 1 }
    },
    'forcemem' => {
        type    => '=s',
        default => 0,
        desc    =>
'Amount of RAM installed in megabytes or with unit (e.g., 10G, 1024M, 128K, 12B)',
        placeholder => '<size>',
        cat         => 'PERFORMANCE',
        validate    => qr/^\d+(?:\.\d+)?(?:[bBkKmMgGtTpP])?$/i
    },
    'forceswap' => {
        type    => '=s',
        default => 0,
        desc    =>
'Amount of swap memory configured in megabytes or with unit (e.g., 10G, 1024M)',
        placeholder => '<size>',
        cat         => 'PERFORMANCE',
        validate    => qr/^\d+(?:\.\d+)?(?:[bBkKmMgGtTpP])?$/i
    },
    'buffers' => {
        type    => '!',
        default => 0,
        desc    => 'Print global and per-thread buffer values',
        cat     => 'PERFORMANCE'
    },
    'passwordfile' => {
        type        => '=s',
        default     => undef,
        desc        => 'Path to a password file list',
        placeholder => '<path>',
        cat         => 'PERFORMANCE'
    },
    'cvefile' => {
        type        => '=s',
        default     => undef,
        desc        => 'CVE File for vulnerability checks',
        placeholder => '<path>',
        cat         => 'PERFORMANCE'
    },
    'sysbench-file' => {
        type    => '=s',
        default => undef,
        desc    =>
          'Path to a sysbench output file for performance metadata integration',
        placeholder => '<path>',
        cat         => 'PERFORMANCE'
    },
    'compare-file' => {
        type        => '=s',
        default     => undef,
        desc        => 'Path to a previous JSON result file for trend analysis',
        placeholder => '<path>',
        cat         => 'PERFORMANCE'
    },
    'outputfile' => {
        type        => '=s',
        default     => undef,
        desc        => 'Path to a output txt file',
        placeholder => '<path>',
        cat         => 'PERFORMANCE'
    },
    'reportfile' => {
        type        => ':s',
        default     => undef,
        desc        => 'Path to a HTML report file',
        placeholder => '<path>',
        cat         => 'PERFORMANCE'
    },
    'json' => {
        type    => '!',
        default => 0,
        desc    => 'Print result as JSON string',
        cat     => 'PERFORMANCE'
    },
    'prettyjson' => {
        type    => '!',
        default => 0,
        desc    => 'Print result as JSON formatted string',
        cat     => 'PERFORMANCE'
    },
    'dumpdir' => {
        type        => '=s',
        default     => undef,
        desc        => 'Path to a directory where to dump information files',
        placeholder => '<path>',
        cat         => 'PERFORMANCE'
    },
    'schemadir' => {
        type    => '=s',
        default => undef,
        desc    =>
          'Path to a directory where to dump one markdown file per schema',
        placeholder => '<path>',
        cat         => 'PERFORMANCE'
    },
    'feature' => {
        type        => '=s',
        default     => undef,
        desc        => 'Run a specific feature',
        placeholder => '<feature>',
        cat         => 'PERFORMANCE',
        implies     => { verbose => 1 }
    },
    'skippassword' => {
        type    => '!',
        default => 0,
        desc    => "Don't perform checks on user passwords",
        cat     => 'PERFORMANCE'
    },

    # Output Options
    'silent' => {
        type    => '!',
        default => 0,
        desc    => "Don't output anything on screen",
        cat     => 'OUTPUT'
    },
    'verbose|v' => {
        type    => '!',
        default => 0,
        desc    => 'Print out all options',
        cat     => 'OUTPUT',
        implies => {
            dbstat     => 1,
            tbstat     => 1,
            idxstat    => 1,
            sysstat    => 1,
            buffers    => 1,
            pfstat     => 1,
            structstat => 1,
            myisamstat => 1,
            plugininfo => 1
        }
    },
    'color!' => {
        type    => '!',
        default => ( -t STDOUT ? 1 : 0 ),
        desc    => 'Print output in color',
        cat     => 'OUTPUT'
    },
    'nobad' => {
        type    => '!',
        default => 0,
        desc    => 'Remove negative/suggestion responses',
        cat     => 'OUTPUT'
    },
    'nogood' => {
        type    => '!',
        default => 0,
        desc    => 'Remove OK responses',
        cat     => 'OUTPUT'
    },
    'noinfo' => {
        type    => '!',
        default => 0,
        desc    => 'Remove informational responses',
        cat     => 'OUTPUT'
    },
    'debug' => {
        type    => '!',
        default => 0,
        desc    => 'Print debug information',
        cat     => 'OUTPUT'
    },
    'dbgpattern' => {
        type        => '=s',
        default     => '',
        desc        => 'Debug pattern (regex)',
        placeholder => '<regex>',
        cat         => 'OUTPUT'
    },
    'noprettyicon' => {
        type    => '!',
        default => 0,
        desc    => 'Print output with legacy tags',
        cat     => 'OUTPUT'
    },
    'experimental' => {
        type    => '!',
        default => 0,
        desc    => 'Print experimental analysis',
        cat     => 'OUTPUT'
    },
    'nondedicated' => {
        type    => '!',
        default => 0,
        desc    => 'Consider server is not dedicated to DB',
        cat     => 'OUTPUT'
    },
    'noprocess' => {
        type    => '!',
        default => 0,
        desc    => 'Consider no other process is running',
        cat     => 'OUTPUT'
    },

    # Stats / Reporting flags
    'dbstat!' => {
        type    => '!',
        default => 0,
        desc    => 'Print database information',
        cat     => 'OUTPUT'
    },
    'tbstat!' => {
        type    => '!',
        default => 0,
        desc    => 'Print table information',
        cat     => 'OUTPUT'
    },
    'colstat!' => {
        type    => '!',
        default => 0,
        desc    => 'Print column information',
        cat     => 'OUTPUT'
    },
    'idxstat!' => {
        type    => '!',
        default => 0,
        desc    => 'Print index information',
        cat     => 'OUTPUT'
    },
    'sysstat!' => {
        type    => '!',
        default => 0,
        desc    => 'Print system stats',
        cat     => 'OUTPUT'
    },
    'pfstat!' => {
        type    => '!',
        default => 0,
        desc    => 'Print Performance schema info',
        cat     => 'OUTPUT'
    },
    'plugininfo!' => {
        type    => '!',
        default => 0,
        desc    => 'Print plugin information',
        cat     => 'OUTPUT'
    },
    'myisamstat!' => {
        type    => '!',
        default => 0,
        desc    => 'Print MyISAM stats',
        cat     => 'OUTPUT'
    },
    'structstat!' => {
        type    => '!',
        default => 0,
        desc    => 'Print table structures',
        cat     => 'OUTPUT'
    },

    # Cloud and Containers
    'cloud' => {
        type    => '!',
        default => 0,
        desc    => 'Enable cloud mode',
        cat     => 'CLOUD'
    },
    'azure' => {
        type    => '!',
        default => 0,
        desc    => 'Enable Azure-specific support',
        cat     => 'CLOUD'
    },
    'ssh-host' => {
        type        => '=s',
        default     => undef,
        desc        => 'The SSH host for cloud connections',
        placeholder => '<host>',
        cat         => 'CLOUD'
    },
    'ssh-user' => {
        type        => '=s',
        default     => undef,
        desc        => 'The SSH user for cloud connections',
        placeholder => '<user>',
        cat         => 'CLOUD'
    },
    'ssh-password' => {
        type        => '=s',
        default     => undef,
        desc        => 'The SSH password for cloud connections',
        placeholder => '<pass>',
        cat         => 'CLOUD'
    },
    'ssh-identity-file' => {
        type        => '=s',
        default     => undef,
        desc        => 'The path to the SSH identity file',
        placeholder => '<path>',
        cat         => 'CLOUD'
    },
    'container' => {
        type        => '=s',
        default     => undef,
        desc        => 'Enable container mode with ID or name',
        placeholder => '<id>',
        cat         => 'CLOUD'
    },
    'server-log' => {
        type        => '=s',
        default     => undef,
        desc        => 'Path to explicit log file (error_log)',
        placeholder => '<path>',
        cat         => 'PERFORMANCE'
    },

    # Misc
    'max-password-checks' => {
        type        => '=i',
        default     => 100,
        desc        => 'Max password checks from dictionary',
        placeholder => '<n>',
        cat         => 'MISC'
    },
    'dump-limit' => {
        type        => '=i',
        default     => 50000,
        desc        => 'Limit number of rows for dumpdir CSV exports',
        placeholder => '<n>',
        cat         => 'MISC',
        validate    => qr/^\d+$/
    },
    'compress-dump' => {
        type    => '!',
        default => 0,
        desc    => 'Compress dumped CSV files using gzip',
        cat     => 'MISC'
    },
    'ignore-tables' => {
        type        => '=s',
        default     => '0',
        desc        => 'Tables to ignore (comma separated)',
        placeholder => '<t>',
        cat         => 'MISC'
    },
    'bannedports' => {
        type        => '=s',
        default     => '0',
        desc        => 'Ports banned separated by comma',
        placeholder => '<p>',
        cat         => 'MISC'
    },
    'maxportallowed' => {
        type        => '=i',
        default     => 0,
        desc        => 'Number of open ports allowable',
        placeholder => '<n>',
        cat         => 'MISC'
    },
    'defaultarch' => {
        type        => '=i',
        default     => 64,
        desc        => 'Default architecture (32 or 64)',
        placeholder => '<32|64>',
        cat         => 'MISC',
        validate    => sub { $_[0] == 32 || $_[0] == 64 }
    },
    'noask' => {
        type    => '!',
        default => 0,
        desc    => "Don't ask for confirmation",
        cat     => 'MISC'
    },
    'help|?' => {
        type    => '',
        default => 0,
        desc    => 'Show this help message',
        cat     => 'MISC'
    },
);

# Initialize %opt from metadata
our %opt = map {
    my ($primary) = split /\|/, $_;
    $primary =~ s/[!+=:].*$//;    # Strip modifiers (Getopt::Long compatibility)
    $primary => $CLI_METADATA{$_}->{default}
} keys %CLI_METADATA;

# Declare shared variables at top level
our (
    $devnull,       $basic_password_files, $outputfile,
    $fh,            $me,                   $good,
    $bad,           $info,                 $deb,
    $cmd,           $end,                  $maxlines,
    $mysqlvermajor, $mysqlverminor,        $mysqlvermicro,
    @banned_ports,  @dblist,               $physical_memory,
    $swap_memory,   $duflags,              $xargsflags,
    $architecture,  $storage_type,         $cloud_type,
    $is_cloud,      %old_result,           @raw_output_lines
);

# Gather the options from the command line
sub parse_cli_args {

    # Build GetOptions arguments dynamically
    my @getopt_args;
    Getopt::Long::Configure( "no_auto_abbrev", "no_ignore_case" );
    foreach my $opt_spec ( sort keys %CLI_METADATA ) {
        my $type      = $CLI_METADATA{$opt_spec}->{type} // '';
        my ($primary) = split /\|/, $opt_spec;
        $primary =~ s/[!+=:].*$//;    # Strip modifiers
        my $final_spec = $opt_spec;

        # Only append type if it's not already part of the specification key
        if ( $type && $opt_spec !~ /\Q$type\E$/ ) {
            $final_spec .= $type;
        }
        push @getopt_args, $final_spec => \$opt{$primary};
    }

    my @errors;
    {
        local $SIG{__WARN__} = sub { push @errors, $_[0] };
        unless ( GetOptions(@getopt_args) ) {
            foreach my $err (@errors) {
                chomp $err;
                print STDERR "ERROR:  $err\n";
            }
            print STDERR "mysqltuner failed with errors\n";
            exit 1;
        }
    }

    # Apply metadata-driven rules (Implications and Validation)
    foreach my $opt_spec ( keys %CLI_METADATA ) {
        my ($primary) = split /\|/, $opt_spec;
        my $meta      = $CLI_METADATA{$opt_spec};

        # Implications (e.g., --feature implies --verbose)
        if ( $opt{$primary} && $meta->{implies} ) {
            while ( my ( $target, $value ) = each %{ $meta->{implies} } ) {
                $opt{$target} = $value;
            }
        }

        # Validation (regex or coderef)
        if ( $opt{$primary} && $meta->{validate} ) {
            my $val      = $opt{$primary};
            my $is_valid = 1;
            if ( ref $meta->{validate} eq 'Regexp' ) {
                $is_valid = ( $val =~ $meta->{validate} );
            }
            elsif ( ref $meta->{validate} eq 'CODE' ) {
                $is_valid = $meta->{validate}->($val);
            }

            unless ($is_valid) {
                print STDERR
                  "ERROR:  Value \"$val\" invalid for option $primary\n";
                print STDERR "mysqltuner failed with errors\n";
                exit 1;
            }
        }
    }
}

sub parse_human_size_to_mb {
    my ($val) = @_;
    return 0 unless defined $val && $val ne '';
    if ( $val =~ /^(\d+(?:\.\d+)?)([bBkKmMgGtTpP])?$/i ) {
        my $num  = $1;
        my $unit = uc( $2 || 'M' );
        if    ( $unit eq 'B' ) { return $num / 1048576; }
        elsif ( $unit eq 'K' ) { return $num / 1024; }
        elsif ( $unit eq 'M' ) { return $num; }
        elsif ( $unit eq 'G' ) { return $num * 1024; }
        elsif ( $unit eq 'T' ) { return $num * 1048576; }
        elsif ( $unit eq 'P' ) { return $num * 1073741824; }
    }
    return $val;
}

sub setup_environment {
    if ( $opt{'help'} ) {
        show_help();
    }

    if ( $opt{'version'} ) {
        subheaderprint("MySQLTuner $tunerversion");
        exit(0);
    }

    $opt{'forcemem'} = parse_human_size_to_mb( $opt{'forcemem'} )
      if $opt{'forcemem'};
    $opt{'forceswap'} = parse_human_size_to_mb( $opt{'forceswap'} )
      if $opt{'forceswap'};

    $devnull = File::Spec->devnull();
    $basic_password_files =
      ( !$opt{passwordfile} )
      ? abs_path( dirname(__FILE__) ) . "/basic_passwords.txt"
      : abs_path( $opt{passwordfile} );

    # Username from envvar
    if ( $opt{userenv} && $ENV{ $opt{userenv} } ) {
        $opt{user} = $ENV{ $opt{userenv} };
    }

    # Related to password option
    if ( $opt{passenv} && $ENV{ $opt{passenv} } ) {
        $opt{pass} = $ENV{ $opt{passenv} };
    }
    $opt{pass} = $opt{password}
      if ( !$opt{pass} and $opt{password} );

    # for RPM distributions
    $basic_password_files = "/usr/share/mysqltuner/basic_passwords.txt"
      unless -f "$basic_password_files";

    $opt{dbgpattern} = '.*' if ( !$opt{dbgpattern} );

    # check if we need to enable verbose mode
    $opt{noprettyicon} = 0 if ( $opt{noprettyicon} // 0 ) != 1;
    $opt{nocolor}      = 1 if $opt{outputfile};
    $opt{noprocess}    = 0
      if ( ( $opt{noprocess} // 0 ) == 1 );    # Don't print process information
    $opt{structstat} = 0
      if ( ( $opt{nostructstat} // 0 ) == 1 )
      ;    # Don't print table struct information
    $opt{myisamstat} = 1
      if ( not defined( $opt{myisamstat} ) );
    $opt{myisamstat} = 0
      if ( ( $opt{nomyisamstat} // 0 ) == 1 )
      ;    # Don't print MyISAM table information

    # Handle cvefile if it was not passed but exists locally
    $opt{cvefile} = './vulnerabilities.csv'
      if ( !$opt{cvefile} && -f './vulnerabilities.csv' );

    $opt{'bannedports'} = '' unless $opt{'bannedports'};
    @banned_ports       = split ',', $opt{'bannedports'};

    $outputfile = undef;
    $outputfile = abs_path( $opt{outputfile} ) if $opt{outputfile};

    $fh               = undef;
    @raw_output_lines = ();
    if ($outputfile) {
        open( $fh, '>', $outputfile )
          or die("Fail opening $outputfile");
    }
    $opt{nocolor} = 1 if $outputfile;
    $opt{nocolor} = 1 unless ( -t STDOUT );

    $opt{nocolor} = 0 if ( ( $opt{color} // 0 ) == 1 );

    # Setting up the colors for the print styles
    $me = ( getpwuid($<) )[0] // $ENV{USER} // $ENV{USERNAME} // 'unknown';

    if ($is_win) { $opt{nocolor} = 1; }
    $good = ( $opt{nocolor} == 0 ) ? "[\e[0;32mOK\e[0m]"  : "[OK]";
    $bad  = ( $opt{nocolor} == 0 ) ? "[\e[0;31m!!\e[0m]"  : "[!!]";
    $info = ( $opt{nocolor} == 0 ) ? "[\e[0;34m--\e[0m]"  : "[--]";
    $deb  = ( $opt{nocolor} == 0 ) ? "[\e[0;31mDG\e[0m]"  : "[DG]";
    $cmd  = ( $opt{nocolor} == 0 ) ? "\e[1;32m[CMD]($me)" : "[CMD]($me)";
    $end  = ( $opt{nocolor} == 0 ) ? "\e[0m"              : "";

    if ( ( not $is_win ) and ( $opt{noprettyicon} == 0 ) ) {
        $good = ( $opt{nocolor} == 0 ) ? "\e[0;32m✔\e[0m " : "✔ ";
        $bad  = ( $opt{nocolor} == 0 ) ? "\e[0;31m✘\e[0m " : "✘ ";
        $info = ( $opt{nocolor} == 0 ) ? "\e[0;34mℹ\e[0m " : "ℹ ";
        $deb  = ( $opt{nocolor} == 0 ) ? "\e[0;31m⚙\e[0m " : "⚙ ";
        $cmd  = ( $opt{nocolor} == 0 ) ? "\e[1;32m⌨️($me)" : "⌨️($me)";
        $end  = ( $opt{nocolor} == 0 ) ? "\e[0m  "         : "  ";
    }

    # Maximum lines of log output to read from end
    $maxlines = 30000;

    # Super structure containing all information
    %result                           = ();
    $result{'MySQLTuner'}{'version'}  = $tunerversion;
    $result{'MySQLTuner'}{'datetime'} = scalar localtime;
    $result{'MySQLTuner'}{'options'}  = \%opt;
}

# Functions that handle the print styles
sub show_help {
    my %categories = (
        'CONNECTION'  => 'CONNECTION AND AUTHENTICATION',
        'CLOUD'       => 'CLOUD SUPPORT',
        'PERFORMANCE' => 'PERFORMANCE AND REPORTING OPTIONS',
        'OUTPUT'      => 'OUTPUT OPTIONS',
        'MISC'        => 'MISCELLANEOUS OPTIONS',
    );

    print "MySQLTuner $tunerversion - MySQL High Performance Tuning Script\n\n";
    print "Usage: ./mysqltuner.pl [options]\n\n";

    foreach my $cat (qw(CONNECTION CLOUD PERFORMANCE OUTPUT MISC)) {
        print "$categories{$cat}:\n";
        foreach my $opt_spec ( sort keys %CLI_METADATA ) {
            next unless $CLI_METADATA{$opt_spec}->{cat} eq $cat;
            my $meta      = $CLI_METADATA{$opt_spec};
            my ($primary) = split /\|/, $opt_spec;
            my $display   = "--$primary";

            # Handle negatable options (trailing !)
            my $is_negatable =
              ( $opt_spec =~ /!$/ || ( $meta->{type} // '' ) eq '!' );
            if ($is_negatable) {

                # Remove ! from display if present
                $display =~ s/!$//;
            }

            $display .= " " . $meta->{placeholder} if $meta->{placeholder};

            # Handle aliases
            my @parts = split /\|/, $opt_spec;
            shift @parts;    # remove primary

            my @display_parts;
            if ($is_negatable) {
                push @display_parts, "no-$primary";
            }
            push @display_parts, @parts;

            if (@display_parts) {

                # Format aliases: length 1 -> -x, length > 1 -> --xx
                $display .= " (" . join(
                    ", ",
                    map {
                        my $p = $_;
                        $p =~ s/!$//;
                        length($p) == 1 ? "-$p" : "--$p"
                    } @display_parts
                ) . ")";
            }

            printf "  %-32s %s", $display, $meta->{desc};

            # Special case for defaults: Don't print if undef or empty string
            if ( defined $meta->{default} && $meta->{default} ne '' ) {
                print " (default: $meta->{default})";
            }
            print "\n";
        }
        print "\n";
    }
    exit 0;
}

sub prettyprint {
    print $_[0] . "\n" unless ( $opt{'silent'} or $opt{'json'} );
    print $fh $_[0] . "\n" if defined($fh);
    my $plain_text = $_[0] // '';
    $plain_text =~ s/\e\[[0-9;]*[mK]//g;
    push @raw_output_lines, $plain_text;
}

sub goodprint {
    my $prefix = $good // '[OK]';
    prettyprint $prefix. " " . $_[0] unless ( $opt{nogood} == 1 );
}

sub infoprint {
    my $prefix = $info // '[--]';
    prettyprint $prefix. " " . $_[0] unless ( $opt{noinfo} == 1 );
}

sub badprint {
    my $prefix = $bad // '[!!]';
    prettyprint $prefix. " " . $_[0] unless ( $opt{nobad} == 1 );
}

sub debugprint {
    my $prefix = $deb // '[DG]';
    prettyprint $prefix. " " . $_[0] unless ( ( $opt{debug} // 0 ) == 0 );
}

sub redwrap {
    return ( $opt{nocolor} == 0 ) ? "\e[0;31m" . $_[0] . "\e[0m" : $_[0];
}

sub greenwrap {
    return ( $opt{nocolor} == 0 ) ? "\e[0;32m" . $_[0] . "\e[0m" : $_[0];
}

sub cmdprint {
    prettyprint $cmd. " " . $_[0] . $end;
}

sub push_recommendation {
    my ( $cat, $msg ) = @_;
    push @generalrec, $msg;
    push @sysrec,     $msg if $cat =~ /sys/i;
    push @secrec,     $msg if $cat =~ /sec/i;
    push @modeling,   $msg if $cat =~ /mod/i;
}

sub calculate_health_score {
    my $score   = 0;
    my %details = ();

    # Performance (40pts)
    # Read Efficiency (BP Hit Rate)
    if ( defined $mycalc{'pct_read_efficiency'} ) {
        if ( $mycalc{'pct_read_efficiency'} > 99 ) { $details{'perf_bp'} = 10; }
        elsif ( $mycalc{'pct_read_efficiency'} > 95 ) {
            $details{'perf_bp'} = 5;
        }
        else { $details{'perf_bp'} = 0; }
    }
    else { $details{'perf_bp'} = 5; }

    # Temp tables on disk
    if ( defined $mycalc{'pct_temp_disk'} ) {
        if    ( $mycalc{'pct_temp_disk'} < 10 ) { $details{'perf_temp'} = 10; }
        elsif ( $mycalc{'pct_temp_disk'} < 25 ) { $details{'perf_temp'} = 5; }
        else                                    { $details{'perf_temp'} = 0; }
    }
    else { $details{'perf_temp'} = 5; }

    # Thread Cache Hit Rate
    if ( defined $mycalc{'thread_cache_hit_rate'} ) {
        if ( $mycalc{'thread_cache_hit_rate'} > 90 ) {
            $details{'perf_thread'} = 10;
        }
        elsif ( $mycalc{'thread_cache_hit_rate'} > 50 ) {
            $details{'perf_thread'} = 5;
        }
        else { $details{'perf_thread'} = 0; }
    }
    else { $details{'perf_thread'} = 5; }

    # Connection Usage
    if ( defined $mycalc{'pct_connections_used'} ) {
        if ( $mycalc{'pct_connections_used'} < 80 ) {
            $details{'perf_conn'} = 10;
        }
        else { $details{'perf_conn'} = 0; }
    }
    else { $details{'perf_conn'} = 5; }

    # Security (30pts)
    $details{'sec_total'} = 30;
    $details{'sec_total'} -= scalar(@secrec) * 5;
    $details{'sec_total'} = 0 if $details{'sec_total'} < 0;

    # Resilience (30pts)
    # Replication Lag
    if (   defined $myrepl{'Seconds_Behind_Source'}
        || defined $myrepl{'Seconds_Behind_Replica'} )
    {
        my $lag = $myrepl{'Seconds_Behind_Source'}
          // $myrepl{'Seconds_Behind_Replica'};
        if    ( $lag < 1 )  { $details{'res_lag'} = 10; }
        elsif ( $lag < 60 ) { $details{'res_lag'} = 5; }
        else                { $details{'res_lag'} = 0; }
    }
    else { $details{'res_lag'} = 10; }

    # Binlog/Redo safety
    $details{'res_logs'} = 10;
    $details{'res_logs'} -= 5
      if grep { /innodb_log_file_size/i || /innodb_redo_log_capacity/i }
      @adjvars;

    # Metadata / Internal Health
    $details{'res_meta'} = 10;
    $details{'res_meta'} -= 5 if scalar(@modeling) > 5;

    $score =
      $details{'perf_bp'} +
      $details{'perf_temp'} +
      $details{'perf_thread'} +
      $details{'perf_conn'} +
      $details{'sec_total'} +
      $details{'res_lag'} +
      $details{'res_logs'} +
      $details{'res_meta'};

    $result{'HealthScore'}         = $score;
    $result{'HealthScoreDetails'}  = \%details;
    $mycalc{'WeightedHealthScore'} = $score;
}

sub display_health_score {
    my $score   = $mycalc{'WeightedHealthScore'} // 0;
    my $details = $result{'HealthScoreDetails'}  // {};

    my $perf_score =
      ( $details->{'perf_bp'}     // 5 ) +
      ( $details->{'perf_temp'}   // 5 ) +
      ( $details->{'perf_thread'} // 5 ) +
      ( $details->{'perf_conn'}   // 5 );
    my $sec_score = $details->{'sec_total'} // 30;
    my $res_score =
      ( $details->{'res_lag'}  // 10 ) +
      ( $details->{'res_logs'} // 10 ) +
      ( $details->{'res_meta'} // 10 );

    my $color =
      $score > 80 ? "\e[0;32m" : ( $score > 50 ? "\e[0;33m" : "\e[0;31m" );
    my $end_c = "\e[0m";

    if ( $opt{'nocolor'} ) {
        $color = "";
        $end_c = "";
    }

    subheaderprint "Health Score KPI";
    prettyprint "Overall Weighted Health Score: $color$score/100$end_c";
    prettyprint "    - Performance: $perf_score/40";
    prettyprint "    - Security:    $sec_score/30";
    prettyprint "    - Resilience:  $res_score/30";
    prettyprint "";

    if ( $score < 70 ) {
        badprint
"Your database health is below optimal levels. Refer to the specific findings below.";
    }
    elsif ( $score < 90 ) {
        infoprint
          "Your database health is good, but there's room for improvement.";
    }
    else {
        goodprint "Your database is in excellent health!";
    }
    prettyprint " ";
}

sub predictive_capacity_analysis {
    subheaderprint "Predictive Capacity Planning";

    # 1. Memory Headroom
    my $peak_mem      = $mycalc{'max_peak_memory'} // 0;
    my $available_mem = $physical_memory + $swap_memory;
    my $headroom      = $available_mem - $peak_mem;

    $result{'Capacity'}{'Memory'}{'Peak'}      = $peak_mem;
    $result{'Capacity'}{'Memory'}{'Available'} = $available_mem;
    $result{'Capacity'}{'Memory'}{'Headroom'}  = $headroom;

    if ( $headroom < 0 ) {
        badprint "MEMORY AT RISK: Maximum peak memory ("
          . hr_bytes($peak_mem)
          . ") exceeds available RAM+Swap ("
          . hr_bytes($available_mem) . ")";
        push @generalrec,
          "Increase RAM or reduce MySQL memory footprint to avoid OOM killer.";
    }
    else {
        goodprint "Memory headroom: "
          . hr_bytes($headroom)
          . " (Theoretical max usage is "
          . ( $mycalc{'pct_max_physical_memory'} // 0 )
          . "% of physical RAM)";
    }

    # 2. Disk Growth Forecasting
    my $total_size = $result{'Databases'}{'All databases'}{'Total Size'} // 0;
    my $uptime     = $mystat{'Uptime'}                                   // 0;

    if ( $uptime > 86400 && $total_size > 0 ) {
        my $growth_per_day = ( $total_size / ( $uptime / 86400 ) );
        infoprint "Estimated data growth: "
          . hr_bytes($growth_per_day)
          . " per day";
        $result{'Capacity'}{'Disk'}{'DailyGrowth'} = $growth_per_day;
    }
    else {
        infoprint
"Growth forecasting: Insufficient uptime (< 24h) for reliable estimation.";
    }
}

sub check_replication_advanced {
    subheaderprint "Cluster & Replication Intelligence";

    my $is_replica = (
             defined $myrepl{'Seconds_Behind_Source'}
          || defined $myrepl{'Seconds_Behind_Replica'}
          || ( defined $myrepl{'Replica_IO_Running'}
            && $myrepl{'Replica_IO_Running'} ne '' )
    );

    if ($is_replica) {
        my $lag = $myrepl{'Seconds_Behind_Source'}
          // $myrepl{'Seconds_Behind_Replica'};

        # Simple IO vs SQL thread bottleneck high-level check
        my $replica_io_running = $myrepl{'Replica_IO_Running'}
          // $myrepl{'Slave_IO_Running'} // '';
        my $replica_sql_running = $myrepl{'Replica_SQL_Running'}
          // $myrepl{'Slave_SQL_Running'} // '';

        if ( $lag > 0 ) {
            if ( $replica_io_running eq 'Yes' && $replica_sql_running eq 'Yes' )
            {
                infoprint
"Replication lag analysis: Likely SQL-bound (applier thread bottleneck).";
            }
            elsif ( $replica_io_running eq 'No' ) {
                badprint
"Replication lag analysis: IO thread is stopped. Check network or source availability.";
                push_recommendation( 'res',
"Replication IO thread is stopped. Check network or source availability."
                );
            }
        }
        else {
            goodprint "Replication is healthy and synchronized.";
        }

        # GTID Gap Analysis
        my $gtid_set = $myrepl{'Executed_Gtid_Set'} // $myvar{'gtid_executed'}
          // '';
        $gtid_set =~ s/\s+//g;
        my @uuids        = split( /,/, $gtid_set );
        my $has_gtid_gap = 0;
        foreach my $u (@uuids) {
            my @parts = split( /:/, $u );
            if ( scalar(@parts) > 2 ) {
                $has_gtid_gap = 1;
                my $uuid = shift @parts;
                badprint "GTID Gap detected for UUID $uuid: missing ranges "
                  . join( ", ", @parts );
                push_recommendation( 'res',
                        "GTID Gap detected for UUID $uuid (ranges: "
                      . join( ", ", @parts )
                      . "). Check replication consistency." );
            }
        }
        if ( $gtid_set && !$has_gtid_gap ) {
            goodprint
              "GTID execution sequence is contiguous (no gaps detected).";
        }

        # Multi-Source Channel Monitoring
        my $is_mysql8_plus =
          ( $myvar{'version'} !~ /mariadb/i && mysql_version_ge( 8, 0, 22 ) );
        my $is_mariadb105_plus =
          ( $myvar{'version'} =~ /mariadb/i && mysql_version_ge( 10, 5 ) );
        my @repl_channels;
        if ( $is_mysql8_plus || $is_mariadb105_plus ) {
            @repl_channels = select_array("SHOW REPLICA STATUS");
        }
        else {
            @repl_channels = select_array("SHOW SLAVE STATUS");
        }
        my $channel_count = scalar(@repl_channels);
        if ( $channel_count > 1 ) {
            goodprint
"Multi-source replication active with $channel_count replication channel(s).";
        }

        # Parallel Applier (MTR) Tuning
        my $workers = $myvar{'replica_parallel_workers'}
          // $myvar{'slave_parallel_workers'}
          // $myvar{'slave_parallel_threads'} // 0;
        if ( $workers > 0 ) {
            goodprint
"Parallel replication applier active with $workers worker threads.";
        }
        else {
            infoprint
"Single-threaded replication applier active (consider enabling parallel replication).";
        }

        if ( defined $myvar{'slave_parallel_mode'} ) {
            my $mode = $myvar{'slave_parallel_mode'};
            if ( $mode eq 'none' ) {
                badprint
"slave_parallel_mode is set to none (recommended: optimistic or conservative for MariaDB)";
                push_recommendation( 'res',
"Configure slave_parallel_mode = optimistic or conservative to enable parallel replication."
                );
            }
        }
    }
    else {
        infoprint "Replication status: Not running as a replica.";
    }

    # GTID Consistency (all nodes)
    my $enforce_gtid = $myvar{'enforce_gtid_consistency'} // '';
    my $gtid_mode    = $myvar{'gtid_mode'}                // '';
    my $binlog_fmt   = $myvar{'binlog_format'}            // '';

    if ( defined $myvar{'gtid_mode'} && $myvar{'gtid_mode'} eq 'ON' ) {
        goodprint "GTID consistency is enabled and active.";
    }
    else {
        if ( $gtid_mode ne 'ON' && $gtid_mode ne '' ) {
            badprint "gtid_mode is $gtid_mode (recommended: ON)";
            push_recommendation( 'res',
"Set gtid_mode = ON to enable global transaction identifier replication."
            );
        }
    }

    if ( $enforce_gtid ne 'ON' && $enforce_gtid ne '' ) {
        badprint "enforce_gtid_consistency is $enforce_gtid (recommended: ON)";
        push_recommendation( 'res',
"Set enforce_gtid_consistency = ON to prevent unsafe replication operations."
        );
    }
    if ( $binlog_fmt ne 'ROW' && $binlog_fmt ne '' ) {
        badprint "binlog_format is $binlog_fmt (recommended: ROW)";
        push_recommendation( 'res',
"Set binlog_format = ROW for robust GTID and replication consistency."
        );
    }

    # Dependency Tracking Analysis (all nodes)
    if ( defined $myvar{'binlog_transaction_dependency_tracking'} ) {
        my $tracking = $myvar{'binlog_transaction_dependency_tracking'};
        if ( $tracking eq 'COMMIT_ORDER' ) {
            badprint
"binlog_transaction_dependency_tracking is set to COMMIT_ORDER (recommended: WRITESET for parallel throughput)";
            push_recommendation( 'res',
"Set binlog_transaction_dependency_tracking = WRITESET to improve parallel replication throughput."
            );
        }
    }

    # Binary Log Compression (all nodes)
    if ( defined $myvar{'binlog_transaction_compression'} ) {
        my $compression = $myvar{'binlog_transaction_compression'};
        if ( $compression ne 'ON' ) {
            infoprint
"binlog_transaction_compression is $compression (consider setting to ON to save network/disk space)";
            push_recommendation( 'res',
"Enable binlog_transaction_compression = ON to reduce replication stream size."
            );
        }
    }

    # Binlog Cache Deep-Dive (all nodes)
    my $binlog_cache_use      = $mystat{'Binlog_cache_use'}      // 0;
    my $binlog_cache_disk_use = $mystat{'Binlog_cache_disk_use'} // 0;
    if ( $binlog_cache_use > 0 ) {
        my $ratio = $binlog_cache_disk_use /
          ( $binlog_cache_use + $binlog_cache_disk_use );
        if ( $ratio > 0.05 ) {
            badprint "Binlog cache disk use ratio is "
              . sprintf( "%.2f%%", $ratio * 100 )
              . " (too many disk spills)";
            push_recommendation( 'res',
"Increase binlog_cache_size to reduce disk spills for binary log transactions."
            );
        }
    }

    # Semi-Sync Safety Check (all nodes)
    my $semi_sync_master = $myvar{'rpl_semi_sync_master_enabled'}
      // $myvar{'rpl_semi_sync_source_enabled'} // '';
    my $semi_sync_wait = $myvar{'rpl_semi_sync_master_wait_point'}
      // $myvar{'rpl_semi_sync_source_wait_point'} // '';
    if ( $semi_sync_master eq 'ON' || $semi_sync_master eq '1' ) {
        if ( $semi_sync_wait eq 'AFTER_COMMIT' ) {
            badprint
              "Semi-sync wait point is AFTER_COMMIT (recommended: AFTER_SYNC)";
            push_recommendation( 'res',
"Set rpl_semi_sync_source_wait_point = AFTER_SYNC to avoid phantom reads on crash."
            );
        }
    }

    # InnoDB Page Integrity Audit & Durability Checksums
    if ( defined $myvar{'innodb_checksums'} ) {
        if (   $myvar{'innodb_checksums'} ne 'ON'
            && $myvar{'innodb_checksums'} ne '1' )
        {
            badprint
"innodb_checksums is disabled! Risk of undetected page corruption.";
            push_recommendation( 'res',
"Enable innodb_checksums = ON to protect against InnoDB page corruption."
            );
        }
    }
    my $is_mariadb = ( $myvar{'version'} =~ /MariaDB/i );
    if ( defined $myvar{'innodb_checksum_algorithm'} ) {
        my $algo = $myvar{'innodb_checksum_algorithm'};
        if ( $is_mariadb && mysql_version_ge( 10, 5 ) ) {
            if ( $algo ne 'full_crc32' ) {
                badprint
"InnoDB checksum algorithm is $algo (recommended: full_crc32)";
                push_recommendation( 'res',
"Set innodb_checksum_algorithm = full_crc32 for better performance and integrity."
                );
            }
        }
        elsif ( !$is_mariadb && mysql_version_ge( 8, 0 ) ) {
            if ( $algo ne 'crc32' && $algo ne 'full_crc32' ) {
                badprint
                  "InnoDB checksum algorithm is $algo (recommended: crc32)";
                push_recommendation( 'res',
                    "Set innodb_checksum_algorithm = crc32." );
            }
        }
    }

    # Redo Log Safety Check
    if ( defined $myvar{'innodb_log_checksums'} ) {
        if (   $myvar{'innodb_log_checksums'} ne 'ON'
            && $myvar{'innodb_log_checksums'} ne '1' )
        {
            badprint
"innodb_log_checksums is disabled! Risk of corruption during crash recovery.";
            push_recommendation( 'res', "Enable innodb_log_checksums = ON." );
        }
    }

    # Doublewrite Consistency
    if ( defined $myvar{'innodb_doublewrite'} ) {
        if (   $myvar{'innodb_doublewrite'} eq 'OFF'
            || $myvar{'innodb_doublewrite'} eq '0' )
        {
            badprint
"InnoDB doublewrite buffer is disabled! Ensure your storage supports atomic writes.";
            push_recommendation( 'res',
"Enable innodb_doublewrite = ON unless using storage with native atomic write guarantees."
            );
        }
    }

    # Replication Pipeline Validation (checksums)
    if ( defined $myvar{'binlog_checksum'} ) {
        if ( $myvar{'binlog_checksum'} eq 'NONE' ) {
            badprint "binlog_checksum is set to NONE (recommended: CRC32)";
            push_recommendation( 'res', "Set binlog_checksum = CRC32." );
        }
    }

    my $src_verify = $myvar{'source_verify_checksum'}
      // $myvar{'master_verify_checksum'} // '';
    my $repl_verify = $myvar{'replica_sql_verify_checksum'}
      // $myvar{'slave_sql_verify_checksum'} // '';
    if ( $src_verify eq 'OFF' || $src_verify eq '0' ) {
        badprint "source_verify_checksum is disabled (recommended: ON)";
        push_recommendation( 'res',
            "Enable source_verify_checksum = ON (or master_verify_checksum)." );
    }
    if ( $repl_verify eq 'OFF' || $repl_verify eq '0' ) {
        badprint "replica_sql_verify_checksum is disabled (recommended: ON)";
        push_recommendation( 'res',
"Enable replica_sql_verify_checksum = ON (or slave_sql_verify_checksum)."
        );
    }
}

sub check_security_2_0 {
    subheaderprint "Security Hardening 2.0";

    # TLS/SSL Cipher Suite
    if ( defined $myvar{'have_ssl'} && $myvar{'have_ssl'} eq 'YES' ) {
        infoprint "TLS/SSL is enabled. Minimum supported TLS version: "
          . ( $myvar{'tls_version'} // 'defaults' );
    }
    else {
        badprint "TLS/SSL is disabled. Connections are unencrypted.";
        push_recommendation( 'sec',
            "Enable TLS/SSL for encrypted connections." );
    }

    # TDE Check (InnoDB)
    if ( defined $myvar{'innodb_redo_log_encrypt'}
        && $myvar{'innodb_redo_log_encrypt'} eq 'ON' )
    {
        goodprint "Encryption at rest (TDE) is active for redo logs.";
    }
}

sub generate_auto_fix_snippets {
    return if $opt{'silent'} || $opt{'json'};
    subheaderprint "Guided Auto-Fix Snippets";

    if ( @adjvars > 0 ) {
        prettyprint "Ready-to-Apply SQL Snippets (SET GLOBAL):";
        foreach my $adj (@adjvars) {
            if ( $adj =~ /^(\w+)\s*=\s*(.+)$/ ) {
                my ( $var, $val ) = ( $1, $2 );

                # Basic escaping for strings
                $val = "'$val'" if $val !~ /^\d+$/ && $val !~ /^[A-Z_]+$/;
                prettyprint "    SET GLOBAL $var = $val;";
            }
        }

        prettyprint "\nReady-to-Apply Configuration Block ([mysqld]):";
        foreach my $adj (@adjvars) {
            prettyprint "    $adj";
        }
    }
    else {
        infoprint
"No auto-fix snippets generated (no variable adjustments recommended).";
    }
}

sub infoprintml {
    for my $ln (@_) { $ln =~ s/\n//g; infoprint "\t$ln"; }
}

sub infoprintcmd {
    cmdprint "@_";
    infoprintml( grep { /\S/ } `@_ 2>&1` );
}

sub subheaderprint {
    my $tln = 100;
    my $sln = 8;
    my $ln  = length("@_") + 2;

    prettyprint " ";
    prettyprint "-" x $sln . " @_ " . "-" x ( $tln - $ln - $sln );
}

sub infoprinthcmd {
    subheaderprint "$_[0]";
    infoprintcmd "$_[1]";
}

sub is_remote() {
    my $host = $opt{'host'};
    return 1 if ( $opt{'cloud'} && $opt{'ssh-host'} );
    return 0 if ( !$host );
    return 0 if ( $host eq 'localhost' );
    return 0 if ( $host eq '127.0.0.1' );
    return 1;
}

sub is_docker() {
    return 1 if -f '/.dockerenv';
    if ( -f '/proc/self/cgroup' ) {
        if ( open( my $fh, '<', '/proc/self/cgroup' ) ) {
            while ( my $line = <$fh> ) {
                if ( $line =~ /docker|kubepods|containerd|podman/ ) {
                    close $fh;
                    return 1;
                }
            }
            close $fh;
        }
    }
    return 1
      if (
        (
            defined $ENV{'container'}
            && $ENV{'container'} =~ /^(docker|podman|lxc)$/
        )
        || $opt{'container'}
      );
    return 0;
}

sub is_int {
    return 0 unless defined $_[0];
    my $str = $_[0];

    #trim whitespace both sides
    $str =~ s/^\s+|\s+$//g;

    #Alternatively, to match any float-like numeric, use:
    # m/^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/

    #flatten to string and match dash or plus and one or more digits
    if ( $str =~ /^(\-|\+)?\d+?$/ ) {
        return 1;
    }
    return 0;
}

# Calculates the number of physical cores
sub cpu_cores {
    if ( $^O eq 'linux' ) {
        if ( get_transport_prefix() eq '' ) {
            my %cpus;
            my %cores;
            if ( open( my $proc_cpuinfo, '<', '/proc/cpuinfo' ) ) {
                while (<$proc_cpuinfo>) {
                    if (/^physical id\s*:\s*(.*)/) { $cpus{$1}  = 1; }
                    if (/^core id\s*:\s*(.*)/)     { $cores{$1} = 1; }
                }
                close $proc_cpuinfo;
                my $cntCPU = ( scalar keys %cpus ) * ( scalar keys %cores );
                return $cntCPU if $cntCPU > 0;
            }
        }
        my $cntCPU =
          execute_system_command(
"awk -F: '/^core id/ && !P[\$2] { CORES++; P[\$2]=1 }; /^physical id/ && !N[\$2] { CPUs++; N[\$2]=1 };  END { print CPUs*CORES }' /proc/cpuinfo"
          );
        chomp $cntCPU;
        return ( $cntCPU == 0 ? execute_system_command("nproc") : $cntCPU ) + 0;
    }

    if ( $^O eq 'freebsd' ) {
        my $cntCPU = execute_system_command("sysctl -n kern.smp.cores");
        chomp $cntCPU;
        return $cntCPU + 0;
    }
    if ($is_win) {
        my $cntCPU =
          execute_system_command(
'wmic cpu get NumberOfCores| perl -ne "s/[^0-9]//g; print if /[0-9]+/;"'
          );
        chomp $cntCPU;
        return $cntCPU + 0;
    }
    return 0;
}

# Calculates the number of logical cores (including HT)
sub logical_cpu_cores {
    if ( $^O eq 'linux' ) {
        if ( get_transport_prefix() eq '' ) {
            my $cntCPU = 0;
            if ( open( my $proc_cpuinfo, '<', '/proc/cpuinfo' ) ) {
                while (<$proc_cpuinfo>) {
                    $cntCPU++ if /^processor\s*:\s*\d+/;
                }
                close $proc_cpuinfo;
                return $cntCPU if $cntCPU > 0;
            }
        }
        my $cntCPU = execute_system_command("grep -c ^processor /proc/cpuinfo");
        chomp $cntCPU;
        if ( $cntCPU == 0 ) {
            $cntCPU = execute_system_command("nproc");
            chomp $cntCPU;
        }
        return $cntCPU + 0;
    }

    if ( $^O eq 'freebsd' ) {
        my $cntCPU = execute_system_command("sysctl -n kern.smp.cpus");
        chomp $cntCPU;
        return $cntCPU + 0;
    }
    if ($is_win) {
        my $cntCPU =
          execute_system_command(
'wmic cpu get NumberOfLogicalProcessors| perl -ne "s/[^0-9]//g; print if /[0-9]+/;"'
          );
        chomp $cntCPU;
        return $cntCPU + 0;
    }
    return cpu_cores();
}

# Calculates the parameter passed in bytes, then rounds it to one decimal place
sub hr_bytes {
    my $num = shift;
    return "0B" if !$num || $num eq "NULL";

    if ( $num >= ( 1024**3 ) ) {    # GB
        return sprintf( "%.1f", ( $num / ( 1024**3 ) ) ) . "G";
    }
    elsif ( $num >= ( 1024**2 ) ) {    # MB
        return sprintf( "%.1f", ( $num / ( 1024**2 ) ) ) . "M";
    }
    elsif ( $num >= 1024 ) {           # KB
        return sprintf( "%.1f", ( $num / 1024 ) ) . "K";
    }
    else {
        return $num . "B";
    }
}

# Calculates the parameter passed in bytes, then rounds it to a practical power-of-2 value in GB.
sub hr_bytes_practical_rnd {
    my $num = shift;
    return "0B" if !$num || $num < 0;

    my $gbs           = $num / ( 1024**3 );    # convert to GB
    my $power_of_2_gb = 1;
    while ( $power_of_2_gb < $gbs ) {
        $power_of_2_gb *= 2;
    }

    return $power_of_2_gb . "G";
}

sub hr_raw {
    my $num = shift;
    return "0" if !$num || $num eq "NULL";
    if ( $num =~ /^(\d+)G$/ ) {
        return $1 * 1024 * 1024 * 1024;
    }
    if ( $num =~ /^(\d+)M$/ ) {
        return $1 * 1024 * 1024;
    }
    if ( $num =~ /^(\d+)K$/ ) {
        return $1 * 1024;
    }
    if ( $num =~ /^(\d+)$/ ) {
        return $1;
    }
    return $num;
}

# Calculates the parameter passed in bytes, then rounds it to the nearest integer
sub hr_bytes_rnd {
    my $num = shift;
    return "0B" if !$num || $num eq "NULL";

    if ( $num >= ( 1024**3 ) ) {    # GB
        return int( ( $num / ( 1024**3 ) ) ) . "G";
    }
    elsif ( $num >= ( 1024**2 ) ) {    # MB
        return int( ( $num / ( 1024**2 ) ) ) . "M";
    }
    elsif ( $num >= 1024 ) {           # KB
        return int( ( $num / 1024 ) ) . "K";
    }
    else {
        return $num . "B";
    }
}

# Calculates the parameter passed to the nearest power of 1000, then rounds it to the nearest integer
sub hr_num {
    my $num = shift;
    if ( $num >= ( 1000**3 ) ) {       # Billions
        return int( ( $num / ( 1000**3 ) ) ) . "B";
    }
    elsif ( $num >= ( 1000**2 ) ) {    # Millions
        return int( ( $num / ( 1000**2 ) ) ) . "M";
    }
    elsif ( $num >= 1000 ) {           # Thousands
        return int( ( $num / 1000 ) ) . "K";
    }
    else {
        return $num;
    }
}

# Calculate Percentage
sub percentage {
    my $value = shift;
    my $total = shift;
    return "0.00"   if !defined $value;
    return "100.00" if !defined $total || $total eq "NULL";

    # Reject non-numeric divisor (prevents division by zero crash)
    return "0.00" unless ( $total =~ /^[+-]?\d*\.?\d+$/ );

    # Legacy behavior: zero total returns 100.00 (idle server metrics)
    return "100.00" if $total == 0;
    $value = 0 unless ( $value =~ /^[+-]?\d*\.?\d+$/ );
    return sprintf( "%.2f", ( $value * 100 / $total ) );
}

# Get maximum value for standard MySQL integer types
sub get_type_max {
    my ( $data_type, $col_type ) = @_;
    $data_type = lc( $data_type // '' );
    $col_type  = lc( $col_type  // '' );

    my $is_unsigned = ( $col_type =~ /unsigned/ ) ? 1 : 0;

    if ( $data_type eq 'tinyint' ) {
        return $is_unsigned ? 255 : 127;
    }
    elsif ( $data_type eq 'smallint' ) {
        return $is_unsigned ? 65535 : 32767;
    }
    elsif ( $data_type eq 'mediumint' ) {
        return $is_unsigned ? 16777215 : 8388607;
    }
    elsif ( $data_type eq 'int' || $data_type eq 'integer' ) {
        return $is_unsigned ? 4294967295 : 2147483647;
    }
    elsif ( $data_type eq 'bigint' ) {
        return $is_unsigned ? "18446744073709551615" : "9223372036854775807";
    }

    # Default fallback to 64-bit unsigned maximum
    return "18446744073709551615";
}

# Calculates uptime to display in a human-readable form
sub pretty_uptime {
    my $uptime  = shift;
    my $seconds = $uptime % 60;
    my $minutes = int( ( $uptime % 3600 ) / 60 );
    my $hours   = int( ( $uptime % 86400 ) / (3600) );
    my $days    = int( $uptime / (86400) );
    my $uptimestring;
    if ( $days > 0 ) {
        $uptimestring = "${days}d ${hours}h ${minutes}m ${seconds}s";
    }
    elsif ( $hours > 0 ) {
        $uptimestring = "${hours}h ${minutes}m ${seconds}s";
    }
    elsif ( $minutes > 0 ) {
        $uptimestring = "${minutes}m ${seconds}s";
    }
    else {
        $uptimestring = "${seconds}s";
    }
    return $uptimestring;
}

# Retrieves the memory installed on this machine

sub memerror {
    badprint
"Unable to determine total memory/swap; use '--forcemem' and '--forceswap'";
    exit 1;
}

sub detect_infrastructure {
    my $prefix = get_transport_prefix();
    my %infra  = (
        'storage_type' => 'unknown',
        'architecture' => 'unknown',
    );

    # Architecture Detection
    my $arch;
    if ($is_win) {
        $arch = $ENV{'PROCESSOR_ARCHITECTURE'} // 'unknown';
    }
    elsif ( $prefix eq '' ) {
        $arch = ( POSIX::uname() )[4];
    }
    else {
        my @output = execute_system_command('uname -m');
        $arch = $output[0] // 'unknown';
        chomp($arch);
    }
    $architecture = $arch;
    $infra{'architecture'} = $arch;

    # Storage Detection (Linux only)
    if ( !$is_win ) {
        if ( $prefix eq '' ) {

            # Local Linux detection
            my @sys_blocks = glob('/sys/block/*');
            my $has_ssd    = 0;
            my $has_hdd    = 0;
            foreach my $block (@sys_blocks) {
                my $name = basename($block);
                next if $name =~ /^loop|^ram|^nbd|^zram/;
                if ( open( my $rot, '<', "$block/queue/rotational" ) ) {
                    my $is_rot = <$rot>;
                    chomp($is_rot);
                    close($rot);
                    if ( defined $is_rot ) {
                        if   ( $is_rot == 0 ) { $has_ssd = 1; }
                        else                  { $has_hdd = 1; }
                    }
                }
            }
            if ( $has_ssd && !$has_hdd ) {
                $infra{'storage_type'} = 'SSD/NVMe';
            }
            elsif ( !$has_ssd && $has_hdd ) { $infra{'storage_type'} = 'HDD'; }
            elsif ( $has_ssd && $has_hdd ) { $infra{'storage_type'} = 'Mixed'; }
        }
    }

    $storage_type                 = $infra{'storage_type'};
    $result{'OS'}{'Architecture'} = $infra{'architecture'};
    $result{'OS'}{'Storage Type'} = $infra{'storage_type'};

    debugprint "Architecture: $infra{'architecture'}";
    debugprint "Storage Type: $infra{'storage_type'}";

    return \%infra;
}

sub mysql_cloud_discovery {
    my $prefix = get_transport_prefix();
    my $cloud  = 'none';

    # 1. AWS Detection (RDS / Aurora)
    if (   ( $myvar{'version_comment'} // '' ) =~ /RDS/i
        || ( $myvar{'version_comment'} // '' ) =~ /Aurora/i
        || defined $myvar{'aurora_version'}
        || ( $myvar{'basedir'} // '' ) =~ m{/rdsdbbin/} )
    {
        if ( ( $myvar{'version_comment'} // '' ) =~ /Aurora/i
            || defined $myvar{'aurora_version'} )
        {
            $cloud = 'AWS Aurora';
        }
        else {
            $cloud = 'AWS RDS';
        }
    }

    # 2. GCP Detection (Cloud SQL)
    elsif (( $myvar{'version_comment'} // '' ) =~ /Google/i
        || defined $myvar{'cloudsql_instance_id'}
        || ( $myvar{'socket'} // '' ) =~ m{/cloudsql/} )
    {
        $cloud = 'GCP Cloud SQL';
    }

    # 3. Azure Detection (Flexible Server / Single Server)
    elsif (( $myvar{'version_comment'} // '' ) =~ /Azure/i
        || ( $myvar{'version_comment'} // '' ) =~ /MSFT/i )
    {
        if (   defined $myvar{'azure.tenant_id'}
            || defined $myvar{'azure.resource_id'} )
        {
            $cloud = 'Azure Flexible Server';
        }
        else {
            $cloud = 'Azure Managed MySQL';
        }
    }

    # 4. DigitalOcean Detection
    elsif ( ( $myvar{'version_comment'} // '' ) =~ /DigitalOcean/i ) {
        $cloud = 'DigitalOcean Managed MySQL';
    }

    $cloud_type      = $cloud;
    $is_cloud        = ( $cloud ne 'none' ) ? 1 : 0;
    $result{'Cloud'} = $cloud;

    if ($is_cloud) {
        debugprint "Cloud environment detected: $cloud";
    }

    return $cloud;
}

sub os_setup {
    detect_infrastructure();
    my $prefix = get_transport_prefix();
    my $os;
    if ($is_win) {
        $os = 'windows';
    }
    elsif ( $prefix eq '' ) {
        $os = ( POSIX::uname() )[0];
    }
    else {
        $os = execute_system_command('uname');
    }

    $duflags    = ( $os =~ /Linux/ )        ? '-b' : '';
    $xargsflags = ( $os =~ /Darwin|SunOS/ ) ? ''   : '-r';
    if ( $opt{'forcemem'} > 0 ) {
        $physical_memory = $opt{'forcemem'} * 1048576;
        infoprint "Assuming $opt{'forcemem'} MB of physical memory";
        if ( $opt{'forceswap'} > 0 ) {
            $swap_memory = $opt{'forceswap'} * 1048576;
            infoprint "Assuming $opt{'forceswap'} MB of swap space";
        }
        else {
            $swap_memory = 0;
            badprint "Assuming 0 MB of swap space (use --forceswap to specify)";
        }
    }
    else {
        if ( $os =~ /Linux|CYGWIN/ ) {
            if ( $prefix eq '' && open( my $meminfo, '<', '/proc/meminfo' ) ) {
                while (<$meminfo>) {
                    if (/^MemTotal:\s+(\d+)/i) { $physical_memory = $1 * 1024; }
                    if (/^SwapTotal:\s+(\d+)/i) { $swap_memory = $1 * 1024; }
                }
                close $meminfo;
            }

            if ( !defined $physical_memory || $physical_memory == 0 ) {
                $physical_memory =
                  execute_system_command(
                    "grep -i memtotal: /proc/meminfo | awk '{print \$2}'")
                  or memerror;
                $physical_memory *= 1024;
            }

            if ( !defined $swap_memory ) {
                $swap_memory =
                  execute_system_command(
                    "grep -i swaptotal: /proc/meminfo | awk '{print \$2}'")
                  or memerror;
                $swap_memory *= 1024;
            }
        }
        elsif ( $os =~ /Darwin/ ) {
            $physical_memory = execute_system_command('sysctl -n hw.memsize')
              or memerror;
            $swap_memory =
              execute_system_command(
                "sysctl -n vm.swapusage | awk '{print \$3}' | sed 's/\..*\$//'")
              or memerror;
        }
        elsif ( $os =~ /NetBSD|OpenBSD|FreeBSD/ ) {
            $physical_memory = execute_system_command('sysctl -n hw.physmem')
              or memerror;
            if ( $physical_memory < 0 ) {
                $physical_memory =
                     execute_system_command('sysctl -n hw.physmem64')
                  or memerror;
            }
            $swap_memory =
              execute_system_command(
                "swapctl -l | grep '^/' | awk '{ s+= \$2 } END { print s }'")
              or memerror;
        }
        elsif ( $os =~ /BSD/ ) {
            $physical_memory = execute_system_command('sysctl -n hw.realmem')
              or memerror;
            $swap_memory =
              execute_system_command(
                "swapinfo | grep '^/' | awk '{ s+= \$2 } END { print s }'");
        }
        elsif ( $os =~ /SunOS/ ) {
            $physical_memory =
              execute_system_command(
                "/usr/sbin/prtconf | grep Memory | cut -f 3 -d ' '")
              or memerror;
            chomp($physical_memory);
            $physical_memory = $physical_memory * 1024 * 1024;
        }
        elsif ( $os =~ /AIX/ ) {
            $physical_memory =
              execute_system_command(
                "lsattr -El sys0 | grep realmem | awk '{print \$2}'")
              or memerror;
            chomp($physical_memory);
            $physical_memory = $physical_memory * 1024;
            $swap_memory     = execute_system_command(
                "lsps -as | awk -F'(MB| +)' '/MB /{print \$2}'")
              or memerror;
            chomp($swap_memory);
            $swap_memory = $swap_memory * 1024 * 1024;
        }
        elsif ( $os =~ /windows/i ) {
            $physical_memory =
              execute_system_command(
'wmic ComputerSystem get TotalPhysicalMemory | perl -ne "s/[^0-9]//g; print if /[0-9]+/;'
              ) or memerror;
            $swap_memory =
              execute_system_command(
'wmic OS get FreeVirtualMemory | perl -ne "s/[^0-9]//g; print if /[0-9]+/;'
              ) or memerror;
        }
    }
    debugprint "Physical Memory: $physical_memory";
    debugprint "Swap Memory: $swap_memory";
    chomp($physical_memory);
    chomp($swap_memory);
    chomp($os);
    $result{'OS'}{'OS Type'}                   = $os;
    $result{'OS'}{'Physical Memory'}{'bytes'}  = $physical_memory;
    $result{'OS'}{'Physical Memory'}{'pretty'} = hr_bytes($physical_memory);
    $result{'OS'}{'Swap Memory'}{'bytes'}      = $swap_memory;
    $result{'OS'}{'Swap Memory'}{'pretty'}     = hr_bytes($swap_memory);
    $result{'OS'}{'Other Processes'}{'bytes'}  = get_other_process_memory();
    $result{'OS'}{'Other Processes'}{'pretty'} =
      hr_bytes( get_other_process_memory() );
}

sub get_http_cli {
    my $httpcli = which( "curl", $ENV{'PATH'} );
    chomp($httpcli);
    if ($httpcli) {
        return $httpcli;
    }

    $httpcli = which( "wget", $ENV{'PATH'} );
    chomp($httpcli);
    if ($httpcli) {
        return $httpcli;
    }
    return "";
}

# Checks for updates to MySQLTuner
sub validate_tuner_version {
    if ( $opt{'checkversion'} eq 0 ) {
        print "\n" unless ( $opt{'silent'} or $opt{'json'} );
        infoprint "Skipped version check for MySQLTuner script";
        return;
    }

    my $url =
'https://raw.githubusercontent.com/jmrenouard/MySQLTuner-perl/master/mysqltuner.pl';
    my $content;

    # Try HTTP::Tiny if available (Core since 5.13.9)
    if ( eval { require HTTP::Tiny; 1 } ) {
        debugprint "Using HTTP::Tiny for version check";
        my $http     = HTTP::Tiny->new( timeout => 3 );
        my $response = $http->get($url);
        if ( $response->{success} ) {
            $content = $response->{content};
        }
        else {
            debugprint
              "HTTP::Tiny failed: $response->{status} $response->{reason}";
        }
    }

   # Fallback to curl/wget if content is still empty or HTTP::Tiny not available
    if ( !$content ) {
        debugprint "Falling back to curl/wget for version check";
        my $httpcli = get_http_cli();
        if ($httpcli) {
            my $cmd_line =
              ( $httpcli =~ /curl$/ )
              ? "$httpcli -sL $url"
              : "$httpcli -q -O - $url";
            $content = execute_system_command($cmd_line);
        }
    }

    if ($content) {

# Robust regex for version extraction (handles my/our/local, spacing, and quotes)
        if ( $content =~
            /^\s*(?:our|my|local)\s+\$tunerversion\s*=\s*["']([\d.]+)["']\s*;/m
          )
        {
            my $update = $1;
            infoprint "VERSION: $update";
            compare_tuner_version($update);
        }
        else {
            badprint
              "Cannot determine latest tuner version from fetched content";
        }
    }
    else {
        badprint "Failed to fetch tuner version information from $url";
    }
    return;
}

# Checks for updates to MySQLTuner
sub update_tuner_version {
    if ( $opt{'updateversion'} eq 0 ) {
        badprint "Skipped version update for MySQLTuner script";
        print "\n" unless ( $opt{'silent'} or $opt{'json'} );
        return;
    }

    my $update;
    my $fullpath = "";
    my $url =
      "https://raw.githubusercontent.com/jmrenouard/MySQLTuner-perl/master/";
    my @scripts =
      ( "mysqltuner.pl", "basic_passwords.txt", "vulnerabilities.csv" );
    my $totalScripts    = scalar(@scripts);
    my $receivedScripts = 0;
    my $httpcli         = get_http_cli();

    foreach my $script (@scripts) {

        if ( $httpcli =~ /curl$/ ) {
            debugprint "$httpcli is available.";

            $fullpath = dirname(__FILE__) . "/" . $script;
            debugprint "FullPath: $fullpath";
            debugprint
"$httpcli -s --connect-timeout 3 '$url$script' 2>$devnull > $fullpath";
            $update =
              execute_system_command(
"$httpcli -s --connect-timeout 3 '$url$script' 2>$devnull > $fullpath"
              );
            chomp($update);
            debugprint "$script updated: $update";

            if ( -s $script eq 0 ) {
                badprint "Couldn't update $script";
            }
            else {
                ++$receivedScripts;
                debugprint "$script updated: $update";
            }
        }
        elsif ( $httpcli =~ /wget$/ ) {

            debugprint "$httpcli is available.";

            debugprint
"$httpcli -qe timestamping=off -t 1 -T 3 -O $script '$url$script'";
            $update =
              execute_system_command(
"$httpcli -qe timestamping=off -t 1 -T 3 -O $script '$url$script'"
              );
            chomp($update);

            if ( -s $script eq 0 ) {
                badprint "Couldn't update $script";
            }
            else {
                ++$receivedScripts;
                debugprint "$script updated: $update";
            }
        }
        else {
            debugprint "curl and wget are not available.";
            infoprint "Unable to check for the latest MySQLTuner version";
        }

    }

    if ( $receivedScripts eq $totalScripts ) {
        goodprint "Successfully updated MySQLTuner script";
    }
    else {
        badprint "Couldn't update MySQLTuner script";
    }
    infoprint "Stopping program: MySQLTuner script must be updated first.";
    exit 0;
}

sub compare_tuner_version {
    my $remoteversion = shift;
    debugprint "Remote data: $remoteversion";

    #exit 0;
    if ( $remoteversion ne $tunerversion ) {
        badprint
          "There is a new version of MySQLTuner available ($remoteversion)";
        update_tuner_version();
        return;
    }
    goodprint "You have the latest version of MySQLTuner ($tunerversion)";
    exit 0 if $opt{'updateversion'};
    return;
}

# Checks to see if a MySQL login is possible
our ( $mysqllogin, $doremote, $remotestring, $mysqlcmd, $mysqladmincmd );

sub cloud_setup {
    if ( $opt{'cloud'} || $opt{'azure'} ) {
        $opt{'cloud'} = 1;    # Ensure cloud is enabled if azure is
        infoprint "Cloud mode activated.";
        if ( $opt{'azure'} ) {
            infoprint
              "Azure-specific checks enabled (currently generic cloud checks).";
        }
        if ( $opt{'ssh-host'} ) {
            infoprint "Cloud SSH mode.";
            my @os_info = execute_system_command('uname -a');
            infoprint "Remote OS Info:";
            infoprintml @os_info;
            my @mem_info =
              execute_system_command('grep MemTotal /proc/meminfo');
            if ( scalar @mem_info > 0 && $mem_info[0] =~ /(\d+)/ ) {
                my $remote_mem_bytes = $1 * 1024;
                $opt{'forcemem'} = $remote_mem_bytes / 1048576;
                infoprint "Remote memory detected: "
                  . hr_bytes($remote_mem_bytes);
            }
            else {
                badprint
"Could not determine remote memory. Using --forcemem if provided, or default.";
                if ( $opt{'forcemem'} == 0 ) {
                    $opt{'forcemem'} = 1024;    # Default to 1GB
                }
            }
            my @swap_info =
              execute_system_command('grep SwapTotal /proc/meminfo');
            if ( scalar @swap_info > 0 && $swap_info[0] =~ /(\d+)/ ) {
                my $remote_swap_bytes = $1 * 1024;
                $opt{'forceswap'} = $remote_swap_bytes / 1048576;
                infoprint "Remote swap detected: "
                  . hr_bytes($remote_swap_bytes);
            }
            else {
                infoprint "Could not determine remote swap. Assuming 0.";
                if ( $opt{'forceswap'} == 0 ) {
                    $opt{'forceswap'} = 0;
                }
            }
        }
        else {
            infoprint "Direct DB Connection mode.";
            $opt{'nosysstat'} = 1;
            if ( $opt{'forcemem'} == 0 ) {
                badprint
                  "Direct cloud connection requires --forcemem. Assuming 1GB.";
                $opt{'forcemem'} = 1024;
            }
        }
    }
}

sub get_ssh_prefix {
    return "" if not( $opt{'cloud'} and $opt{'ssh-host'} );

    my $ssh_base_cmd = 'ssh';
    if ( $opt{'ssh-identity-file'} ) {
        $ssh_base_cmd .= " -i '" . $opt{'ssh-identity-file'} . "'";
    }
    $ssh_base_cmd .=
      " -o 'StrictHostKeyChecking=no' -o 'UserKnownHostsFile=/dev/null'";
    my $ssh_target = '';
    if ( $opt{'ssh-user'} ) {
        $ssh_target = $opt{'ssh-user'} . '@';
    }
    $ssh_target .= $opt{'ssh-host'};

    my $prefix;
    if ( $opt{'ssh-password'} ) {
        my $sshpass_path = which( "sshpass", $ENV{'PATH'} );
        if ($sshpass_path) {
            $prefix =
                "sshpass -p '"
              . $opt{'ssh-password'} . "' "
              . $ssh_base_cmd . " "
              . $ssh_target;
        }
        else {
            badprint
"sshpass is not installed. Password authentication for SSH will not work.";
            $prefix = $ssh_base_cmd . " " . $ssh_target;
        }
    }
    else {
        $prefix = $ssh_base_cmd . " " . $ssh_target;
    }
    return $prefix . " ";
}

sub get_container_prefix {
    return "" if !$opt{'container'};
    my ( $engine, $name ) =
      $opt{'container'} =~ /^(docker|podman|kubectl):(.*)/
      ? ( $1, $2 )
      : ( "docker", $opt{'container'} );
    if ( $engine eq "docker" || $engine eq "podman" ) {
        return "$engine exec $name sh -c ";
    }
    elsif ( $engine eq "kubectl" ) {
        return "kubectl exec $name -- sh -c ";
    }
    return "";
}

sub get_transport_prefix {
    my $prefix = get_ssh_prefix();
    return $prefix if $prefix;
    return get_container_prefix();
}

sub execute_system_command {
    my ($command)        = @_;
    my $ssh_prefix       = get_ssh_prefix();
    my $container_prefix = get_container_prefix();

    # Avoid double transport if the command is already prefixed
    my $full_cmd = $command;
    if ( $ssh_prefix && index( $command, $ssh_prefix ) != 0 ) {
        $full_cmd = "$ssh_prefix '$command'";
    }
    elsif ( $container_prefix
        && index( $command, $container_prefix ) != 0 )
    {
        $command =~ s/'/'\\''/g;
        $full_cmd = "$container_prefix '$command'";
    }

    debugprint "Executing system command: $full_cmd";
    my @output = `$full_cmd 2>&1`;

    # Issue #887: Filter out SSL DISABLED boolean warning from MySQL client
    @output = grep { !/boolean value 'DISABLED' wasn't recognized/ } @output;

    # Filter out MySQL 5.6+ password warning from stderr captured in output
    @output =
      grep { !/Using a password on the command line interface can be insecure/ }
      @output;

    if ( $? != 0 ) {

        # Be less verbose for commands that are expected to fail on some systems
        if ( $command !~
/(?:^|\/)(dmesg|lspci|dmidecode|ipconfig|isainfo|bootinfo|ver|wmic|lsattr|prtconf|swapctl|swapinfo|svcprop|ps|ping|ifconfig|ip|hostname|who|free|top|uptime|netstat|sysctl|mysql|mariadb|curl|wget|which)/
          )
        {
            badprint "System command failed: $command";
            infoprintml @output;
        }
    }

    # Return based on calling context
    return wantarray ? @output : join( "", @output );
}

if ($is_win) {
    eval { require Win32; } or last;
    my $osname = Win32::GetOSName();
    infoprint "* Windows OS ($osname) is not fully tested.\n";

    #exit 1;
}

sub mysql_setup {
    $mysqllogin   = '';
    $doremote     = 0;
    $remotestring = '';
    my $transport_prefix = get_transport_prefix();

    if ( $opt{mysqladmin} ) {
        $mysqladmincmd = $opt{mysqladmin};
    }
    else {
        if ($transport_prefix) {
            my $check = execute_system_command(
                "command -v mariadb-admin || command -v mysqladmin");
            $mysqladmincmd =
              ( $check =~ /mariadb-admin/ ) ? "mariadb-admin" : "mysqladmin";
        }
        else {
            $mysqladmincmd =
              (      which( "mariadb-admin", $ENV{'PATH'} )
                  || which( "mysqladmin", $ENV{'PATH'} ) );
        }
    }
    chomp($mysqladmincmd);
    if ( !$mysqladmincmd
        || ( !$transport_prefix && !-x $mysqladmincmd ) )
    {
        badprint
          "Couldn't find an executable mysqladmin/mariadb-admin command.";
        exit 1;
    }

    if ( $opt{mysqlcmd} ) {
        $mysqlcmd = $opt{mysqlcmd};
    }
    else {
        if ($transport_prefix) {
            my $check =
              execute_system_command("command -v mariadb || command -v mysql");
            $mysqlcmd = ( $check =~ /mariadb/ ) ? "mariadb" : "mysql";
        }
        else {
            $mysqlcmd =
              (      which( "mariadb", $ENV{'PATH'} )
                  || which( "mysql", $ENV{'PATH'} ) );
        }
    }
    chomp($mysqlcmd);
    if ( !$mysqlcmd || ( !$transport_prefix && !-x $mysqlcmd ) ) {
        badprint "Couldn't find an executable mysql/mariadb command.";
        exit 1;
    }

    # Gather defaults file options
    my $defaults_options = '';
    if ( $opt{'defaults-file'} ) {
        if ( -r $opt{'defaults-file'} ) {
            $defaults_options = "--defaults-file=\"$opt{'defaults-file'}\" ";
            debugprint "Using defaults-file: $opt{'defaults-file'}";
        }
        else {
            badprint
              "Defaults file $opt{'defaults-file'} not found or not readable.";
            exit 1;
        }
    }
    if ( $opt{'defaults-extra-file'} ) {
        if ( -r $opt{'defaults-extra-file'} ) {
            $defaults_options .=
              "--defaults-extra-file=\"$opt{'defaults-extra-file'}\" ";
            debugprint "Using defaults-extra-file: $opt{'defaults-extra-file'}";
        }
        else {
            badprint
"Defaults extra file $opt{'defaults-extra-file'} not found or not readable.";
            exit 1;
        }
    }
    if ( $opt{'login-path'} ) {
        $defaults_options .= "--login-path=\"$opt{'login-path'}\" ";
        debugprint "Using login-path: $opt{'login-path'}";
    }

    # MySQL Client defaults
    $mysqlcmd =~ s/\n$//g;
    my $mysqlclidefaults =
      execute_system_command("$mysqlcmd $defaults_options --print-defaults");
    debugprint "MySQL Client: $mysqlclidefaults";
    if ( $mysqlclidefaults =~ /auto-vertical-output/ ) {
        badprint
          "Avoid auto-vertical-output in configuration file(s) for MySQL like";
        exit 1;
    }

    debugprint "MySQL Client: $mysqlcmd";

    # Are we being asked to connect via a socket?
    if ( $opt{socket} ) {
        if ( $opt{port} ) {
            $remotestring = " -S $opt{socket} -P $opt{port}";
        }
        else {
            $remotestring = " -S $opt{socket}";
        }
    }

    # Are we being asked to connect via a named pipe?
    if ( $opt{pipe} ) {
        if ( $opt{pipe_name} ) {
            $remotestring = " -W -S $opt{pipe_name}";
        }
        else {
            $remotestring = " -W";
        }
    }

    if ( $opt{protocol} ) {
        $remotestring = " --protocol=$opt{protocol}";
    }

    # Are we being asked to connect to a remote server?
    if ( $opt{host} ) {
        chomp( $opt{host} );
        $opt{port} = ( !$opt{port} ) ? 3306 : $opt{port};

# If we're doing a remote connection, but forcemem wasn't specified, we need to exit
        if ( !$opt{'forcemem'} && is_remote eq 1 ) {
            badprint "The --forcemem option is required for remote connections";
            badprint
              "Assuming RAM memory is 1Gb for simplify remote connection usage";
            $opt{'forcemem'} = 1024;

            #exit 1;
        }
        if ( !$opt{'forceswap'} && is_remote eq 1 ) {
            badprint
              "The --forceswap option is required for remote connections";
            badprint
              "Assuming Swap size is 1Gb for simplify remote connection usage";
            $opt{'forceswap'} = 1024;

            #exit 1;
        }
        infoprint "Performing tests on $opt{host}:$opt{port}";
        $remotestring = " -h $opt{host} -P $opt{port}";
        $doremote     = is_remote();

    }
    else {
        $opt{host} = '127.0.0.1';
    }

    if ( $opt{'ssl-ca'} ) {
        if ( -e -r -f $opt{'ssl-ca'} ) {
            $remotestring .= " --ssl-ca=$opt{'ssl-ca'}";
            infoprint
              "Will connect using ssl public key passed on the command line";
        }
        else {
            badprint
"Attempted to use passed ssl public key, but it was not found or could not be read";
            exit 1;
        }
    }

    if ( $transport_prefix && !$opt{pass} ) {
        if (   ( $ENV{MARIADB_ROOT_PASSWORD} // '' ) ne ''
            || ( $ENV{MYSQL_ROOT_PASSWORD} // '' ) ne '' )
        {
            $opt{pass} =
              $ENV{MARIADB_ROOT_PASSWORD} || $ENV{MYSQL_ROOT_PASSWORD};
            debugprint "Detected password from container environment";
        }
    }

# Did we already get a username with or without password or login-path on the command line?
    if (   $opt{user}
        || $opt{pass}
        || $opt{container}
        || $opt{'login-path'} )
    {
        my $username     = $opt{user} ? $opt{user} : "root";
        my $escaped_pass = $opt{pass};
        if ( defined $escaped_pass ) {
            $escaped_pass =~ s/'/'\\''/g;
        }
        $mysqllogin =
            "$defaults_options "
          . ( ( $opt{user} || $opt{pass} ) ? "-u $username "      : " " )
          . ( ( $opt{pass} )               ? "-p'$escaped_pass' " : " " )
          . $remotestring;
        my $loginstatus =
          execute_system_command(
            "$mysqlcmd $mysqllogin -Nrs -e 'select \"mysqld is alive\";'");
        if ( $loginstatus =~ /mysqld is alive/ ) {
            goodprint "Logged in using credentials passed on the command line";
            return 1;
        }
        else {
            badprint
              "Attempted to use login credentials, but they were invalid.";
            debugprint "Login failure output: $loginstatus";
        }
    }

    my $svcprop = which( "svcprop", $ENV{'PATH'} );
    if ( $svcprop && substr( $svcprop, 0, 1 ) =~ "/" ) {

        # We are on solaris
        (
            my $mysql_login = execute_system_command(
"svcprop -p quickbackup/username svc:/network/mysql-quickbackup:default"
            )
        ) =~ s/\s+$//;
        (
            my $mysql_pass = execute_system_command(
"svcprop -p quickbackup/password svc:/network/mysql-quickbackup:default"
            )
        ) =~ s/\s+$//;
        if ( substr( $mysql_login, 0, 7 ) ne "svcprop" ) {

            # mysql-quickbackup is installed
            $mysqllogin = "$defaults_options -u $mysql_login -p$mysql_pass";
            my $loginstatus =
              execute_system_command("mysqladmin $mysqllogin ping");
            if ( $loginstatus =~ /mysqld is alive/ ) {
                goodprint "Logged in using credentials from mysql-quickbackup.";
                return 1;
            }
            else {
                badprint
"Attempted to use login credentials from mysql-quickbackup, but they failed.";
                exit 1;
            }
        }
    }
    elsif ( my_file_readable("/etc/psa/.psa.shadow") and $doremote == 0 ) {

        # It's a Plesk box, use the available credentials
        my $psa_pass = execute_system_command("cat /etc/psa/.psa.shadow");
        chomp($psa_pass);
        $psa_pass =~ s/'/'\\''/g;
        $mysqllogin = "$defaults_options -u admin -p'$psa_pass'";
        my $loginstatus =
          execute_system_command("$mysqladmincmd $mysqllogin ping");
        unless ( $loginstatus =~ /mysqld is alive/ ) {

            # Plesk 10+
            my $plesk_pass = execute_system_command(
                "/usr/local/psa/bin/admin --show-password");
            chomp($plesk_pass);

            # Plesk Obsidian 18.0.76.5+ no longer supports --show-password
            if (   $plesk_pass =~ /no longer supported/i
                || $plesk_pass =~ /--get-login-link/i )
            {
                badprint
"Attempted to use login credentials from Plesk and Plesk 10+, but they failed.";
                exit 1;
            }

            $plesk_pass =~ s/'/'\\''/g;
            $mysqllogin = "$defaults_options -u admin -p'$plesk_pass'";
            $loginstatus =
              execute_system_command("$mysqladmincmd $mysqllogin ping");
            unless ( $loginstatus =~ /mysqld is alive/ ) {
                badprint
"Attempted to use login credentials from Plesk and Plesk 10+, but they failed.";
                exit 1;
            }
        }
    }
    elsif ( my_file_readable("/usr/local/directadmin/conf/mysql.conf")
        and $doremote == 0 )
    {

        # It's a DirectAdmin box, use the available credentials
        my $mysqluser =
          execute_system_command(
            "cat /usr/local/directadmin/conf/mysql.conf | egrep '^user=.*'");
        my $mysqlpass =
          execute_system_command(
            "cat /usr/local/directadmin/conf/mysql.conf | egrep '^passwd=.*'");

        $mysqluser =~ s/user=//;
        $mysqluser =~ s/[\r\n]//;
        $mysqlpass =~ s/passwd=//;
        $mysqlpass =~ s/[\r\n]//;

        $mysqlpass =~ s/'/'\\''/g;
        $mysqllogin = "$defaults_options -u $mysqluser -p'$mysqlpass'";

        my $loginstatus = execute_system_command("mysqladmin $mysqllogin ping");
        unless ( $loginstatus =~ /mysqld is alive/ ) {
            badprint
"Attempted to use login credentials from DirectAdmin, but they failed.";
            exit 1;
        }
    }
    elsif ( my_file_readable("/etc/mysql/debian.cnf")
        and $doremote == 0
        and !$opt{'defaults-file'} )
    {

        # We have a Debian maintenance account, use it
        $mysqllogin = "--defaults-file=/etc/mysql/debian.cnf";
        my $loginstatus =
          execute_system_command("$mysqladmincmd $mysqllogin ping");
        if ( $loginstatus =~ /mysqld is alive/ ) {
            goodprint
              "Logged in using credentials from Debian maintenance account.";
            return 1;
        }
        else {
            badprint
"Attempted to use login credentials from Debian maintenance account, but they failed.";
            exit 1;
        }
    }
    elsif ($defaults_options) {

        # defaults-file or defaults-extra-file
        $mysqllogin = "$defaults_options $remotestring";
        my $loginstatus =
          execute_system_command("$mysqladmincmd $mysqllogin ping");
        if ( $loginstatus =~ /mysqld is alive/ ) {
            goodprint "Logged in using credentials from defaults file account.";
            return 1;
        }
    }
    else {
        # It's not Plesk or Debian, we should try a login
        debugprint "$mysqladmincmd $remotestring ping 2>&1";

        #my $loginstatus = "";
        debugprint "Using mysqlcmd: $mysqlcmd";

        #if (defined($mysqladmincmd)) {
        #  infoprint "Using mysqladmin to check login";
        #  $loginstatus=`$mysqladmincmd $remotestring ping 2>&1`;
        #} else {
        infoprint "Using mysql to check login";
        my $loginstatus =
          execute_system_command(
"$mysqlcmd $defaults_options $remotestring -Nrs -e 'select \"mysqld is alive\"' --connect-timeout=3"
          );

        #}

        if ( $loginstatus =~ /mysqld is alive/ ) {

            # Login went just fine
            $mysqllogin = "$defaults_options $remotestring";

       # Did this go well because of a .my.cnf file or is there no password set?
            my $userpath =
              $is_win
              ? ( $ENV{MARIADB_HOME} || $ENV{MYSQL_HOME} || $ENV{USERPROFILE} )
              : ( $ENV{HOME} // '' );
            if ( length($userpath) > 0 ) {
                chomp($userpath);
            }
            unless ( -e "${userpath}/.my.cnf" or -e "${userpath}/.mylogin.cnf" )
            {
                badprint
                  "SECURITY RISK: Successfully authenticated without password";
            }
            return 1;
        }
        else {
            if ( $opt{'noask'} == 1 ) {
                badprint
                  "Attempted to use login credentials, but they were invalid";
                exit 1;
            }
            my ( $name, $password );
            $name     = "";
            $password = "";

            # If --user is defined no need to ask for username
            if ( $opt{user} ) {
                $name = $opt{user};
            }
            else {
                print STDERR "Please enter your MySQL administrative login: ";
                $name = <STDIN>;
            }

            # If --pass is defined no need to ask for password
            if ( $opt{pass} ) {
                $password = $opt{pass};
            }
            else {
                print STDERR
                  "Please enter your MySQL administrative password: ";
                system("stty -echo >$devnull 2>&1");
                $password = <STDIN>;
                system("stty echo >$devnull 2>&1");
            }
            $password = "" unless defined $password;
            $name     = "" unless defined $name;
            chomp($password);
            chomp($name);
            $mysqllogin = "$defaults_options -u $name ";

            if ( length($password) > 0 ) {
                if ($is_win) {
                    $mysqllogin .= " -p\"$password\"";
                }
                else {
                    my $escaped_password = $password;
                    $escaped_password =~ s/'/'\\''/g;
                    $mysqllogin .= " -p'$escaped_password'";
                }
            }
            $mysqllogin .= $remotestring;
            my $loginstatus =
              execute_system_command("$mysqladmincmd ping $mysqllogin");
            if ( $loginstatus =~ /mysqld is alive/ ) {

                #print STDERR "";
                if ( !length($password) ) {

       # Did this go well because of a .my.cnf file or is there no password set?
                    my $userpath =
                      $is_win
                      ? ( $ENV{MARIADB_HOME}
                          || $ENV{MYSQL_HOME}
                          || $ENV{USERPROFILE} )
                      : ( $ENV{HOME} // '' );
                    chomp($userpath);
                    unless ( -e "$userpath/.my.cnf" ) {
                        print STDERR "";
                        badprint
"SECURITY RISK: Successfully authenticated without password";
                    }
                }
                return 1;
            }
            else {
                #print STDERR "";
                badprint
                  "Attempted to use login credentials, but they were invalid.";
                exit 1;
            }
            exit 1;
        }
    }
}

sub build_mysql_connection_command {
    return "$mysqlcmd $mysqllogin";
}

# MySQL Request Array
sub select_array {
    my $req      = shift;
    my $vertical = "";
    if ( $req =~ s/\\G$// ) {
        $vertical = "-E ";
    }
    debugprint "PERFORM: $req ";
    my $req_escaped = $req;
    $req_escaped =~ s/"/\\"/g;
    $req_escaped =~ s/\\*\$/\\\$/g;
    my $mcmd   = $mysqlcmd   // 'mysql';
    my $mlogin = $mysqllogin // '';
    my $dnull  = $devnull    // '/dev/null';
    my @result =
      execute_system_command(
        "$mcmd $mlogin $vertical-Bse \"$req_escaped\" 2>>$dnull");

    if ( $? != 0 ) {
        if ( $req eq '\s' ) {
            debugprint
              "Command \\s is not supported by this client version. Ignoring.";
            return ();
        }
        badprint "Failed to execute: $req";
        badprint "FAIL Execute SQL / return code: $?";
        if ( $opt{debug} ) {
            my $req_debug = $req_escaped;
            debugprint execute_system_command(
                "$mcmd $mlogin $vertical-Bse \"$req_debug\" 2>&1");
        }

        #exit $?;
        return ();
    }
    debugprint "select_array: return code : $?";
    chomp(@result);
    return @result;
}

# MySQL Request Array
sub select_array_with_headers {
    my $req = shift;
    debugprint "PERFORM: $req ";
    my $req_escaped = $req;
    $req_escaped =~ s/"/\\"/g;
    $req_escaped =~ s/\\*\$/\\\$/g;
    my @result =
      execute_system_command(
        "$mysqlcmd $mysqllogin -Bre \"$req_escaped\" 2>>$devnull");
    if ( $? != 0 ) {
        badprint "Failed to execute: $req";
        badprint "FAIL Execute SQL / return code: $?";
        if ( $opt{debug} ) {
            my $req_debug = $req_escaped;
            debugprint execute_system_command(
                "$mysqlcmd $mysqllogin -Bse \"$req_debug\" 2>&1");
        }

        #exit $?;
    }
    debugprint "select_array_with_headers: return code : $?";
    chomp(@result);
    return @result;
}

# MySQL Request Array
sub select_csv_file {
    my $tfile = shift;
    my $req   = shift;

    my $limit = $opt{'dump-limit'} // 50000;
    if ( $limit > 0 ) {
        if ( $req =~ /;\s*$/ ) {
            $req =~ s/;$/ LIMIT $limit;/;
        }
        else {
            $req .= " LIMIT $limit";
        }
    }

    debugprint "PERFORM: $req CSV into $tfile";

    my $start_time =
      eval { require Time::HiRes; Time::HiRes::time(); } || time();

    my @result    = select_array_with_headers($req);
    my $row_count = scalar(@result);
    my $data_rows = $row_count > 0 ? $row_count - 1 : 0;

    my $actual_file = $tfile;
    my $gzip_bin    = which('gzip');
    my $fh;
    my $is_compressed = 0;
    if ( $opt{'compress-dump'} && $gzip_bin ) {
        $actual_file .= '.gz';
        my $escaped_file = $actual_file;
        $escaped_file =~ s/'/'\\''/g;
        open( $fh, '|-', "$gzip_bin -c > '$escaped_file'" )
          or die "Could not open gzip pipe: $!";
        $is_compressed = 1;
    }
    else {
        open( $fh, '>', $actual_file )
          or die "Could not open file '$actual_file' $!";
    }

    for my $l (@result) {
        $l =~ s/\t/","/g;
        $l =~ s/^/"/;
        $l =~ s/$/"\n/;
        print $fh $l;
        print $l if $opt{debug};
    }
    close $fh;

    my $end_time = eval { require Time::HiRes; Time::HiRes::time(); } || time();
    my $duration = $end_time - $start_time;

    if ( $duration > 5.0 ) {
        badprint "I/O Latency Notice: Export of "
          . basename($actual_file)
          . " took "
          . sprintf( "%.2fs", $duration )
          . " (threshold: 5s). Slow disk subsystem?";
    }

    my $size = -s $actual_file;
    my $base = basename($actual_file);
    $exported_manifest{$base} = {
        rows       => $data_rows,
        size       => $size,
        duration   => sprintf( "%.3f", $duration ),
        query      => $req,
        compressed => $is_compressed
    };

    infoprint "CSV file $actual_file created";
}

sub write_manifest_files {
    my $dumpdir = shift;
    return if !$dumpdir;

    opendir( my $dh, $dumpdir ) or die "Cannot open directory $dumpdir: $!";
    while ( my $entry = readdir($dh) ) {
        next if $entry =~ /^\./;
        next if $entry eq 'manifest.json' || $entry eq 'metadata.txt';
        my $full_path = "$dumpdir/$entry";
        next unless -f $full_path;

        if ( !exists $exported_manifest{$entry} ) {
            $exported_manifest{$entry} = {
                rows       => 0,
                size       => -s $full_path,
                duration   => "0.000",
                query      => "generated file",
                compressed => ( $entry =~ /\.gz$/ ) ? 1 : 0
            };
        }
    }
    closedir($dh);

    my @json_parts;
    my $total_size  = 0;
    my $total_files = 0;
    for my $file ( sort keys %exported_manifest ) {
        my $meta = $exported_manifest{$file};
        $total_size += $meta->{size} // 0;
        $total_files++;

        my $q = $meta->{query} // '';
        $q =~ s/\\/\\\\/g;
        $q =~ s/"/\\"/g;
        $q =~ s/\n/\\n/g;
        $q =~ s/\r/\\r/g;
        $q =~ s/\t/\\t/g;

        push @json_parts,
          sprintf(
'    "%s": { "rows": %d, "size_bytes": %d, "duration_seconds": %s, "query": "%s", "compressed": %s }',
            $file,
            $meta->{rows}     // 0,
            $meta->{size}     // 0,
            $meta->{duration} // "0.000",
            $q, ( $meta->{compressed} ? "true" : "false" )
          );
    }

    my $json_content =
      "{\n  \"version\": \"" . ( $tunerversion // '2.8.45' ) . "\",\n";
    $json_content .= "  \"exported_at\": \"" . scalar( gmtime() ) . " UTC\",\n";
    $json_content .= "  \"total_files\": $total_files,\n";
    $json_content .= "  \"total_size_bytes\": $total_size,\n";
    $json_content .=
      "  \"files\": {\n" . join( ",\n", @json_parts ) . "\n  }\n}\n";

    my $manifest_file = "$dumpdir/manifest.json";
    open( my $mfh, '>', $manifest_file )
      or warn "Cannot write manifest.json: $!";
    print $mfh $json_content;
    close $mfh;
    infoprint "Manifest manifest.json created";

    my $meta_content = "MySQLTuner Offline Diagnostic Snapshot Metadata\n";
    $meta_content .= "================================================\n";
    $meta_content .= "Version: " . ( $tunerversion // '2.8.45' ) . "\n";
    $meta_content .= "Exported At: " . scalar( gmtime() ) . " UTC\n";
    $meta_content .= "Host: " . ( $myvar{'hostname'} // 'unknown' ) . "\n";
    $meta_content .=
      "MySQL Version: " . ( $myvar{'version'} // 'unknown' ) . "\n";
    $meta_content .= "Total Exported Files: $total_files\n";
    $meta_content .= "Total Snapshot Size: " . human_size($total_size) . "\n\n";
    $meta_content .= "Files Summary:\n";
    $meta_content .= sprintf( "%-50s %10s %12s %8s %s\n",
        "Filename", "Rows", "Size", "Duration", "Query / Source" );
    $meta_content .= "-" x 100 . "\n";

    for my $file ( sort keys %exported_manifest ) {
        my $meta    = $exported_manifest{$file};
        my $short_q = $meta->{query} // '';
        if ( length($short_q) > 40 ) {
            $short_q = substr( $short_q, 0, 37 ) . "...";
        }
        $meta_content .= sprintf(
            "%-50s %10d %12s %7ss %s\n",
            $file,
            $meta->{rows} // 0,
            human_size( $meta->{size} // 0 ),
            $meta->{duration} // "0.000", $short_q
        );
    }

    my $metadata_file = "$dumpdir/metadata.txt";
    open( my $mtfh, '>', $metadata_file )
      or warn "Cannot write metadata.txt: $!";
    print $mtfh $meta_content;
    close $mtfh;
    infoprint "Metadata metadata.txt created";
}

sub human_size {
    my ( $size, $n ) = ( shift, 0 );
    ++$n and $size /= 1024 until $size < 1024;
    return sprintf "%.2f %s", $size, (qw[ bytes KB MB GB TB ])[$n];
}

# MySQL Request one
sub select_one {
    my $req = shift;
    debugprint "PERFORM: $req ";
    my $mcmd   = $mysqlcmd   // 'mysql';
    my $mlogin = $mysqllogin // '';
    my $dnull  = $devnull    // '/dev/null';
    my $result =
      execute_system_command("$mcmd $mlogin -Bse \"$req\" 2>>$dnull");
    chomp $result if defined $result;
    if ( $? != 0 ) {
        badprint "Failed to execute: $req";
        badprint "FAIL Execute SQL / return code: $?";
        if ( $opt{debug} ) {
            debugprint execute_system_command(
                "$mcmd $mlogin -Bse \"$req\" 2>&1");
        }

        #exit $?;
        return "";
    }
    debugprint "select_array: return code : $?";
    chomp($result);
    return $result;
}

# MySQL Request one
sub select_one_g {
    my $pattern = shift;

    my $req      = shift;
    my $vertical = "";
    if ( $req =~ s/\\G$// ) {
        $vertical = "-E ";
    }
    else {
        $vertical = "-E ";
    }
    debugprint "PERFORM: $req ";
    my @result = execute_system_command(
        "$mysqlcmd $mysqllogin $vertical-re \"$req\" 2>>$devnull");
    if ( $? != 0 ) {
        badprint "Failed to execute: $req";
        badprint "FAIL Execute SQL / return code: $?";
        if ( $opt{debug} ) {
            debugprint execute_system_command(
                "$mysqlcmd $mysqllogin $vertical-re \"$req\" 2>&1");
        }

        #exit $?;
    }
    debugprint "select_array: return code : $?";
    chomp(@result);
    return ( grep { /$pattern/ } @result )[0];
}

sub select_str_g {
    my $pattern = shift;

    my $req = shift;
    my $str = select_one_g $pattern, $req;
    return () unless defined $str;
    my @val = split /:/, $str;
    shift @val;
    return trim(@val);
}

sub select_user_dbs {
    return select_array(
"SELECT DISTINCT TABLE_SCHEMA FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'percona', 'sys')"
    );
}

sub select_tables_db {
    my $schema = shift;
    return select_array(
"SELECT DISTINCT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA='$schema'"
    );
}

sub select_indexes_db {
    my $schema = shift;
    return select_array(
"SELECT DISTINCT INDEX_NAME FROM information_schema.STATISTICS WHERE TABLE_SCHEMA='$schema'"
    );
}

sub select_views_db {
    my $schema = shift;
    return select_array(
"SELECT DISTINCT TABLE_NAME FROM information_schema.VIEWS WHERE TABLE_SCHEMA='$schema'"
    );
}

sub select_triggers_db {
    my $schema = shift;
    return select_array(
"SELECT DISTINCT TRIGGER_NAME FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA='$schema'"
    );
}

sub select_routines_db {
    my $schema = shift;
    return select_array(
"SELECT DISTINCT ROUTINE_NAME FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA='$schema'"
    );
}

sub select_table_indexes_db {
    my $schema = shift;
    my $tbname = shift;
    return select_array(
"SELECT INDEX_NAME FROM information_schema.STATISTICS WHERE TABLE_SCHEMA='$schema' AND TABLE_NAME='$tbname'"
    );
}

sub select_table_columns_db {
    my $schema = shift;
    my $table  = shift;
    return select_array(
"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='$schema' AND TABLE_NAME='$table'"
    );
}

sub get_password_column_name {
    my @mysql_user_columns = select_table_columns_db( 'mysql', 'user' );
    my $pass_column        = '';
    my $auth_column        = '';

    if ( grep { /^authentication_string$/msx } @mysql_user_columns ) {
        $auth_column = 'authentication_string';
    }

    # Case-insensitive match for Password/password
    my @pass_matches = grep { lc($_) eq 'password' } @mysql_user_columns;
    if (@pass_matches) {
        $pass_column = $pass_matches[0];
    }

    if ( $auth_column && $pass_column ) {
        return "IF(plugin='mysql_native_password', $auth_column, $pass_column)";
    }
    elsif ($auth_column) {
        return $auth_column;
    }
    elsif ($pass_column) {
        return $pass_column;
    }

    return '';
}

sub get_tuning_info {
    my @infoconn = select_array("\\s");
    my ( $tkey, $tval );
    @infoconn =
      grep { !/Threads:/ and !/Connection id:/ and !/pager:/ and !/Using/ }
      @infoconn;
    foreach my $line (@infoconn) {
        if ( $line =~ /\s*(.*):\s*(.*)/ ) {
            debugprint "$1 => $2";
            $tkey = $1;
            $tval = $2;
            chomp($tkey);
            chomp($tval);
            $result{'MySQL Client'}{$tkey} = $tval;
        }
    }
    $result{'MySQL Client'}{'Client Path'}         = $mysqlcmd;
    $result{'MySQL Client'}{'Admin Path'}          = $mysqladmincmd;
    $result{'MySQL Client'}{'Authentication Info'} = $mysqllogin;

}

# Populates all of the variable and status hashes

sub arr2hash {
    my $href = shift;
    my $harr = shift;
    my $sep  = shift;
    my $key  = '';
    my $val  = '';

    $sep = '\s' unless defined($sep);
    foreach my $line (@$harr) {
        next if ( $line =~ m/^\*\*\*\*\*\*\*/ );
        $line =~ /([a-zA-Z0-9_\/]*)\s*$sep\s*(.*)/;
        $key         = $1;
        $val         = $2;
        $$href{$key} = $val;

        debugprint " * $key = $val" if $key =~ /$opt{dbgpattern}/i;
    }
}

sub check_privileges {
    debugprint "Checking database privileges...";
    my @grants     = select_array("SHOW GRANTS FOR CURRENT_USER()");
    my $all_grants = join( " ", @grants );

    # If the user has ALL PRIVILEGES or SUPER, we assume they have enough
    if ( $all_grants =~ /ALL PRIVILEGES/i || $all_grants =~ /SUPER/i ) {
        debugprint "Current user has high-level privileges (ALL or SUPER).";
        return;
    }

    my @required_privs =
      ( 'SELECT', 'PROCESS', 'EXECUTE', 'SHOW DATABASES', 'SHOW VIEW' );

    # Version-specific privileges
    if ( mysql_version_ge( 8, 0 ) && $myvar{'version'} !~ /mariadb/i ) {
        push( @required_privs, 'REPLICATION SLAVE', 'REPLICATION CLIENT' );
    }
    elsif ( $myvar{'version'} =~ /mariadb/i && mysql_version_ge( 10, 5 ) ) {
        push( @required_privs,
            'BINLOG MONITOR',
            'REPLICATION MASTER ADMIN',
            'SLAVE MONITOR' );

# MariaDB 11+ might use REPLICA MONITOR instead of SLAVE MONITOR, but SLAVE MONITOR is usually still there as an alias
    }
    else {
        push( @required_privs, 'REPLICATION CLIENT' );
    }

    my @missing_privs = ();
    foreach my $priv (@required_privs) {

        # Use word boundaries and case-insensitive matching
        if ( $all_grants !~ /\b$priv\b/i ) {
            push( @missing_privs, $priv );
        }
    }

    if (@missing_privs) {
        badprint "Current user is missing the following privileges: "
          . join( ", ", @missing_privs );
        badprint "Some checks may be skipped or provide incomplete results.";
        infoprint "Refer to README.md for the minimum required privileges.";
    }
    else {
        debugprint "Current user has all required privileges.";
    }
}

sub get_all_vars {

    # We need to initiate at least one query so that our data is useable
    # Issue #782: Add retry for initial connection check
    my $retries = 3;
    while ( $retries > 0 ) {
        $dummyselect = select_one "SELECT VERSION()";
        last if defined($dummyselect) && $dummyselect ne '';
        $retries--;
        if ( $retries > 0 ) {
            infoprint "Retrying connection check ($retries attempts left)...";
            sleep 1;
        }
    }

    if ( not defined($dummyselect) or $dummyselect eq "" ) {
        badprint
"Failed to connect to the database or insufficient privileges to run MySQLTuner.";
        badprint "Please check your credentials and server status.";
        exit(256);
    }
    $dummyselect =~ s/(.*?)\-.*/$1/;
    debugprint "VERSION: " . $dummyselect . "";
    $result{'MySQL Client'}{'Version'} = $dummyselect;

    my @mysqlvarlist = select_array("SHOW VARIABLES");
    push( @mysqlvarlist, select_array("SHOW GLOBAL VARIABLES") );
    arr2hash( \%myvar,     \@mysqlvarlist );
    arr2hash( \%real_vars, \@mysqlvarlist );
    $result{'Variables'} = \%myvar;

    # Check privileges after we have version and variable information
    check_privileges();

    my @mysqlstatlist = select_array("SHOW STATUS");
    push( @mysqlstatlist, select_array("SHOW GLOBAL STATUS") );
    arr2hash( \%mystat, \@mysqlstatlist );

    my %mystat_raw = %mystat;
    $result{'Status'} = \%mystat_raw;

    my ( $aborted_adj, $conn_adj ) = adjust_aborted_connects();
    $mystat{'Aborted_connects'} = $aborted_adj;
    $mystat{'Connections'}      = $conn_adj;
    unless ( defined( $myvar{'innodb_support_xa'} ) ) {
        $myvar{'innodb_support_xa'} = 'ON';
    }
    $mystat{'Uptime'} = 1
      unless defined( $mystat{'Uptime'} )
      and $mystat{'Uptime'} > 0;
    $myvar{'have_galera'} = "NO";
    if (   defined( $myvar{'wsrep_provider_options'} )
        && $myvar{'wsrep_provider_options'} ne ""
        && $myvar{'wsrep_on'} ne "OFF" )
    {
        $myvar{'have_galera'} = "YES";
        debugprint "Galera options: " . $myvar{'wsrep_provider_options'};
    }

    # Workaround for MySQL bug #59393 wrt. ignore-builtin-innodb
    if ( ( $myvar{'ignore_builtin_innodb'} || "" ) eq "ON" ) {
        $myvar{'have_innodb'} = "NO";
    }

    # Support GTID MODE FOR MARIADB
    # Issue MariaDB GTID mode #513
    $myvar{'gtid_mode'} = 'ON'
      if ( defined( $myvar{'gtid_current_pos'} )
        and $myvar{'gtid_current_pos'} ne '' );

    # Whether the server uses a thread pool to handle client connections
    # MariaDB: thread_handling = pool-of-threads
    # MySQL: thread_handling = loaded-dynamically
    $myvar{'have_threadpool'} = "NO";
    if (
        defined( $myvar{'thread_handling'} )
        and (  $myvar{'thread_handling'} eq 'pool-of-threads'
            || $myvar{'thread_handling'} eq 'loaded-dynamically' )
      )
    {
        $myvar{'have_threadpool'} = "YES";
    }

    # have_* for engines is deprecated and will be removed in MySQL 5.6;
    # check SHOW ENGINES and set corresponding old style variables.
    # Also works around MySQL bug #59393 wrt. skip-innodb
    my @mysqlenginelist = select_array "SHOW ENGINES";
    foreach my $line (@mysqlenginelist) {
        if ( $line =~ /^([a-zA-Z_]+)\s+(\S+)/ ) {
            my $engine = lc($1);

            if ( $engine eq "federated" || $engine eq "blackhole" ) {
                $engine .= "_engine";
            }
            elsif ( $engine eq "berkeleydb" ) {
                $engine = "bdb";
            }
            my $val = ( $2 eq "DEFAULT" ) ? "YES" : $2;
            $myvar{"have_$engine"} = $val;
            $result{'Storage Engines'}{$engine} = $2;
        }
    }

    #debugprint Dumper(@mysqlenginelist);

    my @mysqlreplica;

    # MySQL 8.0.22+: SHOW REPLICA STATUS (deprecated: SHOW SLAVE STATUS)
    # MariaDB 10.5.1+: SHOW REPLICA STATUS (deprecated: SHOW SLAVE STATUS)
    # Older versions: SHOW SLAVE STATUS
    my $is_mysql8_plus =
      ( $myvar{'version'} !~ /mariadb/i && mysql_version_ge( 8, 0, 22 ) );
    debugprint "MySQL 8.4+/9.x detection: "
      . ( $is_mysql8_plus ? "YES" : "NO" )
      . " (Version: $myvar{'version'})";
    my $is_mariadb105_plus =
      ( $myvar{'version'} =~ /mariadb/i && mysql_version_ge( 10, 5 ) );

    if ( $is_mysql8_plus or $is_mariadb105_plus ) {
        @mysqlreplica = select_array("SHOW REPLICA STATUS\\G");
    }
    else {
        @mysqlreplica = select_array("SHOW SLAVE STATUS\\G");
    }
    arr2hash( \%myrepl, \@mysqlreplica, ':' );

    # Terminology mapping for internal consistency
    if ( scalar( keys %myrepl ) > 0 ) {
        $myrepl{'Seconds_Behind_Replica'} = $myrepl{'Seconds_Behind_Source'}
          // $myrepl{'Seconds_Behind_Master'}
          if !defined $myrepl{'Seconds_Behind_Replica'};
        $myrepl{'Replica_IO_Running'} = $myrepl{'Replica_IO_Running'}
          // $myrepl{'Slave_IO_Running'}
          if !defined $myrepl{'Replica_IO_Running'};
        $myrepl{'Replica_SQL_Running'} = $myrepl{'Replica_SQL_Running'}
          // $myrepl{'Slave_SQL_Running'}
          if !defined $myrepl{'Replica_SQL_Running'};
    }

    $result{'Replication'}{'Status'} = \%myrepl;

    # Issue #553: Fix replica host listing commands
    # MySQL 8.0.22+: SHOW REPLICAS (deprecated: SHOW SLAVE HOSTS)
    # MariaDB 10.5.1+: SHOW REPLICA HOSTS (deprecated: SHOW SLAVE HOSTS)
    # Older versions: SHOW SLAVE HOSTS
    my @mysqlreplicas;
    if ($is_mysql8_plus) {
        @mysqlreplicas = select_array("SHOW REPLICAS");
    }
    elsif ($is_mariadb105_plus) {
        @mysqlreplicas = select_array("SHOW REPLICA HOSTS\\G");
    }
    else {
        @mysqlreplicas = select_array("SHOW SLAVE HOSTS\\G");
    }

    my @lineitems = ();
    foreach my $line (@mysqlreplicas) {
        debugprint "L: $line ";
        @lineitems = split /\s+/, $line;
        $myreplicas{ $lineitems[0] }                        = $line;
        $result{'Replication'}{'Replicas'}{ $lineitems[0] } = $lineitems[4];
    }

    # InnoDB Transaction Info
    if ( $myvar{'have_innodb'} eq "YES" ) {
        if ( mysql_version_ge(5) ) {
            $mycalc{'innodb_active_transactions'} =
              select_one("SELECT COUNT(*) FROM information_schema.INNODB_TRX");
            $mycalc{'innodb_longest_transaction_duration'} = select_one(
"SELECT IFNULL(MAX(TIMESTAMPDIFF(SECOND, trx_started, NOW())),0) FROM information_schema.INNODB_TRX"
            );
        }
    }
}

sub remove_cr {
    return map {
        my $line = $_;
        $line =~ s/\n$//g;
        $line =~ s/^\s+$//g;
        $line;
    } @_;
}

sub remove_empty {
    grep { $_ ne '' } @_;
}

sub grep_file_contents {
    my $file = shift;
    my $patt;
}

sub get_file_contents {
    my $file = shift;
    open( my $fh, "<", $file ) or die "Can't open $file for read: $!";
    my @lines = <$fh>;
    close $fh or die "Cannot close $file: $!";
    @lines = remove_cr(@lines);
    return @lines;
}

sub get_basic_passwords {
    return get_file_contents(shift);
}

sub get_state_file_path {
    my $tmpdir    = $ENV{TEMP}       || $ENV{TMP} || '/tmp';
    my $host      = $opt{host}       || 'localhost';
    my $port      = $opt{port}       || '3306';
    my $socket    = $opt{socket}     || '';
    my $ssh_host  = $opt{'ssh-host'} || '';
    my $container = $opt{container}  || '';

    $host      =~ s/[^a-zA-Z0-9_\.-]/_/g;
    $port      =~ s/[^a-zA-Z0-9_\.-]/_/g;
    $socket    =~ s/[^a-zA-Z0-9_\.-]/_/g;
    $ssh_host  =~ s/[^a-zA-Z0-9_\.-]/_/g;
    $container =~ s/[^a-zA-Z0-9_\.-]/_/g;

    my $filename = ".mysqltuner_${host}_${port}";
    $filename .= "_${socket}"              if $socket ne '';
    $filename .= "_ssh_${ssh_host}"        if $ssh_host ne '';
    $filename .= "_container_${container}" if $container ne '';

    return File::Spec->catfile( $tmpdir, $filename );
}

sub adjust_aborted_connects {
    my $state_file           = get_state_file_path();
    my $aborted_adjusted     = $mystat{'Aborted_connects'} // 0;
    my $connections_adjusted = $mystat{'Connections'}      // 0;

    return ( $aborted_adjusted, $connections_adjusted ) unless -f $state_file;
    if ( -l $state_file ) {
        debugprint
          "State file is a symlink, skipping read to prevent symlink attack";
        return ( $aborted_adjusted, $connections_adjusted );
    }

    my $uptime = $mystat{'Uptime'} // 0;

    if ( open( my $fh, '<', $state_file ) ) {
        my $line = <$fh>;
        close($fh);
        if ( defined $line ) {
            chomp($line);
            my ( $stored_uptime, $stored_attempts ) = split( ':', $line );
            $stored_uptime   //= 0;
            $stored_attempts //= 0;

            if ( $uptime >= $stored_uptime ) {
                $previous_failed_attempts = $stored_attempts;
                $aborted_adjusted     -= $stored_attempts;
                $connections_adjusted -= $stored_attempts;

                $aborted_adjusted     = 0 if $aborted_adjusted < 0;
                $connections_adjusted = 0 if $connections_adjusted < 0;
            }
        }
    }
    return ( $aborted_adjusted, $connections_adjusted );
}

sub save_aborted_connects_state {
    my $state_file = get_state_file_path();
    if ( -l $state_file ) {
        debugprint
          "State file is a symlink, skipping write to prevent symlink attack";
        return;
    }

    my $uptime = $mystat{'Uptime'} // 0;
    my $total_attempts =
      $previous_failed_attempts + $failed_connection_attempts;

    my $dir = dirname($state_file);
    my ( $tmp_fh, $tmp_filename );
    eval {
        ( $tmp_fh, $tmp_filename ) = File::Temp::tempfile(
            ".mysqltuner_tmp_XXXXXX",
            DIR    => $dir,
            UNLINK => 0
        );
    };
    if ( $@ || !$tmp_fh ) {
        debugprint "Failed to create temp file for state file: $@";
        return;
    }

    chmod( 0600, $tmp_filename );

    my $write_success = 0;
    if ( print $tmp_fh "$uptime:$total_attempts\n" ) {
        $write_success = 1;
    }
    close($tmp_fh);

    if ($write_success) {
        if ( !rename( $tmp_filename, $state_file ) ) {
            debugprint "Failed to rename temp file to state file: $!";
            unlink($tmp_filename);
        }
    }
    else {
        unlink($tmp_filename);
    }
}

sub get_log_file_real_path {
    my $file     = shift;
    my $hostname = shift;
    my $datadir  = shift;
    if ( defined $file && -f "$file" ) {
        return $file;
    }
    elsif ( -f "$hostname.log" ) {
        return "$hostname.log";
    }
    elsif ( -f "$hostname.err" ) {
        return "$hostname.err";
    }
    elsif ( -f "$datadir$hostname.err" ) {
        return "$datadir$hostname.err";
    }
    elsif ( -f "$datadir$hostname.log" ) {
        return "$datadir$hostname.log";
    }
    elsif ( -f "$datadir" . "mysql_error.log" ) {
        return "$datadir" . "mysql_error.log";
    }
    elsif ( -f "/var/log/mysql.log" ) {
        return "/var/log/mysql.log";
    }
    elsif ( -f "/var/log/mysqld.log" ) {
        return "/var/log/mysqld.log";
    }
    elsif ( -f "/var/log/mysql/$hostname.err" ) {
        return "/var/log/mysql/$hostname.err";
    }
    elsif ( -f "/var/log/mysql/$hostname.log" ) {
        return "/var/log/mysql/$hostname.log";
    }
    elsif ( -f "/var/log/mysql/" . "mysql_error.log" ) {
        return "/var/log/mysql/" . "mysql_error.log";
    }

    # Systemd journal detection (mariadb.service or mysql.service)
    if ( which( 'journalctl', $ENV{'PATH'} ) ) {
        my $units_found = execute_system_command(
"systemctl list-units --type=service --state=active | grep -Ei 'mariadb|mysql|percona' | awk '{print \$1}' | head -n 1"
        );
        chomp $units_found;
        if ( $units_found ne "" ) {
            return "systemd:$units_found";
        }
    }

    # Syslog fallback
    if ( -f "/var/log/syslog" && -r "/var/log/syslog" ) {
        return "/var/log/syslog";
    }
    elsif ( -f "/var/log/messages" && -r "/var/log/messages" ) {
        return "/var/log/messages";
    }

    return $file;
}

sub log_file_recommendations {
    my $has_pfs_error_log = 0;
    if ( $opt{'dbstat'} ) {
        my $pfs_result = select_one(
"SELECT 1 FROM information_schema.tables WHERE table_schema='performance_schema' AND table_name='error_log' LIMIT 1"
        );
        $has_pfs_error_log = 1 if $pfs_result;
    }

    if ( is_remote eq 1 && !$has_pfs_error_log ) {
        infoprint
"Skipping error log files checks on remote host (No Performance Schema error_log)";
        return;
    }
    my $fh;
    $myvar{'log_error'} = $opt{'server-log'}
      || get_log_file_real_path( $myvar{'log_error'}, $myvar{'hostname'},
        $myvar{'datadir'} );

    # Use explicit container if provided
    if ( $opt{'container'} ) {
        my $container_cmd = "docker";
        if ( $opt{'container'} =~ /^(docker|podman|kubectl):(.*)/ ) {
            $myvar{'log_error'} = $opt{'container'};
        }
        else {
            if ( which( "podman", $ENV{'PATH'} )
                && !which( "docker", $ENV{'PATH'} ) )
            {
                $container_cmd = "podman";
            }
            $myvar{'log_error'} = "$container_cmd:$opt{'container'}";
        }
        debugprint "Using explicit container: $myvar{'log_error'}";
    }

    # Try to find logs from docker/podman if file doesn't exist locally
    elsif (!-f "$myvar{'log_error'}"
        && $myvar{'log_error'} !~ /^(docker|podman|kubectl|systemd):/
        && !is_docker() )
    {
        my $container_cmd = "";
        if ( which( "docker", $ENV{'PATH'} ) ) {
            $container_cmd = "docker";
        }
        elsif ( which( "podman", $ENV{'PATH'} ) ) {
            $container_cmd = "podman";
        }

        if ( $container_cmd ne "" ) {
            my $port = $opt{'port'} || 3306;
            my $container =
              execute_system_command(
"$container_cmd ps --filter \"publish=$port\" --format \"{{.Names}}\" | grep -vEi \"traefik|haproxy|maxscale|maxsale|proxy\" | head -n 1"
              );
            chomp $container;
            if ( $container eq "" ) {
                $container =
                  execute_system_command(
"$container_cmd ps --format \"{{.Names}} {{.Image}}\" | grep -Ei \"mysql|mariadb|percona|db|database\" | grep -vEi \"traefik|haproxy|maxscale|maxsale|proxy\" | head -n 1 | awk '{print \$1}'"
                  );
                chomp $container;
            }
            if ( $container ne "" ) {
                $myvar{'log_error'} = "$container_cmd:$container";
                debugprint "Detected $container_cmd container: $container";
            }
        }
    }

    subheaderprint "Log file Recommendations";
    if ( $has_pfs_error_log && !$opt{'server-log'} ) {
        goodprint "Performance Schema error_log table detected";
        my $pfs_count =
          select_one("SELECT COUNT(*) FROM performance_schema.error_log");
        infoprint "Performance Schema error_log: $pfs_count entries detected";

        # Build mysql command for streaming output
        my $mysql_conn = build_mysql_connection_command();
        open( $fh, '-|',
"$mysql_conn -N -s -e \"SELECT DATA FROM performance_schema.error_log ORDER BY LOGGED DESC LIMIT $maxlines\""
        ) || debugprint "Failed to open PFS error_log stream";
        $myvar{'log_error'} = "performance_schema.error_log";
    }
    elsif ( "$myvar{'log_error'}" eq "stderr" ) {
        badprint
"log_error is set to $myvar{'log_error'}, but this script can't read stderr";
        return;
    }
    elsif ( $myvar{'log_error'} =~ /^(docker|podman|kubectl):(.*)/ ) {
        open( $fh, '-|', "$1 logs --tail=$maxlines '$2'" )
          // die "Can't start $1 $!";
        goodprint "Log from cloud` $myvar{'log_error'} exists";
    }
    elsif ( $myvar{'log_error'} =~ /^systemd:(.*)/ ) {
        open( $fh, '-|', "journalctl -n $maxlines -b  -u '$1'" )
          // die "Can't start journalctl $!";
        goodprint "Log journal` $myvar{'log_error'} exists";
    }
    elsif ( -f "$myvar{'log_error'}" ) {

        # Check if it's a syslog file
        if (   $myvar{'log_error'} eq "/var/log/syslog"
            || $myvar{'log_error'} eq "/var/log/messages" )
        {
            open( $fh, '-|',
"grep -Ei 'mysqld|mariadb' '$myvar{'log_error'}' | tail -n $maxlines"
            ) || debugprint "Failed to open syslog stream";
            goodprint "Log file $myvar{'log_error'} (syslog) exists";
        }
        else {
            goodprint "Log file $myvar{'log_error'} exists";
            my $size = ( stat $myvar{'log_error'} )[7];
            infoprint "Log file: "
              . $myvar{'log_error'} . " ("
              . hr_bytes_rnd($size) . ")";

            if ( $size > 0 ) {
                goodprint "Log file $myvar{'log_error'} is not empty";
                if ( $size < 32 * 1024 * 1024 ) {
                    goodprint
                      "Log file $myvar{'log_error'} is smaller than 32 MB";
                }
                else {
                    badprint
                      "Log file $myvar{'log_error'} is bigger than 32 MB";
                    push @generalrec,
                      $myvar{'log_error'}
                      . " is > 32MB, you should analyze why or implement a rotation log strategy such as logrotate!";
                }
            }
            else {
                infoprint
"Log file $myvar{'log_error'} is empty. Assuming log-rotation. Use --server-log={file} for explicit file";
                return;
            }
            if ( !open( $fh, '<', $myvar{'log_error'} ) ) {
                badprint "Log file $myvar{'log_error'} isn't readable.";
                return;
            }
        }
        goodprint "Log file $myvar{'log_error'} is readable.";

        if ( $myvar{'log_error'} !~ /^\/var\/log\/(syslog|messages)$/ ) {
            my $size = ( stat $myvar{'log_error'} )[7];
            if ( $maxlines * 80 < $size ) {
                seek( $fh, -$maxlines * 80, 2 );
                <$fh>;    # discard line fragment
            }
        }
    }
    else {
        badprint "Log file $myvar{'log_error'} doesn't exist";
        return;
    }

    my $numLi     = 0;
    my $nbWarnLog = 0;
    my $nbErrLog  = 0;
    my @lastShutdowns;
    my @lastStarts;

    while ( my $logLi = <$fh> ) {
        chomp $logLi;
        $numLi++;
        debugprint "$numLi: $logLi" if $logLi =~ /\[(warning|error)\]/i;
        $nbErrLog++                 if $logLi =~ /\[error\]/i;
        $nbWarnLog++                if $logLi =~ /\[warning\]/i;
        push @lastShutdowns, $logLi
          if $logLi =~ /Shutdown complete/ and $logLi !~ /Innodb/i;
        push @lastStarts, $logLi if $logLi =~ /ready for connections/;
    }
    close $fh;

    if ( $nbWarnLog > 0 ) {
        badprint "$myvar{'log_error'} contains $nbWarnLog warning(s).";
        push @generalrec, "Check warning line(s) in $myvar{'log_error'} file";
    }
    else {
        goodprint "$myvar{'log_error'} doesn't contain any warning.";
    }
    if ( $nbErrLog > 0 ) {
        badprint "$myvar{'log_error'} contains $nbErrLog error(s).";
        push @generalrec, "Check error line(s) in $myvar{'log_error'} file";
    }
    else {
        goodprint "$myvar{'log_error'} doesn't contain any error.";
    }

    infoprint scalar @lastStarts . " start(s) detected in $myvar{'log_error'}";
    my $nStart = 0;
    my $nEnd   = 10;
    if ( scalar @lastStarts < $nEnd ) {
        $nEnd = scalar @lastStarts;
    }
    for my $startd ( reverse @lastStarts[ -$nEnd .. -1 ] ) {
        $nStart++;
        infoprint "$nStart) $startd";
    }
    infoprint scalar @lastShutdowns
      . " shutdown(s) detected in $myvar{'log_error'}";
    $nStart = 0;
    $nEnd   = 10;
    if ( scalar @lastShutdowns < $nEnd ) {
        $nEnd = scalar @lastShutdowns;
    }
    for my $shutd ( reverse @lastShutdowns[ -$nEnd .. -1 ] ) {
        $nStart++;
        infoprint "$nStart) $shutd";
    }

    #exit 0;
}

sub cve_recommendations {
    subheaderprint "CVE Security Recommendations";
    unless ( defined( $opt{cvefile} ) && $opt{cvefile} ) {
        infoprint "Skipped: --cvefile option not specified";
        return;
    }
    unless ( -f "$opt{cvefile}" ) {
        infoprint "Skipped: CVE file not found ($opt{cvefile})";
        return;
    }

#$mysqlvermajor=10;
#$mysqlverminor=1;
#$mysqlvermicro=17;
#prettyprint "Look for related CVE for $myvar{'version'} or lower in $opt{cvefile}";
    my $cvefound = 0;
    open( my $fh, "<", $opt{cvefile} )
      or die "Can't open $opt{cvefile} for read: $!";
    while ( my $cveline = <$fh> ) {
        my @cve = split( ';', $cveline );
        debugprint
"Comparing $mysqlvermajor\.$mysqlverminor\.$mysqlvermicro with $cve[1]\.$cve[2]\.$cve[3] : "
          . ( mysql_version_le( $cve[1], $cve[2], $cve[3] ) ? '<=' : '>' );

        # Avoid not major/minor version corresponding CVEs
        next
          unless ( int( $cve[1] ) == $mysqlvermajor
            && int( $cve[2] ) == $mysqlverminor );
        if ( int( $cve[3] ) >= $mysqlvermicro ) {
            badprint "$cve[4](<= $cve[1]\.$cve[2]\.$cve[3]) : $cve[6]";
            $result{'CVE'}{'List'}{$cvefound} =
              "$cve[4](<= $cve[1]\.$cve[2]\.$cve[3]) : $cve[6]";
            $cvefound++;
        }
    }
    close $fh or die "Cannot close $opt{cvefile}: $!";
    $result{'CVE'}{'nb'} = $cvefound;

    my $cve_warning_notes = "";
    if ( $cvefound == 0 ) {
        goodprint "NO SECURITY CVE FOUND FOR YOUR VERSION";
        return;
    }
    if ( $mysqlvermajor eq 5 and $mysqlverminor eq 5 ) {
        infoprint
          "False positive CVE(s) for MySQL and MariaDB 5.5.x can be found.";
        infoprint "Check carefully each CVE for those particular versions";
    }
    badprint $cvefound . " CVE(s) found for your MySQL release.";
    push( @generalrec,
        $cvefound
          . " CVE(s) found for your MySQL release. Consider upgrading your version !"
    );
}

sub get_opened_ports {
    my @opened_ports = execute_system_command('netstat -ltn');
    if ($is_win) {
        @opened_ports = grep { /LISTEN/ } execute_system_command('netstat -n');
    }
    @opened_ports = map {
        my $v = $_;
        $v =~ s/^.*:(\d+)\s.*$/$1/;
        $v =~ s/\D//g;
        $v;
    } @opened_ports;
    @opened_ports = sort { $a <=> $b } grep { !/^$/ } @opened_ports;

    #debugprint Dumper \@opened_ports;
    $result{'Network'}{'TCP Opened'} = \@opened_ports;
    return @opened_ports;
}

sub is_open_port {
    my $port = shift;
    if ( grep { /^$port$/ } get_opened_ports ) {
        return 1;
    }
    return 0;
}

sub get_process_memory {
    return 0 if $is_win;    #Windows cmd cannot provide this
    my $pid = shift;

    # Linux /proc fallback
    if ( $^O eq 'linux' && -f "/proc/$pid/statm" ) {
        if ( open( my $fh, '<', "/proc/$pid/statm" ) ) {
            my $line = <$fh>;
            close($fh);
            if ( $line =~ /^\d+\s+(\d+)/ ) {
                my $rss_pages = $1;

       # Get page size (default to 4096 if uncertain, but usually 4096 on Linux)
                my $pagesize = POSIX::sysconf(POSIX::_SC_PAGESIZE) || 4096;
                debugprint "Memory for PID $pid from /proc: "
                  . ( $rss_pages * $pagesize );
                return $rss_pages * $pagesize;
            }
        }
    }

    my @mem = execute_system_command("ps -p $pid -o rss");
    return 0 if scalar @mem != 2;
    return $mem[1] * 1024;
}

my $cached_other_process_memory;

sub get_other_process_memory {
    return 0 if ( $opt{tbstat} == 0 );
    return 0 if $is_win;                 #Windows cmd cannot provide this
    return $cached_other_process_memory if defined $cached_other_process_memory;

    my @procs         = execute_system_command('ps eaxo pid,rss,pcpu,command');
    my $totalMemOther = 0;
    my @csv_rows      = ("PID,Command,Memory_Bytes,CPU_Pct");

    foreach my $line (@procs) {
        $line =~ s/^\s+//;
        next if $line =~ /^PID/i;
        my ( $pid, $rss, $pcpu, $command ) = split( /\s+/, $line, 4 );
        next unless defined $pid && $pid =~ /^\d+$/;
        $rss     //= 0;
        $pcpu    //= 0.0;
        $command //= '';
        chomp($command);

        # Filter out mysqld, systemd, and kernel threads
        next if $command =~ /mysqld/i;
        next if $command =~ /systemd/i;
        next if $command =~ /\[.*\]/;     # Kernel threads

        my $bytes = $rss * 1024;
        $totalMemOther += $bytes;

        push @csv_rows, "$pid,$command,$bytes,$pcpu";
    }

    if ( $opt{dumpdir} && @csv_rows > 1 ) {
        dump_into_file( "non_mysqld_processes.csv", join( "\n", @csv_rows ) );
    }

    $cached_other_process_memory = $totalMemOther;
    return $totalMemOther;
}

sub get_os_release {
    if ( -f "/etc/lsb-release" ) {
        my @info_release = get_file_contents "/etc/lsb-release";
        my $os_release   = $info_release[3];
        $os_release =~ s/.*="//;
        $os_release =~ s/"$//;
        return $os_release;
    }

    if ( -f "/etc/system-release" ) {
        my @info_release = get_file_contents "/etc/system-release";
        return $info_release[0];
    }

    if ( -f "/etc/os-release" ) {
        my @info_release = get_file_contents "/etc/os-release";
        my $os_release   = $info_release[0];
        $os_release =~ s/.*="//;
        $os_release =~ s/"$//;
        return $os_release;
    }

    if ( -f "/etc/issue" ) {
        my @info_release = get_file_contents "/etc/issue";
        my $os_release   = $info_release[0];
        $os_release =~ s/\s+\\n.*//;
        return $os_release;
    }
    return "Unknown OS release";
}

sub get_fs_info {
    my @sinfo = execute_system_command("df -P | grep '%'");
    my @iinfo = execute_system_command("df -Pi| grep '%'");
    shift @sinfo;
    shift @iinfo;

    foreach my $info (@sinfo) {

        #exit(0);
        if ( $info =~ /.*?(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(.*)$/ ) {
            next if $5 =~ m{(run|dev|sys|proc|snap|init)};
            if ( $4 > 85 ) {
                badprint "mount point $5 is using $4 % total space ("
                  . human_size( $2 * 1024 ) . " / "
                  . human_size( $1 * 1024 ) . ")";
                push( @generalrec, "Add some space to $4 mountpoint." );
            }
            else {
                infoprint "mount point $5 is using $4 % total space ("
                  . human_size( $2 * 1024 ) . " / "
                  . human_size( $1 * 1024 ) . ")";
            }
            $result{'Filesystem'}{'Space Pct'}{$5}   = $4;
            $result{'Filesystem'}{'Used Space'}{$5}  = $2;
            $result{'Filesystem'}{'Free Space'}{$5}  = $3;
            $result{'Filesystem'}{'Total Space'}{$5} = $1;
        }
    }

    @iinfo = map {
        my $v = $_;
        $v =~ s/.*\s(\d+)%\s+(.*)/$1\t$2/g;
        $v;
    } @iinfo;
    foreach my $info (@iinfo) {
        next if $info =~ m{(\d+)\t/(run|dev|sys|proc|snap)($|/)};
        if ( $info =~ /(\d+)\t(.*)/ ) {
            if ( $1 > 85 ) {
                badprint "mount point $2 is using $1 % of max allowed inodes";
                push( @generalrec,
"Cleanup files from $2 mountpoint or reformat your filesystem."
                );
            }
            else {
                infoprint "mount point $2 is using $1 % of max allowed inodes";
            }
            $result{'Filesystem'}{'Inode Pct'}{$2} = $1;
        }
    }
}

sub get_fs_info_win {
    my @sinfo =
      execute_system_command('wmic logicaldisk get Name,Size,FreeSpace');

    foreach my $info (@sinfo) {
        if ( $info =~ /^\s*(\d+)\s+(.*?)\s+(\d+)\s*$/ ) {
            my ( $free, $name, $size ) = ( $1, $2, $3 );
            my $used     = $size - $free;
            my $free_pct = int( ( $free / $size ) * 100 );
            my $used_pct = int( ( $used / $size ) * 100 );
            if ( $used_pct > 85 ) {
                badprint "Disk $name is using $used_pct % total space ("
                  . human_size($used) . " / "
                  . human_size($size) . ")";
                push( @generalrec, "Add some space to DIsk $name." );
            }
            else {
                infoprint "Disk $name is using $used_pct % total space ("
                  . human_size($used) . " / "
                  . human_size($size) . ")";
            }
            $result{'Filesystem'}{'Space Pct'}{$name}   = $used_pct;
            $result{'Filesystem'}{'Used Space'}{$name}  = $used;
            $result{'Filesystem'}{'Free Space'}{$name}  = $free;
            $result{'Filesystem'}{'Total Space'}{$name} = $size;
        }
    }
}

sub merge_hash {
    my $h1     = shift;
    my $h2     = shift;
    my %result = ();
    foreach my $substanceref ( $h1, $h2 ) {
        while ( my ( $k, $v ) = each %$substanceref ) {
            next if ( exists $result{$k} );
            $result{$k} = $v;
        }
    }
    return \%result;
}

sub is_virtual_machine {
    my $prefix = get_transport_prefix();
    if ( $^O eq 'linux' ) {
        if ( $prefix eq '' && open( my $cpuinfo, '<', '/proc/cpuinfo' ) ) {
            my $isVm = 0;
            while (<$cpuinfo>) {
                if (/^flags.*\ hypervisor /) { $isVm = 1; last; }
            }
            close $cpuinfo;
            return $isVm;
        }
        my $isVm = execute_system_command(
            "grep -Ec '^flags.*\ hypervisor\ ' /proc/cpuinfo");
        return ( $isVm == 0 ? 0 : 1 );
    }

    if ( $^O eq 'freebsd' ) {
        my $isVm = execute_system_command('sysctl -n kern.vm_guest');
        chomp $isVm;
        print "FARK DEBUG isVm=[$isVm]";
        return ( $isVm eq 'none' ? 0 : 1 );
    }

    if ($is_win) {
        my $isVM = execute_system_command('systeminfo');
        return ( $isVM =~ /System Model:\s*(Virtual Machine|VMware)/i ? 1 : 0 );
    }
    return 0;
}

sub infocmd {
    my $cmd = "@_";
    debugprint "CMD: $cmd";
    my @result = execute_system_command($cmd);
    @result = remove_cr @result;
    for my $l (@result) {
        infoprint "$l";
    }
}

sub infocmd_tab {
    my $cmd = "@_";
    debugprint "CMD: $cmd";
    my @result = execute_system_command($cmd);
    @result = remove_cr @result;
    for my $l (@result) {
        infoprint "\t$l";
    }
}

sub infocmd_one {
    my $cmd    = "@_";
    my @result = execute_system_command("$cmd 2>&1");
    @result = remove_cr @result;
    return join ', ', @result;
}

sub get_kernel_info {
    my $prefix = get_transport_prefix();
    my @params = (
        'fs.aio-max-nr',                 'fs.aio-nr',
        'fs.nr_open',                    'fs.file-max',
        'sunrpc.tcp_fin_timeout',        'sunrpc.tcp_max_slot_table_entries',
        'sunrpc.tcp_slot_table_entries', 'vm.swappiness'
    );
    infoprint "Information about kernel tuning:";
    foreach my $param (@params) {
        if ( $param =~ /^sunrpc/ ) {
            next unless -d "/proc/sys/sunrpc";
        }
        my @res = execute_system_command("sysctl $param 2>/dev/null");
        if ( $? == 0 ) {
            foreach my $l (@res) {
                chomp $l;
                infoprint "\t$l";
            }
            my $val = execute_system_command("sysctl -n $param 2>/dev/null");
            chomp $val;
            $result{'OS'}{'Config'}{$param} = $val;
        }
    }
    $prefix = get_transport_prefix();
    my $swappiness;
    if ( $prefix eq '' && -f "/proc/sys/vm/swappiness" ) {
        if ( open( my $fh, '<', "/proc/sys/vm/swappiness" ) ) {
            $swappiness = <$fh>;
            close $fh;
            chomp $swappiness;
        }
    }
    if ( !defined $swappiness || !$swappiness ) {
        $swappiness = execute_system_command('sysctl -n vm.swappiness');
        chomp $swappiness;
    }

    if ( $swappiness > 10 ) {
        badprint
          "Swappiness is > 10, please consider having a value lower than 10";
        push @generalrec, "setup swappiness lower or equal to 10";
        my $rec =
'vm.swappiness <= 10 (echo 10 > /proc/sys/vm/swappiness) or vm.swappiness=10 in /etc/sysctl.conf';
        push @adjvars, $rec;
        push @sysrec,  $rec;
    }
    else {
        infoprint "Swappiness is < 10.";
    }

    # only if /proc/sys/sunrpc exists
    if ( -d "/proc/sys/sunrpc" ) {
        my $tcp_slot_entries = execute_system_command(
            "sysctl -n sunrpc.tcp_slot_table_entries 2>$devnull");
        chomp $tcp_slot_entries;
        if ( $tcp_slot_entries eq '' or $tcp_slot_entries < 100 ) {
            badprint
"Initial TCP slot entries is < 1M, please consider having a value greater than 100";
            push @generalrec, "setup Initial TCP slot entries greater than 100";
            my $rec =
'sunrpc.tcp_slot_table_entries > 100 (echo 128 > /proc/sys/sunrpc/tcp_slot_table_entries)  or sunrpc.tcp_slot_table_entries=128 in /etc/sysctl.conf';
            push @adjvars, $rec;
            push @sysrec,  $rec;
        }
        else {
            infoprint "TCP slot entries is > 100.";
        }
    }

    if ( -f "/proc/sys/fs/aio-max-nr" ) {
        if ( execute_system_command('sysctl -n fs.aio-max-nr') < 1000000 ) {
            badprint
"Max running total of the number of max. events is < 1M, please consider having a value greater than 1M";
            push @generalrec, "setup Max running number events greater than 1M";
            my $rec =
'fs.aio-max-nr > 1M (echo 1048576 > /proc/sys/fs/aio-max-nr) or fs.aio-max-nr=1048576 in /etc/sysctl.conf';
            push @adjvars, $rec;
            push @sysrec,  $rec;
        }
        else {
            infoprint "Max Number of AIO events is > 1M.";
        }
    }
    if ( -f "/proc/sys/fs/nr_open" ) {
        if ( execute_system_command('sysctl -n fs.nr_open') < 1000000 ) {
            badprint
"Max running total of the number of file open request is < 1M, please consider having a value greater than 1M";
            push @generalrec,
              "setup running number of open request greater than 1M";
            my $rec =
'fs.aio-nr > 1M (echo 1048576 > /proc/sys/fs/nr_open) or fs.nr_open=1048576 in /etc/sysctl.conf';
            push @adjvars, $rec;
            push @sysrec,  $rec;
        }
        else {
            infoprint "Max Number of open file requests is > 1M.";
        }
    }
}

sub get_system_info {
    my $prefix = get_transport_prefix();
    $result{'OS'}{'Release'} = get_os_release();
    infoprint get_os_release;
    if ( is_docker() || $opt{'container'} ) {
        infoprint "Machine type          : Container";
        $result{'OS'}{'Virtual Machine'} = 'YES';
    }
    elsif (is_virtual_machine) {
        infoprint "Machine type          : Virtual machine";
        $result{'OS'}{'Virtual Machine'} = 'YES';
    }
    else {
        infoprint "Machine type          : Physical machine";
        $result{'OS'}{'Virtual Machine'} = 'NO';
    }

    $result{'Network'}{'Connected'} = 'NO';
    my $dnull = $devnull // ( $is_win ? 'NUL' : '/dev/null' );
    if ($is_win) {
        execute_system_command("ping -n 1 ipecho.net > $dnull 2>&1")
          if which( "ping", $ENV{'PATH'} );
    }
    else {
        execute_system_command("ping -c 1 ipecho.net > $dnull 2>&1")
          if which( "ping", $ENV{'PATH'} );
    }
    my $isConnected = $?;
    if ( $isConnected == 0 ) {
        infoprint "Internet              : Connected";
        $result{'Network'}{'Connected'} = 'YES';
    }
    else {
        badprint "Internet              : Disconnected";
    }
    $result{'OS'}{'NbCore'} = cpu_cores;
    infoprint "Number of Core CPU : " . cpu_cores;

    my ( $sysname, $nodename, $release, $version, $machine );
    if ( !$is_win && $prefix eq '' ) {
        ( $sysname, $nodename, $release, $version, $machine ) = POSIX::uname();
    }

    $result{'OS'}{'Type'} =
      $is_win
      ? 'Windows'
      : ( $prefix eq '' ? $sysname : execute_system_command('uname -o') );
    infoprint "Operating System Type : "
      . (
        $is_win
        ? 'Windows'
        : ( $prefix eq '' ? $sysname : execute_system_command('uname -o') )
      );

    $result{'OS'}{'Kernel'} =
      $is_win
      ? execute_system_command('ver')
      : ( $prefix eq '' ? $release : execute_system_command('uname -r') );
    infoprint "Kernel Release        : "
      . (
        $is_win
        ? execute_system_command('ver')
        : ( $prefix eq '' ? $release : execute_system_command('uname -r') )
      );

    $result{'OS'}{'Hostname'} =
      ( !$is_win && $prefix eq '' ) ? $nodename : Sys::Hostname::hostname();

    $result{'Network'}{'Internal Ip'} =
      $is_win
      ? execute_system_command(
'ipconfig |perl -ne "if (/IPv. Address/) {print s/^.*?([\\d\\.]*)\\s*$/$1/r; exit; }"'
      )
      : execute_system_command('hostname -I');
    infoprint "Hostname              : "
      . (
        ( !$is_win && $prefix eq '' ) ? $nodename : Sys::Hostname::hostname() );
    infoprint "Network Cards         : ";

    if ( which( "ip", $ENV{'PATH'} ) ) {
        infocmd_tab "ip addr | grep -A1 mtu";
    }
    elsif ( which( "ifconfig", $ENV{'PATH'} ) ) {
        infocmd_tab "ifconfig| grep -A1 mtu";
    }
    infoprint "Internal IP           : "
      . (
        ( !$is_win && $prefix eq '' )
        ? execute_system_command('hostname -I')
        : infocmd_one "hostname -I"
      );
    if ( which( "ip", $ENV{'PATH'} ) ) {
        $result{'Network'}{'Internal Ip'} =
          execute_system_command('ip addr | grep -A1 mtu');
    }
    elsif ( which( "ifconfig", $ENV{'PATH'} ) ) {
        $result{'Network'}{'Internal Ip'} =
          execute_system_command('ifconfig| grep -A1 mtu');
    }
    my $httpcli = get_http_cli();
    infoprint "HTTP client found: $httpcli" if defined $httpcli;

    my $ext_ip = "";
    if ( defined $httpcli && $httpcli ne '' ) {
        if ( $httpcli =~ /curl$/ ) {
            $ext_ip = infocmd_one "$httpcli -s -m 3 ipecho.net/plain";
        }
        elsif ( $httpcli =~ /wget$/ ) {
            $ext_ip =
              infocmd_one "$httpcli -q -t 1 -T 3 -q -O - ipecho.net/plain";
        }
    }
    infoprint "External IP           : " . $ext_ip;
    $result{'Network'}{'External Ip'} = $ext_ip;
    badprint "External IP           : Can't check, no Internet connectivity"
      unless defined($httpcli);

    my $ns_str = "";
    if ( $prefix eq '' && open( my $ns_file, '<', '/etc/resolv.conf' ) ) {
        my @ns_list;
        while (<$ns_file>) {
            push @ns_list, $1 if /^\s*nameserver\s+([^\s]+)/;
        }
        close $ns_file;
        $ns_str = join( ', ', @ns_list );
    }
    else {
        $ns_str =
          infocmd_one "grep 'nameserver' /etc/resolv.conf \| awk '{print \$2}'";
    }
    infoprint "Name Servers          : " . $ns_str;

    infoprint "Logged In users       : ";
    infocmd_tab "who";
    $result{'OS'}{'Logged users'} = execute_system_command('who');
    infoprint "Ram Usages in MB      : ";
    infocmd_tab "free -m | grep -v +";
    $result{'OS'}{'Free Memory RAM'} =
      execute_system_command('free -m | grep -v +');
    infoprint "Load Average          : ";
    infocmd_tab "top -n 1 -b | grep 'load average:'";
    $result{'OS'}{'Load Average'} =
      execute_system_command("top -n 1 -b | grep 'load average:'");

    infoprint "System Uptime         : ";
    infocmd_tab "uptime";
    $result{'OS'}{'Uptime'} = execute_system_command('uptime');
    if ( defined $mystat{'Uptime'} && $mystat{'Uptime'} > 0 ) {
        infoprint "Database Uptime       : "
          . pretty_uptime( $mystat{'Uptime'} );
    }
}

sub system_recommendations {
    if ( is_remote eq 1 || $is_cloud ) {
        subheaderprint "System Linux Recommendations";
        infoprint
          "Skipping local system checks on remote host or cloud instance ("
          . ( $cloud_type // 'unknown' ) . ")";

        my $mtype =
          $is_cloud
          ? "Cloud instance (" . ( $cloud_type // 'unknown' ) . ")"
          : "Remote host";
        infoprint "Machine type          : $mtype";

        my $host = $myvar{'hostname'} // $opt{'host'} // 'Unknown';
        infoprint "Host Name             : $host";

        my $os = $myvar{'version_compile_os'} // 'Unknown';
        infoprint "Operating System Type : $os";

        my $arch = $myvar{'version_compile_machine'} // 'Unknown';
        infoprint "CPU Architecture      : $arch";

        my $ram_str =
          $physical_memory
          ? hr_bytes($physical_memory)
          : "Unknown (Use --forcemem to specify)";
        infoprint "Physical Memory (RAM) : $ram_str";

        if ( defined $mystat{'Uptime'} && $mystat{'Uptime'} > 0 ) {
            infoprint "Database Uptime       : "
              . pretty_uptime( $mystat{'Uptime'} );
        }
        else {
            infoprint "Database Uptime       : Unknown";
        }

        # If physical memory is set, we can run some basic memory size checks
        if ($physical_memory) {
            if ( $physical_memory >= 1.5 * 1024 * 1024 * 1024 ) {
                goodprint
                  "There is at least 1.5 Gb of RAM dedicated to Linux server.";
            }
            else {
                badprint
"There is less than 1,5 Gb of RAM, consider dedicated 1 Gb for your Linux server";
                push_recommendation( 'System',
"Consider increasing 1,5 / 2 Gb of RAM for your Linux server"
                );
            }
        }
        return;
    }
    return if ( $opt{sysstat} == 0 );
    subheaderprint "System Linux Recommendations";
    my $os = $is_win ? 'windows' : execute_system_command('uname');
    unless ( $os =~ /Linux/i ) {
        infoprint "Skipped due to non Linux server";
        return;
    }
    prettyprint "Look for related Linux system recommendations";

    #prettyprint '-'x78;
    get_system_info();

    my $nb_cpus = cpu_cores;
    if ( $nb_cpus > 1 ) {
        goodprint "There is at least one CPU dedicated to database server.";
    }
    else {
        badprint
"There is only one CPU, consider dedicated one CPU for your database server";
        push_recommendation( 'System',
            "Consider increasing number of CPU for your database server" );
    }

    if ( $physical_memory >= 1.5 * 1024 * 1024 * 1024 ) {
        goodprint "There is at least 1.5 Gb of RAM dedicated to Linux server.";
    }
    else {
        badprint
"There is less than 1,5 Gb of RAM, consider dedicated 1 Gb for your Linux server";
        push_recommendation( 'System',
            "Consider increasing 1,5 / 2 Gb of RAM for your Linux server" );
    }

    my $omem = get_other_process_memory;
    infoprint "User process except mysqld used "
      . hr_bytes_rnd($omem) . " RAM.";
    if ( ( 0.15 * $physical_memory ) < $omem ) {
        if ( $opt{nondedicated} ) {
            infoprint "No warning with --nondedicated option";
            infoprint
"Other user process except mysqld used more than 15% of total physical memory "
              . percentage( $omem, $physical_memory ) . "% ("
              . hr_bytes_rnd($omem) . " / "
              . hr_bytes_rnd($physical_memory) . ")";
        }
        else {
            badprint
"Other user process except mysqld used more than 15% of total physical memory "
              . percentage( $omem, $physical_memory ) . "% ("
              . hr_bytes_rnd($omem) . " / "
              . hr_bytes_rnd($physical_memory) . ")";
            push( @generalrec,
"Consider stopping or dedicate server for additional process other than mysqld."
            );
            push( @sysrec,
"Consider stopping or dedicate server for additional process other than mysqld (other processes use "
                  . percentage( $omem, $physical_memory )
                  . "% of RAM)." );
            push( @adjvars,
"DON'T APPLY SETTINGS BECAUSE THERE ARE TOO MANY PROCESSES RUNNING ON THIS SERVER. OOM KILL CAN OCCUR!"
            );
        }
    }
    else {
        infoprint
"Other user process except mysqld used less than 15% of total physical memory "
          . percentage( $omem, $physical_memory ) . "% ("
          . hr_bytes_rnd($omem) . " / "
          . hr_bytes_rnd($physical_memory) . ")";
    }

    if ( $opt{'maxportallowed'} > 0 ) {
        my @opened_ports = get_opened_ports;
        infoprint "There is "
          . scalar @opened_ports
          . " listening port(s) on this server.";
        if ( scalar(@opened_ports) > $opt{'maxportallowed'} ) {
            badprint "There are too many listening ports: "
              . scalar(@opened_ports)
              . " opened > "
              . $opt{'maxportallowed'}
              . "allowed.";
            push( @generalrec,
"Consider dedicating a server for your database installation with fewer services running on it!"
            );
        }
        else {
            goodprint "There are less than "
              . $opt{'maxportallowed'}
              . " opened ports on this server.";
        }
    }

    foreach my $banport (@banned_ports) {
        if ( is_open_port($banport) ) {
            badprint "Banned port: $banport is opened..";
            push( @generalrec,
"Port $banport is opened. Consider stopping the program over this port."
            );
        }
        else {
            goodprint "$banport is not opened.";
        }
    }

    subheaderprint "Filesystem Linux Recommendations";
    if ($is_win) {
        get_fs_info_win;
    }
    else {
        get_fs_info;
        if ( !is_docker() && ( $opt{'container'} // '' ) eq '' ) {
            subheaderprint "Kernel Information Recommendations";
            get_kernel_info;
        }
    }
}

# ---------------------------------------------------------------------------
# SSL/TLS Security Recommendations
# ---------------------------------------------------------------------------
sub ssl_tls_recommendations {
    subheaderprint "SSL/TLS Security Recommendations";

    my @ssl_csv_rows = ("Variable,Value,IssueType,Description");

# Check current session encryption
# Ssl_cipher session status variable tells us if the current connection is encrypted.
    my $session_ssl = select_one("SHOW SESSION STATUS LIKE 'Ssl_cipher'");
    if ( $session_ssl =~ /Ssl_cipher\s+(.*)/ ) {
        my $cipher = $1;
        $cipher =~ s/^\s+|\s+$//g;
        if ( !$cipher || $cipher eq "NULL" || $cipher eq "0" ) {
            badprint "Current connection is NOT encrypted!";
            push_recommendation( 'Security',
"Current connection is NOT encrypted! Consider using SSL for all connections."
            );
            push @ssl_csv_rows,
                "Ssl_cipher,"
              . ( $cipher // '' )
              . ",UnencryptedConnection,Current connection is NOT encrypted! Consider using SSL for all connections.";
        }
        else {
            goodprint "Current connection is encrypted ($cipher)";
        }
    }

    # Global SSL check
    if ( defined( $myvar{'have_ssl'} ) ) {
        if ( $myvar{'have_ssl'} eq 'DISABLED' ) {
            badprint "SSL is DISABLED on the server.";
            push_recommendation( 'Security',
                "Enable SSL support on the server (check have_ssl variable)." );
            push @ssl_csv_rows,
"have_ssl,DISABLED,SSLDisabled,Enable SSL support on the server (check have_ssl variable).";
        }
        elsif ( $myvar{'have_ssl'} eq 'YES' || $myvar{'have_ssl'} eq 'ON' ) {
            goodprint "SSL support is enabled";
        }
    }

    # require_secure_transport (MySQL 5.7+, MariaDB 10.5+)
    if ( defined( $myvar{'require_secure_transport'} ) ) {
        if ( $myvar{'require_secure_transport'} eq 'OFF' ) {
            badprint "require_secure_transport is OFF";
            push_recommendation( 'Security',
"Enable require_secure_transport to force all connections to use SSL."
            );
            push @ssl_csv_rows,
"require_secure_transport,OFF,SecureTransportOff,Enable require_secure_transport to force all connections to use SSL.";
        }
        else {
            goodprint "require_secure_transport is ON";
        }
    }

    # TLS Versions (MySQL 8.0+, MariaDB 10.4.6+)
    if ( defined( $myvar{'tls_version'} ) ) {
        my $tls_versions = $myvar{'tls_version'};
        if ( $tls_versions =~ /TLSv1\.0/i || $tls_versions =~ /TLSv1\.1/i ) {
            badprint "Insecure TLS versions enabled: $tls_versions";
            push_recommendation( 'Security',
                "Disable TLSv1.0 and TLSv1.1. Use only TLSv1.2 or TLSv1.3." );
            push @ssl_csv_rows,
"tls_version,$tls_versions,InsecureTLS,Disable TLSv1.0 and TLSv1.1. Use only TLSv1.2 or TLSv1.3.";
        }
        if ( $tls_versions !~ /TLSv1\.[23]/ ) {
            badprint "No modern TLS versions (1.2+) detected in: $tls_versions";
            push_recommendation( 'Security',
                "Ensure TLSv1.2 or TLSv1.3 are enabled for improved security."
            );
            push @ssl_csv_rows,
"tls_version,$tls_versions,NoModernTLS,Ensure TLSv1.2 or TLSv1.3 are enabled for improved security.";
        }

        if ( $tls_versions =~ /TLSv1\.[23]/ && $tls_versions !~ /TLSv1\.[01]/ )
        {
            goodprint "Only secure TLS versions enabled: $tls_versions";
        }
    }

    # Certificate presence and local audit
    if ( !$myvar{'ssl_cert'} && !$myvar{'ssl_key'} ) {
        badprint "No SSL certificates configured (ssl_cert/ssl_key are empty)";
        push_recommendation( 'Security',
"Configure SSL certificates (ssl_cert, ssl_key, ssl_ca) to enable encrypted connections."
        );
        push @ssl_csv_rows,
"ssl_cert/ssl_key,empty,NoCertificates,Configure SSL certificates (ssl_cert, ssl_key, ssl_ca) to enable encrypted connections.";
    }
    else {
        check_local_certificates( \@ssl_csv_rows );
    }

    check_remote_user_ssl( \@ssl_csv_rows );

    if ( $opt{dumpdir} && @ssl_csv_rows > 1 ) {
        dump_into_file( "ssl_issues.csv", join( "\n", @ssl_csv_rows ) );
    }
}

sub check_local_certificates {
    my $ssl_csv_rows_ref = shift;
    return if is_remote();

    my @certs = (
        {
            var  => 'ssl_cert',
            path => $myvar{'ssl_cert'},
            desc => 'Public Certificate'
        },
        { var => 'ssl_ca', path => $myvar{'ssl_ca'}, desc => 'CA Certificate' },
    );

    foreach my $cert (@certs) {
        next unless $cert->{path};

        my $in_datadir = 0;
        if ( $myvar{'datadir'} ) {
            my $abs_path    = abs_path( $cert->{path} ) // $cert->{path};
            my $abs_datadir = abs_path( $myvar{'datadir'} )
              // $myvar{'datadir'};
            if ( index( $abs_path, $abs_datadir ) == 0 ) {
                $in_datadir = 1;
            }
        }

        if ( !my_file_exists( $cert->{path} ) ) {
            if ($in_datadir) {
                infoprint
"$cert->{desc} file not found or inaccessible in datadir (skipped): $cert->{path}";
            }
            else {
                badprint "$cert->{desc} file not found: $cert->{path}";
                if ($ssl_csv_rows_ref) {
                    push @$ssl_csv_rows_ref,
"$cert->{var},$cert->{path},CertificateNotFound,Public certificate file not found";
                }
            }
            next;
        }
        if ( !my_file_readable( $cert->{path} ) ) {
            if ($in_datadir) {
                infoprint
"$cert->{desc} file is not readable in datadir (skipped): $cert->{path}";
            }
            else {
                badprint "$cert->{desc} file is not readable: $cert->{path}";
                if ($ssl_csv_rows_ref) {
                    push @$ssl_csv_rows_ref,
"$cert->{var},$cert->{path},CertificateNotReadable,Public certificate file is not readable";
                }
            }
            next;
        }

        # Check expiration using openssl if available
        if ( which('openssl') ) {
            my $enddate = execute_system_command(
                "openssl x509 -enddate -noout -in \"$cert->{path}\" 2>/dev/null"
            );
            if ( $enddate =~ /notAfter=(.*)/ ) {
                my $date_str = $1;
                infoprint "$cert->{desc} expires on: $date_str";

            # Check if it's nearing expiration using native Perl for portability
                my %months = (
                    Jan => 0,
                    Feb => 1,
                    Mar => 2,
                    Apr => 3,
                    May => 4,
                    Jun => 5,
                    Jul => 6,
                    Aug => 7,
                    Sep => 8,
                    Oct => 9,
                    Nov => 10,
                    Dec => 11
                );
                if ( $date_str =~
                    /^(\w+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+(\d+)\s+(\w+)/ )
                {
                    my ( $mon_str, $mday, $hour, $min, $sec, $year, $tz ) =
                      ( $1, $2, $3, $4, $5, $6, $7 );
                    if ( exists $months{$mon_str} ) {
                        use Time::Local;
                        eval {
                            my $expire_time =
                              timegm( $sec, $min, $hour, $mday,
                                $months{$mon_str}, $year );
                            my $days_left =
                              int( ( $expire_time - time() ) / 86400 );

                            if ( $days_left < 0 ) {
                                badprint "$cert->{desc} is EXPIRED ("
                                  . abs($days_left)
                                  . " days ago)!";
                                push_recommendation( 'Security',
                                    "Renew expired $cert->{desc}: $cert->{path}"
                                );
                                if ($ssl_csv_rows_ref) {
                                    push @$ssl_csv_rows_ref,
"$cert->{var},$cert->{path},ExpiredCertificate,Renew expired $cert->{desc}: $cert->{path}";
                                }
                            }
                            elsif ( $days_left < 30 ) {
                                badprint
"$cert->{desc} expires soon ($days_left days remaining)!";
                                push_recommendation( 'Security',
                                    "Renew $cert->{desc} soon: $cert->{path}" );
                                if ($ssl_csv_rows_ref) {
                                    push @$ssl_csv_rows_ref,
"$cert->{var},$cert->{path},CertificateExpiringSoon,Renew $cert->{desc} soon: $cert->{path}";
                                }
                            }
                            else {
                                goodprint
"$cert->{desc} is valid ($days_left days remaining)";
                            }
                        };
                    }
                }
            }
        }
    }
}

sub my_file_exists {
    my $file = shift;
    return -e $file;
}

sub my_file_readable {
    my $file = shift;
    return -r $file;
}

sub check_remote_user_ssl {
    my $ssl_csv_rows_ref = shift;
    my @remote_users;
    if ( mysql_version_ge( 10, 4 ) && $myvar{'version'} =~ /MariaDB/i ) {
        @remote_users = select_array(
"SELECT CONCAT(QUOTE(user), '\@', QUOTE(host)) FROM mysql.global_priv WHERE host NOT IN ('localhost', '127.0.0.1', '::1') AND JSON_VALUE(Priv, '\$.ssl_type') = ''"
        );
    }
    else {
        @remote_users = select_array(
"SELECT CONCAT(QUOTE(user), '\@', QUOTE(host)) FROM mysql.user WHERE host NOT IN ('localhost', '127.0.0.1', '::1') AND (ssl_type = 'NONE' OR ssl_type = '')"
        );
    }

    if (@remote_users) {
        my $count = scalar @remote_users;
        badprint "$count users can connect remotely without SSL enforcement";
        push_recommendation( 'Security',
            "Enforce SSL for remote users (ALTER USER ... REQUIRE SSL)" );
        foreach my $user (@remote_users) {
            debugprint "Remote user without SSL requirement: $user";
            if ($ssl_csv_rows_ref) {
                push @$ssl_csv_rows_ref,
"RemoteUser,$user,RemoteUserWithoutSSLEnforcement,Remote user can connect remotely without SSL enforcement";
            }
        }
    }
    else {
        goodprint
"All remote users have SSL enforcement active (or no remote users exist)";
    }
}

sub check_auth_plugins {
    subheaderprint "Authentication Plugin Security";

    my @mysqlstatlist;
    if ( mysql_version_ge( 10, 4 ) && $myvar{'version'} =~ /MariaDB/i ) {

        # MariaDB 10.4+ uses mysql.global_priv
        @mysqlstatlist = select_array(
"SELECT CONCAT(QUOTE(user), '\@', QUOTE(host)), JSON_VALUE(Priv, '\$.plugin') FROM mysql.global_priv WHERE user != ''"
        );
    }
    else {
        # Standard mysql.user (MySQL and older MariaDB)
        @mysqlstatlist = select_array(
"SELECT CONCAT(QUOTE(user), '\@', QUOTE(host)), plugin FROM mysql.user WHERE user != ''"
        );
    }

    my $mysql_80_plus =
      mysql_version_ge( 8, 0 ) && ( $myvar{'version'} !~ /MariaDB/i );
    my $mysql_84_plus =
      mysql_version_ge( 8, 4 ) && ( $myvar{'version'} !~ /MariaDB/i );
    my $mysql_90_plus =
      mysql_version_ge( 9, 0 ) && ( $myvar{'version'} !~ /MariaDB/i );
    my $is_mariadb = ( $myvar{'version'} =~ /MariaDB/i );

    my $insecure_count        = 0;
    my $sha256_insecure_count = 0;
    my $obsolete_count        = 0;

    my @csv_rows = ("User,Host,Plugin,SecurityLevel,Status");

    foreach my $line (@mysqlstatlist) {
        my ( $user_host, $plugin ) = split( /\t/, $line );
        $plugin //=
          'mysql_native_password';    # Default if empty in older versions

        # Extract user and host for CSV
        my ( $user, $host ) = ( '', '' );
        if ( $user_host =~ /'([^']*)'@'([^']*)'/ ) {
            $user = $1;
            $host = $2;
        }

        if ( $plugin eq 'mysql_old_password' ) {
            badprint "User $user_host uses OBSOLETE/VERY WEAK plugin: $plugin";
            push @secrec,
"User $user_host: Migrate to caching_sha2_password or ed25519/parsec";
            $obsolete_count++;
            $insecure_count++;
            push @csv_rows, "$user,$host,$plugin,Very Low,Obsolete";
        }
        elsif ( $plugin eq 'mysql_native_password' ) {
            if ($mysql_90_plus) {
                badprint
                  "User $user_host uses REMOVED/INSECURE plugin: $plugin";
                push @csv_rows, "$user,$host,$plugin,Low,Removed";
            }
            elsif ($mysql_84_plus) {
                badprint
"User $user_host uses DISABLED BY DEFAULT/INSECURE plugin: $plugin";
                push @csv_rows, "$user,$host,$plugin,Low,DisabledByDefault";
            }
            elsif ($mysql_80_plus) {
                badprint
                  "User $user_host uses DEPRECATED/INSECURE plugin: $plugin";
                push @csv_rows, "$user,$host,$plugin,Low,Deprecated";
            }
            else {
                badprint
                  "User $user_host uses SHA-1 based insecure plugin: $plugin";
                push @csv_rows, "$user,$host,$plugin,Low,Insecure";
            }
            $insecure_count++;
            my $rec =
              $is_mariadb
              ? "Migrate to 'ed25519', 'parsec' or 'unix_socket' for $user_host"
              : "Migrate to 'caching_sha2_password' for $user_host";
            push @secrec, "User $user_host: $rec";
        }
        elsif ( $plugin eq 'sha256_password' ) {
            if ($mysql_84_plus) {
                badprint
                  "User $user_host uses REMOVED/INSECURE plugin: $plugin";
                $insecure_count++;
                push @csv_rows, "$user,$host,$plugin,Low,Removed";
            }
            elsif ($mysql_80_plus) {
                badprint "User $user_host uses DEPRECATED plugin: $plugin";
                push @secrec,
                  "User $user_host: Migrate to 'caching_sha2_password'";
                $sha256_insecure_count++;
                push @csv_rows, "$user,$host,$plugin,Low,Deprecated";
            }
        }
        elsif ( $plugin eq 'caching_sha2_password' ) {
            push @csv_rows, "$user,$host,$plugin,High,Active";
        }
        elsif ( $plugin =~ /^(unix_socket|auth_socket)$/ ) {
            push @csv_rows, "$user,$host,$plugin,Very High,Active";
        }
        elsif ( $plugin eq 'ed25519' ) {
            push @csv_rows, "$user,$host,$plugin,Very High,Active";
        }
        elsif ( $plugin eq 'parsec' ) {
            push @csv_rows, "$user,$host,$plugin,Maximal,Active";
        }
        else {
            push @csv_rows, "$user,$host,$plugin,Unknown,Active";
        }
    }

    if ( $obsolete_count > 0 ) {
        push_recommendation( 'Security',
"Migrate $obsolete_count user(s) from obsolete/very weak plugin mysql_old_password"
        );
    }
    if ( $insecure_count > $obsolete_count ) {
        my $diff = $insecure_count - $obsolete_count;
        my $rec =
          $is_mariadb
          ? "Migrate to 'ed25519', 'parsec' or 'unix_socket' for $diff user(s)"
          : "Migrate to 'caching_sha2_password' for $diff user(s)";
        push_recommendation( 'Security', $rec );
    }
    if ( $sha256_insecure_count > 0 ) {
        push_recommendation( 'Security',
"Migrate to 'caching_sha2_password' for $sha256_insecure_count user(s) (using sha256_password)"
        );
    }

    if ( $insecure_count == 0 && $sha256_insecure_count == 0 ) {
        goodprint
          "No users found using insecure or deprecated authentication plugins";
    }

    if ( $opt{dumpdir} && @csv_rows > 1 ) {
        dump_into_file( "insecure_authentication_plugins.csv",
            join( "\n", @csv_rows ) );
    }
}

sub security_recommendations {
    subheaderprint "Security Recommendations";

    infoprint "$myvar{'version_comment'} - $myvar{'version'}";

    my $PASS_COLUMN_NAME = get_password_column_name();

    if ( !$PASS_COLUMN_NAME ) {
        infoprint "Skipped due to none of known auth columns exists";
        return;
    }
    debugprint "Password column = $PASS_COLUMN_NAME";

    # IS THERE A ROLE COLUMN
    my $is_role_column = select_one
"select count(*) from information_schema.columns where TABLE_NAME='user' AND TABLE_SCHEMA='mysql' and COLUMN_NAME='IS_ROLE'";

    my $extra_user_condition = "";
    $extra_user_condition = "IS_ROLE = 'N' AND" if $is_role_column > 0;
    my @mysqlstatlist;
    if ( $is_role_column > 0 ) {
        @mysqlstatlist = select_array
"SELECT CONCAT(QUOTE(user), '\@', QUOTE(host)) FROM mysql.user WHERE IS_ROLE='Y'";
        foreach my $line ( sort @mysqlstatlist ) {
            chomp($line);
            infoprint "User $line is User Role";
        }
    }
    else {
        debugprint "No Role user detected";
        goodprint "No Role user detected";
    }

    # Looking for Anonymous users
    @mysqlstatlist = select_array
"SELECT CONCAT(QUOTE(user), '\@', QUOTE(host)) FROM mysql.user WHERE $extra_user_condition (TRIM(USER) = '' OR USER IS NULL)";

    #debugprint Dumper \@mysqlstatlist;

    #exit 0;
    if (@mysqlstatlist) {
        push_recommendation( 'Security',
                "Remove Anonymous User accounts: there are "
              . scalar(@mysqlstatlist)
              . " anonymous accounts." );
        foreach my $line ( sort @mysqlstatlist ) {
            chomp($line);
            badprint "User "
              . $line
              . " is an anonymous account. Remove with DROP USER "
              . $line . ";";
        }
    }
    else {
        goodprint "There are no anonymous accounts for any database users";
    }

    if ( $opt{skippassword} eq 1 ) {
        infoprint "Skipped password checks due to --skippassword option";
        return;
    }

    if ( mysql_version_le( 5, 1 ) ) {
        badprint "No more password checks for MySQL version <=5.1";
        badprint "MySQL version <=5.1 is deprecated and end of support.";
        return;
    }

    # Looking for Empty Password
    if ( mysql_version_ge( 10, 4 ) ) {
        @mysqlstatlist = select_array
q{SELECT CONCAT(QUOTE(user), '@', QUOTE(host)) FROM mysql.global_priv WHERE
    ( user != ''
    AND JSON_CONTAINS(Priv, '"mysql_native_password"', '$.plugin') AND JSON_CONTAINS(Priv, '""', '$.authentication_string')
    AND NOT JSON_CONTAINS(Priv, 'true', '$.account_locked')
    )};
    }
    else {
        @mysqlstatlist = select_array
"SELECT CONCAT(QUOTE(user), '\@', QUOTE(host)) FROM mysql.user WHERE ($PASS_COLUMN_NAME = '' OR $PASS_COLUMN_NAME IS NULL)
    AND user != ''
    /*!50501 AND plugin NOT IN ('auth_socket', 'unix_socket', 'win_socket', 'auth_pam_compat') */
    /*!80000 AND account_locked = 'N' AND password_expired = 'N' */";
    }
    if (@mysqlstatlist) {
        foreach my $line ( sort @mysqlstatlist ) {
            chomp($line);
            badprint "User '" . $line . "' has no password set.";
            push_recommendation( 'Security',
"Set up a Secure Password for $line user: SET PASSWORD FOR $line = PASSWORD('secure_password');"
            );
        }
    }
    else {
        goodprint "All database users have passwords assigned";
    }

    if ( mysql_version_ge( 5, 7 ) ) {
        my $valPlugin = select_one(
"select count(*) from information_schema.plugins where PLUGIN_NAME='validate_password' AND PLUGIN_STATUS='ACTIVE'"
        );
        if ( $valPlugin >= 1 ) {
            infoprint
"Bug #80860 MySQL 5.7: Avoid testing password when validate_password is activated";
            return;
        }
    }

    # Looking for User with user/ uppercase /capitalise user as password
    if ( !mysql_version_ge(8) ) {
        @mysqlstatlist = select_array
"SELECT CONCAT(QUOTE(user), '\@', QUOTE(host)) FROM mysql.user WHERE user != '' AND (CAST($PASS_COLUMN_NAME as Binary) = PASSWORD(user) OR CAST($PASS_COLUMN_NAME as Binary) = PASSWORD(UPPER(user)) OR CAST($PASS_COLUMN_NAME as Binary) = PASSWORD(CONCAT(UPPER(LEFT(User, 1)), SUBSTRING(User, 2, LENGTH(User)))))";
        if (@mysqlstatlist) {
            foreach my $line ( sort @mysqlstatlist ) {
                chomp($line);
                badprint "User " . $line . " has user name as password.";
                push( @generalrec,
"Set up a Secure Password for $line user: SET PASSWORD FOR $line = PASSWORD('secure_password');"
                );
            }
        }
    }

    @mysqlstatlist = select_array
      "SELECT CONCAT(QUOTE(user), '\@', host) FROM mysql.user WHERE HOST='%'";
    if ( scalar(@mysqlstatlist) > 0 ) {
        if ( $opt{dumpdir} ) {
            select_csv_file(
                "$opt{dumpdir}/user_with_general_wildcard.csv",
                "SELECT user, host FROM mysql.user WHERE HOST='%'"
            );
        }
        my $luser = 'user_name';
        if ( scalar(@mysqlstatlist) == 1 ) {
            $luser = ( split /@/, $mysqlstatlist[0] )[0];
        }
        foreach my $line ( sort @mysqlstatlist ) {
            chomp($line);
            badprint "User " . $line
              . " does not specify hostname restrictions.";
        }
        push( @generalrec,
            "Restrict Host for $luser\@'%' to $luser\@LimitedIPRangeOrLocalhost"
        );
        push( @generalrec,
                "RENAME USER $luser\@'%' TO "
              . $luser
              . "\@LimitedIPRangeOrLocalhost;" );
    }

    unless ( -f $basic_password_files ) {
        badprint "There is no basic password file list!";
        return;
    }

    my @passwords = get_basic_passwords $basic_password_files;
    infoprint "There are "
      . scalar(@passwords)
      . " basic passwords in the list.";
    my $nbins = 0;
    my $passreq;
    if (@passwords) {
        my $nbInterPass     = 0;
        my $skip_dict_check = 0;

    # Behavioral check for socket authentication or password bypass (issue #875)
    # Testing if any of these passwords work (including random tokens)
        my $target_user = $opt{user} || 'root';
        foreach my $p ( "true", "false",
            "RA-ND-OM-P-ASS-W-ORD-" . int( rand(100000) ) )
        {
            my $check_cmd =
"$mysqlcmd $mysqllogin -u $target_user -p'$p' -Nrs -e 'select \"mysqld is alive\";' 2>$devnull";
            my $alive_res = execute_system_command($check_cmd);
            if ( $alive_res =~ /mysqld is alive/ ) {
                infoprint
"Authentication plugin allows access without a valid password for user '$target_user'. Skipping dictionary check.";
                $skip_dict_check = 1;
                last;
            }
            else {
                $failed_connection_attempts++;
            }
        }

        unless ($skip_dict_check) {

            # Let's check if we can query user list and plugins
            my @users_db;
            if (
                mysql_version_ge(8)
                || ( $myvar{'version'} =~ /mariadb/i
                    && mysql_version_ge( 10, 4 ) )
              )
            {
                if ( $myvar{'version'} =~ /mariadb/i ) {
                    @users_db = select_array(
"SELECT user, host, JSON_VALUE(Priv, '\$.plugin'), JSON_VALUE(Priv, '\$.authentication_string') FROM mysql.global_priv WHERE user != ''"
                    );
                }
                else {
                    @users_db = select_array(
"SELECT user, host, plugin, authentication_string FROM mysql.user WHERE user != ''"
                    );
                }
            }

            my $has_digest_sha      = eval { require Digest::SHA; 1; };
            my $checked_target_user = 0;

            if (@users_db) {

 # We successfully read the user table. Check all native password users offline!
                foreach my $user_line (@users_db) {
                    my ( $user, $host, $plugin, $auth_string ) =
                      split( /\t/, $user_line );
                    next unless defined $user && $user ne '';
                    $plugin      //= '';
                    $auth_string //= '';

                    if (
                        $has_digest_sha
                        && (   $plugin eq 'mysql_native_password'
                            || $auth_string =~ /^\*[0-9A-F]{40}$/i )
                      )
                    {
                        my $target_hash = uc($auth_string);
                        my $found_weak  = 0;
                        foreach my $pass (@passwords) {
                            $pass =~ s/\s//g;
                            chomp($pass);
                            my @variants = ( $pass, uc($pass), ucfirst($pass) );
                            foreach my $v (@variants) {
                                my $computed = '*'
                                  . uc(
                                    Digest::SHA::sha1_hex(
                                        Digest::SHA::sha1($v)
                                    )
                                  );
                                if ( $computed eq $target_hash ) {
                                    badprint
"User '$user'\@'$host' is using a weak password (checked offline)";
                                    push( @generalrec,
"Set up a Secure Password for '$user'\@'$host' user."
                                    );
                                    $nbins++;
                                    $found_weak = 1;
                                    last;
                                }
                            }
                            last if $found_weak;
                        }
                        if ( $user eq $target_user ) {
                            $checked_target_user = 1;
                        }
                    }
                    elsif ( $user eq $target_user ) {

            # Non-native user (like caching_sha2_password), do connection checks
                        my $found_weak = 0;
                        foreach my $pass (@passwords) {
                            $nbInterPass++;
                            last if $nbInterPass > $opt{'max-password-checks'};
                            if ( $nbInterPass % 100 == 0 ) {
                                if ( $myvar{'version'} !~ /mariadb/i
                                    && mysql_version_ge( 8, 0, 0 ) )
                                {
                                    if (
                                        (
                                            $myvar{'performance_schema'}
                                            // 'OFF'
                                        ) eq 'ON'
                                      )
                                    {
                                        select_one(
"TRUNCATE TABLE performance_schema.host_cache;"
                                        );
                                    }
                                }
                                else {
                                    select_one("FLUSH HOSTS;");
                                }
                            }

                            $pass =~ s/\s//g;
                            $pass =~ s/\'/\\\'/g;
                            chomp($pass);

                            my @variants = ( $pass, uc($pass), ucfirst($pass) );
                            foreach my $v (@variants) {
                                my $check_login =
                                  "$mysqllogin -u $target_user -p'$v'";
                                my $alive_res = execute_system_command(
"$mysqlcmd -Nrs -e 'select \"mysqld is alive\";' $check_login 2>$devnull"
                                );
                                if ( $alive_res =~ /mysqld is alive/ ) {
                                    badprint
"User '$target_user' is using a weak password";
                                    push( @generalrec,
"Set up a Secure Password for $target_user user."
                                    );
                                    $nbins++;
                                    $found_weak = 1;
                                    last;
                                }
                                else {
                                    $failed_connection_attempts++;
                                }
                            }
                            last if $found_weak;
                        }
                        $checked_target_user = 1;
                    }
                }
            }

# Fallback connection check if we couldn't query mysql.user or target user was not found/checked
            if ( !$checked_target_user ) {
                foreach my $pass (@passwords) {
                    $nbInterPass++;
                    last if $nbInterPass > $opt{'max-password-checks'};
                    if ( $nbInterPass % 100 == 0 ) {
                        if ( $myvar{'version'} !~ /mariadb/i
                            && mysql_version_ge( 8, 0, 0 ) )
                        {
                            if ( ( $myvar{'performance_schema'} // 'OFF' ) eq
                                'ON' )
                            {
                                select_one(
"TRUNCATE TABLE performance_schema.host_cache;"
                                );
                            }
                        }
                        else {
                            select_one("FLUSH HOSTS;");
                        }
                    }

                    $pass =~ s/\s//g;
                    $pass =~ s/\'/\\\'/g;
                    chomp($pass);

                    if ( !mysql_version_ge(8) ) {

               # Looking for User with user/ uppercase /capitalise weak password
                        @mysqlstatlist = select_array(
"SELECT CONCAT(user, '\@', host) FROM mysql.user WHERE $PASS_COLUMN_NAME = PASSWORD('"
                              . $pass
                              . "') OR $PASS_COLUMN_NAME = PASSWORD(UPPER('"
                              . $pass
                              . "')) OR $PASS_COLUMN_NAME = PASSWORD(CONCAT(UPPER(LEFT('"
                              . $pass
                              . "', 1)), SUBSTRING('"
                              . $pass
                              . "', 2, LENGTH('"
                              . $pass
                              . "'))))" );
                        if (@mysqlstatlist) {
                            foreach my $line (@mysqlstatlist) {
                                chomp($line);
                                badprint "User '" . $line
                                  . "' is using a weak password (lower, upper or capitalize derivative version).";
                                push( @generalrec,
"Set up a Secure Password for $line user: SET PASSWORD FOR '"
                                      . ( split /@/, $line )[0] . "'\@'"
                                      . ( split /@/, $line )[1]
                                      . "' = PASSWORD('secure_password');" );
                                $nbins++;
                            }
                        }
                    }
                    else {
                        # New way to check basic password for MySQL 8.0+
                        my $found_weak = 0;
                        my @variants   = ( $pass, uc($pass), ucfirst($pass) );
                        foreach my $v (@variants) {
                            my $check_login =
                              "$mysqllogin -u $target_user -p'$v'";
                            my $alive_res = execute_system_command(
"$mysqlcmd -Nrs -e 'select \"mysqld is alive\";' $check_login 2>$devnull"
                            );
                            if ( $alive_res =~ /mysqld is alive/ ) {
                                badprint
"User '$target_user' is using a weak password";
                                push( @generalrec,
"Set up a Secure Password for $target_user user."
                                );
                                $nbins++;
                                $found_weak = 1;
                                last;
                            }
                            else {
                                $failed_connection_attempts++;
                            }
                        }
                        last if $found_weak;
                    }
                }
            }
        }
    }
    if ( $nbins > 0 ) {
        push( @generalrec,
            $nbins
              . " user(s) used basic or weak password from basic dictionary." );
    }

    check_auth_plugins();
    save_aborted_connects_state();
}

sub get_replication_status {
    subheaderprint "Replication Metrics";
    infoprint "Galera Synchronous replication: " . $myvar{'have_galera'};
    if ( scalar( keys %myreplicas ) == 0 ) {
        infoprint "No replication replica(s) for this server.";
    }
    else {
        infoprint "This server is acting as source for "
          . scalar( keys %myreplicas )
          . " server(s).";
    }
    infoprint "Binlog format: " . $myvar{'binlog_format'};
    infoprint "XA support enabled: " . $myvar{'innodb_support_xa'};

    infoprint "Semi synchronous replication Source: "
      . (
        (
                 defined( $myvar{'rpl_semi_sync_master_enabled'} )
              or defined( $myvar{'rpl_semi_sync_source_enabled'} )
        )
        ? ( $myvar{'rpl_semi_sync_master_enabled'}
              // $myvar{'rpl_semi_sync_source_enabled'} )
        : 'Not Activated'
      );
    infoprint "Semi synchronous replication Replica: "
      . (
        (
                 defined( $myvar{'rpl_semi_sync_slave_enabled'} )
              or defined( $myvar{'rpl_semi_sync_replica_enabled'} )
        )
        ? ( $myvar{'rpl_semi_sync_slave_enabled'}
              // $myvar{'rpl_semi_sync_replica_enabled'} )
        : 'Not Activated'
      );
    if ( scalar( keys %myrepl ) == 0 and scalar( keys %myreplicas ) == 0 ) {
        infoprint "This is a standalone server";
        return;
    }
    if ( scalar( keys %myrepl ) == 0 ) {
        infoprint
          "No replication setup for this server or replication not started.";
        return;
    }

    $result{'Replication'}{'status'} = \%myrepl;
    my ($io_running) = $myrepl{'Replica_IO_Running'};
    $io_running = '' unless defined $io_running;
    debugprint "IO RUNNING: " . ( $io_running // 'undef' ) . " ";
    my ($sql_running) = $myrepl{'Replica_SQL_Running'};
    $sql_running = '' unless defined $sql_running;
    debugprint "SQL RUNNING: " . ( $sql_running // 'undef' ) . " ";

    my ($seconds_behind_replica) = $myrepl{'Seconds_Behind_Replica'};
    $seconds_behind_replica = 1000000 unless defined($seconds_behind_replica);
    debugprint "SECONDS : $seconds_behind_replica ";

    if ( defined($io_running)
        and ( $io_running !~ /yes/i or $sql_running !~ /yes/i ) )
    {
        badprint
          "This replication replica is not running but seems to be configured.";
    }
    if (   defined($io_running)
        && $io_running  =~ /yes/i
        && $sql_running =~ /yes/i )
    {
        if ( $myvar{'read_only'} eq 'OFF' ) {
            badprint
"This replication replica is running with the read_only option disabled.";
        }
        else {
            goodprint
"This replication replica is running with the read_only option enabled.";
        }
        if ( $seconds_behind_replica > 0 ) {
            badprint
"This replication replica is lagging and has $seconds_behind_replica second(s) behind source host.";
        }
        else {
            goodprint "This replication replica is up to date with source.";
        }
    }

    # Parallel replication checks (MariaDB specific)
    if (   ( $myvar{'version'} =~ /MariaDB/i )
        or ( $myvar{'version_comment'} =~ /MariaDB/i ) )
    {
        my $parallel_threads = $myvar{'slave_parallel_threads'}
          // $myvar{'replica_parallel_threads'} // 0;
        if ( $parallel_threads > 1 ) {
            goodprint
              "Parallel replication is enabled with $parallel_threads threads.";

            # Check parallel mode for MariaDB 10.5+
            if ( mysql_version_ge( 10, 5 ) ) {
                my $parallel_mode = $myvar{'slave_parallel_mode'}
                  // $myvar{'replica_parallel_mode'} // '';
                if ( $parallel_mode eq 'optimistic' ) {
                    goodprint
                      "Parallel replication mode is set to 'optimistic'.";
                }
                else {
                    badprint
"Parallel replication mode is not 'optimistic' (recommended for MariaDB 10.5+).";
                    push( @adjvars, "replica_parallel_mode=optimistic" );
                }
            }
            infoprint
"Ensure binlog_format=ROW is set on the source for parallel replication to work effectively.";
        }
        else {
            badprint "Parallel replication is disabled.";
            push( @adjvars,
                "replica_parallel_threads (set to number of vCPUs)" );
        }
    }
}

# https://endoflife.date/mysql
# https://endoflife.date/mariadb
sub validate_mysql_version {
    return unless defined $myvar{'version'};
    ( $mysqlvermajor, $mysqlverminor, $mysqlvermicro ) =
      $myvar{'version'} =~ /^(\d+)(?:\.(\d+))?(?:\.(\d+))?/;
    $mysqlvermajor ||= 0;
    $mysqlverminor ||= 0;
    $mysqlvermicro ||= 0;

    prettyprint " ";

    if (   mysql_version_eq( 8, 0 )
        or mysql_version_eq( 8,  4 )
        or mysql_version_eq( 9,  7 )
        or mysql_version_eq( 10, 6 )
        or mysql_version_eq( 10, 11 )
        or mysql_version_eq( 11, 4 )
        or mysql_version_eq( 11, 8 )
        or mysql_version_eq( 12, 3 ) )
    {
        goodprint "Currently running supported MySQL/MariaDB version "
          . $myvar{'version'} . "(LTS)";
        return;
    }
    else {
        badprint "Your MySQL version "
          . $myvar{'version'}
          . " is EOL software. Upgrade soon!";
        push( @generalrec,
            "You are using an unsupported version for production environments"
        );
        push( @generalrec,
            "Upgrade as soon as possible to a supported version !" );

    }
}

# Checks if MySQL version is equal to (major, minor, micro)
sub mysql_version_eq {
    my ( $maj, $min, $mic ) = @_;
    return 0 unless defined $myvar{'version'};
    my ( $v_maj, $v_min, $v_mic ) =
      $myvar{'version'} =~ /^(\d+)(?:\.(\d+))?(?:\.(\d+))?/;
    $v_maj //= 0;
    $v_min //= 0;
    $v_mic //= 0;

    return int($v_maj) == int($maj)
      if ( !defined($min) && !defined($mic) );
    return int($v_maj) == int($maj) && int($v_min) == int($min)
      if ( !defined($mic) );
    return ( int($v_maj) == int($maj)
          && int($v_min) == int($min)
          && int($v_mic) == int($mic) );
}

# Checks if MySQL version is greater than equal to (major, minor, micro)
sub mysql_version_ge {
    my ( $maj, $min, $mic ) = @_;
    return 0 unless defined $myvar{'version'};
    $min ||= 0;
    $mic ||= 0;
    my ( $v_maj, $v_min, $v_mic ) =
      $myvar{'version'} =~ /^(\d+)(?:\.(\d+))?(?:\.(\d+))?/;
    $v_maj //= 0;
    $v_min //= 0;
    $v_mic //= 0;

    return
         int($v_maj) > int($maj)
      || ( int($v_maj) == int($maj) && int($v_min) > int($min) )
      || ( int($v_maj) == int($maj)
        && int($v_min) == int($min)
        && int($v_mic) >= int($mic) );
}

# Checks if MySQL version is lower than equal to (major, minor, micro)
sub mysql_version_le {
    my ( $maj, $min, $mic ) = @_;
    return 0 unless defined $myvar{'version'};
    $min ||= 0;
    $mic ||= 0;
    my ( $v_maj, $v_min, $v_mic ) =
      $myvar{'version'} =~ /^(\d+)(?:\.(\d+))?(?:\.(\d+))?/;
    $v_maj //= 0;
    $v_min //= 0;
    $v_mic //= 0;

    return
         int($v_maj) < int($maj)
      || ( int($v_maj) == int($maj) && int($v_min) < int($min) )
      || ( int($v_maj) == int($maj)
        && int($v_min) == int($min)
        && int($v_mic) <= int($mic) );
}

# Checks for 32-bit boxes with more than 2GB of RAM
my ($arch);

sub check_architecture {
    my $prefix = get_transport_prefix();
    if ( is_remote eq 1 || $prefix ne '' ) {
        infoprint "Skipping architecture check on remote host";
        infoprint "Using default $opt{defaultarch} bits as target architecture";
        $arch = $opt{defaultarch};
        return;
    }
    elsif ($is_win) {
        if ( execute_system_command('wmic os get osarchitecture') =~ /64/ ) {
            goodprint "Operating on 64-bit architecture";
            $arch = 64;
        }
    }
    else {
        my ( $sysname, $nodename, $release, $version, $machine );
        if ( $prefix eq '' ) {
            ( $sysname, $nodename, $release, $version, $machine ) =
              POSIX::uname();
        }
        else {
            $sysname = execute_system_command('uname');
            $machine = execute_system_command('uname -m');
        }

        if ( $sysname =~ /SunOS/ ) {
            if ( execute_system_command('isainfo -b') =~ /64/ ) {
                $arch = 64;
                goodprint "Operating on 64-bit architecture";
            }
        }
        elsif ( $sysname =~ /AIX/ ) {
            if ( execute_system_command('bootinfo -K') =~ /64/ ) {
                $arch = 64;
                goodprint "Operating on 64-bit architecture";
            }
        }
        elsif ( $sysname =~ /NetBSD|OpenBSD/ ) {
            if ( execute_system_command('sysctl -b hw.machine') =~ /64/ ) {
                $arch = 64;
                goodprint "Operating on 64-bit architecture";
            }
        }
        elsif ( $sysname =~ /FreeBSD/ ) {
            if ( execute_system_command('sysctl -b hw.machine_arch') =~ /64/ ) {
                $arch = 64;
                goodprint "Operating on 64-bit architecture";
            }
        }
        elsif ( $sysname =~ /Darwin/ && $machine =~ /Power Macintosh/ ) {

# Darwin box.local 9.8.0 Darwin Kernel Version 9.8.0: Wed Jul 15 16:57:01 PDT 2009; root:xnu1228.15.4~1/RELEASE_PPC Power Macintosh
            $arch = 64;
            goodprint "Operating on 64-bit architecture";
        }
        elsif ( $sysname =~ /Darwin/ && $machine =~ /x86_64/ ) {

# Darwin gibas.local 12.6.0 Darwin Kernel Version 12.3.0: Sun Jan 6 22:37:10 PST 2013; root:xnu-2050.22.13~1/RELEASE_X86_64 x86_64
            $arch = 64;
            goodprint "Operating on 64-bit architecture";
        }
        elsif ( $machine =~ /(64|s390x|x86_64|amd64)/ ) {
            $arch = 64;
            goodprint "Operating on 64-bit architecture";
        }
        else {
            $arch = 32;
            if ( $physical_memory > 2147483648 ) {
                badprint
"Switch to 64-bit OS - MySQL cannot currently use all of your RAM";
            }
            else {
                goodprint
                  "Operating on 32-bit architecture with less than 2GB RAM";
            }
        }
    }
    $result{'OS'}{'Architecture'} = "$arch bits";

}

# Start up a ton of storage engine counts/statistics
my ( %enginestats, %enginecount, $fragtables );

sub check_storage_engines {
    subheaderprint "Storage Engine Statistics";
    if ( $opt{skipsize} eq 1 ) {
        infoprint "Skipped due to --skipsize option";
        return;
    }

    my $engines;
    if ( mysql_version_ge( 5, 5 ) ) {
        my @engineresults = select_array
"SELECT ENGINE,SUPPORT FROM information_schema.ENGINES ORDER BY ENGINE ASC";
        foreach my $line (@engineresults) {
            my ( $engine, $engineenabled );
            ( $engine, $engineenabled ) = $line =~ /([a-zA-Z_]*)\s+([a-zA-Z]+)/;
            $result{'Engine'}{$engine}{'Enabled'} = $engineenabled;
            $engines .=
              ( $engineenabled eq "YES" || $engineenabled eq "DEFAULT" )
              ? greenwrap "+" . $engine . " "
              : redwrap "-" . $engine . " ";
        }
    }
    elsif ( mysql_version_ge( 5, 1, 5 ) ) {
        my @engineresults = select_array
"SELECT ENGINE, SUPPORT FROM information_schema.ENGINES WHERE ENGINE NOT IN ('MyISAM', 'MERGE', 'MEMORY') ORDER BY ENGINE";
        foreach my $line (@engineresults) {
            my ( $engine, $engineenabled );
            ( $engine, $engineenabled ) = $line =~ /([a-zA-Z_]*)\s+([a-zA-Z]+)/;
            $result{'Engine'}{$engine}{'Enabled'} = $engineenabled;
            $engines .=
              ( $engineenabled eq "YES" || $engineenabled eq "DEFAULT" )
              ? greenwrap "+" . $engine . " "
              : redwrap "-" . $engine . " ";
        }
    }
    else {
        $engines .=
          ( defined $myvar{'have_archive'} && $myvar{'have_archive'} eq "YES" )
          ? greenwrap "+Archive "
          : redwrap "-Archive ";
        $engines .=
          ( defined $myvar{'have_bdb'} && $myvar{'have_bdb'} eq "YES" )
          ? greenwrap "+BDB "
          : redwrap "-BDB ";
        $engines .=
          ( defined $myvar{'have_federated_engine'}
              && $myvar{'have_federated_engine'} eq "YES" )
          ? greenwrap "+Federated "
          : redwrap "-Federated ";
        $engines .=
          ( defined $myvar{'have_innodb'} && $myvar{'have_innodb'} eq "YES" )
          ? greenwrap "+InnoDB "
          : redwrap "-InnoDB ";
        $engines .=
          ( defined $myvar{'have_isam'} && $myvar{'have_isam'} eq "YES" )
          ? greenwrap "+ISAM "
          : redwrap "-ISAM ";
        $engines .=
          ( defined $myvar{'have_ndbcluster'}
              && $myvar{'have_ndbcluster'} eq "YES" )
          ? greenwrap "+NDBCluster "
          : redwrap "-NDBCluster ";
    }

    my @dblist = grep { $_ ne 'lost+found' } select_array "SHOW DATABASES";

    $result{'Databases'}{'List'} = [@dblist];
    infoprint "Status: $engines";
    if ( mysql_version_ge( 5, 1, 5 ) ) {

# MySQL 5+ servers can have table sizes calculated quickly from information schema
        my @templist = select_array
"SELECT ENGINE, SUM(DATA_LENGTH+INDEX_LENGTH), COUNT(ENGINE), SUM(DATA_LENGTH), SUM(INDEX_LENGTH) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema', 'performance_schema', 'mysql') AND ENGINE IS NOT NULL GROUP BY ENGINE ORDER BY ENGINE ASC;";

        my ( $engine, $size, $count, $dsize, $isize );
        foreach my $line (@templist) {
            ( $engine, $size, $count, $dsize, $isize ) =
              $line =~ /([a-zA-Z_]+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/;
            debugprint "Engine Found: $engine";
            next unless defined $engine;
            $size  //= 0;
            $isize //= 0;
            $dsize //= 0;
            $count //= 0;
            $enginestats{$engine}                      = $size;
            $enginecount{$engine}                      = $count;
            $result{'Engine'}{$engine}{'Table Number'} = $count;
            $result{'Engine'}{$engine}{'Total Size'}   = $size;
            $result{'Engine'}{$engine}{'Data Size'}    = $dsize;
            $result{'Engine'}{$engine}{'Index Size'}   = $isize;
        }

        #print Dumper( \%enginestats ) if $opt{debug};
        my $not_innodb = '';
        if ( not defined $result{'Variables'}{'innodb_file_per_table'} ) {
            $not_innodb = "AND NOT ENGINE='InnoDB'";
        }
        elsif ( $result{'Variables'}{'innodb_file_per_table'} eq 'OFF' ) {
            $not_innodb = "AND NOT ENGINE='InnoDB'";
        }
        $result{'Tables'}{'Fragmented tables'} =
          [ select_array
"SELECT TABLE_SCHEMA, TABLE_NAME, ENGINE, CAST(DATA_FREE AS SIGNED) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema', 'performance_schema', 'mysql') AND DATA_LENGTH/1024/1024>100 AND cast(DATA_FREE as signed)*100/(DATA_LENGTH+INDEX_LENGTH+cast(DATA_FREE as signed)) > 10 AND NOT ENGINE='MEMORY' $not_innodb"
          ];
        $fragtables = scalar @{ $result{'Tables'}{'Fragmented tables'} };
        if ( $opt{dumpdir} ) {
            select_csv_file( "$opt{dumpdir}/fragmented_tables.csv",
"SELECT TABLE_SCHEMA, TABLE_NAME, ENGINE, CAST(DATA_FREE AS SIGNED) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema', 'performance_schema', 'mysql') AND DATA_LENGTH/1024/1024>100 AND cast(DATA_FREE as signed)*100/(DATA_LENGTH+INDEX_LENGTH+cast(DATA_FREE as signed)) > 10 AND NOT ENGINE='MEMORY' $not_innodb"
            );
        }

    }
    else {

        # MySQL < 5 servers take a lot of work to get table sizes
        my @tblist;

# Now we build a database list, and loop through it to get storage engine stats for tables
        foreach my $db (@dblist) {
            chomp($db);
            if (   $db eq "information_schema"
                or $db eq "performance_schema"
                or $db eq "mysql"
                or $db eq "lost+found" )
            {
                next;
            }
            my @ixs = ( 1, 6, 9 );
            if ( !mysql_version_ge( 4, 1 ) ) {

                # MySQL 3.23/4.0 keeps Data_Length in the 5th (0-based) column
                @ixs = ( 1, 5, 8 );
            }
            my $cmd = "SHOW TABLE STATUS FROM \\\`$db\\\`";
            if ($is_win) {
                $cmd = "SHOW TABLE STATUS FROM \`$db\`";
            }
            push( @tblist, map { [ (split)[@ixs] ] } select_array $cmd );
        }

     # Parse through the table list to generate storage engine counts/statistics
        $fragtables = 0;
        foreach my $tbl (@tblist) {

            #debugprint "Data dump " . Dumper(@$tbl) if $opt{debug};
            my ( $engine, $size, $datafree ) = @$tbl;
            next if $engine eq 'NULL' or not defined($engine);
            $size     = 0 if $size eq 'NULL'     or not defined($size);
            $datafree = 0 if $datafree eq 'NULL' or not defined($datafree);
            if ( defined $enginestats{$engine} ) {
                $enginestats{$engine} += $size;
                $enginecount{$engine} += 1;
            }
            else {
                $enginestats{$engine} = $size;
                $enginecount{$engine} = 1;
            }
            if ( $datafree > 0 ) {
                $fragtables++;
            }
        }
    }
    foreach my $engine ( sort keys %enginestats ) {
        my $size = $enginestats{$engine};
        infoprint "Data in $engine tables: "
          . hr_bytes($size)
          . " (Tables: "
          . $enginecount{$engine} . ")" . "";
    }

    # If the storage engine isn't being used, recommend it to be disabled
    if (  !defined $enginestats{'InnoDB'}
        && defined $myvar{'have_innodb'}
        && $myvar{'have_innodb'} eq "YES" )
    {
        badprint "InnoDB is enabled, but isn't being used";
        push( @generalrec,
            "Add skip-innodb to MySQL configuration to disable InnoDB" );
    }
    if (  !defined $enginestats{'BerkeleyDB'}
        && defined $myvar{'have_bdb'}
        && $myvar{'have_bdb'} eq "YES" )
    {
        badprint "BDB is enabled, but isn't being used";
        push( @generalrec,
            "Add skip-bdb to MySQL configuration to disable BDB" );
    }
    if (  !defined $enginestats{'ISAM'}
        && defined $myvar{'have_isam'}
        && $myvar{'have_isam'} eq "YES" )
    {
        badprint "MyISAM is enabled, but isn't being used";
        push( @generalrec,
"Add skip-isam to MySQL configuration to disable MyISAM (MySQL > 4.1.0)"
        );
    }

    # Fragmented tables
    if ( $fragtables > 0 ) {
        badprint "Total fragmented tables: $fragtables";
        push @generalrec,
'Run ALTER TABLE ... FORCE or OPTIMIZE TABLE to defragment tables for better performance';
        my $total_free            = 0;
        my $fragmented_tables_csv = "schema,table,free_space_mb,sql\n";
        foreach my $table_line ( @{ $result{'Tables'}{'Fragmented tables'} } ) {
            my ( $table_schema, $table_name, $engine, $data_free ) =
              split /\t/msx, $table_line;
            $data_free = $data_free / 1024 / 1024;
            $total_free += $data_free;
            my $generalrec;
            my $fragmented_tables_sql;
            if ( $engine eq 'InnoDB' ) {
                $fragmented_tables_sql =
                  "ALTER TABLE `$table_schema`.`$table_name` FORCE;";
                $generalrec = "  $fragmented_tables_sql";
            }
            else {
                $fragmented_tables_sql =
                  "OPTIMIZE TABLE `$table_schema`.`$table_name`;";
                $generalrec = "  $fragmented_tables_sql";
            }
            $fragmented_tables_csv .=
"$table_schema,$table_name,$data_free,\"$fragmented_tables_sql\"\n";
            $generalrec .= " -- can free $data_free MiB";
            push @generalrec, $generalrec;
        }
        dump_into_file( 'fragmented_tables.csv', $fragmented_tables_csv );
        push @generalrec,
"Consider defragmenting $fragtables tables to free up $total_free MiB";
    }
    else {
        goodprint "Total fragmented tables: $fragtables";
    }

    # Auto increments
    my $maxint = select_one "SELECT ~0";
    $result{'MaxInt'} = $maxint;

    if ( mysql_version_ge( 5, 0 ) ) {
        my $ignore_tables_sql = "";
        if ( $opt{'ignore-tables'} ) {
            my @ignored = split /,/, $opt{'ignore-tables'};
            $ignore_tables_sql =
              " AND t.TABLE_NAME NOT IN ('" . join( "','", @ignored ) . "')";
        }
        my @auto_increment_results = select_array(
"SELECT t.TABLE_SCHEMA, t.TABLE_NAME, t.TABLE_ROWS, t.AUTO_INCREMENT, c.DATA_TYPE, c.COLUMN_TYPE "
              . "FROM information_schema.TABLES t "
              . "JOIN information_schema.COLUMNS c ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME "
              . "WHERE t.TABLE_SCHEMA NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys') "
              . "AND t.AUTO_INCREMENT IS NOT NULL "
              . "AND c.EXTRA LIKE '%auto_increment%'$ignore_tables_sql;" );

        foreach my $line (@auto_increment_results) {
            my ( $db, $name, $rows, $autoincrement, $data_type, $col_type ) =
              split /\t/, $line;
            next unless defined $autoincrement && $autoincrement =~ /^\d+?$/;

# Issue #37: Skip tables where AUTO_INCREMENT is at default (never used) and table is empty
            next if ( $autoincrement <= 1 && ( $rows // 0 ) == 0 );

            my $limit = get_type_max( $data_type, $col_type );
            next unless defined $limit && $limit > 0;

            my $percent = percentage( $autoincrement, $limit );
            $result{'PctAutoIncrement'}{"$db.$name"} = $percent;
            if ( $percent >= 75 ) {
                badprint
"Table '$db.$name' has an autoincrement value near max capacity ($percent%)";
            }
        }
    }
    else {
        my %tblist;

# Now we use a database list, and loop through it to get storage engine stats for tables
        foreach my $db (@dblist) {
            chomp($db);

            if ( !$tblist{$db} ) {
                $tblist{$db} = ();
            }

            if ( $db eq "information_schema" ) { next; }
            my @ia = ( 0, 4, 10 );
            if ( !mysql_version_ge( 4, 1 ) ) {

                # MySQL 3.23/4.0 keeps Data_Length in the 5th (0-based) column
                @ia = ( 0, 4, 9 );
            }
            my $cmd = "SHOW TABLE STATUS FROM \\\`$db\\\`";
            if ($is_win) {
                $cmd = "SHOW TABLE STATUS FROM \`$db\`";
            }
            push(
                @{ $tblist{$db} },
                map { [ (split)[@ia] ] } select_array $cmd
            );
        }

        my @dbnames = keys %tblist;

        foreach my $db (@dbnames) {
            foreach my $tbl ( @{ $tblist{$db} } ) {
                my ( $name, $rows, $autoincrement ) = @$tbl;

                if ( $autoincrement && $autoincrement =~ /^\d+?$/ ) {

# Issue #37: Skip tables where AUTO_INCREMENT is at default (never used) and table is empty
                    next if ( $autoincrement <= 1 && ( $rows // 0 ) == 0 );

         # Issue #37: Guard against unresolved column max producing a false 100%
                    next unless defined $maxint && $maxint > 0;

                    my $percent = percentage( $autoincrement, $maxint );
                    $result{'PctAutoIncrement'}{"$db.$name"} = $percent;
                    if ( $percent >= 75 ) {
                        badprint
"Table '$db.$name' has an autoincrement value near max capacity ($percent%)";
                    }
                }
            }
        }
    }
}

sub dump_into_file {
    my $file    = shift;
    my $content = shift;
    if ( defined( $opt{dumpdir} ) && $opt{dumpdir} ne '' && -d $opt{dumpdir} ) {
        my $actual_file   = "$opt{dumpdir}/$file";
        my $gzip_bin      = which('gzip');
        my $is_compressed = 0;
        my $start_time =
          eval { require Time::HiRes; Time::HiRes::time(); } || time();
        my $fh;
        if ( $opt{'compress-dump'} && $gzip_bin && $file =~ /\.csv$/ ) {
            $actual_file .= '.gz';
            my $escaped_file = $actual_file;
            $escaped_file =~ s/'/'\\''/g;
            open( $fh, '|-', "$gzip_bin -c > '$escaped_file'" )
              or die "Can't open gzip pipe: $!";
            $is_compressed = 1;
        }
        else {
            open( $fh, ">$actual_file" ) or die "Can't open $actual_file: $!";
        }
        print $fh $content;
        close $fh;
        my $end_time =
          eval { require Time::HiRes; Time::HiRes::time(); } || time();
        my $duration = $end_time - $start_time;

        if ( $duration > 5.0 ) {
            badprint "I/O Latency Notice: Export of "
              . basename($actual_file)
              . " took "
              . sprintf( "%.2fs", $duration )
              . " (threshold: 5s). Slow disk subsystem?";
        }

        # Count rows in content (excluding header line if csv)
        my @lines     = split( "\n", $content );
        my $row_count = scalar(@lines);
        my $data_rows =
          ( $file =~ /\.csv$/ && $row_count > 0 ) ? $row_count - 1 : $row_count;

        my $size = -s $actual_file;
        my $base = basename($actual_file);
        $exported_manifest{$base} = {
            rows       => $data_rows,
            size       => $size,
            duration   => sprintf( "%.3f", $duration ),
            query      => "dump_into_file",
            compressed => $is_compressed
        };

        infoprint "Data saved to $actual_file";
    }
}

sub calculations {
    if ( $mystat{'Questions'} < 1 ) {
        badprint "Your server has not answered any queries: cannot continue...";
        exit 2;
    }

    my $is_mariadb = ( $myvar{'version'} // '' ) =~ /mariadb/i
      || ( $myvar{'version_comment'} // '' ) =~ /mariadb/i;

    if ( defined $myvar{'version'} ) {
        $myvar{'version'} =~ s/(.+)-.*?$/$1/;
    }

    #infoprint "====>>>> MySQL version updated: $myvar{'version'}";
    # Server-wide memory
    $mycalc{'max_tmp_table_size'} =
      ( $myvar{'tmp_table_size'} > $myvar{'max_heap_table_size'} )
      ? $myvar{'max_heap_table_size'}
      : $myvar{'tmp_table_size'};

    # Per-thread memory
    $mycalc{'per_thread_buffers'} = 0;
    $mycalc{'per_thread_buffers'} += $myvar{'read_buffer_size'}
      if is_int( $myvar{'read_buffer_size'} );
    $mycalc{'per_thread_buffers'} += $myvar{'read_rnd_buffer_size'}
      if is_int( $myvar{'read_rnd_buffer_size'} );
    $mycalc{'per_thread_buffers'} += $myvar{'sort_buffer_size'}
      if is_int( $myvar{'sort_buffer_size'} );
    $mycalc{'per_thread_buffers'} += $myvar{'thread_stack'}
      if is_int( $myvar{'thread_stack'} );
    $mycalc{'per_thread_buffers'} += $myvar{'join_buffer_size'}
      if is_int( $myvar{'join_buffer_size'} );
    $mycalc{'per_thread_buffers'} += $myvar{'binlog_cache_size'}
      if is_int( $myvar{'binlog_cache_size'} );
    $mycalc{'per_thread_buffers'} += $mycalc{'max_tmp_table_size'}
      if is_int( $mycalc{'max_tmp_table_size'} );

    debugprint "per_thread_buffers: $mycalc{'per_thread_buffers'} ("
      . human_size( $mycalc{'per_thread_buffers'} ) . " )";

# Error max_allowed_packet is not included in thread buffers size
#$mycalc{'per_thread_buffers'} += $myvar{'max_allowed_packet'} if is_int($myvar{'max_allowed_packet'});

    # Total per-thread memory
    my $per_thread_buffers_without_tmp = $mycalc{'per_thread_buffers'};
    if ( is_int( $mycalc{'max_tmp_table_size'} ) ) {
        $per_thread_buffers_without_tmp -= $mycalc{'max_tmp_table_size'};
    }

    my $internal_tmp_engine = $myvar{'internal_tmp_mem_storage_engine'}
      // 'TempTable';

    if (   defined $myvar{'temptable_max_ram'}
        && is_int( $myvar{'temptable_max_ram'} )
        && !$is_mariadb
        && $internal_tmp_engine eq 'TempTable' )
    {
        my $total_tmp_connections =
          $per_thread_buffers_without_tmp * $myvar{'max_connections'};
        my $max_tmp_limit =
          ( $mycalc{'max_tmp_table_size'} // 0 ) * $myvar{'max_connections'};
        my $actual_tmp_ram =
          ( $myvar{'temptable_max_ram'} < $max_tmp_limit )
          ? $myvar{'temptable_max_ram'}
          : $max_tmp_limit;
        $mycalc{'total_per_thread_buffers'} =
          $total_tmp_connections + $actual_tmp_ram;

        my $total_tmp_used_connections =
          $per_thread_buffers_without_tmp * $mystat{'Max_used_connections'};
        my $max_tmp_used_limit = ( $mycalc{'max_tmp_table_size'} // 0 ) *
          $mystat{'Max_used_connections'};
        my $actual_tmp_used_ram =
          ( $myvar{'temptable_max_ram'} < $max_tmp_used_limit )
          ? $myvar{'temptable_max_ram'}
          : $max_tmp_used_limit;
        $mycalc{'max_total_per_thread_buffers'} =
          $total_tmp_used_connections + $actual_tmp_used_ram;
    }
    else {
        $mycalc{'total_per_thread_buffers'} =
          $mycalc{'per_thread_buffers'} * $myvar{'max_connections'};

        # Max total per-thread memory reached
        $mycalc{'max_total_per_thread_buffers'} =
          $mycalc{'per_thread_buffers'} * $mystat{'Max_used_connections'};
    }

    $mycalc{'server_buffers'} = $myvar{'key_buffer_size'};
    $mycalc{'server_buffers'} +=
      ( defined $myvar{'innodb_buffer_pool_size'} )
      ? $myvar{'innodb_buffer_pool_size'}
      : 0;
    $mycalc{'server_buffers'} +=
      ( defined $myvar{'innodb_additional_mem_pool_size'} )
      ? $myvar{'innodb_additional_mem_pool_size'}
      : 0;
    $mycalc{'server_buffers'} +=
      ( defined $myvar{'innodb_log_buffer_size'} )
      ? $myvar{'innodb_log_buffer_size'}
      : 0;
    $mycalc{'server_buffers'} +=
      ( defined $myvar{'query_cache_size'} ) ? $myvar{'query_cache_size'} : 0;
    $mycalc{'server_buffers'} +=
      ( defined $myvar{'aria_pagecache_buffer_size'} )
      ? $myvar{'aria_pagecache_buffer_size'}
      : 0;

# Global memory
# Max used memory is memory used by MySQL based on Max_used_connections
# This is the max memory used theoretically calculated with the max concurrent connection number reached by mysql
    $mycalc{'max_used_memory'} =
      $mycalc{'server_buffers'} +
      $mycalc{"max_total_per_thread_buffers"} +
      get_pf_memory();

    #   + get_gcache_memory();
    $mycalc{'pct_max_used_memory'} =
      percentage( $mycalc{'max_used_memory'}, $physical_memory );

# Total possible memory is memory needed by MySQL based on max_connections
# This is the max memory MySQL can theoretically used if all connections allowed has opened by mysql
    $mycalc{'max_peak_memory'} =
      $mycalc{'server_buffers'} +
      $mycalc{'total_per_thread_buffers'} +
      get_pf_memory();

    # +  get_gcache_memory();
    $mycalc{'pct_max_physical_memory'} =
      percentage( $mycalc{'max_peak_memory'}, $physical_memory );

    debugprint "Max Used Memory: "
      . hr_bytes( $mycalc{'max_used_memory'} ) . "";
    debugprint "Max Used Percentage RAM: "
      . $mycalc{'pct_max_used_memory'} . "%";

    debugprint "Max Peak Memory: "
      . hr_bytes( $mycalc{'max_peak_memory'} ) . "";
    debugprint "Max Peak Percentage RAM: "
      . $mycalc{'pct_max_physical_memory'} . "%";

    # Slow queries
    if ( $mystat{'Questions'} > 0 ) {
        $mycalc{'pct_slow_queries'} =
          int( ( $mystat{'Slow_queries'} / $mystat{'Questions'} ) * 100 );
    }
    else {
        $mycalc{'pct_slow_queries'} = 0;
    }

    # Connections
    if ( $myvar{'max_connections'} > 0 ) {
        $mycalc{'pct_connections_used'} = int(
            ( $mystat{'Max_used_connections'} / $myvar{'max_connections'} ) *
              100 );
    }
    else {
        $mycalc{'pct_connections_used'} = 0;
    }
    $mycalc{'pct_connections_used'} =
      ( $mycalc{'pct_connections_used'} > 100 )
      ? 100
      : $mycalc{'pct_connections_used'};

    # Aborted Connections
    if ( $mystat{'Connections'} > 0 ) {
        $mycalc{'pct_connections_aborted'} =
          percentage( $mystat{'Aborted_connects'}, $mystat{'Connections'} );
    }
    else {
        $mycalc{'pct_connections_aborted'} = 0;
    }
    debugprint "Aborted_connects: " . $mystat{'Aborted_connects'} . "";
    debugprint "Connections: " . $mystat{'Connections'} . "";
    debugprint "pct_connections_aborted: "
      . $mycalc{'pct_connections_aborted'} . "";

    # Key buffers
    if (   mysql_version_ge( 4, 1 )
        && defined $myvar{'key_buffer_size'}
        && $myvar{'key_buffer_size'} > 0
        && defined $mystat{'Key_blocks_unused'}
        && defined $myvar{'key_cache_block_size'} )
    {
        $mycalc{'pct_key_buffer_used'} = sprintf(
            "%.1f",
            (
                1 - (
                    (
                        $mystat{'Key_blocks_unused'} *
                          $myvar{'key_cache_block_size'}
                    ) / $myvar{'key_buffer_size'}
                )
            ) * 100
        );
    }
    else {
        $mycalc{'pct_key_buffer_used'} = 0;
    }

    if ( defined $mystat{'Key_read_requests'}
        and $mystat{'Key_read_requests'} > 0 )
    {
        $mycalc{'pct_keys_from_mem'} = sprintf(
            "%.1f",
            (
                100 - (
                    ( $mystat{'Key_reads'} / $mystat{'Key_read_requests'} ) *
                      100
                )
            )
        );
    }
    else {
        $mycalc{'pct_keys_from_mem'} = 0;
    }
    if ( defined( $mystat{'Aria_pagecache_read_requests'} )
        and $mystat{'Aria_pagecache_read_requests'} > 0 )
    {
        $mycalc{'pct_aria_keys_from_mem'} = sprintf(
            "%.1f",
            (
                100 - (
                    (
                        $mystat{'Aria_pagecache_reads'} /
                          $mystat{'Aria_pagecache_read_requests'}
                    ) * 100
                )
            )
        );
    }
    else {
        $mycalc{'pct_aria_keys_from_mem'} = 0;
    }

    if ( defined $mystat{'Key_write_requests'}
        and $mystat{'Key_write_requests'} > 0 )
    {
        $mycalc{'pct_wkeys_from_mem'} = sprintf( "%.1f",
            ( ( $mystat{'Key_writes'} / $mystat{'Key_write_requests'} ) * 100 )
        );
    }
    else {
        $mycalc{'pct_wkeys_from_mem'} = 0;
    }

    if ( defined $doremote and $doremote eq 0 and !mysql_version_ge(5) ) {
        if ($is_win) {
            my $size = 0;
            my @allfiles =
              execute_system_command("dir /-c /s $myvar{'datadir'}");
            foreach (
                map  { /^\s*\d+\/\S+\s+\S+\s+(A|P)M\s+(\d+)\s/i; $2 }
                grep { /\.MYI$/i } @allfiles
              )
            {
                $size += $_;
            }
            $mycalc{'total_myisam_indexes'} = $size;
            $size = 0;
            foreach (
                map  { /^\s*\d+\/\S+\s+\S+\s+(A|P)M\s+(\d+)\s/i; $2 }
                grep { /\.MAI$/i } @allfiles
              )
            {
                $size += $_;
            }
            $mycalc{'total_aria_indexes'} = $size;
        }
        else {
            my $size = 0;
            $size += (split)[0]
              for execute_system_command(
"find '$myvar{'datadir'}' -name '*.MYI' -print0 2>&1 | xargs $xargsflags -0 du -L $duflags 2>&1"
              );
            $mycalc{'total_myisam_indexes'} = $size;
            $size = 0 + (split)[0]
              for execute_system_command(
"find '$myvar{'datadir'}' -name '*.MAI' -print0 2>&1 | xargs $xargsflags -0 du -L $duflags 2>&1"
              );
            $mycalc{'total_aria_indexes'} = $size;
        }
    }
    elsif ( mysql_version_ge(5) ) {
        $mycalc{'total_myisam_indexes'} = select_one
"SELECT IFNULL(SUM(INDEX_LENGTH), 0) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema') AND ENGINE = 'MyISAM';";
        $mycalc{'total_aria_indexes'} = select_one
"SELECT IFNULL(SUM(INDEX_LENGTH), 0) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema') AND ENGINE = 'Aria';";
    }
    if ( defined $mycalc{'total_myisam_indexes'} ) {
        chomp( $mycalc{'total_myisam_indexes'} );
    }
    if ( defined $mycalc{'total_aria_indexes'} ) {
        chomp( $mycalc{'total_aria_indexes'} );
    }

    # Query cache
    if ( mysql_version_ge(8) and mysql_version_le(10) ) {
        $mycalc{'query_cache_efficiency'} = 0;
    }
    elsif ( mysql_version_ge(4) ) {
        if ( ( $mystat{'Com_select'} || 0 ) + ( $mystat{'Qcache_hits'} || 0 ) >
            0 )
        {
            $mycalc{'query_cache_efficiency'} = sprintf(
                "%.1f",
                (
                    $mystat{'Qcache_hits'} /
                      ( $mystat{'Com_select'} + $mystat{'Qcache_hits'} )
                ) * 100
            );
        }
        else {
            $mycalc{'query_cache_efficiency'} = 0;
        }
        if ( $myvar{'query_cache_size'} ) {
            $mycalc{'pct_query_cache_used'} = sprintf(
                "%.1f",
                100 - (
                    $mystat{'Qcache_free_memory'} / $myvar{'query_cache_size'}
                ) * 100
            );
        }
        if ( ( $mystat{'Qcache_lowmem_prunes'} || 0 ) == 0 ) {
            $mycalc{'query_cache_prunes_per_day'} = 0;
        }
        else {
            if ( ( $mystat{'Uptime'} || 0 ) > 0 ) {
                $mycalc{'query_cache_prunes_per_day'} =
                  int( $mystat{'Qcache_lowmem_prunes'} /
                      ( $mystat{'Uptime'} / 86400 ) );
            }
            else {
                $mycalc{'query_cache_prunes_per_day'} = 0;
            }
        }
    }

    # Sorting
    $mycalc{'total_sorts'} =
      ( $mystat{'Sort_scan'} || 0 ) + ( $mystat{'Sort_range'} || 0 );
    if ( $mycalc{'total_sorts'} > 0 ) {
        $mycalc{'pct_temp_sort_table'} = int(
            ( $mystat{'Sort_merge_passes'} / $mycalc{'total_sorts'} ) * 100 );
    }

    # Joins
    $mycalc{'joins_without_indexes'} =
      ( $mystat{'Select_range_check'} || 0 ) +
      ( $mystat{'Select_full_join'}   || 0 );
    if ( ( $mystat{'Uptime'} || 0 ) > 0 ) {
        $mycalc{'joins_without_indexes_per_day'} = int(
            $mycalc{'joins_without_indexes'} / ( $mystat{'Uptime'} / 86400 ) );
    }
    else {
        $mycalc{'joins_without_indexes_per_day'} = 0;
    }

    # Temporary tables
    if ( ( $mystat{'Created_tmp_tables'} // 0 ) > 0 ) {
        if ( ( $mystat{'Created_tmp_disk_tables'} // 0 ) > 0 ) {
            $mycalc{'pct_temp_disk'} = int(
                (
                    $mystat{'Created_tmp_disk_tables'} /
                      $mystat{'Created_tmp_tables'}
                ) * 100
            );
        }
        else {
            $mycalc{'pct_temp_disk'} = 0;
        }
    }

    # Table cache
    if ( defined $mystat{'Opened_tables'} and $mystat{'Opened_tables'} > 0 ) {
        if ( not defined( $mystat{'Table_open_cache_hits'} ) ) {
            $mycalc{'table_cache_hit_rate'} =
              int( ( $mystat{'Open_tables'} // 0 ) * 100 /
                  $mystat{'Opened_tables'} );
        }
        else {
            $mycalc{'table_cache_hit_rate'} = int(
                $mystat{'Table_open_cache_hits'} * 100 / (
                    $mystat{'Table_open_cache_hits'} +
                      $mystat{'Table_open_cache_misses'}
                )
            );
        }
    }
    else {
        $mycalc{'table_cache_hit_rate'} = 100;
    }

    # Open files
    if ( defined $myvar{'open_files_limit'} and $myvar{'open_files_limit'} > 0 )
    {
        $mycalc{'pct_files_open'} =
          int(
            ( $mystat{'Open_files'} // 0 ) * 100 / $myvar{'open_files_limit'} );
    }

    # Table locks
    if ( $mystat{'Table_locks_immediate'} > 0 ) {
        if ( $mystat{'Table_locks_waited'} == 0 ) {
            $mycalc{'pct_table_locks_immediate'} = 100;
        }
        else {
            $mycalc{'pct_table_locks_immediate'} = int(
                $mystat{'Table_locks_immediate'} * 100 / (
                    $mystat{'Table_locks_waited'} +
                      $mystat{'Table_locks_immediate'}
                )
            );
        }
    }

    # Thread cache
    if ( ( $mystat{'Connections'} || 0 ) > 0 ) {
        $mycalc{'thread_cache_hit_rate'} = int(
            100 - (
                (
                    ( $mystat{'Threads_created'} // 0 ) / $mystat{'Connections'}
                ) * 100
            )
        );
    }
    else {
        $mycalc{'thread_cache_hit_rate'} = 100;
    }

    # Other
    if ( ( $mystat{'Connections'} // 0 ) > 0 ) {
        $mycalc{'pct_aborted_connections'} =
          int(
            ( ( $mystat{'Aborted_connects'} // 0 ) / $mystat{'Connections'} ) *
              100 );
    }
    if ( ( $mystat{'Questions'} // 0 ) > 0 ) {
        $mycalc{'total_reads'} = $mystat{'Com_select'} // 0;
        $mycalc{'total_writes'} =
          ( $mystat{'Com_delete'}  // 0 ) +
          ( $mystat{'Com_insert'}  // 0 ) +
          ( $mystat{'Com_update'}  // 0 ) +
          ( $mystat{'Com_replace'} // 0 );
        if ( $mycalc{'total_reads'} == 0 ) {
            $mycalc{'pct_reads'}  = 0;
            $mycalc{'pct_writes'} = 100;
        }
        else {
            $mycalc{'pct_reads'} = int(
                (
                    $mycalc{'total_reads'} /
                      ( $mycalc{'total_reads'} + $mycalc{'total_writes'} )
                ) * 100
            );
            $mycalc{'pct_writes'} = 100 - $mycalc{'pct_reads'};
        }
    }

    # InnoDB
    $myvar{'innodb_log_files_in_group'} = 1
      unless defined( $myvar{'innodb_log_files_in_group'} );
    $myvar{'innodb_log_files_in_group'} = 1
      if $myvar{'innodb_log_files_in_group'} == 0;

    $myvar{"innodb_buffer_pool_instances"} = 1
      unless defined( $myvar{'innodb_buffer_pool_instances'} );
    if ( defined $myvar{'have_innodb'} and $myvar{'have_innodb'} eq "YES" ) {
        if ( defined $myvar{'innodb_redo_log_capacity'} ) {
            $mycalc{'innodb_log_size_pct'} =
              ( $myvar{'innodb_redo_log_capacity'} /
                  $myvar{'innodb_buffer_pool_size'} ) * 100;
        }
        else {
            $mycalc{'innodb_log_size_pct'} = 0;
            if (   defined $myvar{'innodb_log_file_size'}
                && $myvar{'innodb_log_file_size'} ne ''
                && defined $myvar{'innodb_buffer_pool_size'}
                && $myvar{'innodb_buffer_pool_size'} ne ''
                && $myvar{'innodb_buffer_pool_size'} != 0 )
            {
                $mycalc{'innodb_log_size_pct'} =
                  ( $myvar{'innodb_log_file_size'} *
                      $myvar{'innodb_log_files_in_group'} * 100 /
                      $myvar{'innodb_buffer_pool_size'} );
            }
        }
    }
    if ( !defined $myvar{'innodb_buffer_pool_size'} ) {
        $mycalc{'innodb_log_size_pct'}    = 0;
        $myvar{'innodb_buffer_pool_size'} = 0;
    }

    # InnoDB Buffer pool read cache efficiency
    (
        $mystat{'Innodb_buffer_pool_read_requests'},
        $mystat{'Innodb_buffer_pool_reads'}
      )
      = ( 1, 1 )
      unless defined $mystat{'Innodb_buffer_pool_reads'};
    $mycalc{'pct_read_efficiency'} = percentage(
        $mystat{'Innodb_buffer_pool_read_requests'},
        (
            $mystat{'Innodb_buffer_pool_read_requests'} +
              $mystat{'Innodb_buffer_pool_reads'}
        )
    ) if defined $mystat{'Innodb_buffer_pool_read_requests'};
    debugprint "pct_read_efficiency: " . $mycalc{'pct_read_efficiency'} . "";
    debugprint "Innodb_buffer_pool_reads: "
      . $mystat{'Innodb_buffer_pool_reads'} . "";
    debugprint "Innodb_buffer_pool_read_requests: "
      . $mystat{'Innodb_buffer_pool_read_requests'} . "";

    # InnoDB log write cache efficiency
    ( $mystat{'Innodb_log_write_requests'}, $mystat{'Innodb_log_writes'} ) =
      ( 1, 1 )
      unless defined $mystat{'Innodb_log_writes'};
    $mycalc{'pct_write_efficiency'} = percentage(
        ( $mystat{'Innodb_log_write_requests'} - $mystat{'Innodb_log_writes'} ),
        $mystat{'Innodb_log_write_requests'}
    ) if defined $mystat{'Innodb_log_write_requests'};
    debugprint "pct_write_efficiency: " . $mycalc{'pct_write_efficiency'} . "";
    debugprint "Innodb_log_writes: " . $mystat{'Innodb_log_writes'} . "";
    debugprint "Innodb_log_write_requests: "
      . $mystat{'Innodb_log_write_requests'} . "";
    $mycalc{'pct_innodb_buffer_used'} = percentage(
        (
            $mystat{'Innodb_buffer_pool_pages_total'} -
              $mystat{'Innodb_buffer_pool_pages_free'}
        ),
        $mystat{'Innodb_buffer_pool_pages_total'}
    ) if defined $mystat{'Innodb_buffer_pool_pages_total'};

    my $lreq =
        "select  ROUND( 100* sum(allocated)/ "
      . $myvar{'innodb_buffer_pool_size'}
      . ',1) FROM sys.x\$innodb_buffer_stats_by_table;';
    debugprint("lreq: $lreq");
    $mycalc{'innodb_buffer_alloc_pct'} = select_one($lreq)
      if ( $opt{experimental} );

    # Binlog Cache
    if (    defined $myvar{'log_bin'}
        and $myvar{'log_bin'} ne 'OFF'
        and defined $mystat{'Binlog_cache_use'}
        and defined $mystat{'Binlog_cache_disk_use'} )
    {
        $mycalc{'pct_binlog_cache'} = percentage(
            $mystat{'Binlog_cache_use'} - $mystat{'Binlog_cache_disk_use'},
            $mystat{'Binlog_cache_use'} );
    }
}

sub mysql_stats {
    subheaderprint "Performance Metrics";

    # Show uptime, queries per second, connections, traffic stats
    my $qps = 0;
    if ( ( $mystat{'Uptime'} || 0 ) > 0 ) {
        $qps =
          sprintf( "%.3f", ( $mystat{'Questions'} || 0 ) / $mystat{'Uptime'} );
    }
    push( @generalrec,
"MySQL was started within the last 24 hours: recommendations may be inaccurate"
    ) if ( $mystat{'Uptime'} < 86400 );
    infoprint "Up for: "
      . pretty_uptime( $mystat{'Uptime'} ) . " ("
      . hr_num( $mystat{'Questions'} ) . " q ["
      . hr_num($qps)
      . " qps], "
      . hr_num( $mystat{'Connections'} )
      . " conn," . " TX: "
      . hr_bytes_rnd( $mystat{'Bytes_sent'} )
      . ", RX: "
      . hr_bytes_rnd( $mystat{'Bytes_received'} ) . ")";
    infoprint "Reads / Writes: "
      . $mycalc{'pct_reads'} . "% / "
      . $mycalc{'pct_writes'} . "%";

    # Binlog Cache
    if ( $myvar{'log_bin'} eq 'OFF' ) {
        infoprint "Binary logging is disabled";
    }
    else {
        infoprint "Binary logging is enabled (GTID MODE: "
          . ( defined( $myvar{'gtid_mode'} ) ? $myvar{'gtid_mode'} : "OFF" )
          . ")";
    }

    # Memory usage
    infoprint "Physical Memory     : " . hr_bytes($physical_memory);
    infoprint "Max MySQL memory    : " . hr_bytes( $mycalc{'max_peak_memory'} );
    infoprint "Other process memory: " . hr_bytes( get_other_process_memory() );

    infoprint "Total buffers: "
      . hr_bytes( $mycalc{'server_buffers'} )
      . " global + "
      . hr_bytes( $mycalc{'per_thread_buffers'} )
      . " per thread ($myvar{'max_connections'} max threads)";
    infoprint "Performance_schema Max memory usage: "
      . hr_bytes_rnd( get_pf_memory() );
    $result{'Performance_schema'}{'memory'} = get_pf_memory();
    $result{'Performance_schema'}{'pretty_memory'} =
      hr_bytes_rnd( get_pf_memory() );
    infoprint "Galera GCache Max memory usage: "
      . hr_bytes_rnd( get_gcache_memory() );
    $result{'Galera'}{'GCache'}{'memory'} = get_gcache_memory();
    $result{'Galera'}{'GCache'}{'pretty_memory'} =
      hr_bytes_rnd( get_gcache_memory() );

    if ( $opt{buffers} ) {
        infoprint "Global Buffers";
        infoprint " +-- Key Buffer: "
          . hr_bytes( $myvar{'key_buffer_size'} ) . "";
        infoprint " +-- Max Tmp Table: "
          . hr_bytes( $mycalc{'max_tmp_table_size'} ) . "";

        if ( defined $myvar{'query_cache_type'} ) {
            infoprint "Query Cache Buffers";
            infoprint " +-- Query Cache: "
              . $myvar{'query_cache_type'} . " - "
              . (
                $myvar{'query_cache_type'} eq 0 |
                  $myvar{'query_cache_type'} eq 'OFF' ? "DISABLED"
                : (
                    $myvar{'query_cache_type'} eq 1 ? "ALL REQUESTS"
                    : "ON DEMAND"
                )
              ) . "";
            infoprint " +-- Query Cache Size: "
              . hr_bytes( $myvar{'query_cache_size'} ) . "";
        }

        infoprint "Per Thread Buffers";
        infoprint " +-- Read Buffer: "
          . hr_bytes( $myvar{'read_buffer_size'} ) . "";
        infoprint " +-- Read RND Buffer: "
          . hr_bytes( $myvar{'read_rnd_buffer_size'} ) . "";
        infoprint " +-- Sort Buffer: "
          . hr_bytes( $myvar{'sort_buffer_size'} ) . "";
        infoprint " +-- Thread stack: "
          . hr_bytes( $myvar{'thread_stack'} ) . "";
        infoprint " +-- Join Buffer: "
          . hr_bytes( $myvar{'join_buffer_size'} ) . "";
        if ( $myvar{'log_bin'} ne 'OFF' ) {
            infoprint "Binlog Cache Buffers";
            infoprint " +-- Binlog Cache: "
              . hr_bytes( $myvar{'binlog_cache_size'} ) . "";
        }
    }

    if (   $arch
        && $arch == 32
        && ( $mycalc{'max_used_memory'} || 0 ) > 2 * 1024 * 1024 * 1024 )
    {
        badprint
          "Allocating > 2GB RAM on 32-bit systems can cause system instability";
        badprint "Maximum reached memory usage: "
          . hr_bytes( $mycalc{'max_used_memory'} ) . " ("
          . ( $mycalc{'pct_max_used_memory'} // 0 )
          . "% of installed RAM)";
    }
    elsif ( ( $mycalc{'pct_max_used_memory'} || 0 ) > 85 ) {
        badprint "Maximum reached memory usage: "
          . hr_bytes( $mycalc{'max_used_memory'} ) . " ("
          . ( $mycalc{'pct_max_used_memory'} // 0 )
          . "% of installed RAM)";
    }
    else {
        goodprint "Maximum reached memory usage: "
          . hr_bytes( $mycalc{'max_used_memory'} ) . " ("
          . ( $mycalc{'pct_max_used_memory'} // 0 )
          . "% of installed RAM)";
    }

    if ( ( $mycalc{'pct_max_physical_memory'} || 0 ) > 85 ) {
        badprint "Maximum possible memory usage: "
          . hr_bytes( $mycalc{'max_peak_memory'} ) . " ("
          . ( $mycalc{'pct_max_physical_memory'} // 0 )
          . "% of installed RAM)";
        push( @generalrec,
            "Reduce your overall MySQL memory footprint for system stability" );
    }
    else {
        goodprint "Maximum possible memory usage: "
          . hr_bytes( $mycalc{'max_peak_memory'} ) . " ("
          . ( $mycalc{'pct_max_physical_memory'} // 0 )
          . "% of installed RAM)";
    }

    if ( ( $physical_memory || 0 ) <
        ( ( $mycalc{'max_peak_memory'} || 0 ) + get_other_process_memory() ) )
    {
        if ( $opt{nondedicated} ) {
            infoprint "No warning with --nondedicated option";
            infoprint
"Overall possible memory usage with other process exceeded memory";
        }
        else {
            badprint
"Overall possible memory usage with other process exceeded memory";
            push( @generalrec,
                "Dedicate this server to your database for highest performance."
            );
        }
    }
    else {
        goodprint
"Overall possible memory usage with other process is compatible with memory available";
    }

    # Slow queries
    if ( ( $mycalc{'pct_slow_queries'} || 0 ) > 5 ) {
        badprint "Slow queries: "
          . ( $mycalc{'pct_slow_queries'} // 0 ) . "% ("
          . hr_num( $mystat{'Slow_queries'} ) . "/"
          . hr_num( $mystat{'Questions'} ) . ")";
    }
    else {
        goodprint "Slow queries: "
          . ( $mycalc{'pct_slow_queries'} // 0 ) . "% ("
          . hr_num( $mystat{'Slow_queries'} ) . "/"
          . hr_num( $mystat{'Questions'} ) . ")";
    }
    if ( $myvar{'long_query_time'} > 10 ) {
        push( @adjvars, "long_query_time (<= 10)" );
    }
    if ( defined( $myvar{'log_slow_queries'} ) ) {
        if ( $myvar{'log_slow_queries'} eq "OFF" ) {
            push( @generalrec,
                "Enable the slow query log to troubleshoot bad queries" );
        }
    }

    # Connections
    if ( ( $mycalc{'pct_connections_used'} || 0 ) > 85 ) {
        badprint "Highest connection usage: "
          . ( $mycalc{'pct_connections_used'} // 0 ) . "% ("
          . ( $mystat{'Max_used_connections'} // 0 ) . "/"
          . ( $myvar{'max_connections'}       // 0 ) . ")";
        push( @adjvars,
            "max_connections (> " . ( $myvar{'max_connections'} // 0 ) . ")" );
        push( @adjvars,
            "wait_timeout (< " . ( $myvar{'wait_timeout'} // 0 ) . ")",
            "interactive_timeout (< "
              . ( $myvar{'interactive_timeout'} // 0 )
              . ")" );
        push( @generalrec,
"Reduce or eliminate persistent connections to reduce connection usage"
        );
    }
    else {
        goodprint "Highest usage of available connections: "
          . ( $mycalc{'pct_connections_used'} // 0 ) . "% ("
          . ( $mystat{'Max_used_connections'} // 0 ) . "/"
          . ( $myvar{'max_connections'}       // 0 ) . ")";
    }

    # Aborted Connections
    if ( ( $mycalc{'pct_connections_aborted'} || 0 ) > 3 ) {
        badprint "Aborted connections: "
          . ( $mycalc{'pct_connections_aborted'} // 0 ) . "% ("
          . ( $mystat{'Aborted_connects'}        // 0 ) . "/"
          . ( $mystat{'Connections'}             // 0 ) . ")";
        push( @generalrec,
            "Reduce or eliminate unclosed connections and network issues" );
    }
    else {
        goodprint "Aborted connections: "
          . ( $mycalc{'pct_connections_aborted'} // 0 ) . "% ("
          . ( $mystat{'Aborted_connects'}        // 0 ) . "/"
          . ( $mystat{'Connections'}             // 0 ) . ")";
    }

    # name resolution
    debugprint "skip name resolve: $result{'Variables'}{'skip_name_resolve'}"
      if ( defined( $result{'Variables'}{'skip_name_resolve'} ) );
    if ( defined( $result{'Variables'}{'skip_networking'} )
        && $result{'Variables'}{'skip_networking'} eq 'ON' )
    {
        infoprint
"Skipped name resolution test due to skip_networking=ON in system variables.";
    }
    elsif ( not defined( $result{'Variables'}{'skip_name_resolve'} ) ) {
        infoprint
"Skipped name resolution test due to missing skip_name_resolve in system variables.";
    }

    # Cpanel and Skip name resolve (Issue #863)
    # Ref: https://support.cpanel.net/hc/en-us/articles/21664293830423
    elsif (-r "/usr/local/cpanel/cpanel"
        || -r "/var/cpanel/cpanel.config"
        || -r "/etc/cpupdate.conf" )
    {
        if (    $result{'Variables'}{'skip_name_resolve'} ne 'OFF'
            and $result{'Variables'}{'skip_name_resolve'} )
        {
            badprint
"cPanel/Flex system detected: skip-name-resolve should be disabled (OFF)";
            push( @generalrec,
"cPanel recommends keeping skip-name-resolve disabled: https://support.cpanel.net/hc/en-us/articles/21664293830423"
            );
        }
    }
    elsif ( $result{'Variables'}{'skip_name_resolve'} ne 'ON'
        and $result{'Variables'}{'skip_name_resolve'} ne '1' )
    {
        badprint
"Name resolution is active: a reverse name resolution is made for each new connection which can reduce performance";
        push( @generalrec,
"Configure your accounts with ip or subnets only, then update your configuration with skip-name-resolve=ON"
        );
        push( @adjvars, "skip-name-resolve=ON" );
    }

    # Query cache
    if ( !mysql_version_ge(4) ) {

        # MySQL versions < 4.01 don't support query caching
        push( @generalrec,
            "Upgrade MySQL to version 4+ to utilize query caching" );
    }
    elsif ( mysql_version_ge(8) and mysql_version_le( 9, 9 ) ) {
        infoprint "Query cache has been removed since MySQL 8.0";

        #return;
    }
    elsif ($myvar{'query_cache_size'} < 1
        or $myvar{'query_cache_type'} eq "OFF" )
    {
        goodprint
"Query cache is disabled by default due to mutex contention on multiprocessor machines.";
    }
    elsif ( $mystat{'Com_select'} == 0 ) {
        badprint
          "Query cache cannot be analyzed: no SELECT statements executed";
    }
    else {
        if ( $mycalc{'query_cache_efficiency'} < 20 ) {
            badprint
              "Query cache efficiency: $mycalc{'query_cache_efficiency'}% ("
              . hr_num( $mystat{'Qcache_hits'} )
              . " cached / "
              . hr_num( $mystat{'Qcache_hits'} + $mystat{'Com_select'} )
              . " selects)";
            badprint
              "Query cache may be disabled by default due to mutex contention.";
            push( @adjvars, "query_cache_size (=0)" );
            push( @adjvars, "query_cache_type (=0)" );
        }
        else {
            goodprint
              "Query cache efficiency: $mycalc{'query_cache_efficiency'}% ("
              . hr_num( $mystat{'Qcache_hits'} )
              . " cached / "
              . hr_num( $mystat{'Qcache_hits'} + $mystat{'Com_select'} )
              . " selects)";
            if ( $mycalc{'query_cache_prunes_per_day'} > 98 ) {
                badprint
"Query cache prunes per day: $mycalc{'query_cache_prunes_per_day'}";
                if ( $myvar{'query_cache_size'} >= 128 * 1024 * 1024 ) {
                    push( @generalrec,
"Increasing the query_cache size over 128M may reduce performance"
                    );
                    push( @adjvars,
                            "query_cache_size (> "
                          . hr_bytes_rnd( $myvar{'query_cache_size'} )
                          . ") [see warning above]" );
                }
                else {
                    push( @adjvars,
                            "query_cache_size (> "
                          . hr_bytes_rnd( $myvar{'query_cache_size'} )
                          . ")" );
                }
            }
            else {
                goodprint
"Query cache prunes per day: $mycalc{'query_cache_prunes_per_day'}";
            }
        }

    }

    # Sorting
    if ( $mycalc{'total_sorts'} == 0 ) {
        goodprint "No Sort requiring temporary tables";
    }
    elsif ( $mycalc{'pct_temp_sort_table'} > 10 ) {
        badprint
          "Sorts requiring temporary tables: $mycalc{'pct_temp_sort_table'}% ("
          . hr_num( $mystat{'Sort_merge_passes'} )
          . " temp sorts / "
          . hr_num( $mycalc{'total_sorts'} )
          . " sorts)";
        push( @adjvars,
                "sort_buffer_size (> "
              . hr_bytes_rnd( $myvar{'sort_buffer_size'} )
              . ")" );
        push( @adjvars,
                "read_rnd_buffer_size (> "
              . hr_bytes_rnd( $myvar{'read_rnd_buffer_size'} )
              . ")" );
    }
    else {
        goodprint
          "Sorts requiring temporary tables: $mycalc{'pct_temp_sort_table'}% ("
          . hr_num( $mystat{'Sort_merge_passes'} )
          . " temp sorts / "
          . hr_num( $mycalc{'total_sorts'} )
          . " sorts)";
    }

    # Joins
    if ( $mycalc{'joins_without_indexes_per_day'} > 250 ) {
        badprint
          "Joins performed without indexes: $mycalc{'joins_without_indexes'}";
        if ( $myvar{'join_buffer_size'} < 4 * 1024 * 1024 ) {
            push( @adjvars,
                    "join_buffer_size (> "
                  . hr_bytes( $myvar{'join_buffer_size'} )
                  . ", or always use indexes with JOINs)" );
        }
        else {
            push( @adjvars,
                "join_buffer_size (always use indexes with JOINs)" );
        }
        push(
            @generalrec,
"We will suggest raising the 'join_buffer_size' until JOINs not using indexes are found.
             See https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_join_buffer_size"
        );
    }
    else {
        goodprint "No joins without indexes";

        # No joins have run without indexes
    }

    # Temporary tables
    if ( $mystat{'Created_tmp_tables'} > 0 ) {
        if (   $mycalc{'pct_temp_disk'} > 25
            && $mycalc{'max_tmp_table_size'} < 256 * 1024 * 1024 )
        {
            badprint
              "Temporary tables created on disk: $mycalc{'pct_temp_disk'}% ("
              . hr_num( $mystat{'Created_tmp_disk_tables'} )
              . " on disk / "
              . hr_num( $mystat{'Created_tmp_tables'} )
              . " total)";
            push( @adjvars,
                    "tmp_table_size (> "
                  . hr_bytes_rnd( $myvar{'tmp_table_size'} )
                  . ")" );
            push( @adjvars,
                    "max_heap_table_size (> "
                  . hr_bytes_rnd( $myvar{'max_heap_table_size'} )
                  . ")" );
            push( @generalrec,
"When making adjustments, make tmp_table_size/max_heap_table_size equal"
            );
            push( @generalrec,
                "Reduce your SELECT DISTINCT queries which have no LIMIT clause"
            );
        }
        elsif ($mycalc{'pct_temp_disk'} > 25
            && $mycalc{'max_tmp_table_size'} >= 256 * 1024 * 1024 )
        {
            badprint
              "Temporary tables created on disk: $mycalc{'pct_temp_disk'}% ("
              . hr_num( $mystat{'Created_tmp_disk_tables'} )
              . " on disk / "
              . hr_num( $mystat{'Created_tmp_tables'} )
              . " total)";
            push( @generalrec,
                "Temporary table size is already large: reduce result set size"
            );
            push( @generalrec,
                "Reduce your SELECT DISTINCT queries without LIMIT clauses" );
        }
        else {
            goodprint
              "Temporary tables created on disk: $mycalc{'pct_temp_disk'}% ("
              . hr_num( $mystat{'Created_tmp_disk_tables'} )
              . " on disk / "
              . hr_num( $mystat{'Created_tmp_tables'} )
              . " total)";
        }
    }
    else {
        goodprint "No tmp tables created on disk";
    }

    # TempTable mmap disk space check
    if (   defined $myvar{'temptable_max_mmap'}
        && is_int( $myvar{'temptable_max_mmap'} )
        && $myvar{'temptable_max_mmap'} > 0 )
    {
        my $tmpdir       = $myvar{'tmpdir'} // '';
        my @tmpdirs      = split( /[:;]/, $tmpdir );
        my $first_tmpdir = $tmpdirs[0] || '/tmp';

        # Only run df check locally
        if ( !is_remote() && !$is_win ) {
            my $df_bin = which('df');
            if ($df_bin) {
                my $escaped_tmpdir = $first_tmpdir;
                $escaped_tmpdir =~ s/'/'\\''/g;
                my @df_lines =
                  execute_system_command(
                    "$df_bin -P '$escaped_tmpdir' 2>/dev/null");
                if ( scalar(@df_lines) >= 2 ) {
                    my $df_data = $df_lines[1];
                    if ( $df_data =~
                        /\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(.*)$/ )
                    {
                        my $available_bytes = $3 * 1024;
                        if ( $myvar{'temptable_max_mmap'} > $available_bytes ) {
                            badprint "temptable_max_mmap ("
                              . hr_bytes( $myvar{'temptable_max_mmap'} )
                              . ") is greater than available disk space on $first_tmpdir ("
                              . hr_bytes($available_bytes) . ")";
                            push( @generalrec,
"Reduce temptable_max_mmap or free up disk space in $first_tmpdir to avoid TempTable engine failures."
                            );
                        }
                        else {
                            goodprint "temptable_max_mmap ("
                              . hr_bytes( $myvar{'temptable_max_mmap'} )
                              . ") is compatible with available disk space on $first_tmpdir ("
                              . hr_bytes($available_bytes) . ")";
                        }
                    }
                }
            }
        }
    }

    # Thread cache
    if ( defined( $myvar{'have_threadpool'} )
        and $myvar{'have_threadpool'} eq 'YES' )
    {
# https://www.percona.com/doc/percona-server/5.7/performance/threadpool.html#status-variables
# When thread pool is enabled, the value of the thread_cache_size variable
# is ignored. The Threads_cached status variable contains 0 in this case.
        infoprint "Thread cache not used with thread pool enabled";
    }
    else {
        if ( $myvar{'thread_cache_size'} eq 0 ) {
            badprint "Thread cache is disabled";
            push( @generalrec,
                "Set thread_cache_size to 4 as a starting value" );
            push( @adjvars, "thread_cache_size (start at 4)" );
        }
        else {
            if ( $mycalc{'thread_cache_hit_rate'} <= 50 ) {
                badprint
                  "Thread cache hit rate: $mycalc{'thread_cache_hit_rate'}% ("
                  . hr_num( $mystat{'Threads_created'} )
                  . " created / "
                  . hr_num( $mystat{'Connections'} )
                  . " connections)";
                push( @adjvars,
                    "thread_cache_size (> $myvar{'thread_cache_size'})" );
            }
            else {
                goodprint
                  "Thread cache hit rate: $mycalc{'thread_cache_hit_rate'}% ("
                  . hr_num( $mystat{'Threads_created'} )
                  . " created / "
                  . hr_num( $mystat{'Connections'} )
                  . " connections)";
            }
        }
    }

    # Table cache
    my $table_cache_var = "";
    if ( $mystat{'Open_tables'} > 0 ) {
        if ( $mycalc{'table_cache_hit_rate'} < 20 ) {

            unless ( defined( $mystat{'Table_open_cache_hits'} ) ) {
                badprint
                  "Table cache hit rate: $mycalc{'table_cache_hit_rate'}% ("
                  . hr_num( $mystat{'Open_tables'} )
                  . " hits / "
                  . hr_num( $mystat{'Opened_tables'} )
                  . " requests)";
            }
            else {
                badprint
                  "Table cache hit rate: $mycalc{'table_cache_hit_rate'}% ("
                  . hr_num( $mystat{'Table_open_cache_hits'} )
                  . " hits / "
                  . hr_num( $mystat{'Table_open_cache_hits'} +
                      $mystat{'Table_open_cache_misses'} )
                  . " requests)";
            }

            if ( mysql_version_ge( 5, 1 ) ) {
                $table_cache_var = "table_open_cache";
            }
            else {
                $table_cache_var = "table_cache";
            }

            push( @adjvars,
                $table_cache_var . " (> " . $myvar{$table_cache_var} . ")" );

            # table_open_cache_instances recommendation (#480)
            if (    mysql_version_ge( 5, 6, 6 )
                and $myvar{'version'} !~ /mariadb/i
                and defined $myvar{'table_open_cache_instances'} )
            {
                my $logical_cpus = logical_cpu_cores();
                if ( $logical_cpus > 1 ) {
                    my $recommended_instances =
                      POSIX::ceil( $logical_cpus * 0.5 );

                    # Cap at 16 (default for 8.0+)
                    $recommended_instances = 16 if $recommended_instances > 16;
                    if ( $myvar{'table_open_cache_instances'} <
                        $recommended_instances )
                    {
                        badprint
"table_open_cache_instances is set to $myvar{'table_open_cache_instances'} but $recommended_instances is recommended based on CPU cores.";
                        push( @adjvars,
"table_open_cache_instances (=$recommended_instances)"
                        );
                    }
                }
            }
            elsif ( $myvar{'version'} =~ /mariadb/i
                and mysql_version_ge( 10, 2, 2 ) )
            {
                if ( defined $myvar{'table_open_cache_instances'}
                    and $myvar{'table_open_cache_instances'} > 0 )
                {
                    infoprint
"MariaDB 10.2.2+ autosizes table_open_cache_instances. Current value is $myvar{'table_open_cache_instances'}.";
                }
            }

            push( @generalrec,
                    "Increase "
                  . $table_cache_var
                  . " gradually to avoid file descriptor limits" );
            push( @generalrec,
                    "Read this before increasing "
                  . $table_cache_var
                  . " over 64: https://bit.ly/2Fulv7r" );
            push( @generalrec,
                    "Read this before increasing for MariaDB"
                  . " https://mariadb.com/kb/en/library/optimizing-table_open_cache/"
            );
            push( @generalrec,
"This is MyISAM only table_cache scalability problem, InnoDB not affected."
            );
            push( @generalrec,
                "For more details see: https://bugs.mysql.com/bug.php?id=49177"
            );
            push( @generalrec,
"This bug already fixed in MySQL 5.7.9 and newer MySQL versions."
            );
            push( @generalrec,
                    "Beware that open_files_limit ("
                  . $myvar{'open_files_limit'}
                  . ") variable " );
            push( @generalrec,
                    "should be greater than $table_cache_var ("
                  . $myvar{$table_cache_var}
                  . ")" );
        }
        else {
            unless ( defined( $mystat{'Table_open_cache_hits'} ) ) {
                goodprint
                  "Table cache hit rate: $mycalc{'table_cache_hit_rate'}% ("
                  . hr_num( $mystat{'Open_tables'} )
                  . " hits / "
                  . hr_num( $mystat{'Opened_tables'} )
                  . " requests)";
            }
            else {
                goodprint
                  "Table cache hit rate: $mycalc{'table_cache_hit_rate'}% ("
                  . hr_num( $mystat{'Table_open_cache_hits'} )
                  . " hits / "
                  . hr_num( $mystat{'Table_open_cache_hits'} +
                      $mystat{'Table_open_cache_misses'} )
                  . " requests)";
            }
        }
    }

    # Table definition cache
    my $nbtables = select_one('SELECT COUNT(*) FROM information_schema.tables');
    $mycalc{'total_tables'} = $nbtables;
    if ( defined $myvar{'table_definition_cache'} ) {
        if ( $myvar{'table_definition_cache'} == -1 ) {
            infoprint( "table_definition_cache ("
                  . $myvar{'table_definition_cache'}
                  . ") is in autosizing mode" );
        }
        elsif ( $myvar{'table_definition_cache'} < $nbtables ) {
            badprint "table_definition_cache ("
              . $myvar{'table_definition_cache'}
              . ") is less than number of tables ($nbtables) ";
            push( @adjvars,
                    "table_definition_cache ("
                  . $myvar{'table_definition_cache'} . ") > "
                  . $nbtables
                  . " or -1 (autosizing if supported)" );
        }
        else {
            goodprint "table_definition_cache ("
              . $myvar{'table_definition_cache'}
              . ") is greater than number of tables ($nbtables)";
        }
    }
    else {
        infoprint "No table_definition_cache variable found.";
    }

    # Open files
    if ( defined $mycalc{'pct_files_open'} ) {
        if ( $mycalc{'pct_files_open'} > 85 ) {
            badprint "Open file limit used: $mycalc{'pct_files_open'}% ("
              . hr_num( $mystat{'Open_files'} ) . "/"
              . hr_num( $myvar{'open_files_limit'} ) . ")";
            push( @adjvars,
                "open_files_limit (> " . $myvar{'open_files_limit'} . ")" );
        }
        else {
            goodprint "Open file limit used: $mycalc{'pct_files_open'}% ("
              . hr_num( $mystat{'Open_files'} ) . "/"
              . hr_num( $myvar{'open_files_limit'} ) . ")";
        }
    }

    # Table locks
    if ( defined $mycalc{'pct_table_locks_immediate'} ) {
        if ( $mycalc{'pct_table_locks_immediate'} < 95 ) {
            badprint
"Table locks acquired immediately: $mycalc{'pct_table_locks_immediate'}%";
            push( @generalrec,
                "Optimize queries and/or use InnoDB to reduce lock wait" );
        }
        else {
            goodprint
"Table locks acquired immediately: $mycalc{'pct_table_locks_immediate'}% ("
              . hr_num( $mystat{'Table_locks_immediate'} )
              . " immediate / "
              . hr_num( $mystat{'Table_locks_waited'} +
                  $mystat{'Table_locks_immediate'} )
              . " locks)";
        }
    }

    # Binlog cache
    if ( defined $mycalc{'pct_binlog_cache'} ) {
        if (   $mycalc{'pct_binlog_cache'} < 90
            && $mystat{'Binlog_cache_use'} > 0 )
        {
            badprint "Binlog cache memory access: "
              . $mycalc{'pct_binlog_cache'} . "% ("
              . (
                $mystat{'Binlog_cache_use'} - $mystat{'Binlog_cache_disk_use'} )
              . " Memory / "
              . $mystat{'Binlog_cache_use'}
              . " Total)";
            push( @generalrec,
                    "Increase binlog_cache_size (current value: "
                  . $myvar{'binlog_cache_size'}
                  . ")" );
            push( @adjvars,
                    "binlog_cache_size ("
                  . hr_bytes( $myvar{'binlog_cache_size'} + 16 * 1024 * 1024 )
                  . ")" );
        }
        else {
            goodprint "Binlog cache memory access: "
              . $mycalc{'pct_binlog_cache'} . "% ("
              . (
                $mystat{'Binlog_cache_use'} - $mystat{'Binlog_cache_disk_use'} )
              . " Memory / "
              . $mystat{'Binlog_cache_use'}
              . " Total)";
            debugprint "Not enough data to validate binlog cache size\n"
              if $mystat{'Binlog_cache_use'} < 10;
        }
    }

    # Performance options
    if ( !mysql_version_ge( 5, 1 ) ) {
        push( @generalrec, "Upgrade to MySQL 5.5+ to use asynchronous write" );
    }
    elsif ( $myvar{'concurrent_insert'} eq "OFF" ) {
        push( @generalrec, "Enable concurrent_insert by setting it to 'ON'" );
    }
    elsif ( !$myvar{'concurrent_insert'} ) {
        push( @generalrec, "Enable concurrent_insert by setting it to 1" );
    }
}

# Recommendations for MyISAM
sub mysql_myisam {
    return 0 unless ( $opt{'myisamstat'} > 0 );
    subheaderprint "MyISAM Metrics";
    my $nb_myisam_tables = select_one(
"SELECT COUNT(*) FROM information_schema.TABLES WHERE ENGINE='MyISAM' and TABLE_SCHEMA NOT IN ('mysql','information_schema','performance_schema')"
    );
    push( @generalrec,
        "MyISAM engine is deprecated, consider migrating to InnoDB" )
      if $nb_myisam_tables > 0;

    if ( $nb_myisam_tables > 0 ) {
        badprint
          "Consider migrating $nb_myisam_tables following tables to InnoDB:";
        my $sql_mig = "";
        for my $myisam_table (
            select_array(
"SELECT CONCAT('|',TABLE_SCHEMA, '|.|', TABLE_NAME,'|') FROM information_schema.TABLES WHERE ENGINE='MyISAM' and TABLE_SCHEMA NOT IN ('mysql','information_schema','performance_schema')"
            )
          )
        {
            my $myisam_table_escape = $myisam_table;
            $myisam_table_escape =~ s/\|/\`/g;
            $sql_mig =
"${sql_mig}-- InnoDB migration for $myisam_table_escape\nALTER TABLE $myisam_table_escape ENGINE=InnoDB;\n\n";
            infoprint
"* InnoDB migration request for $myisam_table_escape Table: ALTER TABLE $myisam_table_escape ENGINE=InnoDB;";
        }
        dump_into_file( "migrate_myisam_to_innodb.sql", $sql_mig );
    }
    infoprint("General MyIsam metrics:");
    infoprint " +-- Total MyISAM Tables  : $nb_myisam_tables";
    infoprint " +-- Total MyISAM indexes : "
      . hr_bytes( $mycalc{'total_myisam_indexes'} )
      if defined( $mycalc{'total_myisam_indexes'} );
    infoprint " +-- KB Size :" . hr_bytes( $myvar{'key_buffer_size'} );
    infoprint " +-- KB Used Size :"
      . hr_bytes( $myvar{'key_buffer_size'} -
          $mystat{'Key_blocks_unused'} * $myvar{'key_cache_block_size'} );
    infoprint " +-- KB used :" . $mycalc{'pct_key_buffer_used'} . "%";
    infoprint " +-- Read KB hit rate: $mycalc{'pct_keys_from_mem'}% ("
      . hr_num( $mystat{'Key_read_requests'} )
      . " cached / "
      . hr_num( $mystat{'Key_reads'} )
      . " reads)";
    infoprint " +-- Write KB hit rate: $mycalc{'pct_wkeys_from_mem'}% ("
      . hr_num( $mystat{'Key_write_requests'} )
      . " cached / "
      . hr_num( $mystat{'Key_writes'} )
      . " writes)";

    if ( $nb_myisam_tables == 0 ) {
        infoprint "No MyISAM table(s) detected ....";
        return;
    }
    if ( mysql_version_ge(8) and mysql_version_le(10) ) {
        infoprint "MyISAM Metrics are disabled since MySQL 8.0.";
        if ( $myvar{'key_buffer_size'} > 0 ) {
            push( @adjvars, "key_buffer_size=0" );
            push( @generalrec,
                "Buffer Key MyISAM set to 0, no MyISAM table detected" );
        }
        return;
    }

    if ( !defined( $mycalc{'total_myisam_indexes'} ) ) {
        badprint
          "Unable to calculate MyISAM index size on MySQL server < 5.0.0";
        push( @generalrec,
            "Unable to calculate MyISAM index size on MySQL server < 5.0.0" );
        return;
    }
    if ( $mycalc{'pct_key_buffer_used'} == 0 ) {

        # No queries have run that would use keys
        infoprint "Key buffer used: $mycalc{'pct_key_buffer_used'}% ("
          . hr_bytes( $myvar{'key_buffer_size'} -
              $mystat{'Key_blocks_unused'} * $myvar{'key_cache_block_size'} )
          . " used / "
          . hr_bytes( $myvar{'key_buffer_size'} )
          . " cache)";
        infoprint "No SQL statement based on MyISAM table(s) detected ....";
        return;
    }

    # Key buffer usage
    if ( $mycalc{'pct_key_buffer_used'} < 90 ) {
        badprint "Key buffer used: $mycalc{'pct_key_buffer_used'}% ("
          . hr_bytes( $myvar{'key_buffer_size'} -
              $mystat{'Key_blocks_unused'} * $myvar{'key_cache_block_size'} )
          . " used / "
          . hr_bytes( $myvar{'key_buffer_size'} )
          . " cache)";

        push(
            @adjvars,
            "key_buffer_size (\~ "
              . hr_num(
                $myvar{'key_buffer_size'} *
                  $mycalc{'pct_key_buffer_used'} / 100
              )
              . ")"
        );
    }
    else {
        goodprint "Key buffer used: $mycalc{'pct_key_buffer_used'}% ("
          . hr_bytes( $myvar{'key_buffer_size'} -
              $mystat{'Key_blocks_unused'} * $myvar{'key_cache_block_size'} )
          . " used / "
          . hr_bytes( $myvar{'key_buffer_size'} )
          . " cache)";
    }

    # Key buffer size / total MyISAM indexes
    if (   $myvar{'key_buffer_size'} < $mycalc{'total_myisam_indexes'}
        && $mycalc{'pct_keys_from_mem'} < 95
        && $mycalc{'pct_key_buffer_used'} >= 90 )
    {
        badprint "Key buffer size / total MyISAM indexes: "
          . hr_bytes( $myvar{'key_buffer_size'} ) . "/"
          . hr_bytes( $mycalc{'total_myisam_indexes'} ) . "";
        push( @adjvars,
                "key_buffer_size (> "
              . hr_bytes( $mycalc{'total_myisam_indexes'} )
              . ")" );
    }
    else {
        goodprint "Key buffer size / total MyISAM indexes: "
          . hr_bytes( $myvar{'key_buffer_size'} ) . "/"
          . hr_bytes( $mycalc{'total_myisam_indexes'} ) . "";
    }
    if ( $mystat{'Key_read_requests'} > 0 ) {
        if ( $mycalc{'pct_keys_from_mem'} < 95 ) {
            badprint
              "Read Key buffer hit rate: $mycalc{'pct_keys_from_mem'}% ("
              . hr_num( $mystat{'Key_read_requests'} )
              . " cached / "
              . hr_num( $mystat{'Key_reads'} )
              . " reads)";
        }
        else {
            goodprint
              "Read Key buffer hit rate: $mycalc{'pct_keys_from_mem'}% ("
              . hr_num( $mystat{'Key_read_requests'} )
              . " cached / "
              . hr_num( $mystat{'Key_reads'} )
              . " reads)";
        }
    }

    # No queries have run that would use keys
    debugprint "Key buffer size / total MyISAM indexes: "
      . hr_bytes( $myvar{'key_buffer_size'} ) . "/"
      . hr_bytes( $mycalc{'total_myisam_indexes'} ) . "";
    if ( $mystat{'Key_write_requests'} > 0 ) {
        if ( $mycalc{'pct_wkeys_from_mem'} < 95 ) {
            badprint
              "Write Key buffer hit rate: $mycalc{'pct_wkeys_from_mem'}% ("
              . hr_num( $mystat{'Key_write_requests'} )
              . " cached / "
              . hr_num( $mystat{'Key_writes'} )
              . " writes)";
        }
        else {
            goodprint
              "Write Key buffer hit rate: $mycalc{'pct_wkeys_from_mem'}% ("
              . hr_num( $mystat{'Key_write_requests'} )
              . " cached / "
              . hr_num( $mystat{'Key_writes'} )
              . " writes)";
        }
    }
    else {
        # No queries have run that would use keys
        debugprint
          "Write Key buffer hit rate: $mycalc{'pct_wkeys_from_mem'}% ("
          . hr_num( $mystat{'Key_write_requests'} )
          . " cached / "
          . hr_num( $mystat{'Key_writes'} )
          . " writes)";
    }
}

# Recommendations for ThreadPool
# See issue #404: https://github.com/jmrenouard/MySQLTuner-perl/issues/404
sub mariadb_threadpool {
    my $is_mariadb = ( ( $myvar{'version'} // '' ) =~ /mariadb/i );
    my $is_percona = (
             ( $myvar{'version'} // '' ) =~ /percona/i
          or ( $myvar{'version_comment'} // '' ) =~ /percona/i
    );

    # Thread Pool is only relevant for MariaDB and Percona
    return unless ( $is_mariadb or $is_percona );

    my $thread_handling = $myvar{'thread_handling'}
      // 'one-thread-per-connection';
    my $is_threadpool_enabled = ( $thread_handling eq 'pool-of-threads' );

# Recommendation to ENABLE thread pool if connections are high
# https://www.percona.com/blog/2014/01/23/percona-server-improve-scalability-percona-thread-pool/
    if ( !$is_threadpool_enabled
        && ( $mystat{'Max_used_connections'} // 0 ) >= 512 )
    {
        subheaderprint "ThreadPool Metrics";
        infoprint "ThreadPool stat is disabled.";
        badprint
          "Max_used_connections ($mystat{'Max_used_connections'}) is >= 512.";
        push( @generalrec,
"Enabling the thread pool is recommended for servers with max_connections >= 512 (currently $myvar{'max_connections'})"
        );
        push( @adjvars, "thread_handling=pool-of-threads" );
    }

    # If it IS enabled, show metrics and recommendations
    if ($is_threadpool_enabled) {
        subheaderprint "ThreadPool Metrics";
        infoprint "ThreadPool stat is enabled.";
        infoprint "Thread Pool Size: "
          . $myvar{'thread_pool_size'}
          . " thread(s).";

        # Recommendation to DISABLE thread pool if connections are low
        if ( ( $mystat{'Max_used_connections'} // 0 ) < 512 ) {
            badprint
"ThreadPool is enabled but Max_used_connections is < 512 ($mystat{'Max_used_connections'}).";
            push( @generalrec,
"Thread pool is usually only efficient for servers with max_connections >= 512"
            );
        }

        my $np = logical_cpu_cores();
        if ( $np <= 0 ) {
            debugprint
"Unable to detect logical CPU cores for thread_pool_size recommendation.";
            return;
        }

# Percona and MariaDB recommendation: ideally one active thread per CPU
# Efficient range: [NCPU, NCPU + NCPU/2]
# Source: https://mariadb.com/kb/en/library/thread-pool-in-mariadb/
# Source: https://www.percona.com/blog/2014/01/23/percona-server-improve-scalability-percona-thread-pool/
        my $min_tps = $np;
        my $max_tps = int( $np * 1.5 );

        if (   $myvar{'thread_pool_size'} >= $min_tps
            && $myvar{'thread_pool_size'} <= $max_tps )
        {
            goodprint
"thread_pool_size is optimal ($myvar{'thread_pool_size'}) for your $np CPUs (range: $min_tps - $max_tps)";
        }
        else {
            badprint
"thread_pool_size ($myvar{'thread_pool_size'}) is not in the recommended range [$min_tps, $max_tps] for your $np CPUs.";
            push( @adjvars, "thread_pool_size between $min_tps and $max_tps" );
        }
    }
}

sub get_pf_memory {

    # Performance Schema
    return 0 unless defined $myvar{'performance_schema'};
    return 0 if $myvar{'performance_schema'} eq 'OFF';

    my @infoPFSMemory = grep { /\tperformance_schema[.]memory\t/msx }
      select_array("SHOW ENGINE PERFORMANCE_SCHEMA STATUS");
    @infoPFSMemory == 1 || return 0;
    $infoPFSMemory[0] =~ s/.*\s+(\d+)$/$1/g;
    return $infoPFSMemory[0];
}

# Recommendations for Performance Schema
sub mysql_pfs {
    return if ( $opt{pfstat} == 0 );
    subheaderprint "Performance schema";

    # Performance Schema
    debugprint "Performance schema is " . $myvar{'performance_schema'};
    $myvar{'performance_schema'} = 'OFF'
      unless defined( $myvar{'performance_schema'} );
    if ( $myvar{'performance_schema'} eq 'OFF' ) {
        badprint
          "Performance_schema should be activated (observability issue).";
        push( @adjvars, "performance_schema=ON" );
        push( @generalrec,
"Performance schema should be activated for better diagnostics and observability"
        );
    }
    if ( $myvar{'performance_schema'} eq 'ON' ) {
        infoprint "Performance_schema is activated.";
        debugprint "Performance schema is " . $myvar{'performance_schema'};
        infoprint "Memory used by Performance_schema: "
          . hr_bytes( get_pf_memory() );
    }

    unless ( grep /^sys$/, select_array("SHOW DATABASES") ) {
        infoprint "Sys schema is not installed.";
        push( @generalrec,
            mysql_version_ge( 10, 0 )
            ? "Consider installing Sys schema from https://github.com/FromDual/mariadb-sys for MariaDB"
            : "Consider installing Sys schema from https://github.com/mysql/mysql-sys for MySQL"
        ) unless ( mysql_version_le( 5, 6 ) );

        return;
    }
    infoprint "Sys schema is installed.";
    return if ( $opt{pfstat} == 0 or $myvar{'performance_schema'} ne 'ON' );

    infoprint "Sys schema Version: "
      . select_one("select sys_version from sys.version");

    # System databases excluded from schema-aware PFS queries
    # Ref: Client feedback - filter system noise from statement analysis
    my $sys_db_filter_db =
"(db IS NULL OR db NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys'))";
    my $sys_db_filter_ts =
"(table_schema IS NULL OR table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys'))";
    my $sys_db_filter_os =
"(object_schema IS NULL OR object_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys'))";

    # Top user per connection
    subheaderprint "Performance schema: Top 5 user per connection";
    my $nbL = 1;
    for my $lQuery (
        select_array(
'select user, total_connections from sys.user_summary order by total_connections desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery conn(s)";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top user per statement
    subheaderprint "Performance schema: Top 5 user per statement";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select user, statements from sys.user_summary order by statements desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery stmt(s)";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top user per statement latency
    subheaderprint "Performance schema: Top 5 user per statement latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select user, statement_avg_latency from sys.x\\$user_summary order by statement_avg_latency desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top user per lock latency
    subheaderprint "Performance schema: Top 5 user per lock latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select user, lock_latency from sys.x\\$user_summary_by_statement_latency order by lock_latency desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top user per full scans
    subheaderprint "Performance schema: Top 5 user per nb full scans";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select user, full_scans from sys.x\\$user_summary_by_statement_latency order by full_scans desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top user per row_sent
    subheaderprint "Performance schema: Top 5 user per rows sent";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select user, rows_sent from sys.x\\$user_summary_by_statement_latency order by rows_sent desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top user per row modified
    subheaderprint "Performance schema: Top 5 user per rows modified";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select user, rows_affected from sys.x\\$user_summary_by_statement_latency order by rows_affected desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top user per io
    subheaderprint "Performance schema: Top 5 user per IO";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select user, file_ios from sys.x\\$user_summary order by file_ios desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top user per io latency
    subheaderprint "Performance schema: Top 5 user per IO latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select user, file_io_latency from sys.x\\$user_summary order by file_io_latency desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top host per connection
    subheaderprint "Performance schema: Top 5 host per connection";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select host, total_connections from sys.x\\$host_summary order by total_connections desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery conn(s)";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top host per statement
    subheaderprint "Performance schema: Top 5 host per statement";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select host, statements from sys.x\\$host_summary order by statements desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery stmt(s)";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top host per statement latency
    subheaderprint "Performance schema: Top 5 host per statement latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select host, statement_avg_latency from sys.x\\$host_summary order by statement_avg_latency desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top host per lock latency
    subheaderprint "Performance schema: Top 5 host per lock latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select host, lock_latency from sys.x\\$host_summary_by_statement_latency order by lock_latency desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top host per full scans
    subheaderprint "Performance schema: Top 5 host per nb full scans";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select host, full_scans from sys.x\\$host_summary_by_statement_latency order by full_scans desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top host per rows sent
    subheaderprint "Performance schema: Top 5 host per rows sent";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select host, rows_sent from sys.x\\$host_summary_by_statement_latency order by rows_sent desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top host per rows modified
    subheaderprint "Performance schema: Top 5 host per rows modified";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select host, rows_affected from sys.x\\$host_summary_by_statement_latency order by rows_affected desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top host per io
    subheaderprint "Performance schema: Top 5 host per io";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select host, file_ios from sys.x\\$host_summary order by file_ios desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top 5 host per io latency
    subheaderprint "Performance schema: Top 5 host per io latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select host, file_io_latency from sys.x\\$host_summary order by file_io_latency desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top IO type order by total io
    subheaderprint "Performance schema: Top IO type order by total io";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select substring(event_name,14), SUM(total)AS total from sys.x\\$host_summary_by_file_io_type GROUP BY substring(event_name,14) ORDER BY total DESC;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery i/o";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top IO type order by total latency
    subheaderprint "Performance schema: Top IO type order by total latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select substring(event_name,14), ROUND(SUM(total_latency),1) AS total_latency from sys.x\\$host_summary_by_file_io_type GROUP BY substring(event_name,14) ORDER BY total_latency DESC;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top IO type order by max latency
    subheaderprint "Performance schema: Top IO type order by max latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select substring(event_name,14), MAX(max_latency) as max_latency from sys.x\\$host_summary_by_file_io_type GROUP BY substring(event_name,14) ORDER BY max_latency DESC;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top Stages order by total io
    subheaderprint "Performance schema: Top Stages order by total io";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select substring(event_name,7), SUM(total)AS total from sys.x\\$host_summary_by_stages GROUP BY substring(event_name,7) ORDER BY total DESC;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery i/o";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top Stages order by total latency
    subheaderprint "Performance schema: Top Stages order by total latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select substring(event_name,7), ROUND(SUM(total_latency),1) AS total_latency from sys.x\\$host_summary_by_stages GROUP BY substring(event_name,7) ORDER BY total_latency DESC;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top Stages order by avg latency
    subheaderprint "Performance schema: Top Stages order by avg latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select substring(event_name,7), MAX(avg_latency) as avg_latency from sys.x\\$host_summary_by_stages GROUP BY substring(event_name,7) ORDER BY avg_latency DESC;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top host per table scans
    subheaderprint "Performance schema: Top 5 host per table scans";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select host, table_scans from sys.x\\$host_summary order by table_scans desc LIMIT 5'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # InnoDB Buffer Pool by schema
    subheaderprint "Performance schema: InnoDB Buffer Pool by schema";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select object_schema, allocated, data, pages from sys.x\\\$innodb_buffer_stats_by_schema WHERE $sys_db_filter_os ORDER BY pages DESC"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery page(s)";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # InnoDB Buffer Pool by table
    subheaderprint "Performance schema: 40 InnoDB Buffer Pool by table";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select object_schema, object_name, allocated,data, pages from sys.x\\\$innodb_buffer_stats_by_table WHERE $sys_db_filter_os ORDER BY pages DESC LIMIT 40"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery page(s)";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Process per allocated memory
    subheaderprint "Performance schema: Process per time";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select user, Command AS PROC, time from sys.x\\$processlist ORDER BY time DESC;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # InnoDB Lock Waits
    subheaderprint "Performance schema: InnoDB Lock Waits";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select wait_age_secs, locked_table, locked_type, waiting_query from sys.x\\$innodb_lock_waits order by wait_age_secs DESC;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Threads IO Latency
    subheaderprint "Performance schema: Thread IO Latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select user, total_latency, max_latency from sys.x\\$io_by_thread_by_latency order by total_latency DESC;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # High Cost SQL statements
    subheaderprint "Performance schema: Top 15 Most latency statements";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select LEFT(query, 120), avg_latency from sys.x\\\$statement_analysis WHERE $sys_db_filter_db order by avg_latency desc LIMIT 15"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top 5% slower queries
    subheaderprint "Performance schema: Top 15 slower queries";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select LEFT(query, 120), exec_count from sys.x\\\$statements_with_runtimes_in_95th_percentile WHERE $sys_db_filter_db order by exec_count desc LIMIT 15"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery s";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top 10 nb statement type
    subheaderprint "Performance schema: Top 15 nb statement type";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select statement, sum(total) as total from sys.x\\$host_summary_by_statement_type group by statement order by total desc LIMIT 15;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top statement by total latency
    subheaderprint "Performance schema: Top 15 statement by total latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select statement, sum(total_latency) as total from sys.x\\$host_summary_by_statement_type group by statement order by total desc LIMIT 15;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top statement by lock latency
    subheaderprint "Performance schema: Top 15 statement by lock latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select statement, sum(lock_latency) as total from sys.x\\$host_summary_by_statement_type group by statement order by total desc LIMIT 15;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top statement by full scans
    subheaderprint "Performance schema: Top 15 statement by full scans";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select statement, sum(full_scans) as total from sys.x\\$host_summary_by_statement_type group by statement order by total desc LIMIT 15;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top statement by rows sent
    subheaderprint "Performance schema: Top 15 statement by rows sent";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select statement, sum(rows_sent) as total from sys.x\\$host_summary_by_statement_type group by statement order by total desc LIMIT 15;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Top statement by rows modified
    subheaderprint "Performance schema: Top 15 statement by rows modified";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select statement, sum(rows_affected) as total from sys.x\\$host_summary_by_statement_type group by statement order by total desc LIMIT 15;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Use temporary tables
    subheaderprint "Performance schema: 15 sample queries using temp table";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select left(query, 120) from sys.x\\$statements_with_temp_tables LIMIT 15'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Unused Indexes
    subheaderprint "Performance schema: Unused indexes";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select CONCAT(object_schema, '.', object_name, ' (', index_name, ')') from sys.schema_unused_indexes where object_schema not in ('performance_schema', 'mysql', 'information_schema', 'sys')"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        my ( $schema, $table, $index ) = $lQuery =~ /^(.*?)\.(.*?)\s\((.*?)\)$/;
        push(
            @modeling,
            {
                type   => 'unused_index',
                schema => $schema,
                table  => $table,
                index  => $index,
            }
        );
        $nbL++;
    }
    if ( $nbL > 1 ) {
        my $idx_count = $nbL - 1;
        badprint "Performance schema: $idx_count unused index(es) found.";
        push( @generalrec,
"Unused indexes found: $idx_count index(es) should be reviewed and potentially removed."
        );
    }
    else {
        infoprint "No information found or indicators deactivated.";
    }

    # Full table scans
    subheaderprint "Performance schema: Tables with full table scans";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select * from sys.x\\\$schema_tables_with_full_table_scans WHERE $sys_db_filter_os order by rows_full_scanned DESC"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Latest file IO by latency
    subheaderprint "Performance schema: Latest File IO by latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select thread, file, latency, operation from sys.x\\$latest_file_io ORDER BY latency LIMIT 10;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # FILE by IO read bytes
    subheaderprint "Performance schema: File by IO read bytes";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select file, total_read from sys.x\\$io_global_by_file_by_bytes order by total_read DESC LIMIT 15;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # FILE by IO written bytes
    subheaderprint "Performance schema: File by IO written bytes";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select file, total_written from sys.x\\$io_global_by_file_by_bytes order by total_written DESC LIMIT 15'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # file per IO total latency
    subheaderprint "Performance schema: File per IO total latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select file, total_latency from sys.x\\$io_global_by_file_by_latency ORDER BY total_latency DESC LIMIT 20;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # file per IO read latency
    subheaderprint "Performance schema: file per IO read latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select file, read_latency from sys.x\\$io_global_by_file_by_latency ORDER BY read_latency DESC LIMIT 20;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # file per IO write latency
    subheaderprint "Performance schema: file per IO write latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select file, write_latency from sys.x\\$io_global_by_file_by_latency ORDER BY write_latency DESC LIMIT 20;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Event Wait by read bytes
    subheaderprint "Performance schema: Event Wait by read bytes";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select event_name, total_read from sys.x\\$io_global_by_wait_by_bytes order by total_read DESC LIMIT 15;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Event Wait by write bytes
    subheaderprint "Performance schema: Event Wait written bytes";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select event_name, total_written from sys.x\\$io_global_by_wait_by_bytes order by total_written DESC LIMIT 15;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # event per wait total latency
    subheaderprint "Performance schema: event per wait total latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select event_name, total_latency from sys.x\\$io_global_by_wait_by_latency ORDER BY total_latency DESC LIMIT 20;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # event per wait read latency
    subheaderprint "Performance schema: event per wait read latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select event_name, read_latency from sys.x\\$io_global_by_wait_by_latency ORDER BY read_latency DESC LIMIT 20;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # event per wait write latency
    subheaderprint "Performance schema: event per wait write latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select event_name, write_latency from sys.x\\$io_global_by_wait_by_latency ORDER BY write_latency DESC LIMIT 20;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    #schema_index_statistics
    # TOP 15 most read index
    subheaderprint "Performance schema: Top 15 most read indexes";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select table_schema, table_name,index_name, rows_selected from sys.x\\\$schema_index_statistics WHERE $sys_db_filter_ts ORDER BY ROWs_selected DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # TOP 15 most used index
    subheaderprint "Performance schema: Top 15 most modified indexes";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select table_schema, table_name,index_name, rows_inserted+rows_updated+rows_deleted AS changes from sys.x\\\$schema_index_statistics WHERE $sys_db_filter_ts ORDER BY rows_inserted+rows_updated+rows_deleted DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # TOP 15 high read latency index
    subheaderprint "Performance schema: Top 15 high read latency index";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select table_schema, table_name,index_name, select_latency from sys.x\\\$schema_index_statistics WHERE $sys_db_filter_ts ORDER BY select_latency DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # TOP 15 high insert latency index
    subheaderprint "Performance schema: Top 15 most modified indexes";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select table_schema, table_name,index_name, insert_latency from sys.x\\\$schema_index_statistics WHERE $sys_db_filter_ts ORDER BY insert_latency DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # TOP 15 high update latency index
    subheaderprint "Performance schema: Top 15 high update latency index";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select table_schema, table_name,index_name, update_latency from sys.x\\\$schema_index_statistics WHERE $sys_db_filter_ts ORDER BY update_latency DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # TOP 15 high delete latency index
    subheaderprint "Performance schema: Top 15 high delete latency index";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select table_schema, table_name,index_name, delete_latency from sys.x\\\$schema_index_statistics WHERE $sys_db_filter_ts ORDER BY delete_latency DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # TOP 15 most read tables
    subheaderprint "Performance schema: Top 15 most read tables";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select table_schema, table_name, rows_fetched from sys.x\\\$schema_table_statistics WHERE $sys_db_filter_ts ORDER BY ROWs_fetched DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # TOP 15 most used tables
    subheaderprint "Performance schema: Top 15 most modified tables";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select table_schema, table_name, rows_inserted+rows_updated+rows_deleted AS changes from sys.x\\\$schema_table_statistics WHERE $sys_db_filter_ts ORDER BY rows_inserted+rows_updated+rows_deleted DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # TOP 15 high read latency tables
    subheaderprint "Performance schema: Top 15 high read latency tables";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select table_schema, table_name, fetch_latency from sys.x\\\$schema_table_statistics WHERE $sys_db_filter_ts ORDER BY fetch_latency DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # TOP 15 high insert latency tables
    subheaderprint "Performance schema: Top 15 high insert latency tables";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select table_schema, table_name, insert_latency from sys.x\\\$schema_table_statistics WHERE $sys_db_filter_ts ORDER BY insert_latency DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # TOP 15 high update latency tables
    subheaderprint "Performance schema: Top 15 high update latency tables";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select table_schema, table_name, update_latency from sys.x\\\$schema_table_statistics WHERE $sys_db_filter_ts ORDER BY update_latency DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # TOP 15 high delete latency tables
    subheaderprint "Performance schema: Top 15 high delete latency tables";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select table_schema, table_name, delete_latency from sys.x\\\$schema_table_statistics WHERE $sys_db_filter_ts ORDER BY delete_latency DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    # Redundant indexes
    subheaderprint "Performance schema: Redundant indexes";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select CONCAT(table_schema, ".", table_name, " (", redundant_index_name, ") redundant of ", dominant_index_name, " - SQL: ", sql_drop_index) from sys.schema_redundant_indexes;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        my ( $schema, $table, $redundant, $dominant, $sql ) =
          $lQuery =~
          /^(.*?)\.(.*?)\s\((.*?)\)\sredundant\sof\s(.*?)\s-\sSQL:\s(.*)$/;
        push(
            @modeling,
            {
                type           => 'redundant_index',
                schema         => $schema,
                table          => $table,
                index          => $redundant,
                dominant_index => $dominant,
                sql            => $sql,
            }
        );
        $nbL++;
    }
    if ( $nbL > 1 ) {
        my $idx_count = $nbL - 1;
        badprint "Performance schema: $idx_count redundant index(es) found.";
        push( @generalrec,
"Redundant indexes found: $idx_count index(es) should be reviewed and potentially removed."
        );
    }
    else {
        infoprint "No information found or indicators deactivated.";
    }

    subheaderprint "Performance schema: Table not using InnoDB buffer";
    $nbL = 1;
    for my $lQuery (
        select_array(
" Select table_schema, table_name from sys.x\\\$schema_table_statistics_with_buffer where innodb_buffer_allocated IS NULL AND $sys_db_filter_ts;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 15 Tables using InnoDB buffer";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select table_schema,table_name,innodb_buffer_allocated from sys.x\\\$schema_table_statistics_with_buffer where innodb_buffer_allocated IS NOT NULL AND $sys_db_filter_ts ORDER BY innodb_buffer_allocated DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 15 Tables with InnoDB buffer free";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select table_schema,table_name,innodb_buffer_free from sys.x\\\$schema_table_statistics_with_buffer where innodb_buffer_allocated IS NOT NULL AND $sys_db_filter_ts ORDER BY innodb_buffer_free DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 15 Most executed queries";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select db, LEFT(query, 120), exec_count from sys.x\\\$statement_analysis WHERE $sys_db_filter_db order by exec_count DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint
      "Performance schema: Latest SQL queries in errors or warnings";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select LEFT(query, 120), last_seen from sys.x\\\$statements_with_errors_or_warnings WHERE $sys_db_filter_db ORDER BY last_seen LIMIT 40;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 20 queries with full table scans";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select db, LEFT(query, 120), exec_count from sys.x\\\$statements_with_full_table_scans WHERE $sys_db_filter_db order BY exec_count DESC LIMIT 20;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Last 50 queries with full table scans";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select db, LEFT(query, 120), last_seen from sys.x\\\$statements_with_full_table_scans WHERE $sys_db_filter_db order BY last_seen DESC LIMIT 50;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 15 reader queries (95% percentile)";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select db, LEFT(query, 120), rows_sent from sys.x\\\$statements_with_runtimes_in_95th_percentile WHERE $sys_db_filter_db ORDER BY ROWs_sent DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint
      "Performance schema: Top 15 most row look queries (95% percentile)";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select db, LEFT(query, 120), rows_examined AS search from sys.x\\\$statements_with_runtimes_in_95th_percentile WHERE $sys_db_filter_db ORDER BY rows_examined DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint
      "Performance schema: Top 15 total latency queries (95% percentile)";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select db, LEFT(query, 120), total_latency AS search from sys.x\\\$statements_with_runtimes_in_95th_percentile WHERE $sys_db_filter_db ORDER BY total_latency DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint
      "Performance schema: Top 15 max latency queries (95% percentile)";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select db, LEFT(query, 120), max_latency AS search from sys.x\\\$statements_with_runtimes_in_95th_percentile WHERE $sys_db_filter_db ORDER BY max_latency DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint
      "Performance schema: Top 15 average latency queries (95% percentile)";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select db, LEFT(query, 120), avg_latency AS search from sys.x\\\$statements_with_runtimes_in_95th_percentile WHERE $sys_db_filter_db ORDER BY avg_latency DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 20 queries with sort";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select db, LEFT(query, 120), exec_count from sys.x\\\$statements_with_sorting WHERE $sys_db_filter_db order BY exec_count DESC LIMIT 20;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Last 50 queries with sort";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select db, LEFT(query, 120), last_seen from sys.x\\\$statements_with_sorting WHERE $sys_db_filter_db order BY last_seen DESC LIMIT 50;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 15 row sorting queries with sort";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select db, LEFT(query, 120), rows_sorted from sys.x\\\$statements_with_sorting WHERE $sys_db_filter_db ORDER BY ROWs_sorted DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 15 total latency queries with sort";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select db, LEFT(query, 120), total_latency AS search from sys.x\\\$statements_with_sorting WHERE $sys_db_filter_db ORDER BY total_latency DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 15 merge queries with sort";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select db, LEFT(query, 120), sort_merge_passes AS search from sys.x\\\$statements_with_sorting WHERE $sys_db_filter_db ORDER BY sort_merge_passes DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint
      "Performance schema: Top 15 average sort merges queries with sort";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select db, LEFT(query, 120), avg_sort_merges AS search from sys.x\\\$statements_with_sorting WHERE $sys_db_filter_db ORDER BY avg_sort_merges DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 15 scans queries with sort";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select db, LEFT(query, 120), sorts_using_scans AS search from sys.x\\\$statements_with_sorting WHERE $sys_db_filter_db ORDER BY sorts_using_scans DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 15 range queries with sort";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select db, LEFT(query, 120), sort_using_range AS search from sys.x\\\$statements_with_sorting WHERE $sys_db_filter_db ORDER BY sort_using_range DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

##################################################################################

    #statements_with_temp_tables

#mysql> desc statements_with_temp_tables;
#+--------------------------+---------------------+------+-----+---------------------+-------+
#| Field                    | Type                | Null | Key | Default             | Extra |
#+--------------------------+---------------------+------+-----+---------------------+-------+
#| query                    | longtext            | YES  |     | NULL                |       |
#| db                       | varchar(64)         | YES  |     | NULL                |       |
#| exec_count               | bigint(20) unsigned | NO   |     | NULL                |       |
#| total_latency            | text                | YES  |     | NULL                |       |
#| memory_tmp_tables        | bigint(20) unsigned | NO   |     | NULL                |       |
#| disk_tmp_tables          | bigint(20) unsigned | NO   |     | NULL                |       |
#| avg_tmp_tables_per_query | decimal(21,0)       | NO   |     | 0                   |       |
#| tmp_tables_to_disk_pct   | decimal(24,0)       | NO   |     | 0                   |       |
#| first_seen               | timestamp           | NO   |     | 0000-00-00 00:00:00 |       |
#| last_seen                | timestamp           | NO   |     | 0000-00-00 00:00:00 |       |
#| digest                   | varchar(32)         | YES  |     | NULL                |       |
#+--------------------------+---------------------+------+-----+---------------------+-------+
#11 rows in set (0,01 sec)#
#
    subheaderprint "Performance schema: Top 20 queries with temp table";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select db, LEFT(query, 120), exec_count from sys.x\\\$statements_with_temp_tables WHERE $sys_db_filter_db order BY exec_count DESC LIMIT 20;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Last 50 queries with temp table";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select db, LEFT(query, 120), last_seen from sys.x\\\$statements_with_temp_tables WHERE $sys_db_filter_db order BY last_seen DESC LIMIT 50;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint
      "Performance schema: Top 15 total latency queries with temp table";
    $nbL = 1;
    for my $lQuery (
        select_array(
"select db, LEFT(query, 120), total_latency AS search from sys.x\\\$statements_with_temp_tables WHERE $sys_db_filter_db ORDER BY total_latency DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 15 queries with temp table to disk";
    $nbL = 1;
    for my $lQuery (
        select_array(
"use sys;select db, LEFT(query, 120), disk_tmp_tables from sys.x\\\$statements_with_temp_tables WHERE $sys_db_filter_db ORDER BY disk_tmp_tables DESC LIMIT 15;"
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

##################################################################################
    #wait_classes_global_by_latency

#mysql> select * from wait_classes_global_by_latency;
#-----------------+-------+---------------+-------------+-------------+-------------+
# event_class     | total | total_latency | min_latency | avg_latency | max_latency |
#-----------------+-------+---------------+-------------+-------------+-------------+
# wait/io/file    | 15381 | 1.23 s        | 0 ps        | 80.12 us    | 230.64 ms   |
# wait/io/table   |    59 | 7.57 ms       | 5.45 us     | 128.24 us   | 3.95 ms     |
# wait/lock/table |    69 | 3.22 ms       | 658.84 ns   | 46.64 us    | 1.10 ms     |
#-----------------+-------+---------------+-------------+-------------+-------------+
# rows in set (0,00 sec)

    subheaderprint "Performance schema: Top 15 class events by number";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select event_class, total from sys.x\\$wait_classes_global_by_latency ORDER BY total DESC LIMIT 15;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 30 events by number";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select events, total from sys.x\\$waits_global_by_latency ORDER BY total DESC LIMIT 30;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 15 class events by total latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select event_class, total_latency from sys.x\\$wait_classes_global_by_latency ORDER BY total_latency DESC LIMIT 15;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 30 events by total latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'use sys;select events, total_latency from sys.x\\$waits_global_by_latency ORDER BY total_latency DESC LIMIT 30;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 15 class events by max latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select event_class, max_latency from sys.x\\$wait_classes_global_by_latency ORDER BY max_latency DESC LIMIT 15;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

    subheaderprint "Performance schema: Top 30 events by max latency";
    $nbL = 1;
    for my $lQuery (
        select_array(
'select events, max_latency from sys.x\\$waits_global_by_latency ORDER BY max_latency DESC LIMIT 30;'
        )
      )
    {
        infoprint " +-- $nbL: $lQuery";
        $nbL++;
    }
    infoprint "No information found or indicators deactivated."
      if ( $nbL == 1 );

}

# Recommendations for Aria Engine
sub mariadb_aria {
    subheaderprint "Aria Metrics";

    # Aria
    if ( !defined $myvar{'have_aria'} ) {
        infoprint "Aria Storage Engine not available.";
        return;
    }
    if ( $myvar{'have_aria'} ne "YES" ) {
        infoprint "Aria Storage Engine is disabled.";
        return;
    }
    infoprint "Aria Storage Engine is enabled.";

    # Aria pagecache
    if ( !defined( $mycalc{'total_aria_indexes'} ) ) {
        push( @generalrec,
            "Unable to calculate Aria index size on MySQL server" );
    }
    else {
        if (
            $myvar{'aria_pagecache_buffer_size'} < $mycalc{'total_aria_indexes'}
            && $mycalc{'pct_aria_keys_from_mem'} < 95 )
        {
            badprint "Aria pagecache size / total Aria indexes: "
              . hr_bytes( $myvar{'aria_pagecache_buffer_size'} ) . "/"
              . hr_bytes( $mycalc{'total_aria_indexes'} ) . "";
            push( @adjvars,
                    "aria_pagecache_buffer_size (> "
                  . hr_bytes( $mycalc{'total_aria_indexes'} )
                  . ")" );
        }
        else {
            goodprint "Aria pagecache size / total Aria indexes: "
              . hr_bytes( $myvar{'aria_pagecache_buffer_size'} ) . "/"
              . hr_bytes( $mycalc{'total_aria_indexes'} ) . "";
        }
        if ( $mystat{'Aria_pagecache_read_requests'} > 0 ) {
            if ( $mycalc{'pct_aria_keys_from_mem'} < 95 ) {
                badprint
"Aria pagecache hit rate: $mycalc{'pct_aria_keys_from_mem'}% ("
                  . hr_num( $mystat{'Aria_pagecache_read_requests'} )
                  . " cached / "
                  . hr_num( $mystat{'Aria_pagecache_reads'} )
                  . " reads)";
            }
            else {
                goodprint
"Aria pagecache hit rate: $mycalc{'pct_aria_keys_from_mem'}% ("
                  . hr_num( $mystat{'Aria_pagecache_read_requests'} )
                  . " cached / "
                  . hr_num( $mystat{'Aria_pagecache_reads'} )
                  . " reads)";
            }
        }
        else {

            # No queries have run that would use keys
        }
    }
}

# Recommendations for TokuDB
sub mariadb_tokudb {
    subheaderprint "TokuDB Metrics";

    # AriaDB
    unless ( defined $myvar{'have_tokudb'}
        && $myvar{'have_tokudb'} eq "YES" )
    {
        infoprint "TokuDB is disabled.";
        return;
    }
    infoprint "TokuDB is enabled.";

    # Not implemented
}

# Recommendations for XtraDB
sub mariadb_xtradb {
    subheaderprint "XtraDB Metrics";

    # XtraDB
    unless ( defined $myvar{'have_xtradb'}
        && $myvar{'have_xtradb'} eq "YES" )
    {
        infoprint "XtraDB is disabled.";
        return;
    }
    infoprint "XtraDB is enabled.";
    infoprint "Note that MariaDB 10.2 makes use of InnoDB, not XtraDB."

      # Not implemented
}

# Recommendations for RocksDB
sub mariadb_rockdb {
    subheaderprint "RocksDB Metrics";

    # RocksDB
    unless ( defined $myvar{'have_rocksdb'}
        && $myvar{'have_rocksdb'} eq "YES" )
    {
        infoprint "RocksDB is disabled.";
        return;
    }
    infoprint "RocksDB is enabled.";

    # Not implemented
}

# Recommendations for Spider
sub mariadb_spider {
    subheaderprint "Spider Metrics";

    # Spider
    unless ( defined $myvar{'have_spider'}
        && $myvar{'have_spider'} eq "YES" )
    {
        infoprint "Spider is disabled.";
        return;
    }
    infoprint "Spider is enabled.";

    # Not implemented
}

# Recommendations for Connect
sub mariadb_connect {
    subheaderprint "Connect Metrics";

    # Connect
    unless ( defined $myvar{'have_connect'}
        && $myvar{'have_connect'} eq "YES" )
    {
        infoprint "Connect is disabled.";
        return;
    }
    infoprint "Connect is enabled.";

    # Not implemented
}

# Perl trim function to remove whitespace from the start and end of the string
sub trim {
    my $string = shift;
    return "" unless defined($string);
    $string =~ s/^\s+//;
    $string =~ s/\s+$//;
    return $string;
}

sub get_wsrep_options {
    return () unless $myvar{'wsrep_provider_options'};

    my @galera_options = split /;/, $myvar{'wsrep_provider_options'};
    @galera_options = remove_cr @galera_options;
    @galera_options = remove_empty @galera_options;

    #debugprint Dumper( \@galera_options ) if $opt{debug};
    return @galera_options;
}

sub get_gcache_memory {
    my $gCacheMem = hr_raw( get_wsrep_option('gcache.size') );

    return 0 unless $gCacheMem;
    return $gCacheMem;
}

sub get_wsrep_option {
    my $key = shift;
    return '' unless $myvar{'wsrep_provider_options'};
    my @galera_options = get_wsrep_options;
    return '' unless @galera_options;
    my @memValues = grep /\s*$key =/, @galera_options;
    my $memValue  = $memValues[0];
    return 0 unless defined $memValue;
    $memValue =~ s/.*=\s*(.+)$/$1/g;
    return $memValue;
}

# REcommendations for Tables
sub mysql_table_structures {
    return 0 unless ( $opt{structstat} > 0 );
    subheaderprint "Table structures analysis";

    my @primaryKeysNbTables = select_array(
        "Select CONCAT(c.table_schema, ',' , c.table_name)
from information_schema.columns c
join information_schema.tables t using (TABLE_SCHEMA, TABLE_NAME)
where c.table_schema not in ('sys', 'mysql', 'information_schema', 'performance_schema')
  and t.table_type = 'BASE TABLE'
group by c.table_schema,c.table_name
having sum(if(c.column_key in ('PRI', 'UNI'), 1, 0)) = 0"
    );

    my $tmpContent = 'Schema,Table';
    if ( scalar(@primaryKeysNbTables) > 0 ) {
        badprint "Following table(s) don't have primary key:";
        foreach my $badtable (@primaryKeysNbTables) {
            badprint "\t$badtable";
            push @{ $result{'Tables without PK'} }, $badtable;
            $tmpContent .= "\n$badtable";
        }
        push @generalrec,
"Ensure that all table(s) get an explicit primary keys for performance, maintenance and also for replication";
        push @modeling, "Following table(s) don't have primary key: "
          . join( ', ', @primaryKeysNbTables );

    }
    else {
        goodprint "All tables get a primary key";
    }
    dump_into_file( "tables_without_primary_keys.csv", $tmpContent );

    # Advanced PK checks
    my @pkInfo = select_array(
"SELECT c.TABLE_SCHEMA, c.TABLE_NAME, c.COLUMN_NAME, c.DATA_TYPE, c.COLUMN_TYPE
FROM information_schema.columns c
JOIN information_schema.tables t USING (TABLE_SCHEMA, TABLE_NAME)
WHERE t.TABLE_TYPE = 'BASE TABLE'
  AND c.COLUMN_KEY = 'PRI'
  AND c.TABLE_SCHEMA NOT IN ('sys', 'mysql', 'information_schema', 'performance_schema')"
    );

    my $pk_naming_issues_count = 0;
    my $bigint_pk_issues_count = 0;
    my @pk_csv_rows =
      ("Schema,Table,Column,DataType,ColumnType,IssueType,Description");

    foreach my $pk (@pkInfo) {
        my ( $schema, $table, $column, $datatype, $columntype ) = split /\t/,
          $pk;
        $schema     //= '';
        $table      //= '';
        $column     //= '';
        $datatype   //= '';
        $columntype //= '';

        # PK Naming Convention
        if ( $column ne 'id' && $column ne "${table}_id" ) {
            badprint
"Table $schema.$table: Primary key '$column' does not follow 'id' or '${table}_id' naming convention";

            push @modeling,
"Table $schema.$table: Primary key '$column' does not follow naming convention (id or ${table}_id)";
            $pk_naming_issues_count++;
            push @pk_csv_rows,
"$schema,$table,$column,$datatype,$columntype,Naming,Primary key '$column' does not follow 'id' or '${table}_id' naming convention";
        }

        # Surrogate Key Recommendation
        if (   $datatype !~ /int/i
            || $columntype !~ /unsigned/i
            || $columntype !~ /auto_increment/i )
        {
            # Check if it might be a UUID
            if ( $column =~ /uuid/i ) {
                if ( $datatype !~ /binary/i || $columntype !~ /16/ ) {
                    badprint
"Table $schema.$table: UUID primary key '$column' is not optimized (use BINARY(16))";
                    push @generalrec,
"Use optimized BINARY(16) for UUID Primary Keys in $schema.$table";
                    push @modeling,
"Table $schema.$table: UUID primary key '$column' is not optimized (use BINARY(16))";
                    push @pk_csv_rows,
"$schema,$table,$column,$datatype,$columntype,UUIDOptimization,UUID primary key '$column' is not optimized (use BINARY(16))";
                }
            }
            else {
                badprint
"Table $schema.$table: Primary key '$column' is not a recommended surrogate key (BIGINT UNSIGNED AUTO_INCREMENT)";

                push @modeling,
"Table $schema.$table: Primary key '$column' is not a recommended surrogate key (BIGINT UNSIGNED AUTO_INCREMENT)";
                $bigint_pk_issues_count++;
                push @pk_csv_rows,
"$schema,$table,$column,$datatype,$columntype,SurrogateKeyType,Primary key '$column' is not a recommended surrogate key (BIGINT UNSIGNED AUTO_INCREMENT)";
            }
        }
    }

    if ( $pk_naming_issues_count > 0 ) {
        push @generalrec,
"Use 'id' or '_<table>_id' for Primary Key naming in $pk_naming_issues_count table(s)";
    }
    if ( $bigint_pk_issues_count > 0 ) {
        push @generalrec,
"Use BIGINT UNSIGNED AUTO_INCREMENT for Primary Keys in $bigint_pk_issues_count table(s)";
    }

    if ( $opt{dumpdir} && @pk_csv_rows > 1 ) {
        dump_into_file( "primary_key_issues.csv", join( "\n", @pk_csv_rows ) );
    }

    # Large Tables (>1GB) without Secondary Indexes
    my @largeTablesWithoutIndexes = select_array(
        "SELECT TABLE_SCHEMA, TABLE_NAME, (DATA_LENGTH + INDEX_LENGTH)
FROM information_schema.tables t
WHERE TABLE_TYPE = 'BASE TABLE'
  AND (DATA_LENGTH + INDEX_LENGTH) > 1024*1024*1024
  AND (SELECT COUNT(*) FROM information_schema.statistics s WHERE s.TABLE_SCHEMA = t.TABLE_SCHEMA AND s.TABLE_NAME = t.TABLE_NAME AND s.INDEX_NAME != 'PRIMARY') = 0
  AND TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')"
    );
    if (@largeTablesWithoutIndexes) {
        foreach my $lt (@largeTablesWithoutIndexes) {
            my ( $schema, $table, $size ) = split /\t/, $lt;
            $schema //= '';
            $table  //= '';
            $size   //= 0;
            badprint "Table $schema.$table is large ("
              . hr_bytes($size)
              . ") and has no secondary indexes";
            push @generalrec,
"Add secondary indexes to large table $schema.$table to improve query performance";
            push @modeling,
                "Table $schema.$table is large ("
              . hr_bytes($size)
              . ") and has no secondary indexes";
        }
    }

    # Foreign Key Type Mismatches
    my @fkMismatches = select_array(
"SELECT CONCAT(k.TABLE_SCHEMA, '.', k.TABLE_NAME, ' (', k.COLUMN_NAME, ': ', c1.COLUMN_TYPE, ') -> ', k.REFERENCED_TABLE_SCHEMA, '.', k.REFERENCED_TABLE_NAME, ' (', k.REFERENCED_COLUMN_NAME, ': ', c2.COLUMN_TYPE, ')')
FROM information_schema.KEY_COLUMN_USAGE k
JOIN information_schema.COLUMNS c1 ON k.TABLE_SCHEMA = c1.TABLE_SCHEMA AND k.TABLE_NAME = c1.TABLE_NAME AND k.COLUMN_NAME = c1.COLUMN_NAME
JOIN information_schema.COLUMNS c2 ON k.REFERENCED_TABLE_SCHEMA = c2.TABLE_SCHEMA AND k.REFERENCED_TABLE_NAME = c2.TABLE_NAME AND k.REFERENCED_COLUMN_NAME = c2.COLUMN_NAME
WHERE k.REFERENCED_TABLE_NAME IS NOT NULL
  AND (c1.DATA_TYPE != c2.DATA_TYPE OR c1.COLUMN_TYPE != c2.COLUMN_TYPE)
  AND k.TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')"
    );
    if (@fkMismatches) {
        badprint "Following Foreign Key(s) have data type mismatches:";
        foreach my $fk (@fkMismatches) {
            badprint "\t$fk";
            push @generalrec, "Fix data type mismatch for Foreign Key: $fk";
            push @modeling,   "Foreign Key type mismatch: $fk";
        }
    }

    my @nonInnoDBTables = select_array(
        "select table_schema, table_name, ENGINE 
FROM information_schema.tables t
WHERE ENGINE <> 'InnoDB' 
and t.table_type = 'BASE TABLE'
and table_schema not in 
('sys', 'mysql', 'performance_schema', 'information_schema')"
    );
    $tmpContent = 'Schema,Table,Engine';
    if ( scalar(@nonInnoDBTables) > 0 ) {
        badprint "Following table(s) are not InnoDB table:";
        push @generalrec,
"Ensure that all table(s) are InnoDB tables for performance and also for replication";
        push @modeling, "Following table(s) are not InnoDB: "
          . join( ', ', @nonInnoDBTables );
        foreach my $badtable (@nonInnoDBTables) {
            if ( $badtable =~ /Memory/i ) {
                badprint
"Table $badtable is a MEMORY table. It's suggested to use only InnoDB tables in production";
            }
            else {
                badprint "\t$badtable";
            }
            $tmpContent .= "\n$badtable";
        }
    }
    else {
        goodprint "All tables are InnoDB tables";
    }
    dump_into_file( "tables_non_innodb.csv", $tmpContent );

    my @nonutf8columns = select_array(
"SELECT CONCAT(table_schema, ',', table_name, ',', column_name, ',', CHARacter_set_name, ',', COLLATION_name, ',', data_type, ',', CHARACTER_MAXIMUM_LENGTH)
from information_schema.columns
WHERE table_schema not in ('sys', 'mysql', 'performance_schema', 'information_schema')
and (CHARacter_set_name  NOT LIKE 'utf8%'
or COLLATION_name NOT LIKE 'utf8%');"
    );
    $tmpContent =
      'Schema,Table,Column, Charset, Collation, Data Type, Max Length';
    if ( scalar(@nonutf8columns) > 0 ) {
        badprint "Following character columns(s) are not utf8 compliant:";
        push @generalrec,
"Ensure that all text colums(s) are UTF-8 compliant for encoding support and performance";
        push @modeling, "Following collection(s) are not UTF-8 compliant: "
          . join( ', ', @nonutf8columns );
        foreach my $badtable (@nonutf8columns) {
            badprint "\t$badtable";
            $tmpContent .= "\n$badtable";
        }
    }
    else {
        goodprint "All columns are UTF-8 compliant";
    }
    dump_into_file( "columns_non_utf8.csv", $tmpContent );

    my @utf8columns = select_array(
"SELECT CONCAT(table_schema, ',', table_name, ',', column_name, ',', CHARacter_set_name, ',', COLLATION_name, ',', data_type, ',', CHARACTER_MAXIMUM_LENGTH)
from information_schema.columns
WHERE table_schema not in ('sys', 'mysql', 'performance_schema', 'information_schema')
and (CHARacter_set_name  LIKE 'utf8%'
or COLLATION_name LIKE 'utf8%');"
    );
    $tmpContent =
      'Schema,Table,Column, Charset, Collation, Data Type, Max Length';
    foreach my $badtable (@utf8columns) {
        $tmpContent .= "\n$badtable";
    }
    dump_into_file( "columns_utf8.csv", $tmpContent );

    my @ftcolumns = select_array(
"SELECT CONCAT(table_schema, ',', table_name, ',', column_name, ',', data_type)
from information_schema.columns
WHERE table_schema not in ('sys', 'mysql', 'performance_schema', 'information_schema')
AND data_type='FULLTEXT';"
    );
    $tmpContent = 'Schema,Table,Column, Data Type';
    foreach my $ctable (@ftcolumns) {
        $tmpContent .= "\n$ctable";
    }
    dump_into_file( "fulltext_columns.csv", $tmpContent );

    mysql_naming_conventions();
    mysql_foreign_key_checks();
    mysql_80_modeling_checks();
    mysql_datatype_optimization();
    mysql_schema_sanitization();
}

sub mysql_80_modeling_checks {
    return unless mysql_version_ge( 8, 0 );

    my $is_mariadb = (
        ( defined $myvar{'version'} && $myvar{'version'} =~ /MariaDB/i )
          or ( defined $myvar{'version_comment'}
            && $myvar{'version_comment'} =~ /MariaDB/i )
    );
    my $header =
      $is_mariadb
      ? "MariaDB 10.x+ Specific Modeling"
      : "MySQL 8.0+ Specific Modeling";
    subheaderprint $header;

    my $modeling80Count = 0;

    # JSON indexability
    my @jsonColumns = select_array(
"SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME FROM information_schema.columns WHERE DATA_TYPE = 'json' AND TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')"
    );
    my $json_columns_without_virtual_count = 0;
    my @json_csv_rows = ("Schema,Table,Column,IssueType,Description");

    foreach my $jc (@jsonColumns) {
        my ( $schema, $table, $column ) = split /\t/, $jc;
        $schema //= '';
        $table  //= '';
        $column //= '';

        # Check if there are generated columns for this table
        my @genCols = select_array(
"SELECT COLUMN_NAME FROM information_schema.columns WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND EXTRA LIKE '%VIRTUAL%'"
        );
        if ( scalar(@genCols) == 0 ) {
            infoprint
"Table $schema.$table: JSON column '$column' detected without Virtual Generated Columns for indexing";

            push @modeling,
"Table $schema.$table: JSON column '$column' detected without Virtual Generated Columns for indexing";
            $json_columns_without_virtual_count++;
            $modeling80Count++;
            push @json_csv_rows,
"$schema,$table,$column,MissingVirtualColumn,JSON column detected without Virtual Generated Columns for indexing";
        }
    }
    if ( $json_columns_without_virtual_count > 0 ) {
        push @generalrec,
"Consider using Generated Columns to index frequently searched attributes in JSON column in $json_columns_without_virtual_count column(s)";
    }

    if ( $opt{dumpdir} && @json_csv_rows > 1 ) {
        dump_into_file( "json_columns_without_virtual.csv",
            join( "\n", @json_csv_rows ) );
    }

    # Invisible Indexes (MySQL: IS_VISIBLE='NO', MariaDB: IGNORED='YES')
    my $visible_col = $is_mariadb ? "IGNORED" : "IS_VISIBLE";
    my $visible_val = $is_mariadb ? "'YES'"   : "'NO'";

    my @invisibleIdx = select_array(
"SELECT TABLE_SCHEMA, TABLE_NAME, INDEX_NAME FROM information_schema.statistics WHERE $visible_col = $visible_val AND TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')"
    );
    foreach my $ii (@invisibleIdx) {
        my ( $schema, $table, $index ) = split /\t/, $ii;
        $schema //= '';
        $table  //= '';
        $index  //= '';
        infoprint "Index $schema.$table.$index is INVISIBLE";
        push @modeling, "Index $schema.$table.$index is INVISIBLE";
        $modeling80Count++;
    }

    # Check Constraints
    if ( mysql_version_ge( 8, 0, 16 ) ) {
        my @checkConstraints = select_array(
"SELECT CONSTRAINT_SCHEMA, TABLE_NAME, CONSTRAINT_NAME FROM information_schema.table_constraints WHERE CONSTRAINT_TYPE = 'CHECK' AND CONSTRAINT_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')"
        );
        if ( scalar(@checkConstraints) == 0 ) {

# Maybe too noisy to always suggest, but it's a good practice
# infoprint "No CHECK constraints detected; consider using them for data integrity";
        }
        else {
            # We skip counting these as we don't badprint/infoprint them for now
        }
    }
    goodprint "No MySQL 8.0+ specific modeling issues found"
      if $modeling80Count == 0;
}

sub mysql_datatype_optimization {
    subheaderprint "Data Type optimization";

    # NULLability
    my @nullableCols = select_array(
"SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME FROM information_schema.columns WHERE IS_NULLABLE = 'YES' AND TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')"
    );
    my $nullableCount = scalar(@nullableCols);
    if ( $nullableCount > 20 ) {
        infoprint
"There are $nullableCount columns with NULL enabled. Consider using NOT NULL where possible for better performance.";
        push @modeling,
"There are $nullableCount columns with NULL enabled. Consider using NOT NULL where possible for better performance.";
    }
    else {
        goodprint "No data type optimization recommendations";
    }

    # BIGINT vs INT
    # This is a bit hard to check without looking at table rows and max values
}

sub get_compatible_styles {
    my ($name) = @_;
    return () unless defined $name && $name ne '';
    my @styles;
    if ( $name =~ /^[a-z0-9]+(?:_[a-z0-9]+)*$/ ) {
        push @styles, 'snake_case';
    }
    if ( $name =~ /^[a-z0-9]+(?:[A-Z0-9][a-z0-9]*)*$/ ) {
        push @styles, 'camelCase';
    }
    if ( $name =~ /^[A-Z0-9][a-z0-9]*(?:[A-Z0-9][a-z0-9]*)*$/ ) {
        push @styles, 'PascalCase';
    }
    if ( $name =~ /^[a-z0-9]+(?:-[a-z0-9]+)*$/ ) {
        push @styles, 'kebab-case';
    }
    if ( $name =~ /^[A-Z0-9]+(?:_[A-Z0-9]+)*$/ ) {
        push @styles, 'UPPER_SNAKE_CASE';
    }
    return @styles;
}

sub find_dominant_style {
    my ($names_ref) = @_;
    my %style_counts;
    foreach my $name (@$names_ref) {
        my @styles = get_compatible_styles($name);
        foreach my $style (@styles) {
            $style_counts{$style}++;
        }
    }
    my $dominant  = 'snake_case';    # Default fallback
    my $max_count = 0;
    foreach my $style (
        qw(snake_case camelCase PascalCase kebab-case UPPER_SNAKE_CASE))
    {
        if ( ( $style_counts{$style} // 0 ) > $max_count ) {
            $max_count = $style_counts{$style};
            $dominant  = $style;
        }
    }
    return $dominant;
}

sub mysql_naming_conventions {
    subheaderprint "Naming conventions analysis";

    my $namingIssues              = 0;
    my $plural_table_issues_count = 0;
    my $table_style_issues_count  = 0;
    my $view_style_issues_count   = 0;
    my $index_style_issues_count  = 0;
    my $column_style_issues_count = 0;

    my @naming_csv_rows = ("ObjectType,Schema,Object,Detail,DeviationType");

    # Table Naming
    my @tables = select_array(
"SELECT TABLE_SCHEMA, TABLE_NAME FROM information_schema.tables WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')"
    );
    my @table_names          = map { ( split /\t/, $_ )[1] // '' } @tables;
    my $dominant_table_style = find_dominant_style( \@table_names );

    foreach my $t (@tables) {
        my ( $schema, $table ) = split /\t/, $t;
        $schema //= '';
        $table  //= '';

        # Plural check (very basic: ends with 's' but not 'ss')
        if (   ( $table // '' ) =~ /[^s]s$/i
            && ( $table // '' ) !~ /status|address|glass|process/i )
        {
            badprint
              "Table $schema.$table: Plural name detected (prefer singular)";

            push @modeling,
              "Table $schema.$table: Plural name detected (prefer singular)";
            $plural_table_issues_count++;
            $namingIssues++;
            push @naming_csv_rows,
"Table,$schema,$table,Plural name detected (prefer singular),PluralName";
        }

        # Casing check (detect CamelCase/PascalCase or other non-dominant)
        my @compat        = get_compatible_styles($table);
        my $is_compatible = grep { $_ eq $dominant_table_style } @compat;
        if ( !$is_compatible ) {
            badprint
              "Table $schema.$table: Non-${dominant_table_style} name detected";

            push @modeling,
              "Table $schema.$table: Non-${dominant_table_style} name detected";
            $table_style_issues_count++;
            $namingIssues++;
            push @naming_csv_rows,
"Table,$schema,$table,Non-${dominant_table_style} name detected,StyleCasing";
        }
    }

    # View Naming
    my @views = select_array(
"SELECT TABLE_SCHEMA, TABLE_NAME FROM information_schema.tables WHERE TABLE_TYPE = 'VIEW' AND TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')"
    );
    my @view_names          = map { ( split /\t/, $_ )[1] // '' } @views;
    my $dominant_view_style = find_dominant_style( \@view_names );

    foreach my $v (@views) {
        my ( $schema, $view ) = split /\t/, $v;
        $schema //= '';
        $view   //= '';

        my @compat        = get_compatible_styles($view);
        my $is_compatible = grep { $_ eq $dominant_view_style } @compat;
        if ( !$is_compatible ) {
            badprint
              "View $schema.$view: Non-${dominant_view_style} name detected";
            push @modeling,
              "View $schema.$view: Non-${dominant_view_style} name detected";
            $view_style_issues_count++;
            $namingIssues++;
            push @naming_csv_rows,
"View,$schema,$view,Non-${dominant_view_style} name detected,StyleCasing";
        }
    }

    # Index Naming
    my @indexes = select_array(
"SELECT DISTINCT TABLE_SCHEMA, TABLE_NAME, INDEX_NAME FROM information_schema.statistics WHERE INDEX_NAME != 'PRIMARY' AND TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')"
    );
    my @index_names          = map { ( split /\t/, $_ )[2] // '' } @indexes;
    my $dominant_index_style = find_dominant_style( \@index_names );

    foreach my $idx (@indexes) {
        my ( $schema, $table, $index ) = split /\t/, $idx;
        $schema //= '';
        $table  //= '';
        $index  //= '';

        my @compat        = get_compatible_styles($index);
        my $is_compatible = grep { $_ eq $dominant_index_style } @compat;
        if ( !$is_compatible ) {
            badprint
"Index $schema.$table.$index: Non-${dominant_index_style} name detected";
            push @modeling,
"Index $schema.$table.$index: Non-${dominant_index_style} name detected";
            $index_style_issues_count++;
            $namingIssues++;
            push @naming_csv_rows,
"Index,$schema,$table.$index,Non-${dominant_index_style} name detected,StyleCasing";
        }
    }

    # Column Naming
    my @columns = select_array(
"SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM information_schema.columns WHERE TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')"
    );
    my @column_names          = map { ( split /\t/, $_ )[2] // '' } @columns;
    my $dominant_column_style = find_dominant_style( \@column_names );

    foreach my $c (@columns) {
        my ( $schema, $table, $column, $datatype ) = split /\t/, $c;
        $schema   //= '';
        $table    //= '';
        $column   //= '';
        $datatype //= '';

        # Casing check
        my @compat        = get_compatible_styles($column);
        my $is_compatible = grep { $_ eq $dominant_column_style } @compat;
        if ( !$is_compatible ) {
            badprint
"Column $schema.$table.$column: Non-${dominant_column_style} name detected";

            push @modeling,
"Column $schema.$table.$column: Non-${dominant_column_style} name detected";
            $column_style_issues_count++;
            $namingIssues++;
            push @naming_csv_rows,
"Column,$schema,$table.$column,Non-${dominant_column_style} name detected,StyleCasing";
        }

        # Boolean naming
        if ( ( $datatype // '' ) =~ /tinyint\(1\)|bool/i ) {
            if ( ( $column // '' ) !~ /^(is_|has_|was_|had_)/ ) {
                infoprint
"Column $schema.$table.$column: Boolean-like column missing verbal prefix (is_, has_, etc.)";
                push @modeling,
"Column $schema.$table.$column: Boolean-like column missing verbal prefix (is_, has_, etc.)";

                $namingIssues++;
                push @naming_csv_rows,
"Column,$schema,$table.$column,Boolean-like column missing verbal prefix (is_ has_ etc),VerbalPrefix";
            }
        }

        # Date naming
        if ( ( $datatype // '' ) =~ /date|time/i ) {
            if ( ( $column // '' ) !~ /(_at|_date|_time)$/ ) {
                infoprint
"Column $schema.$table.$column: Date/Time column missing explicit suffix (_at, _date, _time)";
                push @modeling,
"Column $schema.$table.$column: Date/Time column missing explicit suffix (_at, _date, _time)";
                $namingIssues++;
                push @naming_csv_rows,
"Column,$schema,$table.$column,Date/Time column missing explicit suffix (_at _date _time),Suffix";
            }
        }
    }

    if ( $plural_table_issues_count > 0 ) {
        push @generalrec,
          "Use singular names for table in $plural_table_issues_count table(s)";
    }
    if ( $table_style_issues_count > 0 ) {
        push @generalrec,
"Use $dominant_table_style for table in $table_style_issues_count table(s)";
    }
    if ( $view_style_issues_count > 0 ) {
        push @generalrec,
"Use $dominant_view_style for view in $view_style_issues_count view(s)";
    }
    if ( $index_style_issues_count > 0 ) {
        push @generalrec,
"Use $dominant_index_style for index in $index_style_issues_count index(es)";
    }
    if ( $column_style_issues_count > 0 ) {
        push @generalrec,
"Use $dominant_column_style for column in $column_style_issues_count column(s)";
    }

    if ( $opt{dumpdir} && @naming_csv_rows > 1 ) {
        dump_into_file(
            "naming_convention_deviations.csv",
            join( "\n", @naming_csv_rows )
        );
    }

    goodprint "No naming convention issues found" if $namingIssues == 0;
}

sub mysql_foreign_key_checks {
    subheaderprint "Foreign Key analysis";

    my $fkIssues    = 0;
    my @fk_csv_rows = ("Schema,Table,Column,IssueType,Description");

    # Unconstrained _id columns
    my @unconstrainedId = select_array(
        "SELECT c.TABLE_SCHEMA, c.TABLE_NAME, c.COLUMN_NAME
FROM information_schema.columns c
LEFT JOIN information_schema.key_column_usage k ON c.TABLE_SCHEMA = k.TABLE_SCHEMA AND c.TABLE_NAME = k.TABLE_NAME AND c.COLUMN_NAME = k.COLUMN_NAME AND k.REFERENCED_TABLE_NAME IS NOT NULL
WHERE c.COLUMN_NAME LIKE '%_id'
  AND k.COLUMN_NAME IS NULL
  AND c.TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')"
    );
    my $unconstrained_id_count = 0;
    foreach my $id (@unconstrainedId) {
        my ( $schema, $table, $column ) = split /\t/, $id;
        $schema //= '';
        $table  //= '';
        $column //= '';

        # Exclude PKs that are named table_id
        next if $column eq "${table}_id";

        badprint
"Column $schema.$table.$column ends in '_id' but has no FOREIGN KEY constraint";

        push @modeling,
"Column $schema.$table.$column ends in '_id' but has no FOREIGN KEY constraint";
        $unconstrained_id_count++;
        $fkIssues++;
        push @fk_csv_rows,
"$schema,$table,$column,MissingForeignKey,Column ends in '_id' but has no FOREIGN KEY constraint";
    }
    if ( $unconstrained_id_count > 0 ) {
        push @generalrec,
          "Add FOREIGN KEY constraint to $unconstrained_id_count column(s)";
    }

    # FK Actions
    my @fkActions = select_array(
"SELECT rc.CONSTRAINT_SCHEMA, rc.TABLE_NAME, k.COLUMN_NAME, rc.REFERENCED_TABLE_NAME, k.REFERENCED_COLUMN_NAME, rc.DELETE_RULE
FROM information_schema.referential_constraints rc
JOIN information_schema.key_column_usage k ON rc.CONSTRAINT_SCHEMA = k.CONSTRAINT_SCHEMA AND rc.CONSTRAINT_NAME = k.CONSTRAINT_NAME
WHERE rc.CONSTRAINT_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')"
    );
    foreach my $fk (@fkActions) {
        my ( $schema, $table, $column, $ref_table, $ref_column, $delete_rule )
          = split /\t/, $fk;
        $schema      //= '';
        $table       //= '';
        $column      //= '';
        $ref_table   //= '';
        $ref_column  //= '';
        $delete_rule //= '';
        if ( $delete_rule eq 'CASCADE' ) {
            infoprint
"Constraint on $schema.$table.$column uses ON DELETE CASCADE; ensure this is intended.";
            push @modeling,
"Constraint on $schema.$table.$column uses ON DELETE CASCADE; ensure this is intended.";
            $fkIssues++;
        }
    }

    # Foreign Key Type Mismatches
    my @fkTypeMismatches = select_array(
"SELECT k.CONSTRAINT_SCHEMA, k.TABLE_NAME, k.COLUMN_NAME, c1.COLUMN_TYPE, k.REFERENCED_TABLE_NAME, k.REFERENCED_COLUMN_NAME, c2.COLUMN_TYPE
FROM information_schema.key_column_usage k
JOIN information_schema.columns c1 ON k.TABLE_SCHEMA = c1.TABLE_SCHEMA AND k.TABLE_NAME = c1.TABLE_NAME AND k.COLUMN_NAME = c1.COLUMN_NAME
JOIN information_schema.columns c2 ON k.REFERENCED_TABLE_SCHEMA = c2.TABLE_SCHEMA AND k.REFERENCED_TABLE_NAME = c2.TABLE_NAME AND k.REFERENCED_COLUMN_NAME = c2.COLUMN_NAME
WHERE k.REFERENCED_TABLE_NAME IS NOT NULL
  AND c1.COLUMN_TYPE != c2.COLUMN_TYPE
  AND k.CONSTRAINT_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')"
    );
    foreach my $mm (@fkTypeMismatches) {
        my ( $schema, $table, $col, $type1, $ref_table, $ref_col, $type2 ) =
          split /\t/, $mm;
        $schema    //= '';
        $table     //= '';
        $col       //= '';
        $type1     //= '';
        $ref_table //= '';
        $ref_col   //= '';
        $type2     //= '';
        badprint
"FK Type Mismatch: $schema.$table.$col ($type1) -> $ref_table.$ref_col ($type2)";
        push @generalrec,
"Fix data type mismatch in Foreign Key $schema.$table.$col ($type1 vs $type2)";
        push @modeling,
"FK Type Mismatch: $schema.$table.$col ($type1) references $ref_table.$ref_col ($type2)";
        $fkIssues++;
        push @fk_csv_rows,
"$schema,$table,$col,TypeMismatch,Foreign key type mismatch: $type1 references $ref_table.$ref_col ($type2)";
    }

    if ( $opt{dumpdir} && @fk_csv_rows > 1 ) {
        dump_into_file( "missing_foreign_keys.csv",
            join( "\n", @fk_csv_rows ) );
    }

    goodprint "No foreign key issues found" if $fkIssues == 0;
}

sub mysql_schema_sanitization {
    subheaderprint "Schema sanitization";

    my @emptyOrViewOnlySchemas = select_array(
        "SELECT TABLE_SCHEMA, 
       SUM(CASE WHEN TABLE_TYPE = 'BASE TABLE' THEN 1 ELSE 0 END), 
       SUM(CASE WHEN TABLE_TYPE = 'VIEW' THEN 1 ELSE 0 END)
FROM information_schema.tables
WHERE TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')
GROUP BY TABLE_SCHEMA
HAVING SUM(CASE WHEN TABLE_TYPE = 'BASE TABLE' THEN 1 ELSE 0 END) = 0"
    );

    if ( scalar(@emptyOrViewOnlySchemas) == 0 ) {
        goodprint "No empty or view-only schemas detected";
    }
    else {
        foreach my $s (@emptyOrViewOnlySchemas) {
            my ( $schema, $tables, $views ) = split /\t/, $s;
            $schema //= '';
            $tables //= 0;
            $views  //= 0;
            if ( $tables == 0 && $views == 0 ) {
                infoprint "Schema $schema is empty (no tables or views)";
                push @modeling, "Schema $schema is empty (no tables or views)";
            }
            elsif ( $tables == 0 && $views > 0 ) {
                infoprint "Schema $schema contains only views ($views views)";
                push @modeling,
                  "Schema $schema contains only views ($views views)";
            }
        }
    }
}

# Recommendations for Galera
sub mariadb_galera {
    subheaderprint "Galera Metrics";

    # Galera Cluster
    unless ( defined $myvar{'have_galera'}
        && $myvar{'have_galera'} eq "YES" )
    {
        infoprint "Galera is disabled.";
        return;
    }
    infoprint "Galera is enabled.";
    debugprint "Galera variables:";
    foreach my $gvar ( keys %myvar ) {
        next unless $gvar =~ /^wsrep.*/;
        next if $gvar eq 'wsrep_provider_options';
        debugprint "\t" . trim($gvar) . " = " . $myvar{$gvar};
        $result{'Galera'}{'variables'}{$gvar} = $myvar{$gvar};
    }
    if ( not defined( $myvar{'wsrep_on'} ) or $myvar{'wsrep_on'} ne "ON" ) {
        infoprint "Galera is disabled.";
        return;
    }
    debugprint "Galera wsrep provider Options:";
    my @galera_options = get_wsrep_options;
    $result{'Galera'}{'wsrep options'} = get_wsrep_options();
    foreach my $gparam (@galera_options) {
        debugprint "\t" . trim($gparam);
    }
    debugprint "Galera status:";
    foreach my $gstatus ( keys %mystat ) {
        next unless $gstatus =~ /^wsrep.*/;
        debugprint "\t" . trim($gstatus) . " = " . $mystat{$gstatus};
        $result{'Galera'}{'status'}{$gstatus} = $myvar{$gstatus};
    }
    infoprint "GCache is using "
      . hr_bytes_rnd( get_wsrep_option('gcache.mem_size') );

    infoprint "CPU cores detected : " . (cpu_cores);
    my $wsrep_threads_var_name = 'wsrep_slave_threads';
    if ( defined( $myvar{'wsrep_applier_threads'} ) ) {
        $wsrep_threads_var_name = 'wsrep_applier_threads';
    }

  # Use 1 as a fallback if $myvar{$wsrep_threads_var_name} is undefined or zero,
  # to ensure there is at least one thread for Galera replication.
    my $wsrep_threads_value = $myvar{$wsrep_threads_var_name} || 1;

    infoprint "$wsrep_threads_var_name: " . $wsrep_threads_value;

    if (   $wsrep_threads_value > ( (cpu_cores) * 4 )
        or $wsrep_threads_value < ( (cpu_cores) * 2 ) )
    {
        badprint
"$wsrep_threads_var_name is not equal to 2, 3 or 4 times the number of CPU(s)";
        push @adjvars, "$wsrep_threads_var_name = " . ( (cpu_cores) * 4 );
    }
    else {
        goodprint
"$wsrep_threads_var_name is equal to 2, 3 or 4 times the number of CPU(s)";
    }

    if ( $wsrep_threads_value > 1 ) {
        infoprint
          "wsrep parallel replica can cause frequent inconsistency crash.";
        push @adjvars,
"Set $wsrep_threads_var_name to 1 in case of HA_ERR_FOUND_DUPP_KEY crash on replica";

        # check options for parallel replica
        if ( get_wsrep_option('wsrep_slave_FK_checks') eq "OFF" ) {
            badprint "wsrep_slave_FK_checks is off with parallel replica";
            push @adjvars,
              "wsrep_slave_FK_checks should be ON when using parallel replica";
        }

        # wsrep_slave_UK_checks seems useless in MySQL source code
        if ( $myvar{'innodb_autoinc_lock_mode'} != 2 ) {
            badprint
              "innodb_autoinc_lock_mode is incorrect with parallel replica";
            push @adjvars,
"innodb_autoinc_lock_mode should be 2 when using parallel replica";
        }
    }

    if ( get_wsrep_option('gcs.fc_limit') != $wsrep_threads_value * 5 ) {
        badprint
          "gcs.fc_limit should be equal to 5 * $wsrep_threads_var_name (="
          . ( $wsrep_threads_value * 5 ) . ")";
        push @adjvars, "gcs.fc_limit= $wsrep_threads_var_name * 5 (="
          . ( $wsrep_threads_value * 5 ) . ")";
    }
    else {
        goodprint "gcs.fc_limit is equal to 5 * $wsrep_threads_var_name ( ="
          . get_wsrep_option('gcs.fc_limit') . ")";
    }

    if ( get_wsrep_option('gcs.fc_factor') != 0.8 ) {
        badprint "gcs.fc_factor should be equal to 0.8 (="
          . get_wsrep_option('gcs.fc_factor') . ")";
        push @adjvars, "gcs.fc_factor=0.8";
    }
    else {
        goodprint "gcs.fc_factor is equal to 0.8";
    }
    if ( get_wsrep_option('wsrep_flow_control_paused') > 0.02 ) {
        badprint "Fraction of time node pause flow control > 0.02";
    }
    else {
        goodprint
"Flow control fraction seems to be OK (wsrep_flow_control_paused <= 0.02)";
    }

    if ( defined $myvar{'binlog_format'} and $myvar{'binlog_format'} ne 'ROW' )
    {
        badprint "Binlog format should be in ROW mode.";
        push @adjvars, "binlog_format = ROW";
    }
    else {
        goodprint "Binlog format is in ROW mode.";
    }
    if ( defined $myvar{'innodb_flush_log_at_trx_commit'}
        and $myvar{'innodb_flush_log_at_trx_commit'} != 0 )
    {
        badprint "InnoDB flush log at each commit should be disabled.";
        push @adjvars, "innodb_flush_log_at_trx_commit = 0";
    }
    else {
        goodprint "InnoDB flush log at each commit is disabled for Galera.";
    }

    if ( defined $myvar{'wsrep_causal_reads'}
        and $myvar{'wsrep_causal_reads'} ne '' )
    {
        infoprint "Read consistency mode :" . $myvar{'wsrep_causal_reads'};
    }
    elsif ( defined $myvar{'wsrep_sync_wait'} ) {
        infoprint "Sync Wait mode : " . $myvar{'wsrep_sync_wait'};
    }

    if ( defined( $myvar{'wsrep_cluster_name'} )
        and $myvar{'wsrep_on'} eq "ON" )
    {
        goodprint "Galera WsREP is enabled.";
        if ( defined( $myvar{'wsrep_cluster_address'} )
            and trim("$myvar{'wsrep_cluster_address'}") ne "" )
        {
            goodprint "Galera Cluster address is defined: "
              . $myvar{'wsrep_cluster_address'};
            my @NodesTmp = split /,/, $myvar{'wsrep_cluster_address'};
            my $nbNodes  = @NodesTmp;
            infoprint "There are $nbNodes nodes in wsrep_cluster_address";
            my $nbNodesSize = trim( $mystat{'wsrep_cluster_size'} );
            if ( $nbNodesSize == 3 or $nbNodesSize == 5 ) {
                goodprint "There are $nbNodesSize nodes in wsrep_cluster_size.";
            }
            else {
                badprint
"There are $nbNodesSize nodes in wsrep_cluster_size. Prefer 3 or 5 nodes architecture.";
                push @generalrec, "Prefer 3 or 5 nodes architecture.";
            }

            # wsrep_cluster_address doesn't include garbd nodes
            if ( $nbNodes > $nbNodesSize ) {
                badprint
"All cluster nodes are not detected. wsrep_cluster_size less than node count in wsrep_cluster_address";
            }
            else {
                goodprint "All cluster nodes detected.";
            }
        }
        else {
            badprint "Galera Cluster address is undefined";
            push @adjvars,
              "set up wsrep_cluster_address variable for Galera replication";
        }
        if ( defined( $myvar{'wsrep_cluster_name'} )
            and trim( $myvar{'wsrep_cluster_name'} ) ne "" )
        {
            goodprint "Galera Cluster name is defined: "
              . $myvar{'wsrep_cluster_name'};
        }
        else {
            badprint "Galera Cluster name is undefined";
            push @adjvars,
              "set up wsrep_cluster_name variable for Galera replication";
        }
        if ( defined( $myvar{'wsrep_node_name'} )
            and trim( $myvar{'wsrep_node_name'} ) ne "" )
        {
            goodprint "Galera Node name is defined: "
              . $myvar{'wsrep_node_name'};
        }
        else {
            badprint "Galera node name is undefined";
            push @adjvars,
              "set up wsrep_node_name variable for Galera replication";
        }
        if ( trim( $myvar{'wsrep_notify_cmd'} ) ne "" ) {
            goodprint "Galera Notify command is defined.";
        }
        else {
            badprint "Galera Notify command is not defined.";
            push( @adjvars,
                "set up parameter wsrep_notify_cmd to be notified" );
        }
        if (    trim( $myvar{'wsrep_sst_method'} ) !~ "^xtrabackup.*"
            and trim( $myvar{'wsrep_sst_method'} ) !~ "^mariabackup" )
        {
            badprint "Galera SST method is not xtrabackup based.";
            push( @adjvars,
"set up parameter wsrep_sst_method to xtrabackup based parameter"
            );
        }
        else {
            goodprint "SST Method is based on xtrabackup.";
        }
        if (
            (
                defined( $myvar{'wsrep_OSU_method'} )
                && trim( $myvar{'wsrep_OSU_method'} ) eq "TOI"
            )
            || ( defined( $myvar{'wsrep_osu_method'} )
                && trim( $myvar{'wsrep_osu_method'} ) eq "TOI" )
          )
        {
            goodprint "TOI is default mode for upgrade.";
        }
        else {
            badprint "Schema upgrade are not replicated automatically";
            push( @adjvars, "set up parameter wsrep_OSU_method to TOI" );
        }
        infoprint "Max WsRep message : "
          . hr_bytes( $myvar{'wsrep_max_ws_size'} );
    }
    else {
        badprint "Galera WsREP is disabled";
    }

    if ( defined( $mystat{'wsrep_connected'} )
        and $mystat{'wsrep_connected'} eq "ON" )
    {
        goodprint "Node is connected";
    }
    else {
        badprint "Node is disconnected";
    }
    if ( defined( $mystat{'wsrep_ready'} ) and $mystat{'wsrep_ready'} eq "ON" )
    {
        goodprint "Node is ready";
    }
    else {
        badprint "Node is not ready";
    }
    infoprint "Cluster status :" . ( $mystat{'wsrep_cluster_status'} // '' );
    if ( defined( $mystat{'wsrep_cluster_status'} )
        and $mystat{'wsrep_cluster_status'} eq "Primary" )
    {
        goodprint "Galera cluster is consistent and ready for operations";
    }
    else {
        badprint "Cluster is not consistent and ready";
    }
    if (    defined $mystat{'wsrep_local_state_uuid'}
        and defined $mystat{'wsrep_cluster_state_uuid'}
        and $mystat{'wsrep_local_state_uuid'} eq
        $mystat{'wsrep_cluster_state_uuid'} )
    {
        goodprint "Node and whole cluster at the same level: "
          . $mystat{'wsrep_cluster_state_uuid'};
    }
    else {
        badprint "Node and whole cluster not the same level";
        infoprint "Node    state uuid: "
          . ( $mystat{'wsrep_local_state_uuid'} // '' );
        infoprint "Cluster state uuid: "
          . ( $mystat{'wsrep_cluster_state_uuid'} // '' );
    }
    if ( defined $mystat{'wsrep_local_state_comment'}
        and $mystat{'wsrep_local_state_comment'} eq 'Synced' )
    {
        goodprint "Node is synced with whole cluster.";
    }
    else {
        badprint "Node is not synced";
        infoprint "Node State : "
          . ( $mystat{'wsrep_local_state_comment'} // '' );
    }
    if ( defined $mystat{'wsrep_local_cert_failures'}
        and $mystat{'wsrep_local_cert_failures'} == 0 )
    {
        goodprint "There is no certification failures detected.";
    }
    else {
        badprint "There is "
          . ( $mystat{'wsrep_local_cert_failures'} // 0 )
          . " certification failure(s)detected.";
    }

    for my $key ( keys %mystat ) {
        if ( $key =~ /wsrep_|galera/i ) {
            debugprint "WSREP: $key = $mystat{$key}";
        }
    }

    #debugprint Dumper get_wsrep_options() if $opt{debug};
}

# Recommendations for InnoDB
sub mysql_innodb {
    subheaderprint "InnoDB Metrics";

    # InnoDB
    unless ( defined $myvar{'have_innodb'}
        && $myvar{'have_innodb'} eq "YES" )
    {
        infoprint "InnoDB is disabled.";
        if ( mysql_version_ge( 5, 5 ) ) {
            my $defengine = 'InnoDB';
            $defengine = $myvar{'default_storage_engine'}
              if defined( $myvar{'default_storage_engine'} );
            badprint
"InnoDB Storage engine is disabled. $defengine is the default storage engine"
              if $defengine eq 'InnoDB';
            infoprint
"InnoDB Storage engine is disabled. $defengine is the default storage engine"
              if $defengine ne 'InnoDB';
        }
        return;
    }
    infoprint "InnoDB is enabled.";
    check_removed_innodb_variables();
    check_migration_advisor();
    if ( !defined $enginestats{'InnoDB'} ) {
        if ( ( $opt{skipsize} // 0 ) eq 1 ) {
            infoprint "Skipped due to --skipsize option";
            return;
        }
        badprint "No tables are Innodb";
        $enginestats{'InnoDB'} = 0;
    }

    if ( $opt{buffers} ) {
        infoprint "InnoDB Buffers";
        if ( defined $myvar{'innodb_buffer_pool_size'} ) {
            infoprint " +-- InnoDB Buffer Pool: "
              . hr_bytes( $myvar{'innodb_buffer_pool_size'} ) . "";
        }
        if ( defined $myvar{'innodb_buffer_pool_instances'} ) {
            infoprint " +-- InnoDB Buffer Pool Instances: "
              . $myvar{'innodb_buffer_pool_instances'} . "";
        }

        if ( defined $myvar{'innodb_buffer_pool_chunk_size'} ) {
            infoprint " +-- InnoDB Buffer Pool Chunk Size: "
              . hr_bytes( $myvar{'innodb_buffer_pool_chunk_size'} ) . "";
        }
        if ( defined $myvar{'innodb_additional_mem_pool_size'} ) {
            infoprint " +-- InnoDB Additional Mem Pool: "
              . hr_bytes( $myvar{'innodb_additional_mem_pool_size'} ) . "";
        }
        if ( defined $myvar{'innodb_redo_log_capacity'} ) {
            infoprint " +-- InnoDB Redo Log Capacity: "
              . hr_bytes( $myvar{'innodb_redo_log_capacity'} );
        }
        else {
            if ( defined $myvar{'innodb_log_file_size'} ) {
                infoprint " +-- InnoDB Log File Size: "
                  . hr_bytes( $myvar{'innodb_log_file_size'} );
            }
            if ( defined $myvar{'innodb_log_files_in_group'} ) {
                infoprint " +-- InnoDB Log File In Group: "
                  . $myvar{'innodb_log_files_in_group'};
                infoprint " +-- InnoDB Total Log File Size: "
                  . hr_bytes( $myvar{'innodb_log_files_in_group'} *
                      $myvar{'innodb_log_file_size'} )
                  . "("
                  . $mycalc{'innodb_log_size_pct'}
                  . " % of buffer pool)";
            }
            else {
                infoprint " +-- InnoDB Total Log File Size: "
                  . hr_bytes( $myvar{'innodb_log_file_size'} ) . "("
                  . $mycalc{'innodb_log_size_pct'}
                  . " % of buffer pool)";
            }
        }
        if ( defined $myvar{'innodb_log_buffer_size'} ) {
            infoprint " +-- InnoDB Log Buffer: "
              . hr_bytes( $myvar{'innodb_log_buffer_size'} );
        }
        if ( defined $mystat{'Innodb_buffer_pool_pages_free'} ) {
            infoprint " +-- InnoDB Buffer Free: "
              . hr_bytes( $mystat{'Innodb_buffer_pool_pages_free'} ) . "";
        }
        if ( defined $mystat{'Innodb_buffer_pool_pages_total'} ) {
            infoprint " +-- InnoDB Buffer Used: "
              . hr_bytes( $mystat{'Innodb_buffer_pool_pages_total'} ) . "";
        }
    }

    if ( defined $myvar{'innodb_thread_concurrency'} ) {
        infoprint "InnoDB Thread Concurrency: "
          . $myvar{'innodb_thread_concurrency'};
    }

    # InnoDB Buffer Pool Size
    if ( ( $myvar{'innodb_file_per_table'} // '' ) eq "ON" ) {
        goodprint "InnoDB File per table is activated";
    }
    else {
        badprint "InnoDB File per table is not activated";
        push( @adjvars, "innodb_file_per_table=ON" );
    }

    # InnoDB Buffer Pool Size
    if (   ( $arch // 0 ) == 32
        && ( $myvar{'innodb_buffer_pool_size'} // 0 ) > 4294967295 )
    {
        badprint
          "InnoDB Buffer Pool size limit reached for 32 bits architecture: ("
          . hr_bytes(4294967295) . " )";
        push( @adjvars,
                "limit innodb_buffer_pool_size under "
              . hr_bytes(4294967295)
              . " for 32 bits architecture" );
    }
    if (   ( $arch // 0 ) == 32
        && ( $myvar{'innodb_buffer_pool_size'} // 0 ) < 4294967295 )
    {
        goodprint "InnoDB Buffer Pool size ( "
          . hr_bytes( $myvar{'innodb_buffer_pool_size'} )
          . " ) under limit for 32 bits architecture: ("
          . hr_bytes(4294967295) . ")";
    }
    if (   ( $arch // 0 ) == 64
        && ( $myvar{'innodb_buffer_pool_size'} // 0 ) > 18446744073709551615 )
    {
        badprint "InnoDB Buffer Pool size limit("
          . hr_bytes(18446744073709551615)
          . ") reached for 64 bits architecture";
        push( @adjvars,
                "limit innodb_buffer_pool_size under "
              . hr_bytes(18446744073709551615)
              . " for 64 bits architecture" );
    }

    if (   ( $arch // 0 ) == 64
        && ( $myvar{'innodb_buffer_pool_size'} // 0 ) < 18446744073709551615 )
    {
        goodprint "InnoDB Buffer Pool size ( "
          . hr_bytes( $myvar{'innodb_buffer_pool_size'} )
          . " ) under limit for 64 bits architecture: ("
          . hr_bytes(18446744073709551615) . " )";
    }
    if ( ( $myvar{'innodb_buffer_pool_size'} // 0 ) >
        ( $enginestats{'InnoDB'} // 0 ) )
    {
        goodprint "InnoDB buffer pool / data size: "
          . hr_bytes( $myvar{'innodb_buffer_pool_size'} ) . " / "
          . hr_bytes( $enginestats{'InnoDB'} ) . "";
    }
    else {
        badprint "InnoDB buffer pool / data size: "
          . hr_bytes( $myvar{'innodb_buffer_pool_size'} ) . " / "
          . hr_bytes( $enginestats{'InnoDB'} ) . "";
        push( @adjvars,
                "innodb_buffer_pool_size (>= "
              . hr_bytes( $enginestats{'InnoDB'} )
              . ") if possible." );
    }

  # select  round( 100* sum(allocated)/( select VARIABLE_VALUE
  #                                  FROM information_schema.global_variables
  #                              where VARIABLE_NAME='innodb_buffer_pool_size' )
  # ,2) as "PCT ALLOC/BUFFER POOL"
  #from sys.x$innodb_buffer_stats_by_table;

    if ( $opt{experimental} ) {
        debugprint( 'innodb_buffer_alloc_pct: "'
              . $mycalc{innodb_buffer_alloc_pct}
              . '"' );
        if ( defined $mycalc{innodb_buffer_alloc_pct}
            and $mycalc{innodb_buffer_alloc_pct} ne '' )
        {
            if ( $mycalc{innodb_buffer_alloc_pct} < 80 ) {
                badprint "Ratio Buffer Pool allocated / Buffer Pool Size: "
                  . $mycalc{'innodb_buffer_alloc_pct'} . '%';
            }
            else {
                goodprint "Ratio Buffer Pool allocated / Buffer Pool Size: "
                  . $mycalc{'innodb_buffer_alloc_pct'} . '%';
            }
        }
    }

# InnoDB Log File Size / InnoDB Redo Log Capacity Recommendations
# For MySQL < 8.0.30, the recommendation is based on innodb_log_file_size and innodb_log_files_in_group.
# For MySQL >= 8.0.30, innodb_redo_log_capacity replaces the old system.
    if ( mysql_version_ge( 8, 0, 30 ) ) {
        if ( defined $myvar{'innodb_redo_log_capacity'} ) {
            infoprint "InnoDB Redo Log Capacity is set to "
              . hr_bytes( $myvar{'innodb_redo_log_capacity'} );

            if ( defined $myvar{'innodb_dedicated_server'}
                and $myvar{'innodb_dedicated_server'} eq 'ON' )
            {
                goodprint
"innodb_dedicated_server is ON. MySQL is managing Redo Log Capacity automatically.";
                if (   defined $opt{'defaults-file'}
                    || defined $opt{'defaults-extra-file'} )
                {
                    infoprint
"If innodb_redo_log_capacity is manually set in config, consider removing it.";
                }
            }
            else {
                my $innodb_os_log_written =
                  $mystat{'Innodb_os_log_written'} || 0;
                my $uptime = $mystat{'Uptime'} || 1;

                if ( $uptime > 3600 ) {
                    my $hourly_rate =
                      $innodb_os_log_written / ( $uptime / 3600 );
                    infoprint "Hourly InnoDB log write rate: "
                      . hr_bytes($hourly_rate) . "/hour";

                    # Determine recommendation based on workload
                    my $recommended_bytes = $hourly_rate;

                    # Sensible minimum based on RAM
                    my $min_bytes = 100 * 1024 * 1024;
                    if ( $physical_memory > 8 * 1024 * 1024 * 1024 ) {
                        $min_bytes = 1 * 1024 * 1024 * 1024;
                    }
                    $recommended_bytes = $min_bytes
                      if $recommended_bytes < $min_bytes;

                    # Cap at 16GB
                    my $max_bytes = 16 * 1024 * 1024 * 1024;
                    $recommended_bytes = $max_bytes
                      if $recommended_bytes > $max_bytes;

                    # Rounding
                    if ( $recommended_bytes < 1024 * 1024 * 1024 ) {
                        $recommended_bytes =
                          POSIX::ceil(
                            $recommended_bytes / ( 100 * 1024 * 1024 ) ) *
                          ( 100 * 1024 * 1024 );
                    }
                    else {
                        $recommended_bytes =
                          POSIX::ceil(
                            $recommended_bytes / ( 1024 * 1024 * 1024 ) ) *
                          ( 1024 * 1024 * 1024 );
                    }

                    my $recommended_str = hr_bytes($recommended_bytes);
                    if ( $myvar{'innodb_redo_log_capacity'} <
                        $recommended_bytes * 0.9 )
                    {
                        badprint
"Your innodb_redo_log_capacity is smaller than the recommended $recommended_str based on your workload.";
                        push @adjvars,
                          "innodb_redo_log_capacity (>= $recommended_str)";
                    }
                    else {
                        goodprint
"Your innodb_redo_log_capacity is sized correctly for your workload ($recommended_str recommended).";
                    }
                }
                else {
                    infoprint
"Server uptime is less than 1 hour. Cannot make a reliable recommendation for innodb_redo_log_capacity.";
                }
            }
        }
    }
    else {
        # MySQL < 8.0.30: logic based on 25% ratio of buffer pool
        if (
            defined $mycalc{'innodb_log_size_pct'}
            and (  $mycalc{'innodb_log_size_pct'} < 20
                or $mycalc{'innodb_log_size_pct'} > 30 )
          )
        {
            if ( defined $myvar{'innodb_redo_log_capacity'} ) {
                badprint
                  "Ratio InnoDB redo log capacity / InnoDB Buffer pool size ("
                  . ( $mycalc{'innodb_log_size_pct'} // 0 ) . "%): "
                  . hr_bytes( $myvar{'innodb_redo_log_capacity'} ) . " / "
                  . hr_bytes( $myvar{'innodb_buffer_pool_size'} )
                  . " should be equal to 25%";
                push( @adjvars,
                        "innodb_redo_log_capacity should be (="
                      . hr_bytes_rnd( $myvar{'innodb_buffer_pool_size'} / 4 )
                      . ") if possible, so InnoDB Redo log Capacity equals 25% of buffer pool size."
                );
                push( @generalrec,
"Be careful, increasing innodb_redo_log_capacity means higher crash recovery mean time"
                );
            }
            else {
                badprint
                  "Ratio InnoDB log file size / InnoDB Buffer pool size ("
                  . ( $mycalc{'innodb_log_size_pct'}            // 0 ) . "%): "
                  . hr_bytes( $myvar{'innodb_log_file_size'}    // 0 ) . " * "
                  . ( $myvar{'innodb_log_files_in_group'}       // 0 ) . " / "
                  . hr_bytes( $myvar{'innodb_buffer_pool_size'} // 0 )
                  . " should be equal to 25%";
                push(
                    @adjvars,
                    "innodb_log_file_size should be (="
                      . hr_bytes(
                        (
                            defined $myvar{'innodb_buffer_pool_size'}
                              && $myvar{'innodb_buffer_pool_size'} ne ''
                            ? $myvar{'innodb_buffer_pool_size'}
                            : 0
                        ) / (
                            defined $myvar{'innodb_log_files_in_group'}
                              && $myvar{'innodb_log_files_in_group'} ne ''
                              && $myvar{'innodb_log_files_in_group'} != 0
                            ? $myvar{'innodb_log_files_in_group'}
                            : 1
                        ) / 4
                      )
                      . ") if possible, so InnoDB total log file size equals 25% of buffer pool size."
                );
                push( @generalrec,
"Be careful, increasing innodb_log_file_size / innodb_log_files_in_group means higher crash recovery mean time"
                );
            }
            if ( mysql_version_le( 5, 6, 2 ) ) {
                push( @generalrec,
"For MySQL 5.6.2 and lower, total innodb_log_file_size should have a ceiling of (4096MB / log files in group) - 1MB."
                );
            }
        }
        else {
            if ( defined $myvar{'innodb_redo_log_capacity'} ) {
                goodprint
                  "Ratio InnoDB Redo Log Capacity / InnoDB Buffer pool size: "
                  . hr_bytes( $myvar{'innodb_redo_log_capacity'} ) . "/"
                  . hr_bytes( $myvar{'innodb_buffer_pool_size'} )
                  . " should be equal to 25%";
            }
            else {
                push( @generalrec,
"Before changing innodb_log_file_size and/or innodb_log_files_in_group read this: https://bit.ly/2TcGgtU"
                );
                goodprint
                  "Ratio InnoDB log file size / InnoDB Buffer pool size: "
                  . hr_bytes( $myvar{'innodb_log_file_size'}    // 0 ) . " * "
                  . ( $myvar{'innodb_log_files_in_group'}       // 0 ) . "/"
                  . hr_bytes( $myvar{'innodb_buffer_pool_size'} // 0 )
                  . " should be equal to 25%";
            }
        }
    }

    # InnoDB Buffer Pool Instances (MySQL 5.6.6+)
    if ( not mysql_version_ge( 10, 4 )
        and defined( $myvar{'innodb_buffer_pool_instances'} ) )
    {

        # Bad Value if > 64
        if ( $myvar{'innodb_buffer_pool_instances'} > 64 ) {
            badprint "InnoDB buffer pool instances: "
              . $myvar{'innodb_buffer_pool_instances'} . "";
            push( @adjvars, "innodb_buffer_pool_instances (<= 64)" );
        }

        # InnoDB Buffer Pool Size > 1Go
        if ( $myvar{'innodb_buffer_pool_size'} > 1024 * 1024 * 1024 ) {

# InnoDB Buffer Pool Size / 1Go = InnoDB Buffer Pool Instances limited to 64 max.

            #  InnoDB Buffer Pool Size > 64Go
            my $max_innodb_buffer_pool_instances =
              int( $myvar{'innodb_buffer_pool_size'} / ( 1024 * 1024 * 1024 ) );

            my $nb_cpus = cpu_cores();
            if ( $nb_cpus > 0 && $max_innodb_buffer_pool_instances > $nb_cpus )
            {
                infoprint
"Recommendation for innodb_buffer_pool_instances is capped by the number of CPU cores ($nb_cpus).";
                $max_innodb_buffer_pool_instances = $nb_cpus;
            }

            $max_innodb_buffer_pool_instances = 64
              if ( $max_innodb_buffer_pool_instances > 64 );

            if ( $myvar{'innodb_buffer_pool_instances'} !=
                $max_innodb_buffer_pool_instances )
            {
                badprint "InnoDB buffer pool instances: "
                  . $myvar{'innodb_buffer_pool_instances'} . "";
                push( @adjvars,
                        "innodb_buffer_pool_instances(="
                      . $max_innodb_buffer_pool_instances
                      . ")" );
            }
            else {
                goodprint "InnoDB buffer pool instances: "
                  . $myvar{'innodb_buffer_pool_instances'} . "";
            }

            # InnoDB Buffer Pool Size < 1Go
        }
        else {
            if ( $myvar{'innodb_buffer_pool_instances'} != 1 ) {
                badprint
"InnoDB buffer pool <= 1G and Innodb_buffer_pool_instances(!=1).";
                push( @adjvars, "innodb_buffer_pool_instances (=1)" );
            }
            else {
                goodprint "InnoDB buffer pool instances: "
                  . $myvar{'innodb_buffer_pool_instances'} . "";
            }
        }
    }

    # InnoDB Used Buffer Pool Size vs CHUNK size
    if (
        (
            ( defined $myvar{'version'} && $myvar{'version'} =~ /MariaDB/i )
            or ( defined $myvar{'version_comment'}
                && $myvar{'version_comment'} =~ /MariaDB/i )
        )
        and mysql_version_ge( 10, 8 )
        and defined( $myvar{'innodb_buffer_pool_chunk_size'} )
        and $myvar{'innodb_buffer_pool_chunk_size'} == 0
      )
    {
        infoprint
"innodb_buffer_pool_chunk_size is set to 'autosize' (0) in MariaDB >= 10.8. Skipping chunk size checks.";
    }
    elsif (!defined( $myvar{'innodb_buffer_pool_chunk_size'} )
        || $myvar{'innodb_buffer_pool_chunk_size'} == 0
        || !defined( $myvar{'innodb_buffer_pool_size'} )
        || $myvar{'innodb_buffer_pool_size'} == 0
        || !defined( $myvar{'innodb_buffer_pool_instances'} )
        || $myvar{'innodb_buffer_pool_instances'} == 0 )
    {

        badprint
"Cannot calculate InnoDB Buffer Pool Chunk breakdown due to missing or zero values:";

        infoprint " - innodb_buffer_pool_size: "
          . (
            defined $myvar{'innodb_buffer_pool_size'}
            ? $myvar{'innodb_buffer_pool_size'}
            : "undefined"
          );
        infoprint " - innodb_buffer_pool_chunk_size: "
          . (
            defined $myvar{'innodb_buffer_pool_chunk_size'}
            ? $myvar{'innodb_buffer_pool_chunk_size'}
            : "undefined"
          );
        infoprint " - innodb_buffer_pool_instances: "
          . (
            defined $myvar{'innodb_buffer_pool_instances'}
            ? $myvar{'innodb_buffer_pool_instances'}
            : "undefined"
          );

    }
    else {
        my $num_chunks = int( $myvar{'innodb_buffer_pool_size'} /
              $myvar{'innodb_buffer_pool_chunk_size'} );
        infoprint "Number of InnoDB Buffer Pool Chunk: $num_chunks for "
          . $myvar{'innodb_buffer_pool_instances'}
          . " Buffer Pool Instance(s)";

        my $expected_size = int( $myvar{'innodb_buffer_pool_chunk_size'} ) *
          int( $myvar{'innodb_buffer_pool_instances'} );

        if ( int( $myvar{'innodb_buffer_pool_size'} ) % $expected_size == 0 ) {
            goodprint
"Innodb_buffer_pool_size aligned with Innodb_buffer_pool_chunk_size & Innodb_buffer_pool_instances";
        }
        else {
            badprint
"Innodb_buffer_pool_size not aligned with Innodb_buffer_pool_chunk_size & Innodb_buffer_pool_instances";

            push( @adjvars,
"innodb_buffer_pool_size must always be equal to or a multiple of innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances"
            );
        }
    }

    # InnoDB Read efficiency
    if ( ( $mystat{'Innodb_buffer_pool_reads'} // 0 ) >
        ( $mystat{'Innodb_buffer_pool_read_requests'} // 0 ) )
    {
        infoprint
"InnoDB Read buffer efficiency: metrics are not reliable (reads > read requests)";
    }
    elsif ( defined $mycalc{'pct_read_efficiency'}
        && $mycalc{'pct_read_efficiency'} < 90 )
    {
        badprint "InnoDB Read buffer efficiency: "
          . $mycalc{'pct_read_efficiency'} . "% ("
          . ( $mystat{'Innodb_buffer_pool_read_requests'} // 0 )
          . " hits / "
          . ( ( $mystat{'Innodb_buffer_pool_reads'} // 0 ) +
              ( $mystat{'Innodb_buffer_pool_read_requests'} // 0 ) )
          . " total)";
    }
    else {
        goodprint "InnoDB Read buffer efficiency: "
          . ( $mycalc{'pct_read_efficiency'}              // 0 ) . "% ("
          . ( $mystat{'Innodb_buffer_pool_read_requests'} // 0 )
          . " hits / "
          . ( ( $mystat{'Innodb_buffer_pool_reads'} // 0 ) +
              ( $mystat{'Innodb_buffer_pool_read_requests'} // 0 ) )
          . " total)";
    }

    # InnoDB Write efficiency
    if (   !defined $mystat{'Innodb_log_writes'}
        || !defined $mystat{'Innodb_log_write_requests'} )
    {
        debugprint
"InnoDB Write Log efficiency: metrics missing (writes or write requests)";
    }
    elsif (
        $mystat{'Innodb_log_writes'} > $mystat{'Innodb_log_write_requests'} )
    {
        infoprint
"InnoDB Write Log efficiency: metrics are not reliable (writes > write requests)";
    }
    elsif ( defined $mycalc{'pct_write_efficiency'}
        && $mycalc{'pct_write_efficiency'} < 90 )
    {
        badprint "InnoDB Write Log efficiency: "
          . abs( $mycalc{'pct_write_efficiency'} ) . "% ("
          . abs( ( $mystat{'Innodb_log_write_requests'} // 0 ) -
              ( $mystat{'Innodb_log_writes'} // 0 ) )
          . " hits / "
          . ( $mystat{'Innodb_log_write_requests'} // 0 )
          . " total)";
        push( @adjvars,
                "innodb_log_buffer_size (> "
              . hr_bytes_rnd( $myvar{'innodb_log_buffer_size'} )
              . ")" );
    }
    else {
        goodprint "InnoDB Write Log efficiency: "
          . ( $mycalc{'pct_write_efficiency'} // 0 ) . "% ("
          . ( ( $mystat{'Innodb_log_write_requests'} // 0 ) -
              ( $mystat{'Innodb_log_writes'} // 0 ) )
          . " hits / "
          . ( $mystat{'Innodb_log_write_requests'} // 0 )
          . " total)";
    }

    # InnoDB Log Waits
    $mystat{'Innodb_log_waits_computed'} = 0;

    if (    defined( $mystat{'Innodb_log_waits'} )
        and defined( $mystat{'Innodb_log_writes'} )
        and $mystat{'Innodb_log_writes'} > 0.000001 )
    {
        $mystat{'Innodb_log_waits_computed'} =
          $mystat{'Innodb_log_waits'} / $mystat{'Innodb_log_writes'};
    }
    else {
        undef $mystat{'Innodb_log_waits_computed'};
    }

    if ( defined $mystat{'Innodb_log_waits_computed'}
        && $mystat{'Innodb_log_waits_computed'} > 0.000001 )
    {
        badprint "InnoDB log waits: "
          . percentage( $mystat{'Innodb_log_waits'},
            $mystat{'Innodb_log_writes'} )
          . "% ("
          . $mystat{'Innodb_log_waits'}
          . " waits / "
          . $mystat{'Innodb_log_writes'}
          . " writes)";
        push( @adjvars,
                "innodb_log_buffer_size (> "
              . hr_bytes_rnd( $myvar{'innodb_log_buffer_size'} )
              . ")" );
    }
    else {
        goodprint "InnoDB log waits: "
          . percentage(
            ( $mystat{'Innodb_log_waits'}  // 0 ),
            ( $mystat{'Innodb_log_writes'} // 0 )
          )
          . "% ("
          . ( $mystat{'Innodb_log_waits'} // 0 )
          . " waits / "
          . ( $mystat{'Innodb_log_writes'} // 0 )
          . " writes)";
    }

    # InnoDB Transaction Isolation and Metrics
    subheaderprint "InnoDB Transactions";
    my $isolation =
         $myvar{'transaction_isolation'}
      || $myvar{'tx_isolation'}
      || $myvar{'isolation_level'};
    if ( defined $isolation ) {
        infoprint("Transaction Isolation Level: $isolation");
    }

    if ( defined $myvar{'innodb_snapshot_isolation'} ) {
        infoprint( "InnoDB Snapshot Isolation: "
              . $myvar{'innodb_snapshot_isolation'} );
        if ( $myvar{'innodb_snapshot_isolation'} eq 'OFF'
            && ( $isolation || '' ) eq 'REPEATABLE-READ' )
        {
            badprint(
"innodb_snapshot_isolation is OFF with REPEATABLE-READ (Stricter snapshot isolation is disabled)"
            );
            push( @adjvars, "innodb_snapshot_isolation=ON" );
        }
    }

    if ( defined $mycalc{'innodb_active_transactions'} ) {
        infoprint "Active InnoDB Transactions: "
          . $mycalc{'innodb_active_transactions'};
    }
    if ( defined $mycalc{'innodb_longest_transaction_duration'}
        && $mycalc{'innodb_longest_transaction_duration'} > 0 )
    {
        infoprint "Longest InnoDB Transaction Duration: "
          . pretty_uptime( $mycalc{'innodb_longest_transaction_duration'} );
        if ( $mycalc{'innodb_longest_transaction_duration'} > 3600 ) {
            badprint "Long running InnoDB transaction detected ("
              . pretty_uptime( $mycalc{'innodb_longest_transaction_duration'} )
              . ")";
            push( @generalrec,
"Long running transactions can cause InnoDB history list length to increase and impact performance."
            );
        }
    }

    # Infrastructure-Aware InnoDB Tuning
    if ( defined $storage_type && $storage_type ne 'unknown' ) {
        subheaderprint "Infrastructure-Aware InnoDB Tuning";
        infoprint "Detected Storage Type: $storage_type";

        # innodb_flush_neighbors
        if ( defined $myvar{'innodb_flush_neighbors'} ) {
            if ( $storage_type =~ /SSD|NVMe/ ) {
                if ( $myvar{'innodb_flush_neighbors'} != 0 ) {
                    badprint "innodb_flush_neighbors is enabled on SSD/NVMe: "
                      . $myvar{'innodb_flush_neighbors'};
                    push @adjvars,
                      "innodb_flush_neighbors = 0 (Recommended for SSD/NVMe)";
                }
                else {
                    goodprint "innodb_flush_neighbors is disabled for SSD/NVMe";
                }
            }
            elsif ( $storage_type eq 'HDD' ) {
                if ( $myvar{'innodb_flush_neighbors'} == 0 ) {
                    badprint "innodb_flush_neighbors is disabled on HDD";
                    push @adjvars,
                      "innodb_flush_neighbors = 1 or 2 (Recommended for HDD)";
                }
                else {
                    goodprint "innodb_flush_neighbors is enabled for HDD: "
                      . $myvar{'innodb_flush_neighbors'};
                }
            }
        }

        # innodb_io_capacity
        if ( defined $myvar{'innodb_io_capacity'} ) {
            if (   $storage_type =~ /SSD|NVMe/
                && $myvar{'innodb_io_capacity'} <= 200 )
            {
                badprint
                  "innodb_io_capacity is set to default (200) on SSD/NVMe";
                push @adjvars,
                  "innodb_io_capacity (>= 2000) for SSD/NVMe storage";
            }
        }
    }

    # Index/Data Ratio Check for tables > 50k rows
    subheaderprint "InnoDB Index/Data Ratio Check";
    my @ratio_tables = select_array(
"SELECT TABLE_SCHEMA, TABLE_NAME, DATA_LENGTH, INDEX_LENGTH, TABLE_ROWS "
          . "FROM information_schema.TABLES "
          . "WHERE ENGINE='InnoDB' AND TABLE_ROWS > 50000 "
          . "AND TABLE_SCHEMA NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')"
    );

    my $under_indexed_count = 0;
    my $over_indexed_count  = 0;
    my $total_ratio_checked = 0;
    my @csv_rows            = (
"\"Database\",\"Table\",\"Ratio\",\"Data Size\",\"Index Size\",\"Status\",\"Rows\""
    );

    foreach my $row (@ratio_tables) {
        my ( $schema, $name, $data_len, $index_len, $rows ) =
          split( /\t/, $row );
        next unless defined $schema && defined $name;
        $total_ratio_checked++;
        $data_len  //= 0;
        $index_len //= 0;
        $rows      //= 0;

        my $ratio = 0;
        if ( $data_len > 0 ) {
            $ratio = sprintf( "%.2f", $index_len / $data_len );
        }

        my $status = "Ideal";
        if ( $ratio < 0.30 ) {
            $status = "Under-indexed";
            $under_indexed_count++;
        }
        elsif ( $ratio > 0.60 ) {
            $status = "Over-indexed";
            $over_indexed_count++;
        }

        push @csv_rows,
          sprintf( "\"%s\",\"%s\",%.2f,%d,%d,\"%s\",%d",
            $schema, $name, $ratio, $data_len, $index_len, $status, $rows );
    }

    if ( $total_ratio_checked > 0 ) {
        infoprint
          "Checked InnoDB tables with > 50,000 rows: $total_ratio_checked";
        if ( $under_indexed_count > 0 ) {
            badprint "Under-indexed tables (ratio < 0.3): $under_indexed_count";
        }
        if ( $over_indexed_count > 0 ) {
            badprint "Over-indexed tables (ratio > 0.6): $over_indexed_count";
        }
        if ( $under_indexed_count == 0 && $over_indexed_count == 0 ) {
            goodprint
"All checked InnoDB tables have an ideal index/data ratio (between 0.3 and 0.6)";
        }

        if ( defined $opt{dumpdir} ) {
            dump_into_file( "table_indexes_potential_issues.csv",
                join( "\n", @csv_rows ) );
            infoprint
"Dumped index ratio analysis to: $opt{dumpdir}/table_indexes_potential_issues.csv";
        }
    }
    else {
        infoprint
"No InnoDB tables with > 50,000 rows found to calculate index/data ratios.";
    }

    $result{'Calculations'} = {%mycalc};
}

sub check_removed_innodb_variables {
    my $is_mariadb = (
        ( defined $myvar{'version'} && $myvar{'version'} =~ /MariaDB/i )
          or ( defined $myvar{'version_comment'}
            && $myvar{'version_comment'} =~ /MariaDB/i )
    );
    my $is_mysql = !$is_mariadb;

    my @removed_vars = ();
    if ($is_mariadb) {
        @removed_vars = (
            {
                var     => 'have_innodb',
                removed => [ 10, 0, 0 ],
                note    =>
'Replaced by checking INFORMATION_SCHEMA.PLUGINS or SHOW ENGINES.'
            },
            {
                var     => 'innodb_adaptive_flushing_method',
                removed => [ 10, 0, 0 ],
                note => 'Replaced with InnoDB flushing method from MySQL 5.6.'
            },
            {
                var     => 'innodb_checksums',
                removed => [ 10, 0, 0 ],
                note    => 'Replaced by innodb_checksum_algorithm.'
            },
            {
                var     => 'innodb_stats_sample_pages',
                removed => [ 10, 5, 0 ],
                note    => 'Replaced by innodb_stats_transient_sample_pages.'
            },
            {
                var     => 'innodb_ibuf_active_contract',
                removed => [ 10, 0, 0 ],
                note    =>
'This Percona XtraDB variable has not been ported to XtraDB 5.6.'
            },
            {
                var     => 'innodb_ibuf_max_size',
                removed => [ 10, 0, 0 ],
                note    =>
'This Percona XtraDB variable has not been ported to XtraDB 5.6.'
            },
            {
                var     => 'innodb_file_format',
                removed => [ 10, 6, 0 ],
                note    => 'Antelope file format is removed.'
            },
            {
                var     => 'innodb_file_format_check',
                removed => [ 10, 6, 0 ],
                note    => 'File format checking is removed.'
            },
            {
                var     => 'innodb_file_format_max',
                removed => [ 10, 6, 0 ],
                note    => 'File format max is removed.'
            },
            {
                var     => 'innodb_large_prefix',
                removed => [ 10, 6, 0 ],
                note    => 'Always enabled.'
            },
            {
                var     => 'innodb_locks_unsafe_for_binlog',
                removed => [ 10, 6, 0 ],
                note    => 'No longer supported.'
            },
            {
                var     => 'innodb_prefix_index_cluster_optimization',
                removed => [ 10, 10, 0 ],
                note    => 'Always enabled.'
            },
        );
    }
    elsif ($is_mysql) {
        @removed_vars = (
            {
                var     => 'innodb_locks_unsafe_for_binlog',
                removed => [ 8, 0, 0 ],
                note    => 'Use READ COMMITTED isolation level instead.'
            },
            {
                var     => 'innodb_support_xa',
                removed => [ 8, 0, 0 ],
                note    => 'XA support is now always enabled.'
            },
            {
                var     => 'innodb_file_format',
                removed => [ 8, 0, 0 ],
                note    => 'Barracuda is the only supported format.'
            },
            {
                var     => 'innodb_file_format_check',
                removed => [ 8, 0, 0 ],
                note    => 'Barracuda is the only supported format.'
            },
            {
                var     => 'innodb_file_format_max',
                removed => [ 8, 0, 0 ],
                note    => 'Barracuda is the only supported format.'
            },
            {
                var     => 'innodb_large_prefix',
                removed => [ 8, 0, 0 ],
                note    => 'Always enabled for Barracuda.'
            },
            {
                var     => 'tx_isolation',
                removed => [ 8, 0, 0 ],
                note    => 'Use transaction_isolation instead.'
            },
            {
                var     => 'tx_read_only',
                removed => [ 8, 0, 0 ],
                note    => 'Use transaction_read_only instead.'
            },
            {
                var     => 'innodb_undo_logs',
                removed => [ 8, 0, 0 ],
                note    => 'Replaced by innodb_rollback_segments.'
            },
            {
                var     => 'innodb_undo_tablespaces',
                removed => [ 9, 0, 0 ],
                note    => 'Managed automatically now.'
            },
            {
                var     => 'innodb_log_file_size',
                removed => [ 9, 0, 0 ],
                note    => 'Replaced by innodb_redo_log_capacity.'
            },
            {
                var     => 'innodb_log_files_in_group',
                removed => [ 9, 0, 0 ],
                note    => 'Replaced by innodb_redo_log_capacity.'
            },
        );
    }

    foreach my $entry (@removed_vars) {
        my $var = $entry->{var};
        if (   exists $real_vars{$var}
            && $real_vars{$var} ne 'OFF'
            && mysql_version_ge( @{ $entry->{removed} } ) )
        {
            badprint "InnoDB variable '$var' is removed in "
              . ( $is_mariadb ? "MariaDB " : "MySQL " )
              . join( '.', @{ $entry->{removed} } ) . ".";
            push( @generalrec,
                "Remove '$var' from your configuration. " . $entry->{note} );
        }
    }
}

sub check_migration_advisor {
    subheaderprint "Smart Migration LTS Advisor";
    my $is_mariadb = (
        ( defined $myvar{'version'} && $myvar{'version'} =~ /MariaDB/i )
          or ( defined $myvar{'version_comment'}
            && $myvar{'version_comment'} =~ /MariaDB/i )
    );
    my $is_mysql = !$is_mariadb;

    my @risks = ();

    # MySQL 8.4 / 9.0 Risks
    if ($is_mysql) {
        push @risks,
          {
            var  => 'default_authentication_plugin',
            note =>
'Removed in MySQL 9.0. Use caching_sha2_password or mysql-native-password (if enabled) instead.'
          }
          if defined $myvar{'default_authentication_plugin'};

        push @risks,
          {
            var  => 'binlog_transaction_dependency_tracking',
            note => 'Removed in MySQL 8.4.'
          }
          if defined $myvar{'binlog_transaction_dependency_tracking'};

        push @risks,
          {
            var  => 'avoid_temporal_upgrade',
            note =>
'Removed in MySQL 8.4. Upgrading from very old versions (5.5) might need manual temporal upgrade.'
          }
          if defined $myvar{'avoid_temporal_upgrade'};

        push @risks,
          {
            var  => 'expire_logs_days',
            note =>
              'Removed in MySQL 8.4. Use binlog_expire_logs_seconds instead.'
          }
          if defined $myvar{'expire_logs_days'};

        # Character set audit
        if ( defined $myvar{'character_set_server'}
            && $myvar{'character_set_server'} eq 'utf8' )
        {
            push @risks,
              {
                var  => 'character_set_server',
                note =>
'utf8 (alias for utf8mb3) is deprecated and will be removed. Use utf8mb4 instead.'
              };
        }
    }

    # MariaDB 11.x Risks
    if ($is_mariadb) {
        push @risks,
          {
            var  => 'innodb_defragment',
            note =>
'Removed in MariaDB 11.x. Defragmentation is no longer supported in this way.'
          }
          if defined $myvar{'innodb_defragment'};

        push @risks,
          {
            var  => 'innodb_change_buffering',
            note =>
              'Removed in MariaDB 11.x. Change buffering is no longer used.'
          }
          if defined $myvar{'innodb_change_buffering'};

        push @risks,
          {
            var  => 'max_tmp_tables',
            note => 'Removed in MariaDB 11.3+.'
          }
          if defined $myvar{'max_tmp_tables'};
    }

    # SQL Mode audit
    if ( defined $myvar{'sql_mode'} ) {
        if ( $myvar{'sql_mode'} =~ /NO_AUTO_CREATE_USER/ ) {
            push @risks,
              {
                var  => 'sql_mode',
                note =>
'NO_AUTO_CREATE_USER is removed in modern versions. Ensure your scripts do not rely on it.'
              };
        }
    }

    if (@risks) {
        badprint "Found " . scalar(@risks) . " potential migration risks.";
        foreach my $risk (@risks) {
            infoprint " - [Migration Risk] $risk->{var}: $risk->{note}";
            push @generalrec, "Migration Risk ($risk->{var}): $risk->{note}";
        }
    }
    else {
        goodprint "No major migration risks detected for modern LTS versions.";
    }
}

sub historical_comparison {
    my $file = $opt{'compare-file'};
    return if !$file;

    subheaderprint "Historical Trend Analysis";
    if ( !-e $file ) {
        badprint "Comparison file not found: $file";
        return;
    }

    eval { require JSON };
    if ($@) {
        badprint "JSON module required for comparison analysis.";
        return;
    }

    my $fh;
    if ( !open( $fh, '<', $file ) ) {
        badprint "Unable to open comparison file: $file";
        return;
    }

    my $json_text = join( '', <$fh> );
    close($fh);

    my $old;
    eval { $old = JSON->new->decode($json_text); };
    if ($@) {
        badprint "Error decoding JSON from $file: $@";
        return;
    }

    infoprint "Comparing current results with snapshot from: "
      . ( $old->{'General'}{'Date'} // 'unknown' );

    # 1. Compare QPS
    if ( defined $result{'Stats'}{'QPS'} && defined $old->{'Stats'}{'QPS'} ) {
        my $diff = $result{'Stats'}{'QPS'} - $old->{'Stats'}{'QPS'};
        my $pct =
            ( $old->{'Stats'}{'QPS'} > 0 )
          ? ( $diff / $old->{'Stats'}{'QPS'} ) * 100
          : 0;
        my $trend = ( $diff >= 0 ) ? "+" : "";
        infoprint sprintf(
            "QPS Trend: %.2f -> %.2f (%s%.2f%%)",
            $old->{'Stats'}{'QPS'},
            $result{'Stats'}{'QPS'},
            $trend, $pct
        );
    }

    # 2. Compare Total Data Size
    if (   defined $result{'Stats'}{'Total Data Size'}
        && defined $old->{'Stats'}{'Total Data Size'} )
    {
        my $diff = $result{'Stats'}{'Total Data Size'} -
          $old->{'Stats'}{'Total Data Size'};
        infoprint "Data Growth: "
          . hr_bytes( $old->{'Stats'}{'Total Data Size'} ) . " -> "
          . hr_bytes( $result{'Stats'}{'Total Data Size'} ) . " ("
          . hr_bytes($diff) . ")";
    }

    push @adjvars, "Historical comparison performed against " . basename($file);
}

sub process_sysbench_metrics {
    my $file = $opt{'sysbench-file'};
    return if !$file;

    subheaderprint "Sysbench Performance Metrics";
    if ( !-e $file ) {
        badprint "Sysbench file not found: $file";
        return;
    }

    my $fh;
    if ( !open( $fh, '<', $file ) ) {
        badprint "Unable to open sysbench file: $file";
        return;
    }

    infoprint "Parsing sysbench output from: $file";
    my %sysbench;
    while (<$fh>) {
        if (/queries:\s+(\d+)\s+\(([\d.]+) per sec\.\)/) {
            $sysbench{'qps'} = $2;
        }
        elsif (/transactions:\s+(\d+)\s+\(([\d.]+) per sec\.\)/) {
            $sysbench{'tps'} = $2;
        }
        elsif (/avg:\s+([\d.]+)/) {
            $sysbench{'latency_avg'} = $1;
        }
        elsif (/95th percentile:\s+([\d.]+)/) {
            $sysbench{'latency_95th'} = $1;
        }
        elsif (/max:\s+([\d.]+)/) {
            $sysbench{'latency_max'} = $1;
        }
    }
    close($fh);

    if ( defined $sysbench{'tps'} ) {
        goodprint "Sysbench TPS: $sysbench{'tps'}";
        $result{'Sysbench'}{'TPS'} = $sysbench{'tps'};
    }
    if ( defined $sysbench{'qps'} ) {
        goodprint "Sysbench QPS: $sysbench{'qps'}";
        $result{'Sysbench'}{'QPS'} = $sysbench{'qps'};
    }
    if ( defined $sysbench{'latency_avg'} ) {
        infoprint "Sysbench Average Latency: $sysbench{'latency_avg'} ms";
        $result{'Sysbench'}{'Latency Avg'} = $sysbench{'latency_avg'};
    }
    if ( defined $sysbench{'latency_95th'} ) {
        infoprint
          "Sysbench 95th Percentile Latency: $sysbench{'latency_95th'} ms";
    }

    push @adjvars, "Performance baseline captured from sysbench results."
      if %sysbench;
}

sub check_query_anti_patterns {
    subheaderprint "Query Anti-Pattern Detection (Experimental)";

    unless ( mysql_version_ge( 5, 6 ) ) {
        infoprint
          "Skipped: Performance Schema analysis requires MySQL/MariaDB 5.6+";
        return;
    }

    my $pfs_enabled = select_one("SHOW VARIABLES LIKE 'performance_schema'");
    unless ( defined $pfs_enabled && $pfs_enabled eq 'ON' ) {
        infoprint "Skipped: Performance Schema is disabled.";
        return;
    }

    # 1. High Full Table Scan Queries
    infoprint "Analyzing Performance Schema Statement Digests...";
    my @full_scans = select_array(
"SELECT digest_text, count_star, sum_no_index_used, sum_no_good_index_used "
          . "FROM performance_schema.events_statements_summary_by_digest "
          . "WHERE (sum_no_index_used > 0 OR sum_no_good_index_used > 0) "
          . "AND (SCHEMA_NAME IS NULL OR SCHEMA_NAME NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')) "
          . "ORDER BY sum_no_index_used DESC LIMIT 5" );

    if (@full_scans) {
        badprint "Found "
          . scalar(@full_scans)
          . " query digests with high full table scan counts.";
        foreach my $row (@full_scans) {
            my ( $digest, $count, $no_idx, $no_good_idx ) = split( /\t/, $row );
            $digest =~ s/\s+/ /g;
            infoprint " - [Full Scan: $no_idx/$count] "
              . substr( $digest, 0, 100 ) . "...";
            push @modeling,
              "Query Digest with high full scans: " . substr( $digest, 0, 150 );
        }
        push @generalrec,
"Optimize queries with high full table scans; use EXPLAIN to identify missing indexes.";
    }
    else {
        goodprint
          "No major full table scan patterns detected in statement digests.";
    }

    # 2. Internal Disk-Based Temp Table Usage
    my @disk_tmp = select_array(
            "SELECT digest_text, count_star, sum_created_tmp_disk_tables "
          . "FROM performance_schema.events_statements_summary_by_digest "
          . "WHERE sum_created_tmp_disk_tables > 0 "
          . "AND (SCHEMA_NAME IS NULL OR SCHEMA_NAME NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')) "
          . "ORDER BY sum_created_tmp_disk_tables DESC LIMIT 5" );

    if (@disk_tmp) {
        badprint "Found "
          . scalar(@disk_tmp)
          . " query digests with high internal disk-based temp table usage.";
        foreach my $row (@disk_tmp) {
            my ( $digest, $count, $disk_tmp_count ) = split( /\t/, $row );
            $digest =~ s/\s+/ /g;
            infoprint " - [Disk Temp: $disk_tmp_count/$count] "
              . substr( $digest, 0, 100 ) . "...";
            push @modeling, "Query Digest with high disk temp tables: "
              . substr( $digest, 0, 150 );
        }
        push @generalrec,
"Identify queries creating excessive disk-based temporary tables; consider increasing tmp_table_size or optimizing query.";
    }
}

sub mariadb_query_cache_info {
    subheaderprint "Query Cache Information";

    unless ( ( $myvar{'version'} =~ /MariaDB/i )
        or ( $myvar{'version_comment'} =~ /MariaDB/i ) )
    {
        infoprint
          "Not a MariaDB server. Skipping Query Cache Info plugin check.";
        return;
    }

    my $plugin_status = select_one(
"SELECT PLUGIN_STATUS FROM information_schema.PLUGINS WHERE PLUGIN_NAME = 'QUERY_CACHE_INFO'"
    );

    if ( defined $plugin_status and $plugin_status eq 'ACTIVE' ) {
        goodprint "QUERY_CACHE_INFO plugin is installed and active.";

        my $query =
"SELECT CONCAT_WS(';;', statement_schema, LEFT(statement_text, 80), result_blocks_count, result_blocks_size) FROM information_schema.query_cache_info";
        my @query_cache_data = select_array($query);

        if (@query_cache_data) {
            infoprint sprintf(
                "%-20s | %-82s | %-10s | %-10s",
                "Schema", "Query (truncated)",
                "Blocks", "Size"
            );
            infoprint "-" x 130;
            foreach my $line (@query_cache_data) {
                my ( $schema, $text, $blocks, $size ) = split( /;;/, $line );
                infoprint sprintf( "%-20s | %-82s | %-10s | %-10s",
                    $schema, $text, $blocks, hr_bytes($size) );
            }
        }
        else {
            infoprint "No queries found in the query cache.";
        }
    }
    else {
        infoprint "QUERY_CACHE_INFO plugin is not active or not installed.";
        return;
    }
}

sub check_metadata_perf {
    subheaderprint "Analysis Performance Metrics";
    if ( defined $myvar{'innodb_stats_on_metadata'} ) {
        infoprint "innodb_stats_on_metadata: "
          . $myvar{'innodb_stats_on_metadata'};
        if ( $myvar{'innodb_stats_on_metadata'} eq 'ON' ) {
            badprint "Stat are updated during querying INFORMATION_SCHEMA.";
            push @adjvars, "SET innodb_stats_on_metadata = OFF";

            #Disabling innodb_stats_on_metadata
            select_one("SET GLOBAL innodb_stats_on_metadata = OFF;");
            return 1;
        }
    }
    goodprint "No stat updates during querying INFORMATION_SCHEMA.";
    return 0;
}

sub mysql_plugins {
    return if ( $opt{plugininfo} == 0 );
    subheaderprint "Plugin Information";

    my $query =
"SELECT PLUGIN_NAME, PLUGIN_VERSION, PLUGIN_STATUS, PLUGIN_TYPE FROM information_schema.PLUGINS WHERE PLUGIN_STATUS = 'ACTIVE' AND PLUGIN_TYPE != 'INFORMATION SCHEMA' ORDER BY PLUGIN_TYPE, PLUGIN_NAME";
    my @plugin_data = select_array($query);

    if (@plugin_data) {
        infoprint sprintf( "%-30s | %-10s | %-10s | %-20s",
            "Plugin", "Version", "Status", "Type" );
        infoprint "-" x 80;
        foreach my $line (@plugin_data) {
            my ( $name, $version, $status, $type ) = split( /\t/, $line );
            infoprint sprintf( "%-30s | %-10s | %-10s | %-20s",
                $name, $version, $status, $type );
        }
    }
    else {
        infoprint
"No ACTIVE plugins found (excluding INFORMATION SCHEMA) in the information_schema.";
    }
}

# Recommendations for Database metrics
sub mysql_databases {
    return if ( $opt{dbstat} == 0 );

    subheaderprint "Database Metrics";
    unless ( mysql_version_ge( 5, 5 ) ) {
        infoprint
"Database metrics from information schema are missing in this version. Skipping...";
        return;
    }

    my $ignore_tables_sql = "";
    if ( $opt{'ignore-tables'} ) {
        my @ignored = split /,/, $opt{'ignore-tables'};
        $ignore_tables_sql =
          " AND TABLE_NAME NOT IN ('" . join( "','", @ignored ) . "')";
    }

    @dblist = select_array(
"SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME NOT IN ( 'mysql', 'performance_schema', 'information_schema', 'sys' );"
    );
    infoprint "There is " . scalar(@dblist) . " Database(s).";
    my @totaldbinfo = split /\s/,
      select_one(
"SELECT SUM(TABLE_ROWS), SUM(DATA_LENGTH), SUM(INDEX_LENGTH), SUM(DATA_LENGTH+INDEX_LENGTH), COUNT(TABLE_NAME), COUNT(DISTINCT(TABLE_COLLATION)), COUNT(DISTINCT(ENGINE)) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')$ignore_tables_sql;"
      );
    infoprint "All User Databases:";
    infoprint " +-- TABLE : "
      . select_one(
"SELECT count(*) from information_schema.TABLES WHERE TABLE_TYPE ='BASE TABLE' AND TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')$ignore_tables_sql"
      ) . "";
    infoprint " +-- VIEW  : "
      . select_one(
"SELECT count(*) from information_schema.TABLES WHERE TABLE_TYPE ='VIEW' AND TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')$ignore_tables_sql"
      ) . "";
    infoprint " +-- INDEX : "
      . select_one(
"SELECT count(distinct(concat(TABLE_NAME, TABLE_SCHEMA, INDEX_NAME))) from information_schema.STATISTICS WHERE TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')$ignore_tables_sql"
      ) . "";

    infoprint " +-- CHARS : "
      . ( $totaldbinfo[5] eq 'NULL' ? 0 : $totaldbinfo[5] ) . " ("
      . (
        join ", ",
        select_array(
"select distinct(CHARACTER_SET_NAME) from information_schema.columns WHERE CHARACTER_SET_NAME IS NOT NULL AND TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')$ignore_tables_sql;"
        )
      ) . ")";
    infoprint " +-- COLLA : "
      . ( $totaldbinfo[5] eq 'NULL' ? 0 : $totaldbinfo[5] ) . " ("
      . (
        join ", ",
        select_array(
"SELECT DISTINCT(TABLE_COLLATION) FROM information_schema.TABLES WHERE TABLE_COLLATION IS NOT NULL AND TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')$ignore_tables_sql;"
        )
      ) . ")";
    infoprint " +-- ROWS  : "
      . ( $totaldbinfo[0] eq 'NULL' ? 0 : $totaldbinfo[0] ) . "";
    infoprint " +-- DATA  : "
      . hr_bytes( $totaldbinfo[1] ) . "("
      . percentage( $totaldbinfo[1], $totaldbinfo[3] ) . "%)";
    infoprint " +-- INDEX : "
      . hr_bytes( $totaldbinfo[2] ) . "("
      . percentage( $totaldbinfo[2], $totaldbinfo[3] ) . "%)";
    infoprint " +-- SIZE  : " . hr_bytes( $totaldbinfo[3] ) . "";
    infoprint " +-- ENGINE: "
      . ( $totaldbinfo[6] eq 'NULL' ? 0 : $totaldbinfo[6] ) . " ("
      . (
        join ", ",
        select_array(
"SELECT DISTINCT(ENGINE) FROM information_schema.TABLES WHERE ENGINE IS NOT NULL AND TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')$ignore_tables_sql;"
        )
      ) . ")";

    $result{'Databases'}{'All databases'}{'Rows'} =
      ( $totaldbinfo[0] eq 'NULL' ? 0 : $totaldbinfo[0] );
    $result{'Databases'}{'All databases'}{'Data Size'} = $totaldbinfo[1];
    $result{'Databases'}{'All databases'}{'Data Pct'} =
      percentage( $totaldbinfo[1], $totaldbinfo[3] ) . "%";
    $result{'Databases'}{'All databases'}{'Index Size'} = $totaldbinfo[2];
    $result{'Databases'}{'All databases'}{'Index Pct'} =
      percentage( $totaldbinfo[2], $totaldbinfo[3] ) . "%";
    $result{'Databases'}{'All databases'}{'Total Size'} = $totaldbinfo[3];
    print "\n" unless ( $opt{'silent'} or $opt{'json'} );
    my $nbViews  = 0;
    my $nbTables = 0;

    foreach (@dblist) {
        my @dbinfo = split /\s/,
          select_one(
"SELECT TABLE_SCHEMA, SUM(TABLE_ROWS), SUM(DATA_LENGTH), SUM(INDEX_LENGTH), SUM(DATA_LENGTH+INDEX_LENGTH), COUNT(DISTINCT ENGINE), COUNT(TABLE_NAME), COUNT(DISTINCT(TABLE_COLLATION)), COUNT(DISTINCT(ENGINE)) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$_'$ignore_tables_sql GROUP BY TABLE_SCHEMA ORDER BY TABLE_SCHEMA"
          );
        next unless defined $dbinfo[0];

        infoprint "Database: " . $dbinfo[0] . "";
        $nbTables = select_one(
"SELECT count(*) from information_schema.TABLES WHERE TABLE_TYPE ='BASE TABLE' AND TABLE_SCHEMA='$_'$ignore_tables_sql"
        );
        infoprint " +-- TABLE : $nbTables";
        infoprint " +-- VIEW  : "
          . select_one(
"SELECT count(*) from information_schema.TABLES WHERE TABLE_TYPE ='VIEW' AND TABLE_SCHEMA='$_'$ignore_tables_sql"
          ) . "";
        infoprint " +-- INDEX : "
          . select_one(
"SELECT count(distinct(concat(TABLE_NAME, TABLE_SCHEMA, INDEX_NAME))) from information_schema.STATISTICS WHERE TABLE_SCHEMA='$_'$ignore_tables_sql"
          ) . "";
        infoprint " +-- CHARS : "
          . ( $totaldbinfo[5] eq 'NULL' ? 0 : $totaldbinfo[5] ) . " ("
          . (
            join ", ",
            select_array(
"select distinct(CHARACTER_SET_NAME) from information_schema.columns WHERE CHARACTER_SET_NAME IS NOT NULL AND TABLE_SCHEMA='$_'$ignore_tables_sql;"
            )
          ) . ")";
        infoprint " +-- COLLA : "
          . ( $dbinfo[7] eq 'NULL' ? 0 : $dbinfo[7] ) . " ("
          . (
            join ", ",
            select_array(
"SELECT DISTINCT(TABLE_COLLATION) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$_' AND TABLE_COLLATION IS NOT NULL$ignore_tables_sql;"
            )
          ) . ")";
        infoprint " +-- ROWS  : "
          . ( !defined( $dbinfo[1] ) or $dbinfo[1] eq 'NULL' ? 0 : $dbinfo[1] )
          . "";
        infoprint " +-- DATA  : "
          . hr_bytes( $dbinfo[2] ) . "("
          . percentage( $dbinfo[2], $dbinfo[4] ) . "%)";
        infoprint " +-- INDEX : "
          . hr_bytes( $dbinfo[3] ) . "("
          . percentage( $dbinfo[3], $dbinfo[4] ) . "%)";
        infoprint " +-- TOTAL : " . hr_bytes( $dbinfo[4] ) . "";
        infoprint " +-- ENGINE: "
          . ( $dbinfo[8] eq 'NULL' ? 0 : $dbinfo[8] ) . " ("
          . (
            join ", ",
            select_array(
"SELECT DISTINCT(ENGINE) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$_' AND ENGINE IS NOT NULL$ignore_tables_sql"
            )
          ) . ")";

        foreach my $eng (
            select_array(
"SELECT DISTINCT(ENGINE) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$_' AND ENGINE IS NOT NULL$ignore_tables_sql"
            )
          )
        {
            infoprint " +-- ENGINE $eng : "
              . select_one(
"SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$dbinfo[0]' AND ENGINE='$eng'$ignore_tables_sql"
              ) . " TABLE(s)";
        }

        if ( $nbTables == 0 ) {
            badprint " No table in $dbinfo[0] database";
            next;
        }
        badprint "Index size is larger than data size for $dbinfo[0] \n"
          if ( $dbinfo[2] ne 'NULL' )
          and ( $dbinfo[3] ne 'NULL' )
          and ( $dbinfo[2] < $dbinfo[3] );
        if ( $dbinfo[5] > 1 and $nbTables > 0 ) {
            badprint "There are "
              . $dbinfo[5]
              . " storage engines. Be careful. \n";
            push @generalrec,
"Select one storage engine (InnoDB is a good choice) for all tables in $dbinfo[0] database ($dbinfo[5] engines detected)";
        }
        $result{'Databases'}{ $dbinfo[0] }{'Rows'}       = $dbinfo[1];
        $result{'Databases'}{ $dbinfo[0] }{'Tables'}     = $dbinfo[6];
        $result{'Databases'}{ $dbinfo[0] }{'Collations'} = $dbinfo[7];
        $result{'Databases'}{ $dbinfo[0] }{'Data Size'}  = $dbinfo[2];
        $result{'Databases'}{ $dbinfo[0] }{'Data Pct'} =
          percentage( $dbinfo[2], $dbinfo[4] ) . "%";
        $result{'Databases'}{ $dbinfo[0] }{'Index Size'} = $dbinfo[3];
        $result{'Databases'}{ $dbinfo[0] }{'Index Pct'} =
          percentage( $dbinfo[3], $dbinfo[4] ) . "%";
        $result{'Databases'}{ $dbinfo[0] }{'Total Size'} = $dbinfo[4];

        if ( $dbinfo[7] > 1 ) {
            badprint $dbinfo[7]
              . " different collations for database "
              . $dbinfo[0];
            push( @generalrec,
                "Check all table collations are identical for all tables in "
                  . $dbinfo[0]
                  . " database." );
        }
        else {
            goodprint $dbinfo[7]
              . " collation for "
              . $dbinfo[0]
              . " database.";
        }
        if ( $dbinfo[8] > 1 ) {
            badprint $dbinfo[8]
              . " different engines for database "
              . $dbinfo[0];
            push( @generalrec,
                    "Check all table engines are identical for all tables in "
                  . $dbinfo[0]
                  . " database." );
        }
        else {
            goodprint $dbinfo[8] . " engine for " . $dbinfo[0] . " database.";
        }

        my @distinct_column_charset = select_array(
"select DISTINCT(CHARACTER_SET_NAME) from information_schema.COLUMNS where CHARACTER_SET_NAME IS NOT NULL AND TABLE_SCHEMA ='$_' AND CHARACTER_SET_NAME IS NOT NULL"
        );
        infoprint "Charsets for $dbinfo[0] database table column: "
          . join( ', ', @distinct_column_charset );
        if ( scalar(@distinct_column_charset) > 1 ) {
            badprint $dbinfo[0]
              . " table column(s) has several charsets defined for all text like column(s).";
            push( @generalrec,
                    "Limit charset for column to one charset if possible for "
                  . $dbinfo[0]
                  . " database." );
        }
        else {
            goodprint $dbinfo[0]
              . " table column(s) has same charset defined for all text like column(s).";
        }

        my @distinct_column_collation = select_array(
"select DISTINCT(COLLATION_NAME) from information_schema.COLUMNS where COLLATION_NAME IS NOT NULL AND TABLE_SCHEMA ='$_' AND COLLATION_NAME IS NOT NULL"
        );
        infoprint "Collations for $dbinfo[0] database table column: "
          . join( ', ', @distinct_column_collation );
        if ( scalar(@distinct_column_collation) > 1 ) {
            badprint $dbinfo[0]
              . " table column(s) has several collations defined for all text like column(s).";
            push( @generalrec,
                "Limit collations for column to one collation if possible for "
                  . $dbinfo[0]
                  . " database." );
        }
        else {
            goodprint $dbinfo[0]
              . " table column(s) has same collation defined for all text like column(s).";
        }
    }
}

# Recommendations for database columns
sub mysql_tables {
    return if ( $opt{tbstat} == 0 );

    subheaderprint "Table Column Metrics";
    unless ( mysql_version_ge( 5, 5 ) ) {
        infoprint
"Table column metrics from information schema are missing in this version. Skipping...";
        return;
    }
    if ( mysql_version_ge(8) and not mysql_version_eq(10) ) {
        infoprint
"MySQL and Percona version 8.0 and greater have removed PROCEDURE ANALYSE feature";
        $opt{colstat} = 0;
        infoprint "Disabling colstat parameter";

    }

    my $ignore_tables_sql = "";
    if ( $opt{'ignore-tables'} ) {
        my @ignored = split /,/, $opt{'ignore-tables'};
        $ignore_tables_sql =
          " AND TABLE_NAME NOT IN ('" . join( "','", @ignored ) . "')";
    }

    my $schema_doc = "";
    my $mermaid_er = "";
    if ( $opt{dumpdir} or $opt{schemadir} ) {
        $schema_doc = "# Database Schema Documentation\n\n";
        $schema_doc .=
          "Generated by MySQLTuner on " . scalar(localtime) . "\n\n";
        $mermaid_er = "## Visual Database Schema (Mermaid)\n\n";
        $mermaid_er .= "```mermaid\nerDiagram\n";
    }

    if ( $opt{schemadir} ) {
        $opt{schemadir} = abs_path( $opt{schemadir} );
        if ( !-d $opt{schemadir} ) {
            mkdir $opt{schemadir}
              or die "Cannot create directory $opt{schemadir}: $!";
        }
    }

    foreach ( select_user_dbs() ) {
        my $dbname = $_;
        next unless defined $_;

        my $current_schema_doc = "";
        my $current_mermaid_er = "";
        if ( $opt{schemadir} ) {
            $current_schema_doc = "# Database: $dbname\n\n";
            $current_schema_doc .=
              "Generated by MySQLTuner on " . scalar(localtime) . "\n\n";
            $current_mermaid_er = "## Visual Database Schema (Mermaid)\n\n";
            $current_mermaid_er .= "```mermaid\nerDiagram\n";
        }

        infoprint "Database: " . $_ . "";
        if ( $opt{dumpdir} or $opt{schemadir} ) {
            $schema_doc         .= "## Database: $dbname\n\n";
            $current_schema_doc .= "### Tables\n\n" if $opt{schemadir};
        }

        my @dbtable = select_array(
"SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA='$dbname' AND TABLE_TYPE='BASE TABLE'$ignore_tables_sql ORDER BY TABLE_NAME"
        );
        foreach my $table_info (@dbtable) {
            my ( $tbname, $engine ) = split /\t/, $table_info;
            next unless defined $tbname;
            $engine //= '';
            infoprint " +-- TABLE: $tbname";
            infoprint "     +-- TYPE: $engine";
            if ( $opt{dumpdir} or $opt{schemadir} ) {
                my $table_info = "### Table: $tbname\n";
                $table_info .= "- **Engine**: $engine\n\n";
                $table_info .= "#### Indexes\n";

                $schema_doc         .= $table_info;
                $current_schema_doc .= $table_info if $opt{schemadir};

                $mermaid_er         .= "    $tbname {\n";
                $current_mermaid_er .= "    $tbname {\n"
                  if $opt{schemadir};
            }

            my $selIdxReq = <<"ENDSQL";
      SELECT  index_name AS idxname, 
              GROUP_CONCAT(column_name ORDER BY seq_in_index) AS cols, 
              INDEX_TYPE as type
              FROM information_schema.statistics
              WHERE INDEX_SCHEMA='$dbname'
              AND TABLE_NAME='$tbname'
              GROUP BY idxname, type
ENDSQL
            my @tbidx = select_array($selIdxReq);
            my $found = 0;
            foreach my $idx (@tbidx) {
                my @info     = split /\s/, $idx;
                my $idx_name = $info[0] // '';
                my $idx_cols = $info[1] // '';
                my $idx_type = $info[2] // '';
                next if $idx_name eq 'NULL';
                infoprint
"     +-- Index $idx_name - Cols: $idx_cols - Type: $idx_type";
                if ( $opt{dumpdir} or $opt{schemadir} ) {
                    my $idx_info = "- **$idx_name**: $idx_cols ($idx_type)\n";
                    $schema_doc         .= $idx_info;
                    $current_schema_doc .= $idx_info if $opt{schemadir};
                }
                $found++;
            }
            if ( $found == 0 ) {
                badprint("Table $dbname.$tbname has no index defined");
                if ( $opt{dumpdir} or $opt{schemadir} ) {
                    $schema_doc         .= "- *No indexes defined*\n";
                    $current_schema_doc .= "- *No indexes defined*\n"
                      if $opt{schemadir};
                }
                push @generalrec,
                  "Add at least a primary key on table $dbname.$tbname";
            }

            if ( $opt{dumpdir} or $opt{schemadir} ) {
                $schema_doc         .= "\n#### Columns\n";
                $current_schema_doc .= "\n#### Columns\n"
                  if $opt{schemadir};
            }

            my @tbcol = select_array(
"SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='$dbname' AND TABLE_NAME='$tbname' ORDER BY ORDINAL_POSITION"
            );
            foreach my $col_info (@tbcol) {
                my ( $col_name, $ctype, $isnull ) = split /\t/, $col_info;
                next unless defined $col_name;
                $ctype  //= '';
                $isnull //= '';

                my $current_type =
                  uc($ctype) . ( $isnull eq 'NO' ? " NOT NULL" : " NULL" );
                my $optimal_type = '';
                infoprint "     +-- Column $tbname.$col_name: $current_type";
                if ( $opt{dumpdir} or $opt{schemadir} ) {
                    my $col_info = "- **$col_name**: $current_type\n";
                    $schema_doc         .= $col_info;
                    $current_schema_doc .= $col_info if $opt{schemadir};

                    my $mtype = $ctype;
                    $mtype =~ s/\(.*\)//g;    # Strip lengths for Mermaid
                    $mermaid_er         .= "        $mtype $col_name\n";
                    $current_mermaid_er .= "        $mtype $col_name\n"
                      if $opt{schemadir};
                }
                if ( $opt{colstat} == 1 ) {
                    $optimal_type = select_str_g( "Optimal_fieldtype",
"SELECT \`$col_name\` FROM \`$dbname\`.\`$tbname\` PROCEDURE ANALYSE(100000)"
                      )
                      unless ( mysql_version_ge(8)
                        and not mysql_version_eq(10) );
                }
                if ( $optimal_type eq '' ) {

                    #infoprint "     +-- Current Fieldtype: $current_type";

                    #infoprint "      Optimal Fieldtype: Not available";
                }
                elsif ( $current_type ne $optimal_type
                    and $current_type !~ /.*DATETIME.*/
                    and $current_type !~ /.*TIMESTAMP.*/ )
                {
                    infoprint "     +-- Current Fieldtype: $current_type";
                    if ( $optimal_type =~ /.*ENUM\(.*/ ) {
                        $optimal_type = "ENUM( ... )";
                    }
                    infoprint "     +-- Optimal Fieldtype: $optimal_type ";
                    if ( $optimal_type !~ /.*ENUM\(.*/ ) {
                        badprint
"Consider changing type for column $col_name in table $dbname.$tbname";
                        push( @generalrec,
"ALTER TABLE \`$dbname\`.\`$tbname\` MODIFY \`$col_name\` $optimal_type;"
                        );
                    }
                }
                else {
                    goodprint "$dbname.$tbname ($col_name) type: $current_type";
                }
            }
            if ( $opt{dumpdir} or $opt{schemadir} ) {
                $schema_doc         .= "\n---\n\n";
                $current_schema_doc .= "\n---\n\n" if $opt{schemadir};

                $mermaid_er         .= "    }\n";
                $current_mermaid_er .= "    }\n" if $opt{schemadir};
            }
        }

        if ( $opt{schemadir} ) {
            $current_mermaid_er .= "```\n\n";
            $current_schema_doc .= $current_mermaid_er;
            my $doc_file = "$opt{schemadir}/$dbname.md";
            if ( open( my $fh, '>', $doc_file ) ) {
                binmode( $fh, ":utf8" );
                print $fh $current_schema_doc;
                close($fh);
                infoprint
                  "Schema documentation for $dbname generated in $doc_file";
            }
            else {
                badprint
"Could not write schema documentation for $dbname to $doc_file: $!";
            }
        }
    }
    if ( $opt{dumpdir} ) {
        $mermaid_er .= "```\n\n";
        $schema_doc .= $mermaid_er;
        my $doc_file = "$opt{dumpdir}/schema_documentation.md";
        if ( open( my $fh, '>', $doc_file ) ) {
            binmode( $fh, ":utf8" );
            print $fh $schema_doc;
            close($fh);
            infoprint
              "Consolidated schema documentation generated in $doc_file";
        }
        else {
            badprint
"Could not write consolidated schema documentation to $doc_file: $!";
        }
    }
}

# Recommendations for Indexes metrics
sub mysql_indexes {
    return if ( $opt{idxstat} == 0 );

    subheaderprint "Indexes Metrics";
    unless ( mysql_version_ge( 5, 5 ) ) {
        infoprint
"Index metrics from information schema are missing in this version. Skipping...";
        return;
    }

#    unless ( mysql_version_ge( 5, 6 ) ) {
#        infoprint
#"Skip Index metrics from information schema due to erroneous information provided in this version";
#        return;
#    }
    my $ignore_tables_sql = "";
    if ( $opt{'ignore-tables'} ) {
        my @ignored = split /,/, $opt{'ignore-tables'};
        $ignore_tables_sql =
          " AND TABLE_NAME NOT IN ('" . join( "','", @ignored ) . "')";
    }

    my $selIdxReq = <<"ENDSQL";
SELECT
  CONCAT(t.TABLE_SCHEMA, '.', t.TABLE_NAME) AS 'table', 
  CONCAT(s.INDEX_NAME, '(', s.COLUMN_NAME, ')') AS 'index'
 , s.SEQ_IN_INDEX AS 'seq'
 , s2.max_columns AS 'maxcol'
 , s.CARDINALITY  AS 'card'
 , t.TABLE_ROWS   AS 'est_rows'
 , INDEX_TYPE as type
 , ROUND(((s.CARDINALITY / IFNULL(t.TABLE_ROWS, 0.01)) * 100), 2) AS 'sel'
FROM INFORMATION_SCHEMA.STATISTICS s
 INNER JOIN INFORMATION_SCHEMA.TABLES t
  ON s.TABLE_SCHEMA = t.TABLE_SCHEMA
  AND s.TABLE_NAME = t.TABLE_NAME
 INNER JOIN (
  SELECT
     TABLE_SCHEMA
   , TABLE_NAME
   , INDEX_NAME
   , MAX(SEQ_IN_INDEX) AS max_columns
  FROM INFORMATION_SCHEMA.STATISTICS
  WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema')$ignore_tables_sql
  AND INDEX_TYPE <> 'FULLTEXT'
  GROUP BY TABLE_SCHEMA, TABLE_NAME, INDEX_NAME
 ) AS s2
 ON s.TABLE_SCHEMA = s2.TABLE_SCHEMA
 AND s.TABLE_NAME = s2.TABLE_NAME
 AND s.INDEX_NAME = s2.INDEX_NAME
WHERE t.TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema')$ignore_tables_sql
AND t.TABLE_ROWS > 10
AND s.CARDINALITY IS NOT NULL
AND (s.CARDINALITY / IFNULL(t.TABLE_ROWS, 0.01)) < 8.00
ORDER BY sel
LIMIT 10;
ENDSQL
    my @idxinfo = select_array($selIdxReq);
    infoprint "Worst selectivity indexes:";
    foreach (@idxinfo) {
        debugprint "$_";
        my @info = split /\s/;
        infoprint "Index: " . $info[1] . "";

        infoprint " +-- COLUMN      : " . $info[0] . "";
        infoprint " +-- NB SEQS     : " . $info[2] . " sequence(s)";
        infoprint " +-- NB COLS     : " . $info[3] . " column(s)";
        infoprint " +-- CARDINALITY : " . $info[4] . " distinct values";
        infoprint " +-- NB ROWS     : " . $info[5] . " rows";
        infoprint " +-- TYPE        : " . $info[6];
        infoprint " +-- SELECTIVITY : " . $info[7] . "%";

        $result{'Indexes'}{ $info[1] }{'Column'}           = $info[0];
        $result{'Indexes'}{ $info[1] }{'Sequence number'}  = $info[2];
        $result{'Indexes'}{ $info[1] }{'Number of column'} = $info[3];
        $result{'Indexes'}{ $info[1] }{'Cardinality'}      = $info[4];
        $result{'Indexes'}{ $info[1] }{'Row number'}       = $info[5];
        $result{'Indexes'}{ $info[1] }{'Index Type'}       = $info[6];
        $result{'Indexes'}{ $info[1] }{'Selectivity'}      = $info[7];
        if ( $info[7] < 25 ) {
            badprint "$info[1] has a low selectivity";
        }
    }
    infoprint "Indexes per database:";
    foreach my $dbname ( select_user_dbs() ) {
        infoprint "Database: " . $dbname . "";
        $selIdxReq = <<"ENDSQL";
        SELECT  concat(table_name, '.', index_name) AS idxname,
                GROUP_CONCAT(column_name ORDER BY seq_in_index) AS cols,
                SUM(CARDINALITY) as card,
                INDEX_TYPE as type
        FROM information_schema.statistics
        WHERE INDEX_SCHEMA='$dbname'
        AND index_name IS NOT NULL$ignore_tables_sql
        GROUP BY table_name, idxname, type
ENDSQL
        my $found = 0;
        foreach my $idxinfo ( select_array($selIdxReq) ) {
            my @info = split /\s/, $idxinfo;
            next if $info[0] eq 'NULL';
            infoprint " +-- INDEX      : " . $info[0];
            infoprint " +-- COLUMNS    : " . $info[1];
            infoprint " +-- CARDINALITY: " . $info[2];
            infoprint " +-- TYPE        : " . $info[4] if defined $info[4];
            infoprint " +-- COMMENT     : " . $info[5] if defined $info[5];
            $found++;
        }
        my $nbTables = select_one(
"SELECT count(*) from information_schema.TABLES WHERE TABLE_TYPE ='BASE TABLE' AND TABLE_SCHEMA='$dbname'"
        );
        badprint "No index found for $dbname database"
          if $found == 0 and $nbTables > 1;
        push @generalrec, "Add indexes on tables from $dbname database"
          if $found == 0 and $nbTables > 1;
    }
    return
      unless ( defined( $myvar{'performance_schema'} )
        and $myvar{'performance_schema'} eq 'ON' );

    $selIdxReq = <<"ENDSQL";
SELECT CONCAT(object_schema, '.', object_name) AS 'table', index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
AND count_star = 0
AND index_name <> 'PRIMARY'
AND object_schema NOT IN ('mysql', 'performance_schema', 'information_schema')$ignore_tables_sql
ORDER BY count_star, object_schema, object_name;
ENDSQL
    @idxinfo = select_array($selIdxReq);
    infoprint "Unused indexes:";
    push( @generalrec, "Remove unused indexes." ) if ( scalar(@idxinfo) > 0 );
    foreach (@idxinfo) {
        debugprint "$_";
        my @info = split /\s/;
        badprint "Index: $info[1] on $info[0] is not used.";
        push @{ $result{'Indexes'}{'Unused Indexes'} },
          $info[0] . "." . $info[1];
    }
}

sub mysql_views {
    subheaderprint "Views Metrics";
    unless ( mysql_version_ge( 5, 5 ) ) {
        infoprint
"Views metrics from information schema are missing in this version. Skipping...";
        return;
    }
}

sub mysql_routines {
    subheaderprint "Routines Metrics";
    unless ( mysql_version_ge( 5, 5 ) ) {
        infoprint
"Routines metrics from information schema are missing in this version. Skipping...";
        return;
    }
}

sub mysql_triggers {
    subheaderprint "Triggers Metrics";
    unless ( mysql_version_ge( 5, 5 ) ) {
        infoprint
"Trigger metrics from information schema are missing in this version. Skipping...";
        return;
    }
}

# Take the two recommendation arrays and display them at the end of the output
sub make_recommendations {
    $result{'Recommendations'} = \@generalrec;
    $result{'AdjustVariables'} = \@adjvars;
    $result{'Modeling'}        = \@modeling;

    # Modular structure for modern reporting
    $result{'Modules'} = {
        'System'      => \@sysrec,
        'Performance' => \@adjvars,
        'Modeling'    => \@modeling,
        'Security'    => \@secrec,
    };

    calculate_health_score();
    display_health_score();

    subheaderprint "Recommendations";
    if ( @generalrec > 0 ) {
        prettyprint "General recommendations:";
        foreach (@generalrec) { prettyprint "    " . $_ . ""; }
    }
    if ( @adjvars > 0 ) {
        prettyprint "Variables to adjust:";
        if ( $mycalc{'pct_max_physical_memory'} > 90 ) {
            prettyprint
              "  *** MySQL's maximum memory usage is dangerously high ***\n"
              . "  *** Add RAM before increasing MySQL buffer variables ***";
        }
        foreach (@adjvars) { prettyprint "    " . $_ . ""; }
    }
    if ( @generalrec == 0 && @adjvars == 0 ) {
        prettyprint "No additional performance recommendations are available.";
    }
}

sub close_outputfile {
    close($fh) if defined($fh);
}

sub headerprint {
    prettyprint " >>  MySQLTuner $tunerversion\n"
      . "\t * Jean-Marie Renouard <jmrenouard\@gmail.com>\n"
      . "\t * Major Hayden <major\@mhtx.net>\n"
      . " >>  Bug reports, feature requests, and downloads at http://mysqltuner.pl/\n"
      . " >>  Run with '--help' for additional options and output filtering";
    debugprint( "Debug: " . $opt{debug} );
    debugprint( "Experimental: " . $opt{experimental} );
}

sub string2file {
    my $filename = shift;
    my $content  = shift;
    open my $fh, q(>), $filename
      or die
"Unable to open $filename in write mode. Please check permissions for this file or directory";
    print $fh $content if defined($content);
    close $fh;
    debugprint $content;
}

sub file2array {
    my $filename = shift;
    debugprint "* reading $filename";
    my $fh;
    open( $fh, q(<), "$filename" )
      or die "Couldn't open $filename for reading: $!\n";
    my @lines = <$fh>;
    close($fh);
    return @lines;
}

sub file2string {
    return join( '', file2array(@_) );
}

sub escape_html {
    my $str = shift // '';
    $str =~ s/&/&amp;/g;
    $str =~ s/</&lt;/g;
    $str =~ s/>/&gt;/g;
    $str =~ s/"/&quot;/g;
    $str =~ s/'/&#39;/g;
    return $str;
}

sub format_recommendation_item {
    my $item = shift;
    if ( ref($item) eq 'HASH' ) {
        if ( defined $item->{type} ) {
            if ( $item->{type} eq 'unused_index' ) {
                my $schema = $item->{schema} // '';
                my $table  = $item->{table}  // '';
                my $index  = $item->{index}  // '';
                return "Unused index: $schema.$table ($index)";
            }
            elsif ( $item->{type} eq 'redundant_index' ) {
                my $schema   = $item->{schema}         // '';
                my $table    = $item->{table}          // '';
                my $index    = $item->{index}          // '';
                my $dominant = $item->{dominant_index} // '';
                my $sql      = $item->{sql}            // '';
                return
"Redundant index: $schema.$table ($index) redundant of $dominant (suggested SQL: $sql)";
            }
        }
        return
          join( ', ', map { "$_: " . ( $item->{$_} // '' ) } sort keys %$item );
    }
    return $item // '';
}

sub dump_result {

    #debugprint Dumper( \%result ) if ( $opt{'debug'} );
    debugprint "HTML REPORT: " . ( $opt{'reportfile'} // 'undef' );

    if ( defined $opt{'reportfile'} ) {

        # Check if reportfile option was empty/boolean (e.g. from --reportfile)
        if ( $opt{'reportfile'} eq '' || $opt{'reportfile'} eq '1' ) {
            $opt{'reportfile'} = 'mysqltuner.html';
        }

        my $score = $mycalc{'WeightedHealthScore'} // $result{'HealthScore'}
          // 0;
        my $details = $result{'HealthScoreDetails'} // {};
        my $perf_score =
          ( $details->{'perf_bp'}     // 5 ) +
          ( $details->{'perf_temp'}   // 5 ) +
          ( $details->{'perf_thread'} // 5 ) +
          ( $details->{'perf_conn'}   // 5 );
        my $sec_score = $details->{'sec_total'} // 30;
        my $res_score =
          ( $details->{'res_lag'}  // 10 ) +
          ( $details->{'res_logs'} // 10 ) +
          ( $details->{'res_meta'} // 10 );

        # Render lists
        my $general_rec_html = join(
            "\n",
            map {
"<li class='flex items-start py-2 border-b border-slate-700/30'><span class='text-blue-400 mr-2.5 font-bold'>•</span><span>"
                  . escape_html( format_recommendation_item($_) )
                  . "</span></li>"
            } @generalrec
          )
          || "<li class='text-slate-400 italic py-2'>No general recommendations.</li>";

        my $adjvars_html = join(
            "\n",
            map {
"<li class='flex items-start py-2 border-b border-slate-700/30'><span class='text-amber-400 mr-2.5 font-bold'>•</span><span class='font-mono'>"
                  . escape_html( format_recommendation_item($_) )
                  . "</span></li>"
            } @adjvars
          )
          || "<li class='text-slate-400 italic py-2'>No variable adjustments required.</li>";

        my $modeling_html = join(
            "\n",
            map {
"<li class='flex items-start py-2 border-b border-slate-700/30'><span class='text-cyan-400 mr-2.5 font-bold'>•</span><span>"
                  . escape_html( format_recommendation_item($_) )
                  . "</span></li>"
            } @modeling
          )
          || "<li class='text-slate-400 italic py-2'>No database modeling findings.</li>";

        my $secrec_html = join(
            "\n",
            map {
"<li class='flex items-start py-2 border-b border-slate-700/30'><span class='text-rose-400 mr-2.5 font-bold'>•</span><span>"
                  . escape_html( format_recommendation_item($_) )
                  . "</span></li>"
            } @secrec
          )
          || "<li class='text-slate-400 italic py-2'>No security concerns identified.</li>";

        my $sysrec_html = join(
            "\n",
            map {
"<li class='flex items-start py-2 border-b border-slate-700/30'><span class='text-emerald-400 mr-2.5 font-bold'>•</span><span>"
                  . escape_html( format_recommendation_item($_) )
                  . "</span></li>"
            } @sysrec
          )
          || "<li class='text-slate-400 italic py-2'>No OS or system modifications suggested.</li>";

        # Build raw output HTML
        my $raw_output_html =
          join( "\n", map { escape_html($_) } @raw_output_lines );

        # Determine health score color class
        my $score_color_class =
          $score > 80
          ? 'text-emerald-400'
          : ( $score > 50 ? 'text-amber-400' : 'text-rose-500' );
        my $score_bg_class =
          $score > 80 ? 'bg-emerald-500/10 border-emerald-500/30'
          : (
            $score > 50 ? 'bg-amber-500/10 border-amber-500/30'
            : 'bg-rose-500/10 border-rose-500/30'
          );

        # Timestamp
        my $timestamp  = scalar localtime;
        my $db_ver     = escape_html( $myvar{'version'} );
        my $db_comment = escape_html( $myvar{'version_comment'} // 'N/A' );

        my $circle_offset = 264 - ( 264 * $score / 100 );
        my $badge_text =
          $score >= 90
          ? 'Excellent Health'
          : ( $score >= 70 ? 'Good Health' : 'Action Required' );
        my $perf_width = int( $perf_score * 100 / 40 );
        my $sec_width  = int( $sec_score * 100 / 30 );
        my $res_width  = int( $res_score * 100 / 30 );

        # HTML Content template
        my $html_content = <<"HTML";
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MySQLTuner Report</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
    <style>
        body {
            font-family: 'Outfit', sans-serif;
        }
        pre, code {
            font-family: 'JetBrains Mono', monospace;
        }
    </style>
</head>
<body class="bg-slate-950 text-slate-100 min-h-screen antialiased selection:bg-blue-500/30 selection:text-blue-200">
    <div class="max-w-6xl mx-auto px-4 py-8">
        
        <!-- Header -->
        <header class="bg-slate-900/60 border border-slate-800/80 backdrop-blur-md rounded-2xl p-6 mb-8 shadow-xl flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
            <div>
                <h1 class="text-3xl font-extrabold bg-gradient-to-r from-blue-400 via-cyan-400 to-indigo-400 bg-clip-text text-transparent tracking-tight">MySQLTuner Audit Report</h1>
                <p class="text-slate-400 mt-1.5 text-sm flex flex-wrap items-center gap-x-3 gap-y-1">
                    <span><i class="fas fa-database text-blue-400 mr-1.5"></i>$db_ver ($db_comment)</span>
                    <span class="text-slate-700">•</span>
                    <span><i class="fas fa-tools text-indigo-400 mr-1.5"></i>Tuner $tunerversion</span>
                </p>
            </div>
            <div class="text-left md:text-right border-t md:border-t-0 border-slate-800/80 pt-4 md:pt-0 w-full md:w-auto">
                <div class="text-xs text-slate-500 uppercase font-semibold tracking-wider">Generated On</div>
                <div class="text-sm font-medium text-slate-300 mt-0.5">$timestamp</div>
            </div>
        </header>

        <!-- Main Dashboard Grid -->
        <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
            
            <!-- Sidebar: Health Score Details -->
            <div class="lg:col-span-1 space-y-6">
                
                <!-- Health Score Meter -->
                <div class="bg-slate-900/40 border border-slate-800 rounded-2xl p-6 shadow-xl flex flex-col items-center justify-center text-center">
                    <h2 class="text-lg font-bold text-slate-400 mb-6 flex items-center gap-2"><i class="fas fa-heartbeat text-rose-500"></i>Overall Health Score</h2>
                    
                    <!-- Circular Score Gauge -->
                    <div class="relative w-40 h-40 flex items-center justify-center mb-6">
                        <svg class="w-full h-full transform -rotate-90" viewBox="0 0 100 100">
                            <circle cx="50" cy="50" r="42" stroke="currentColor" stroke-width="8" class="text-slate-800" fill="transparent" />
                            <circle cx="50" cy="50" r="42" stroke="currentColor" stroke-width="8" class="$score_color_class" fill="transparent"
                                    stroke-dasharray="264" stroke-dashoffset="$circle_offset" />
                        </svg>
                        <div class="absolute flex flex-col items-center">
                            <span class="text-4xl font-extrabold tracking-tight">$score</span>
                            <span class="text-xs text-slate-500 font-semibold uppercase tracking-wider mt-0.5">out of 100</span>
                        </div>
                    </div>

                    <!-- Health Badge -->
                    <div class="px-4 py-1.5 rounded-full text-xs font-bold border $score_bg_class $score_color_class">
                        $badge_text
                    </div>
                </div>

                <!-- KPI Scores Breakdown -->
                <div class="bg-slate-900/40 border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
                    <h3 class="text-sm font-bold text-slate-400 uppercase tracking-wider mb-2">Category Scores</h3>
                    
                    <!-- Performance -->
                    <div class="space-y-1">
                        <div class="flex justify-between items-center text-sm font-medium">
                            <span class="text-slate-300 flex items-center gap-2"><i class="fas fa-bolt text-amber-400 w-4"></i>Performance</span>
                            <span class="font-mono text-slate-400">$perf_score / 40</span>
                        </div>
                        <div class="w-full bg-slate-800 rounded-full h-2">
                            <div class="bg-amber-400 h-2 rounded-full" style="width: $perf_width%"></div>
                        </div>
                    </div>

                    <!-- Security -->
                    <div class="space-y-1">
                        <div class="flex justify-between items-center text-sm font-medium">
                            <span class="text-slate-300 flex items-center gap-2"><i class="fas fa-user-shield text-rose-500 w-4"></i>Security</span>
                            <span class="font-mono text-slate-400">$sec_score / 30</span>
                        </div>
                        <div class="w-full bg-slate-800 rounded-full h-2">
                            <div class="bg-rose-500 h-2 rounded-full" style="width: $sec_width%"></div>
                        </div>
                    </div>

                    <!-- Resilience -->
                    <div class="space-y-1">
                        <div class="flex justify-between items-center text-sm font-medium">
                            <span class="text-slate-300 flex items-center gap-2"><i class="fas fa-shield-virus text-emerald-400 w-4"></i>Resilience</span>
                            <span class="font-mono text-slate-400">$res_score / 30</span>
                        </div>
                        <div class="w-full bg-slate-800 rounded-full h-2">
                            <div class="bg-emerald-400 h-2 rounded-full" style="width: $res_width%"></div>
                        </div>
                    </div>
                </div>

            </div>

            <!-- Content Area: Recommendations lists -->
            <div class="lg:col-span-2 space-y-6">

                <!-- General Recommendations Panel -->
                <section class="bg-slate-900/40 border border-slate-800 rounded-2xl shadow-xl overflow-hidden">
                    <div class="bg-slate-900/80 px-6 py-4 border-b border-slate-800 flex items-center gap-3">
                        <i class="fas fa-list-alt text-blue-400 text-lg"></i>
                        <h2 class="text-lg font-bold text-slate-200">General Recommendations</h2>
                    </div>
                    <div class="p-6">
                        <ul class="text-sm text-slate-300 space-y-1">
                            $general_rec_html
                        </ul>
                    </div>
                </section>

                <!-- Variables to Adjust Panel -->
                <section class="bg-slate-900/40 border border-slate-800 rounded-2xl shadow-xl overflow-hidden">
                    <div class="bg-slate-900/80 px-6 py-4 border-b border-slate-800 flex items-center gap-3">
                        <i class="fas fa-sliders-h text-amber-400 text-lg"></i>
                        <h2 class="text-lg font-bold text-slate-200">Variables to Adjust</h2>
                    </div>
                    <div class="p-6">
                        <ul class="text-sm text-slate-300 space-y-1">
                            $adjvars_html
                        </ul>
                    </div>
                </section>

                <!-- Database Modeling & Schema Findings Panel -->
                <section class="bg-slate-900/40 border border-slate-800 rounded-2xl shadow-xl overflow-hidden">
                    <div class="bg-slate-900/80 px-6 py-4 border-b border-slate-800 flex items-center gap-3">
                        <i class="fas fa-project-diagram text-cyan-400 text-lg"></i>
                        <h2 class="text-lg font-bold text-slate-200">Database Modeling Findings</h2>
                    </div>
                    <div class="p-6">
                        <ul class="text-sm text-slate-300 space-y-1">
                            $modeling_html
                        </ul>
                    </div>
                </section>

                <!-- Security Recommendations Panel -->
                <section class="bg-slate-900/40 border border-slate-800 rounded-2xl shadow-xl overflow-hidden">
                    <div class="bg-slate-900/80 px-6 py-4 border-b border-slate-800 flex items-center gap-3">
                        <i class="fas fa-user-shield text-rose-500 text-lg"></i>
                        <h2 class="text-lg font-bold text-slate-200">Security Findings</h2>
                    </div>
                    <div class="p-6">
                        <ul class="text-sm text-slate-300 space-y-1">
                            $secrec_html
                        </ul>
                    </div>
                </section>

                <!-- System & OS Recommendations Panel -->
                <section class="bg-slate-900/40 border border-slate-800 rounded-2xl shadow-xl overflow-hidden">
                    <div class="bg-slate-900/80 px-6 py-4 border-b border-slate-800 flex items-center gap-3">
                        <i class="fas fa-server text-emerald-400 text-lg"></i>
                        <h2 class="text-lg font-bold text-slate-200">System &amp; OS Recommendations</h2>
                    </div>
                    <div class="p-6">
                        <ul class="text-sm text-slate-300 space-y-1">
                            $sysrec_html
                        </ul>
                    </div>
                </section>

            </div>

        </div>

        <!-- Raw Console Output Panel -->
        <section class="bg-slate-900/40 border border-slate-800 rounded-2xl shadow-xl overflow-hidden mb-8">
            <details class="group">
                <summary class="bg-slate-900/80 px-6 py-4 border-b border-slate-800 flex justify-between items-center cursor-pointer list-none select-none">
                    <div class="flex items-center gap-3">
                        <i class="fas fa-chevron-right text-slate-500 transition-transform group-open:rotate-90"></i>
                        <i class="fas fa-terminal text-blue-400 text-lg"></i>
                        <h2 class="text-lg font-bold text-slate-200">Full Console Output Trace</h2>
                    </div>
                    <span class="text-xs font-semibold uppercase tracking-wider text-slate-500 group-open:hidden">Show Logs</span>
                    <span class="text-xs font-semibold uppercase tracking-wider text-slate-500 hidden group-open:inline">Hide Logs</span>
                </summary>
                <div class="p-6 bg-slate-950 border-t border-slate-900/80">
                    <pre class="text-xs leading-relaxed text-slate-400 font-mono overflow-x-auto max-h-[500px] overflow-y-auto whitespace-pre-wrap">$raw_output_html</pre>
                </div>
            </details>
        </section>

        <!-- Footer -->
        <footer class="text-center text-slate-600 text-xs border-t border-slate-900 pt-8 mt-12">
            <p>Generated dynamically by MySQLTuner.pl | Zero External Dependencies</p>
            <p class="mt-1.5">&copy; 2006-2026 Major Hayden &amp; Jean-Marie Renouard</p>
        </footer>

    </div>
</body>
</html>
HTML

        open my $rfh, '>', $opt{'reportfile'}
          or die "Unable to open $opt{'reportfile'} in write mode: $!";
        print $rfh $html_content;
        close $rfh;
        goodprint "HTML Report successfully generated: " . $opt{'reportfile'};
    }

    if ( $opt{'json'} ) {
        eval { require JSON };
        if ($@) {
            print "$bad JSON Module is needed.\n";
            return 1;
        }

        my $json = JSON->new->allow_nonref;
        print $json->utf8(1)->pretty( ( $opt{'prettyjson'} ? 1 : 0 ) )
          ->encode( \%result );

        if ( $opt{'outputfile'} ) {
            unlink $opt{'outputfile'} if ( -e $opt{'outputfile'} );
            open my $fh, q(>), $opt{'outputfile'}
              or die
"Unable to open $opt{'outputfile'} in write mode. please check permissions for this file or directory";
            print $fh $json->utf8(1)->pretty( ( $opt{'prettyjson'} ? 1 : 0 ) )
              ->encode( \%result );
            close $fh;
        }
    }
}

sub which {
    my $prog_name   = shift;
    my $path_string = shift;
    my @path_array  = split /:/, $ENV{'PATH'};
    if ($is_win) {
        my $path_env = $ENV{'PATH'};
        $path_env =~ s/\\/\//g;
        @path_array = split /;/, $path_env;
    }

    for my $path (@path_array) {
        if ($is_win) {
            return "$path/$prog_name.exe" if ( -x "$path/$prog_name.exe" );
            return "$path/$prog_name.com" if ( -x "$path/$prog_name.com" );
            return "$path/$prog_name.bat" if ( -x "$path/$prog_name.bat" );
        }
        else {
            return "$path/$prog_name" if ( -x "$path/$prog_name" );
        }
    }

    return 0;
}

sub dump_csv_files {
    return if !$opt{dumpdir};

    subheaderprint "Dumping CSV files";

    $opt{dumpdir} = abs_path( $opt{dumpdir} );
    if ( !-d $opt{dumpdir} ) {
        mkdir $opt{dumpdir} or die "Cannot create directory $opt{dumpdir}: $!";
    }

    infoprint("Dumpdir: $opt{dumpdir}");

    # Always create raw_mysqltuner.txt in dumpdir for complete analysis output
    # This is independent of --outputfile option
    my $raw_output_file = "$opt{dumpdir}/raw_mysqltuner.txt";
    infoprint("Auto-generating raw output file: $raw_output_file");

   # If outputfile is not already set, use raw_mysqltuner.txt as the main output
    if ( !$opt{outputfile} ) {
        $opt{outputfile} = $raw_output_file;
        my $outputfile_path = abs_path( $opt{outputfile} );
        open( $fh, '>', $outputfile_path )
          or die("Failed to open $outputfile_path for writing: $!");
        $opt{nocolor} = 1;    # Disable colors in file output
    }

    # If outputfile is already set, create a second file handle for raw output
    else {
        my $raw_fh;
        open( $raw_fh, '>', $raw_output_file )
          or die("Failed to open $raw_output_file for writing: $!");

        # Duplicate all output to both file handles
        # We'll need to modify prettyprint to write to both $fh and $raw_fh
        # For now, just create a symlink
        close($raw_fh);
        unlink($raw_output_file);
        my $target = abs_path( $opt{outputfile} );
        symlink( $target, $raw_output_file )
          or warn("Could not create symlink $raw_output_file -> $target: $!");
    }

    # Store schema of user databases
    infoprint("Dumping user databases schemas");
    my @user_dbs = select_user_dbs();
    foreach my $db (@user_dbs) {
        infoprint "Dumping schema of $db into $opt{dumpdir}";
        my $dumpcmd = $mysqlcmd;
        if ( $dumpcmd =~ /mariadb$/ ) {
            my $mariadump = $dumpcmd;
            $mariadump =~ s/mariadb$/mariadump/;
            my $mariadb_dump = $dumpcmd;
            $mariadb_dump =~ s/mariadb$/mariadb-dump/;

            if ( -x $mariadump ) {
                $dumpcmd = $mariadump;
            }
            elsif ( -x $mariadb_dump ) {
                $dumpcmd = $mariadb_dump;
            }
            else {
                my $check = execute_system_command(
"command -v mariadump || command -v mariadb-dump || command -v mysqldump"
                );
                chomp($check);
                $dumpcmd = $check || $mariadump;
            }
        }
        else {
            $dumpcmd =~ s/mysql$/mysqldump/;
        }

        my $sql_file = "$opt{dumpdir}/schema_$db.sql";
        my $gzip_bin = which('gzip');
        my $cmd;
        my $is_compressed = 0;
        if ( $opt{'compress-dump'} && $gzip_bin ) {
            $sql_file .= ".gz";
            $cmd =
"$dumpcmd $mysqllogin --no-data --databases \"$db\" | $gzip_bin -c > \"$sql_file\" 2>>$devnull";
            $is_compressed = 1;
        }
        else {
            $cmd =
"$dumpcmd $mysqllogin --no-data --databases \"$db\" > \"$sql_file\" 2>>$devnull";
        }

        my $start_time =
          eval { require Time::HiRes; Time::HiRes::time(); } || time();
        execute_system_command($cmd);
        my $end_time =
          eval { require Time::HiRes; Time::HiRes::time(); } || time();
        my $duration = $end_time - $start_time;

        my $base = basename($sql_file);
        $exported_manifest{$base} = {
            rows       => 0,
            size       => -s $sql_file,
            duration   => sprintf( "%.3f", $duration ),
            query      => "mysqldump --no-data",
            compressed => $is_compressed
        };
    }

    # Lookup: sys views with a schema-filterable column
    # Ref: Client feedback - exclude system databases from dumpdir exports
    my %sys_schema_filter_cols = (
        'statement_analysis'                            => 'db',
        'x$statement_analysis'                          => 'db',
        'statements_with_errors_or_warnings'            => 'db',
        'x$statements_with_errors_or_warnings'          => 'db',
        'statements_with_full_table_scans'              => 'db',
        'x$statements_with_full_table_scans'            => 'db',
        'statements_with_runtimes_in_95th_percentile'   => 'db',
        'x$statements_with_runtimes_in_95th_percentile' => 'db',
        'statements_with_sorting'                       => 'db',
        'x$statements_with_sorting'                     => 'db',
        'statements_with_temp_tables'                   => 'db',
        'x$statements_with_temp_tables'                 => 'db',
        'schema_index_statistics'                       => 'table_schema',
        'x$schema_index_statistics'                     => 'table_schema',
        'schema_table_statistics'                       => 'table_schema',
        'x$schema_table_statistics'                     => 'table_schema',
        'schema_table_statistics_with_buffer'           => 'table_schema',
        'x$schema_table_statistics_with_buffer'         => 'table_schema',
        'schema_tables_with_full_table_scans'           => 'object_schema',
        'x$schema_tables_with_full_table_scans'         => 'object_schema',
        'schema_unused_indexes'                         => 'object_schema',
        'schema_redundant_indexes'                      => 'table_schema',
        'schema_auto_increment_columns'                 => 'table_schema',
        'innodb_buffer_stats_by_schema'                 => 'object_schema',
        'x$innodb_buffer_stats_by_schema'               => 'object_schema',
        'innodb_buffer_stats_by_table'                  => 'object_schema',
        'x$innodb_buffer_stats_by_table'                => 'object_schema',
        'schema_table_lock_waits'                       => 'object_schema',
        'x$schema_table_lock_waits'                     => 'object_schema',
        'schema_object_overview'                        => 'db',
        'processlist'                                   => 'db',
        'x$processlist'                                 => 'db',
        'session'                                       => 'db',
        'x$session'                                     => 'db',
        'x$ps_schema_table_statistics_io'               => 'table_schema',
        'x$schema_flattened_keys'                       => 'table_schema',
    );
    my $sys_excl_list =
      "'mysql','information_schema','performance_schema','sys'";

    for my $sys_view ( select_array('use sys;show tables;') ) {
        if (   $sys_view =~ /innodb_buffer_stats/
            or $sys_view =~ /schema_table_statistics_with_buffer/
            or $sys_view =~ /ps_schema_table_statistics_io/ )
        {
            infoprint("SKIPPING $sys_view");
            next;
        }
        infoprint "Dumping $sys_view into $opt{dumpdir}";
        my $sys_view_table = $sys_view;
        $sys_view_table =~ s/\$/\\\$/g;

        # Unfiltered export
        my $query_unfiltered =
          'use sys; select * from sys.\`' . $sys_view_table . '\`';
        select_csv_file( "$opt{dumpdir}/sys_$sys_view.csv", $query_unfiltered );

        # Filtered export
        if ( my $col = $sys_schema_filter_cols{$sys_view} ) {
            my $query_filtered = $query_unfiltered
              . " WHERE ($col IS NULL OR $col NOT IN ($sys_excl_list))";
            select_csv_file( "$opt{dumpdir}/sys_${sys_view}_filtered.csv",
                $query_filtered );
        }
    }

    # Store all information schema in dumpdir if defined
    infoprint("Dumping information schema");
    for my $info_s_table ( select_array('use information_schema;show tables;') )
    {
        if (   $info_s_table =~ /INNODB_BUFFER_PAGE/
            or $info_s_table =~ /RDS_CONTROL_PERFORMANCE_INSIGHTS_STATUS/
            or $info_s_table =~ /RDS_METRICS_COUNTER/
            or $info_s_table =~ /RDS_METRICS_GAUGE/ )
        {
            infoprint("SKIPPING $info_s_table");
            next;
        }
        infoprint "Dumping $info_s_table into $opt{dumpdir}";
        select_csv_file(
            "$opt{dumpdir}/ifs_${info_s_table}.csv",
            "select * from information_schema.$info_s_table"
        );
    }

    # Store all performance schema in dumpdir if defined
    infoprint("Dumping performance schema");
    for
      my $info_pf_table ( select_array('use performance_schema;show tables;') )
    {
        if ( $info_pf_table =~ /^events_/ ) {
            infoprint("SKIPPING $info_pf_table");
            next;
        }
        infoprint
          "Performance Schema Dumping $info_pf_table into $opt{dumpdir}";
        select_csv_file(
            "$opt{dumpdir}/ps_${info_pf_table}.csv",
            "select * from performance_schema.$info_pf_table"
        );
    }

    write_manifest_files( $opt{dumpdir} );
}

# ---------------------------------------------------------------------------
# BEGIN 'MAIN'
# ---------------------------------------------------------------------------
if ( !caller ) {
    parse_cli_args;       # Parse CLI arguments
    setup_environment;    # Initialize variables and handle early exits
    headerprint;          # Header Print

    validate_tuner_version;    # Check latest version
    cloud_setup;
    mysql_setup;               # Gotta login first
    debugprint "MySQL FINAL Client : $mysqlcmd $mysqllogin";
    debugprint "MySQL Admin FINAL Client : $mysqladmincmd $mysqllogin";

    dump_csv_files;            # dump csv files
    os_setup;                  # Set up some OS variables
    get_all_vars;              # Toss variables/status into hashes
    mysql_cloud_discovery;     # Auto-discover cloud environment
    get_tuning_info;           # Get information about the tuning connection
    calculations;              # Calculate everything we need
    check_architecture;        # Suggest 64-bit upgrade
    check_storage_engines;     # Show enabled storage engines

    if ( $opt{'feature'} ) {
        subheaderprint "See FEATURES.md for more information";
        no strict 'refs';
        for my $feature ( split /,/, $opt{'feature'} ) {
            subheaderprint "Running feature: $feature";
            $feature->();
        }
        make_recommendations;
        goodprint "Terminated successfully";
        exit(0);
    }
    my @REPORT_SECTIONS = (
        \&validate_mysql_version,     \&system_recommendations,
        \&log_file_recommendations,   \&check_metadata_perf,
        \&mysql_databases,            \&mysql_tables,
        \&mysql_table_structures,     \&mysql_indexes,
        \&mysql_views,                \&mysql_triggers,
        \&mysql_routines,             \&security_recommendations,
        \&ssl_tls_recommendations,    \&cve_recommendations,
        \&mysql_plugins,              \&mysql_stats,
        \&mysql_pfs,                  \&mariadb_threadpool,
        \&mysql_myisam,               \&mysql_innodb,
        \&mariadb_query_cache_info,   \&check_query_anti_patterns,
        \&mariadb_aria,               \&mariadb_tokudb,
        \&mariadb_xtradb,             \&mariadb_galera,
        \&get_replication_status,     \&process_sysbench_metrics,
        \&historical_comparison,      \&predictive_capacity_analysis,
        \&check_replication_advanced, \&check_security_2_0,
        \&generate_auto_fix_snippets,
    );

    foreach my $section (@REPORT_SECTIONS) {
        $section->();
    }

    make_recommendations;    # Make recommendations based on stats
    dump_result;             # Dump result if debug is on
    goodprint "Terminated successfully";
    close_outputfile;        # Close reportfile if needed

   # ---------------------------------------------------------------------------
   # END 'MAIN'
   # ---------------------------------------------------------------------------
}
1;

__END__

=pod

=encoding UTF-8

=head1 NAME

 MySQLTuner 2.8.45 - MySQL High Performance Tuning Script

=head1 IMPORTANT USAGE GUIDELINES

To run the script with the default options, run the script without arguments
Allow MySQL server to run for at least 24-48 hours before trusting suggestions
Some routines may require root level privileges (script will provide warnings)
You must provide the remote server's total memory when connecting to other servers

=head1 OPTIONS

See C<mysqltuner --help> for a full list of available options and their categories.

=head1 VERSION

Version 2.8.45
=head1 PERLDOC

You can find documentation for this module with the perldoc command.

  perldoc mysqltuner

=head2 INTERNALS

L<https://github.com/jmrenouard/MySQLTuner-perl/blob/master/INTERNALS.md>

 Internal documentation

=head1 AUTHORS

Major Hayden - major@mhtx.net
Jean-Marie Renouard - jmrenouard@gmail.com

=head1 CONTRIBUTORS

=over 4

=item *

Matthew Montgomery

=item *

Paul Kehrer

=item *

Dave Burgess

=item *

Jonathan Hinds

=item *

Mike Jackson

=item *

Nils Breunese

=item *

Shawn Ashlee

=item *

Luuk Vosslamber

=item *

Ville Skytta

=item *

Trent Hornibrook

=item *

Jason Gill

=item *

Mark Imbriaco

=item *

Greg Eden

=item *

Aubin Galinotti

=item *

Giovanni Bechis

=item *

Bill Bradford

=item *

Ryan Novosielski

=item *

Michael Scheidell

=item *

Blair Christensen

=item *

Hans du Plooy

=item *

Victor Trac

=item *

Everett Barnes

=item *

Tom Krouper

=item *

Gary Barrueto

=item *

Simon Greenaway

=item *

Adam Stein

=item *

Isart Montane

=item *

Baptiste M.

=item *

Cole Turner

=item *

Daniel Lewart

=item *

Jason Gill

=item *

Jean-Marie Renouard

=item *

Major Hayden

=item *

Matthew Montgomery

=item *

Stephan GroBberndt

=item *

Christian Loos

=item *

Long Radix

=back

=head1 SUPPORT


Bug reports, feature requests, and downloads at http://mysqltuner.pl/

Bug tracker can be found at https://github.com/jmrenouard/MySQLTuner-perl/issues

Maintained by Jean-Marie Renouard (jmrenouard\@gmail.com) - Licensed under GPL

=head1 SOURCE CODE

L<https://github.com/jmrenouard/MySQLTuner-perl/>

 git clone https://github.com/jmrenouard/MySQLTuner-perl/.git

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2006-2026 Major Hayden - major@mhtx.net
# Copyright (C) 2015-2026 Jean-Marie Renouard - jmrenouard@gmail.com

For the latest updates, please visit http://mysqltuner.pl/

Git repository available at https://github.com/jmrenouard/MySQLTuner-perl/

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

 See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

=cut

# Local variables:
# indent-tabs-mode: t
# cperl-indent-level: 8
# perl-indent-level: 8
# End:
