package DBIx::Fast::Profile::MariaDB;

use v5.38;
use Object::Pad 0.807;
use Time::HiRes qw(time);

class DBIx::Fast::Profile::MariaDB :isa(DBIx::Fast::Profile::Base) {

    # Strip control characters before printing strings that originate in OTHER
    # database sessions (processlist queries, usernames): they could embed ANSI
    # escapes that manipulate the terminal of whoever runs the monitor.
    sub _term_safe ($str) {
        return $str unless defined $str;
        $str =~ s/[^\P{Cc}\n\t]//g;
        return $str;
    }

    method init () {
        $self->dbix->exec('SET profiling = 1');
        $self->dbix->exec('SET profiling_history_size = 100');
        eval { $self->dbix->exec('SET session profiling_use_getrusage = 1') };
    }

    method get_slow_queries () {
        my $queries = $self->dbix->all(q{
            SELECT
                query_id      AS query_id,
                SUM(duration) as total_time,
                query as sql_text,
                COUNT(*) as stages
            FROM information_schema.profiling
            GROUP BY query_id, query
            HAVING total_time > ?
            ORDER BY total_time DESC
        }, $self->slow_query);

        return $queries;
    }

    method get_query_profile ($query_id) {
        return $self->dbix->all(q{
            SELECT
                state               AS state,
                duration            AS duration,
                cpu_user            AS cpu_user,
                cpu_system          AS cpu_system,
                context_voluntary   AS context_voluntary,
                context_involuntary AS context_involuntary,
                block_ops_in        AS block_ops_in,
                block_ops_out       AS block_ops_out,
                messages_sent       AS messages_sent,
                messages_received   AS messages_received,
                page_faults_major   AS page_faults_major,
                page_faults_minor   AS page_faults_minor,
                swaps               AS swaps,
                source_function     AS source_function,
                source_file         AS source_file,
                source_line         AS source_line
            FROM information_schema.profiling
            WHERE query_id = ?
            ORDER BY seq
        }, $query_id);
    }

    method get_stats () {
        my $stats = {};

        my $global = $self->dbix->all(q{
            SHOW GLOBAL STATUS WHERE Variable_name IN (
                'Questions',
                'Slow_queries',
                'Com_select',
                'Com_insert',
                'Com_update',
                'Com_delete',
                'Created_tmp_tables',
                'Created_tmp_disk_tables',
                'Select_full_join',
                'Select_full_range_join',
                'Select_range',
                'Select_range_check',
                'Select_scan',
                'Sort_merge_passes',
                'Sort_range',
                'Sort_rows',
                'Sort_scan'
            )
        });

        $stats->{global} =
          { map { $_->{Variable_name} => $_->{Value} } @$global };

        my $session = $self->dbix->hash(q{
            SELECT
                COUNT(*) as total_queries,
                SUM(CASE WHEN command = 'Query' THEN 1 ELSE 0 END) as query_count,
                SUM(CASE WHEN command = 'Sleep' THEN 1 ELSE 0 END) as sleep_count
            FROM information_schema.processlist
            WHERE id = CONNECTION_ID()
        });

        $stats->{session} = $session;

        return $stats;
    }

