
#                                      Copyright 2024 WebPros International, LLC
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited.

package Whostmgr::360Monitoring::Agent;

use cPstrict;
use Config::Tiny                                           ();
use File::Slurper                                          ();
use Types::Common                                          qw(HashRef ArrayRef ConsumerOf InstanceOf NonEmptyStr);
use Whostmgr::360Monitoring::Agent::Settings::Agent        ();
use Whostmgr::360Monitoring::Agent::Settings::Data         ();
use Whostmgr::360Monitoring::Agent::Settings::Execution    ();
use Whostmgr::360Monitoring::Agent::Settings::GlobalPlugin ();
use Whostmgr::360Monitoring::Util                          ();
use Cpanel::FileUtils::Write                               qw(overwrite);
use Cpanel::Imports;
use autodie;
use Moo;

use constant {
    SANDBOX_TOUCH_FILE        => '/var/cpanel/dev_sandbox',
    STAGING_DEFAULT_AGENT_INI => '/var/cpanel/plugins/monitoring/agent360.staging.ini',
    RUNTIME_AGENT_INI         => '/etc/agent360.ini',
    IS_STAGING                => -e '/var/cpanel/use_monitoring_stage',
};

has 'default_plugins' => (
    is      => 'ro',
    isa     => ArrayRef [NonEmptyStr],
    default => sub {
        +[
            'httpd',
        ];
    }
);

has 'plugins' => (
    is      => 'rwp',
    isa     => HashRef [ ConsumerOf ['Whostmgr::360Monitoring::Agent::Plugins::Roles::Serializable'] &InstanceOf['Whostmgr::360Monitoring::Agent::Plugins'] ],
    default => sub { +{} }
);

has 'settings_global_plugin' => (
    is     => 'ro',
    isa    => InstanceOf ['Whostmgr::360Monitoring::Agent::Settings::GlobalPlugin'],
    coerce => sub ($options) {
        Whostmgr::360Monitoring::Agent::Settings::GlobalPlugin->new($options);
    }
);

has 'settings_agent' => (
    is     => 'ro',
    isa    => InstanceOf ['Whostmgr::360Monitoring::Agent::Settings::Agent'],
    coerce => sub ($options) {
        Whostmgr::360Monitoring::Agent::Settings::Agent->new($options);
    }
);

has 'settings_data' => (
    is     => 'ro',
    isa    => InstanceOf ['Whostmgr::360Monitoring::Agent::Settings::Data'],
    coerce => sub ($options) {
        Whostmgr::360Monitoring::Agent::Settings::Data->new($options);
    }
);

has 'settings_execution' => (
    is     => 'ro',
    isa    => InstanceOf ['Whostmgr::360Monitoring::Agent::Settings::Execution'],
    coerce => sub ($options) {
        Whostmgr::360Monitoring::Agent::Settings::Execution->new($options);
    }
);

has '_topsec' => (
    is       => 'ro',
    isa      => HashRef,
    init_arg => 'DEFAULT',
);

# This processes from our own state
sub run ( $self, @plugins ) {
    if (@plugins) {
        foreach my $plugin (@plugins) {
            if ( $self->get_plugin($plugin)->can("run") ) {
                $self->get_plugin($plugin)->run;
            }
            else {
                $self->get_plugin($plugin)->agent360_run;
            }
        }
    }
    else {
        foreach my $plugin ( values $self->plugins->%* ) {
            next unless $plugin->enabled;
            if ( $self->get_plugin($plugin)->can("run") ) {
                $self->get_plugin($plugin)->run;
            }
            else {
                $self->get_plugin($plugin)->agent360_run;
            }
        }
    }
}

# This just spawns out to agent360 directly.
sub agent360_run (@plugins) {
    require Cpanel::SafeRun::Errors;
    if (@plugins) {
        return Cpanel::SafeRun::Errors::saferunallerrors( '/usr/local/bin/agent360', '--test', @plugins );
    }
    else {
        return Cpanel::SafeRun::Errors::saferunallerrors( '/usr/local/bin/agent360', '--test' );
    }
}

sub agent360_info {
    require Cpanel::SafeRun::Errors;
    my $output = Cpanel::SafeRun::Errors::saferunallerrors( '/usr/local/bin/agent360', '--info' );
    $output;
}