    method print_stats () {
        my $stats = $self->get_stats();
        return if $self->emit( stats => $stats );

        $self->out("\n=== MariaDB Query Statistics ===\n");
        $self->out("\nGlobal Statistics:\n");
        $self->outf("Questions: %d\n",      $stats->{global}->{Questions});
        $self->outf("Slow Queries: %d\n",   $stats->{global}->{Slow_queries});
        $self->outf("SELECT queries: %d\n", $stats->{global}->{Com_select});
        $self->outf("INSERT queries: %d\n", $stats->{global}->{Com_insert});
        $self->outf("UPDATE queries: %d\n", $stats->{global}->{Com_update});
        $self->outf("DELETE queries: %d\n", $stats->{global}->{Com_delete});

        $self->out("\nOptimization Statistics:\n");
        $self->outf("Temp tables: %d (disk: %d)\n",
          $stats->{global}->{Created_tmp_tables},
          $stats->{global}->{Created_tmp_disk_tables});
        $self->outf("Full table scans: %d\n", $stats->{global}->{Select_scan});
        $self->outf("Range queries: %d\n",    $stats->{global}->{Select_range});
        $self->outf("Sort merges: %d\n", $stats->{global}->{Sort_merge_passes});

        $self->out("\nSession Statistics:\n");
        $self->outf("Total Queries: %d\n", $stats->{session}->{total_queries} // 0);
        $self->outf("Query Count: %d\n",   $stats->{session}->{query_count} // 0);
        $self->outf("Sleep Count: %d\n",   $stats->{session}->{sleep_count} // 0);
    }

    method _validate_explainable_sql ($sql_text) {
        return unless defined $sql_text && length $sql_text;

        $sql_text =~ s/^\s+|\s+$//g;

        return unless $sql_text =~ /^(?:SELECT|INSERT|UPDATE|DELETE|TABLE|WITH)\b/i;

        return if $sql_text =~ m{;|--|/\*|#};                 # terminators / comments

        # Scan the dangerous-construct checks on a copy with quoted string
        # literals removed, so a legitimate value like '0x41' or 'load_file note'
        # is not mistaken for a hex literal / file function.
        (my $bare = $sql_text) =~ s/'(?:[^'\\]|\\.)*'//g;
        $bare =~ s/"(?:[^"\\]|\\.)*"//g;

        return if $bare =~ /\b0x[0-9a-f]+/i;                  # hex literals
        return if $bare =~ /\bLOAD_FILE\b/i;                  # file read
        return if $bare =~ /\bINTO\s+(?:OUTFILE|DUMPFILE)\b/i;  # file write
        # EXPLAIN-time DoS: older optimizers materialize derived tables.
        return if $bare =~ /\b(?:SLEEP|BENCHMARK|GET_LOCK)\s*\(/i;

        # The stripping above assumes backslash escapes; a server running in
        # NO_BACKSLASH_ESCAPES mode parses quote boundaries differently, so a
        # crafted literal could hide a token from $bare. Strip again WITHOUT
        # escape handling and require both parsings to be clean, covering both
        # server modes while still allowing benign literals like 'load_file x'.
        (my $bare2 = $sql_text) =~ s/'[^']*'//g;
        $bare2 =~ s/"[^"]*"//g;
        return if $bare2 =~ /\b0x[0-9a-f]+/i;
        return if $bare2 =~ /\bLOAD_FILE\b/i;
        return if $bare2 =~ /\bINTO\s+(?:OUTFILE|DUMPFILE)\b/i;
        return if $bare2 =~ /\b(?:SLEEP|BENCHMARK|GET_LOCK)\s*\(/i;

        return $sql_text;
    }

    method explain_query ($sql, @params) {
        my $safe = $self->_validate_explainable_sql($sql);
        return unless $safe;
        return $self->dbix->all( "EXPLAIN EXTENDED $safe", @params );
    }

    # Explicit lowercase aliases: MySQL 8 returns information_schema column
    # names UPPERCASE unless aliased (MariaDB returns them as written), and
    # Profile::mysql inherits these queries.
    method get_table_stats ($table) {
        return $self->dbix->all(q{
            SELECT
                table_name     AS table_name,
                engine         AS engine,
                table_rows     AS table_rows,
                avg_row_length AS avg_row_length,
                data_length    AS data_length,
                index_length   AS index_length
            FROM information_schema.tables
            WHERE table_schema = DATABASE()
            AND table_name = ?
        }, $table);
    }

    method get_index_stats ($table) {
        return $self->dbix->all(q{
            SELECT
                index_name   AS index_name,
                column_name  AS column_name,
                seq_in_index AS seq_in_index,
                cardinality  AS cardinality
            FROM information_schema.statistics
            WHERE table_schema = DATABASE()
            AND table_name = ?
            ORDER BY index_name, seq_in_index
        }, $table);
    }

    method analyze_indexes ($table) {
        my $stats = {
            table           => $table,
            indexes         => {},
            problems        => [],
            recommendations => []
        };

        my $indexes = $self->dbix->all(q{
            SELECT
                index_name  AS index_name,
                GROUP_CONCAT(column_name ORDER BY seq_in_index) as columns,
                index_type  AS index_type,
                non_unique  AS non_unique,
                cardinality AS cardinality,
                nullable    AS nullable,
                GROUP_CONCAT(seq_in_index) as column_positions
            FROM information_schema.statistics
            WHERE table_schema = DATABASE()
            AND table_name = ?
            GROUP BY index_name
            ORDER BY index_name, seq_in_index
        }, $table);

        for my $index (@$indexes) {
            $stats->{indexes}->{ $index->{index_name} } = $index;
        }

        my $index_usage = $self->dbix->all(q{
            SELECT
                index_name,
                count_star as uses,
                count_read as reads,
                count_write as writes,
                count_fetch as fetches
            FROM performance_schema.table_io_waits_summary_by_index_usage
            WHERE object_schema = DATABASE()
            AND object_name = ?
        }, $table);

        for my $usage (@$index_usage) {
            $stats->{indexes}->{ $usage->{index_name} }->{usage} = $usage;
        }

        $self->_analyze_index_problems($stats);
        $self->_generate_index_recommendations($stats);

        return $stats;
    }

    method _analyze_index_problems ($stats) {
        for my $index_name ( keys %{ $stats->{indexes} } ) {
            my $index = $stats->{indexes}->{$index_name};
            my $usage = $index->{usage} || {};

            if ( !$usage->{uses} && $index_name ne 'PRIMARY' ) {
                push @{ $stats->{problems} },
                  {
                    type    => 'unused_index',
                    index   => $index_name,
                    message => "Index '$index_name' has never been used"
                  };
            }

            if ( $usage->{uses} && $usage->{uses} < 10
                && $index_name ne 'PRIMARY' )
            {
                push @{ $stats->{problems} },
                  {
                    type  => 'rarely_used_index',
                    index => $index_name,
                    message =>
                      "Index '$index_name' is rarely used (only $usage->{uses} times)"
                  };
            }

            if ( $index->{cardinality} && $index->{cardinality} < 10 ) {
                push @{ $stats->{problems} },
                  {
                    type  => 'low_cardinality',
                    index => $index_name,
                    message =>
                      "Index '$index_name' has very low cardinality ($index->{cardinality})"
                  };
            }
        }

        $self->_check_redundant_indexes($stats);
    }

    method _check_redundant_indexes ($stats) {
        my %column_sets;
        for my $index_name ( keys %{ $stats->{indexes} } ) {
            my $index   = $stats->{indexes}->{$index_name};
            my @columns = split /,/, $index->{columns};

            my $column_key = join( ",", sort @columns );

            if ( $column_sets{$column_key} ) {
                push @{ $stats->{problems} },
                  {
                    type         => 'redundant_index',
                    index        => $index_name,
                    duplicate_of => $column_sets{$column_key},
                    message =>
                      "Index '$index_name' is redundant with '$column_sets{$column_key}'"
                  };
            }
            $column_sets{$column_key} = $index_name;
        }
    }

    method _generate_index_recommendations ($stats) {
        my $frequent_queries = $self->dbix->all(q{
            SELECT
                sql_text,
                count_star as executions,
                sum_timer_wait/1000000000000 as total_latency
            FROM performance_schema.events_statements_summary_by_digest
            WHERE schema_name = DATABASE()
            AND sql_text LIKE ?
            ORDER BY count_star DESC
            LIMIT 10
        }, '%' . $stats->{table} . '%');

        for my $query (@$frequent_queries) {
            my $safe_sql = $self->_validate_explainable_sql($query->{sql_text});
            next unless $safe_sql;

            my $explained = eval {
                $self->dbix->all("EXPLAIN $safe_sql");
            };
            next unless $explained && @$explained;

            my $rows = $explained->[0]->{rows} // 0;
            if ( $rows > 1000 ) {
                push @{ $stats->{recommendations} },
                  {
                    type  => 'missing_index',
                    query => $query->{sql_text},
                    rows  => $rows,
                    message =>
                      "Consider adding an index for query executed $query->{executions} times (examines $rows rows)"
                  };
            }
        }
    }