# Serialize to hashref, want to go the other way... use new()...
sub serialize ($self) {

    my %db;
    $db{DEFAULT}   = $self->settings_global_plugin->serialize if $self->settings_global_plugin->serialize->%*;
    $db{data}      = $self->settings_data->serialize          if $self->settings_data->serialize->%*;
    $db{agent}     = $self->settings_agent->serialize         if $self->settings_agent->serialize->%*;
    $db{execution} = $self->settings_execution->serialize     if $self->settings_execution->serialize->%*;

    foreach my $name ( sort keys %{ $self->plugins } ) {
        $db{$name} = $self->get_plugin($name)->serialize();
    }

    \%db;
}

sub to_INI ($self) {
    my $conf = $self->serialize;
    while ( my ( $section, $contents ) = each $conf->%* ) {
        while ( my ( $k, $v ) = each $contents->%* ) {
            if ( ref $v eq 'ARRAY' ) {
                $conf->{$section}{$k} = join ',', @$v;
            }
            elsif ( Types::Serialiser::is_true($v) ) {
                $conf->{$section}{$k} = 'yes';
            }
            elsif ( Types::Serialiser::is_false($v) ) {
                $conf->{$section}{$k} = 'no';
            }
        }
    }

    my $header = <<~"END_HEADER";
        # --------------------------------------------------------
        # Warning: DO NOT EDIT THIS FILE.
        # --------------------------------------------------------
        # Description: agent360.ini for cPanel & WHM server. This
        # configuration file is generated automatically by the
        # cpanel-monitoring-plugin and used in conjunction with
        # the 360 Monitoring.
        # --------------------------------------------------------
        END_HEADER

    local $Config::Tiny::errstr = undef;
    my $output = $header . Config::Tiny->new($conf)->write_string();

    if ( my $err = Config::Tiny->errstr ) {
        die "Cannot convert data to file format for writing: “$err”";
    }

    return $output;
}

sub from_INI ($contents) {
    die "from_INI's argument must be a reference to a string" if !ref $contents;

    local $Config::Tiny::errstr = undef;
    my $config = Config::Tiny->read_string($$contents);
    die Config::Tiny->errstr if !defined $config;

    $config->{DEFAULT} //= {};
    my $agent = __PACKAGE__->new(
        {
            DEFAULT            => delete $config->{DEFAULT},
            settings_agent     => delete $config->{agent},
            settings_execution => delete $config->{execution},
            settings_data      => delete $config->{data},
        }
    );

    my %plugins;
    while ( my ( $key, $value ) = each %{$config} ) {
        $plugins{$key} = _new_plugin( $agent, $key, $value );
    }
    $agent->_set_plugins( \%plugins );
    return $agent;
}

sub get_plugin ( $self, $name ) {
    return $self->plugins->{$name};
}

sub _new_plugin ( $agent, $name, $conf ) {
    my $package = Whostmgr::360Monitoring::Util::get_plugin_package( Whostmgr::360Monitoring::Util::snake_to_uppercamel($name) );

    # Dynamically load the module by string
    # Use "use" rather than "require" because the latter will affect
    # the namespace table, even if the module fails to load.
    eval "use $package;";
    if ($@) {
        warn $@;
    }

    my $plugin = do {
        local $@;
        my $plugin = eval { $package->new( $agent, $conf ) };
        $@ =~ s' at /.*'';
        die "Error loading plugin [$name]: $@\n" if $@;
        $plugin;
    };

    return $plugin;
}

# Return undef if the plugin does not exist
sub add_to_plugins ( $self, $name, $conf ) {
    my $plugin = _new_plugin( $self, $name, $conf );

    return if !$plugin;

    my $plugins = $self->plugins;
    $plugins->{$name} = $plugin;
    $self->_set_plugins($plugins);
    return $self;
}

sub install_plugins ($self) {
    foreach my $plugin_name ( $self->default_plugins->@* ) {
        $self->add_to_plugins( $plugin_name, {} );
        my $plugin = $self->get_plugin($plugin_name);
        logger()->info("Setting up 360 monitoring for plugin: '$plugin_name'.");
        if ( $plugin->does("Whostmgr::360Monitoring::Agent::Plugins::Roles::Installable") ) {
            $plugin->install_and_enable();    # optional role for plugin classes
        }
        else {
            $plugin->enabled(1);              # inherited role for all plugin classes
        }
    }

    return $self->save_to_file();
}

sub uninstall_plugins ($self) {
    foreach my $plugin_name ( $self->default_plugins->@* ) {
        logger()->info("Disabling 360 monitoring for plugin: '$plugin_name'.");
        my $plugin = $self->get_plugin($plugin_name);
        next if !$plugin;
        if ( $plugin->does("Whostmgr::360Monitoring::Agent::Plugins::Roles::Installable") ) {
            $plugin->uninstall_and_disable();    # optional role for plugin classes
        }
        else {
            $plugin->enabled(0);                 # inherited role for all plugin classes
        }
    }

    return $self->save_to_file();
}