    method print_index_analysis ($table) {
        my $stats = $self->analyze_indexes($table);
        return if $self->emit( index_analysis => $stats );

        $self->out("\n=== Index Analysis for table '$table' ===\n\n");

        $self->out("Current Indexes:\n");
        $self->out("-" x 80, "\n");
        for my $index_name ( sort keys %{ $stats->{indexes} } ) {
            my $index = $stats->{indexes}->{$index_name};
            my $usage = $index->{usage} || {};

            $self->outf("Index: %s\n",       $index_name);
            $self->outf("  Columns: %s\n",   $index->{columns});
            $self->outf("  Type: %s\n",      $index->{index_type});
            $self->outf("  Cardinality: %s\n", $index->{cardinality} || 'N/A');
            $self->outf("  Usage:\n");
            $self->outf("    Total uses: %d\n", $usage->{uses}    || 0);
            $self->outf("    Reads: %d\n",      $usage->{reads}   || 0);
            $self->outf("    Writes: %d\n",     $usage->{writes}  || 0);
            $self->outf("    Fetches: %d\n",    $usage->{fetches} || 0);
            $self->out("\n");
        }

        if ( @{ $stats->{problems} } ) {
            $self->out("Problems Found:\n");
            $self->out("-" x 80, "\n");
            for my $problem ( @{ $stats->{problems} } ) {
                $self->outf("* %s\n", $problem->{message});
            }
            $self->out("\n");
        }

        if ( @{ $stats->{recommendations} } ) {
            $self->out("Recommendations:\n");
            $self->out("-" x 80, "\n");
            for my $rec ( @{ $stats->{recommendations} } ) {
                $self->outf("* %s\n", $rec->{message});
                $self->outf("  Query: %s\n", $rec->{query}) if $rec->{query};
            }
        }
    }

    method get_index_metrics ($table) {
        return $self->dbix->all(q{
            SELECT
                object_name as table_name,
                index_name,
                count_star as uses,
                count_read as reads,
                count_write as writes,
                count_fetch as fetches,
                sum_timer_wait/1000000000000 as total_latency,
                avg_timer_wait/1000000000000 as avg_latency
            FROM performance_schema.table_io_waits_summary_by_index_usage
            WHERE object_schema = DATABASE()
            AND object_name = ?
            ORDER BY sum_timer_wait DESC
        }, $table);
    }

    method monitor_connections () {
        my $stats = {
            current   => $self->get_current_connections(),
            processes => $self->get_processes(),
            variables => $self->get_connection_variables(),
            status    => $self->get_connection_status(),
            usage     => $self->get_connection_usage(),
            problems  => [],
        };

        $self->_analyze_connection_problems($stats);

        return $stats;
    }

    method get_current_connections () {
        return $self->dbix->hash(q{
            SELECT
                COUNT(*) as total_connections,
                SUM(CASE WHEN command = 'Sleep' THEN 1 ELSE 0 END) as sleeping,
                SUM(CASE WHEN command != 'Sleep' THEN 1 ELSE 0 END) as active,
                MAX(time) as max_connection_time
            FROM information_schema.processlist
        });
    }

    method get_processes () {
        # Portable across MariaDB and MySQL: no MariaDB-only time_ms column
        # (and TIME is already the running time in seconds).
        return $self->dbix->all(q{
            SELECT
                id      AS id,
                user    AS user,
                host    AS host,
                db      AS db,
                command AS command,
                time    AS time,
                state   AS state,
                info    AS query,
                time    AS seconds_running
            FROM information_schema.processlist
            ORDER BY time DESC
        });
    }

    method get_connection_variables () {
        my $vars = $self->dbix->all(q{
            SHOW VARIABLES WHERE Variable_name IN (
                'max_connections',
                'max_user_connections',
                'wait_timeout',
                'interactive_timeout',
                'connect_timeout',
                'max_allowed_packet',
                'thread_cache_size',
                'thread_stack'
            )
        });
        return { map { $_->{Variable_name} => $_->{Value} } @$vars };
    }

    method get_connection_status () {
        my $status = $self->dbix->all(q{
            SHOW GLOBAL STATUS WHERE Variable_name IN (
                'Threads_connected',
                'Threads_created',
                'Threads_cached',
                'Threads_running',
                'Connections',
                'Aborted_connects',
                'Aborted_clients',
                'Connection_errors_accept',
                'Connection_errors_internal',
                'Connection_errors_max_connections',
                'Connection_errors_peer_address',
                'Connection_errors_select',
                'Connection_errors_tcpwrap'
            )
        });

        return { map { $_->{Variable_name} => $_->{Value} } @$status };
    }

    method get_connection_usage () {
        return $self->dbix->all(q{
            SELECT
                user as username,
                COUNT(*) as total_connections,
                SUM(CASE WHEN command = 'Sleep' THEN 1 ELSE 0 END) as sleeping,
                SUM(CASE WHEN command != 'Sleep' THEN 1 ELSE 0 END) as active,
                MAX(time) as max_time
            FROM information_schema.processlist
            GROUP BY user
        });
    }

    method _analyze_connection_problems ($stats) {
        my $max_conn     = $stats->{variables}->{max_connections} || 1;
        my $current_conn = $stats->{status}->{Threads_connected} || 0;
        my $usage_percent = ( $current_conn / $max_conn ) * 100;

        if ( $usage_percent > 80 ) {
            push @{ $stats->{problems} },
              {
                type     => 'high_connection_usage',
                message  => sprintf(
                    "High connection usage: %.2f%% (%d of %d)",
                    $usage_percent, $current_conn, $max_conn
                ),
                severity => 'warning'
              };
        }

        my $total_conns  = $stats->{status}->{Connections} || 1;
        my $aborted_rate =
          ($stats->{status}->{Aborted_connects} || 0)
          / $total_conns * 100;

        if ( $aborted_rate > 5 ) {
            push @{ $stats->{problems} },
              {
                type     => 'high_abort_rate',
                message  => sprintf( "High connection abort rate: %.2f%%",
                    $aborted_rate ),
                severity => 'error'
              };
        }

        if ( $stats->{current}->{sleeping} > 50 ) {
            push @{ $stats->{problems} },
              {
                type     => 'many_sleeping_connections',
                message  => sprintf(
                    "High number of sleeping connections: %d",
                    $stats->{current}->{sleeping}
                ),
                severity => 'warning'
              };
        }

        for my $error_type ( grep /^Connection_errors_/,
            keys %{ $stats->{status} } )
        {
            if ( $stats->{status}->{$error_type} > 0 ) {
                push @{ $stats->{problems} },
                  {
                    type     => 'connection_errors',
                    message  => sprintf( "%s: %d occurrences",
                        $error_type, $stats->{status}->{$error_type} ),
                    severity => 'error'
                  };
            }
        }
    }