# Constructor builds from file
sub load_from_file ( $ini = RUNTIME_AGENT_INI ) {
    my $contents = File::Slurper::read_binary($ini);
    return from_INI( \$contents );
}

sub save_to_file ( $self, $absolute_location = RUNTIME_AGENT_INI ) {
    my $contents = $self->to_INI();
    logger->info("Updating INI: '$absolute_location'");
    Cpanel::FileUtils::Write::overwrite( $absolute_location, $contents, 0640 );
    chown( 0, ( getgrnam 'agent360' )[2] // 0, $absolute_location );
    return 1;
}

around 'BUILDARGS' => sub ( $orig, $class, $options = {}, @args ) {
    $options->{DEFAULT}                //= {};
    $options->{settings_global_plugin} //= $options->{DEFAULT};
    $options->{settings_execution}     //= $options->{DEFAULT};
    $options->{settings_data}          //= $options->{DEFAULT};
    $options->{settings_agent}         //= $options->{DEFAULT};
    return $class->$orig($options);
};

1;

__END__

=head1 NAME

Whostmgr::360Monitoring::Agent - A class modelling agent360, reading/writing the config that controls agent plugins.

=head1 DESCRIPTION

Agent is responsible reading from and writing to INI files. It contains global
plugin settings and all of the plugins specified in the INI file.

=head1 SYNOPSIS

    # Create a blank agent from default INI file
    my $agent = Whostmgr::360Monitoring::Agent->new;

    # Or create an agent initialized from a custom INI file
    my $agent = Whostmgr::360Monitoring::Agent::load_from_file;
    my $agent = Whostmgr::360Monitoring::Agent::load_from_file("/etc/custom_360.ini");

    # Create a plugin
    my $plugin = $agent->add_to_plugins("httpd", $configOptions);

    # Or get a plugin from the agent
    my $plugin = $agent->get_plugin("httpd");

    # Once you have the plugin, you can set values on it
    $plugin->enabled(1); # Enable
    $plugin->enabled(0); # Disable

    # When you're done, write it back to an ini file
    $agent->save_to_file;
    $agent->save_to_file("/etc/custom_360.ini");

=head1 ATTRIBUTES

=over 15

=item plugins

An attribute representing an ArrayRef of plugins.

=item settings_global_plugin

An attribute representing settings_global_plugin. The default values for the plugins.

=item settings_execution

An attribute representing the [execution] settings. These are configuration
values that affect all plugins.

=item settings_data

An attribute representing the [data] settings. These configure the behavior of
agent360.

=item settings_agent

An attribute representing the [agent] settings. These configure the behavior of
agent360.

=back

=head1 METHODS

=head2 Agent360 Methods

=over 8

=item agent360_info

Simply calls agent360 with C<--info> argument.

We do not control the arguments to agent360. See their documentation for more information.

=item agent360_run([plugins])

Simply calls agent360 with C<--run> argument.

We do not control the arguments to agent360. See their documentation for more information.

=back

=item setup()

Setups up all the standard plugins if they are supported and saves the configuration to disk.

=head2 Agent Methods

=over 8

=item run([plugins])

Our own implementation of C<run>. This is where we reserve the right to deviate
from the behavior of agent360.

If called with a list a plugins, calls the C<run> method on each plugin
provided a custom implementation of C<run> exists, or else falls back
to the generic plugin's implementation of C<agent360_run>.

If called without a list of plugins, it calls the C<run> method on every
enabled plugin the Agent has, or if a custom C<run> implementation is not
provided, falls back to the generic plugin's implementation of C<agent360_run>.

=item add_to_plugins( $plugin_name, $config_options_hr )

Adds a plugin to the agent object.

    $agent->add_to_plugins( "fake", {enabled=>'yes'} );

=back

=head2 INI Functions

=over 8

=item from_INI(\$ini)

Creates an agent from a scalar reference to a string containing an INI.

    my $agent = Whostmgr::360Monitoring::Agent::from_INI( \$ini_file_contents );

=item to_INI(\$ini)

Returns a string representing an INI file for the Agent.

    my $ini_str = $agent->to_INI;

=back

=head2 File Functions

=over 8

=item save_to_file([$INI])

Saves the INI produced by C<to_INI> to a file if provided, or to the calculated INI location if a location is not provided.

    $agent->save_to_file('/path/to/file.ini');

=back

=head1 SEE ALSO

=over 8

=item L<Implementation of Plugin on Github|https://github.com/plesk/agent360/tree/master/agent360/plugins/agent.py>

=back