    method print_connection_monitor () {
        my $stats = $self->monitor_connections();
        # json/callback consumers get the raw struct; _term_safe sanitization
        # and query truncation are terminal-only concerns (JSON escapes
        # control characters natively).
        return if $self->emit( connection_monitor => $stats );

        $self->out("\n=== MariaDB Connection Monitor ===\n\n");

        $self->out("Current Connections:\n");
        $self->out("-" x 80, "\n");
        $self->outf("Total: %d (Active: %d, Sleeping: %d)\n",
          $stats->{current}->{total_connections},
          $stats->{current}->{active},
          $stats->{current}->{sleeping});
        $self->outf("Max Connection Time: %d seconds\n\n",
          $stats->{current}->{max_connection_time} // 0);

        $self->out("Connection Settings:\n");
        $self->out("-" x 80, "\n");
        for my $var ( sort keys %{ $stats->{variables} } ) {
            $self->outf("%-25s: %s\n", $var, $stats->{variables}->{$var});
        }
        $self->out("\n");

        $self->out("Connection Status:\n");
        $self->out("-" x 80, "\n");
        for my $var ( sort keys %{ $stats->{status} } ) {
            $self->outf("%-30s: %s\n", $var, $stats->{status}->{$var});
        }
        $self->out("\n");

        $self->out("Connection Usage by User:\n");
        $self->out("-" x 80, "\n");
        for my $user ( @{ $stats->{usage} } ) {
            $self->outf("User: %s\n",               _term_safe( $user->{username} ));
            $self->outf("  Total Connections: %d\n", $user->{total_connections});
            $self->outf("  Active: %d\n",            $user->{active});
            $self->outf("  Sleeping: %d\n",          $user->{sleeping});
            $self->outf("  Max Time: %d seconds\n",  $user->{max_time} // 0);
            $self->out("\n");
        }

        $self->out("Active Processes:\n");
        $self->out("-" x 80, "\n");
        for my $proc ( @{ $stats->{processes} } ) {
            next if $proc->{command} eq 'Sleep';
            $self->outf("ID: %d\n",                    $proc->{id});
            $self->outf("  User: %s\n",                _term_safe( $proc->{user} ));
            $self->outf("  Host: %s\n",                _term_safe( $proc->{host} ));
            $self->outf("  DB: %s\n",                  _term_safe( $proc->{db} )    || 'None');
            $self->outf("  State: %s\n",               _term_safe( $proc->{state} ) || 'None');
            # TIME is NULL for some processlist rows (e.g. daemon threads).
            $self->outf("  Running Time: %d seconds\n", $proc->{seconds_running} // 0);
            if ( $proc->{query} ) {
                $self->outf("  Query: %s\n",
                  _term_safe( substr( $proc->{query}, 0, 100 )
                  . ( length( $proc->{query} ) > 100 ? '...' : '' ) ));
            }
            $self->out("\n");
        }

        if ( @{ $stats->{problems} } ) {
            $self->out("Problems Detected:\n");
            $self->out("-" x 80, "\n");
            for my $problem ( @{ $stats->{problems} } ) {
                $self->outf("[%s] %s\n",
                  uc( $problem->{severity} ),
                  $problem->{message});
            }
        }
    }

    method start_connection_monitor ($interval = 60, $duration = 3600) {
        my $start_time = time();
        my $end_time   = $start_time + $duration;

        while ( time() < $end_time ) {
            # Terminal chrome (clear screen, footer) only in text mode; in
            # json/callback mode each iteration is one connection_monitor event.
            $self->out("\033[2J\033[H") if $self->output_is_text;
            $self->print_connection_monitor();
            if ( $self->output_is_text ) {
                $self->out("\nMonitoring... (Press Ctrl+C to stop)\n");
                $self->outf("Running for: %d seconds\n", ( time() - $start_time ));
            }
            sleep($interval);
        }
    }
}

1;

__END__

=head1 NAME

DBIx::Fast::Profile::MariaDB - MariaDB/MySQL profiling and diagnostics

=head1 DESCRIPTION

Provides MariaDB-native query profiling, index analysis, and connection
monitoring using C<information_schema> and C<performance_schema>. Extends
L<DBIx::Fast::Profile::Base>.

=head1 METHODS

=head2 init

Enables MariaDB session profiling (C<SET profiling = 1>) and configures
history size and resource usage tracking.

=head2 get_slow_queries

Returns queries from C<information_schema.profiling> whose total duration
exceeds the C<slow_query> threshold.

=head2 get_query_profile

    my $profile = $mariadb->get_query_profile($query_id);

Returns detailed stage-by-stage profiling data for a specific query ID,
including CPU, I/O, and memory metrics.

=head2 get_stats

Returns a hashref with C<global> server status counters and C<session>-level
query statistics for the current connection.

=head2 print_stats

Prints a formatted report of global and session query statistics.

=head2 explain_query

    my $plan = $mariadb->explain_query($sql, @params);

Runs C<EXPLAIN EXTENDED> on the given SQL and returns the execution plan.
The SQL is validated before execution: it must begin with a DML keyword
(SELECT, INSERT, UPDATE, DELETE, TABLE, WITH) and must not contain
statement terminators or comment markers. Returns C<undef> if the SQL
fails validation.

=head2 get_table_stats

    my $stats = $mariadb->get_table_stats($table);

Returns table metadata from C<information_schema.tables> including engine,
row count, and data/index sizes.

=head2 get_index_stats

    my $stats = $mariadb->get_index_stats($table);

Returns index column details and cardinality from C<information_schema.statistics>.

=head2 analyze_indexes

    my $analysis = $mariadb->analyze_indexes($table);

Performs a comprehensive index analysis combining schema metadata with
C<performance_schema> usage data. Returns indexes, detected problems, and
optimization recommendations.

=head2 print_index_analysis

    $mariadb->print_index_analysis($table);

Prints a formatted index analysis report for the given table, including usage
stats, problems, and recommendations.

=head2 get_index_metrics

    my $metrics = $mariadb->get_index_metrics($table);

Returns per-index I/O wait metrics (reads, writes, fetches, latency) from
C<performance_schema>.

=head2 monitor_connections

    my $stats = $mariadb->monitor_connections();

Returns a comprehensive snapshot of current connections, processes, variables,
status counters, per-user usage, and detected problems.

=head2 print_connection_monitor

Prints a formatted connection monitoring report including active processes,
settings, status, and any detected problems.

=head2 start_connection_monitor

    $mariadb->start_connection_monitor($interval, $duration);

Runs a continuous connection monitor that refreshes every C<$interval> seconds
(default 60) for up to C<$duration> seconds (default 3600).

=head1 AUTHOR

SeHarrys

=head1 LICENSE

This is free software under the Artistic License 2.0.

=cut
