
#                                      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::Moo;

use Moo;
use v5.36;

use constant LIST_NAME => 'serializable_attributes';

# `indexed_has` works just like `has` except it will save the method name in an
# array in the package by the attribute you provide to `list`
# indexed_has $attr => ( list => "storeAttributeInPackageArray" ... )
# .. push @storeAttributeInPackageArray, $attr;
sub _indexed_has {
    my $target = caller;
    my $attr   = $_[0];
    my %args   = @_[ 1 .. $#_ ];

    if ( my $list = delete $args{list} ) {
        {
            no strict 'refs';
            no warnings 'once';
            my $sym = "$target\::$list";
            $sym->{$attr} = \%args;
        }
    }
    goto \&{"$target\::has"};
}

# `conf_has` adds the attribute to the serialize list
# also makes it such that
#   ->foo($x) sets foo
#   ->foo() returns ->foo // ->agent->settings->global_plugin->foo ->_default_foo
#   serialize calls private method ->_get_foo() which isn't subject to above
sub conf_has {
    my $target = caller;
    my $attr   = $_[0];
    my %args   = @_[ 1 .. $#_ ];

    foreach my $key (qw/is reader writer predicate list/) {
        Carp::confess "Attempted to set $key in has constructor"
          if exists $args{$key};
    }

    my $attr_reader    = "_get_$attr";
    my $attr_writer    = "_write_$attr";
    my $attr_predicate = "_has_$attr";
    my $attr_default   = "_default_$attr";

    if ( exists $args{default} ) {
        no strict 'refs';
        my $val = delete $args{default};
        if ( ref $args{default} eq 'CODE' ) {
            *{"${target}::${attr_default}"} = $val;
        }
        else {
            *{"${target}::${attr_default}"} = sub { return $val };
        }
    }

    my @__SECRET_CONSTRUCTOR_ARGS = (
        $attr,
        is               => 'rw',
        isa              => $args{isa},
        reader           => $attr_reader,
        predicate        => $attr_predicate,
        writer           => $attr_writer,
        list             => LIST_NAME(),
        coerce           => $args{coerce}           // !!1,
        required         => $args{required}         // !!0,
        lazy             => $args{lazy}             // !!0,
        clearer          => $args{clearer}          // !!0,
        always_serialize => $args{always_serialize} // !!0
    );

    my $code = <<~"INJECT";
        package $target {
            _indexed_has( \@__SECRET_CONSTRUCTOR_ARGS );
        }
        INJECT

    my $err;
    {
        local $@;
        eval $code;
        $err = $@;
    }
    die $err if $err;

    {
        no strict 'refs';
        no warnings 'redefine';
        no warnings 'once';
        my $sym = "$target\::$attr";
        use Class::Method::Modifiers ();
        Class::Method::Modifiers::install_modifier(
            $target, 'fresh',
            $attr => sub ( $self, @args ) {
                if (@args) { return $self->$attr_writer(@args) }
                else {
                    # If we're set
                    if ( $self->$attr_predicate ) {
                        return $self->$attr_reader;
                    }

                    # If we have a plugin default
                    elsif ( $self->agent->settings_global_plugin->can($attr_predicate)
                        and $self->agent->settings_global_plugin->$attr_predicate ) {
                        return $self->agent->settings_global_plugin->$attr_reader;
                    }

                    # https://webpros.slack.com/archives/C065WF6PDLL/p1718112209118159?thread_ts=1718053987.353099&cid=C065WF6PDLL
                    elsif ( $attr eq 'interval' && $self->agent->settings_execution->$attr_predicate ) {
                        return $self->agent->settings_execution->$attr_reader;
                    }

                    # If the attribute has a default
                    elsif ( $self->can("$attr_default") ) {
                        return $self->$attr_default;
                    }

                    else {
                        die "Not set, no default $attr";
                    }
                }
            }
        );
    }

}

sub import {
    my $target = caller;
    no strict 'refs';
    *{"$target\::conf_has"}     = \&conf_has;
    *{"$target\::_indexed_has"} = \&_indexed_has;
}

1;

__END__

=head1 NAME

Whostmgr::360Monitoring::Moo - An extension to Moo that provides C<conf_has>

=head1 DESCRIPTION

Ever needed a list of attributes installed with Moo? Now you've got it. This
module provides C<conf_has> which is a higher-ordered function that calls
C<Moo::has> underneath.  This is used to enable an API available in
C<Whostmgr::360Agent::Util> which the L<Agent|Whostmgr::360Monitoring::Agent>
makes use of in the C<serialize()> when it must decide what attributes to add
to the INI it generates.

The differences are as follows:

=over 12

=item The arguments used in creation of the attribute are stored in a package
variable. This is made so the
L<Serializable|Whostmgr::360Monitoring::Agent::Plugins::Roles::Serializable>
trait can read this to decide what needs to get written back to the ini file.
This is used in a blanket implementation of C<serialize()> which the
L<Agent|Whostmgr::360Monitoring::Agent> calls in the C<save_to_file>;

=item The functionality for the reader and writer change, so we can implement
proper defaulting. In our Agent.pm we have two notions of defaults: the
C<settings_global_plugin> on the agent; and, the defaults on the attributes. A
default on the attribute should be a NOOP so long as there is either an
explicit setting provided by the plugin, or an explicit global plugin setting
on the agent. If neither of these are set, the `default` on the attribute
itself should be read. The C<conf_has> implements this, albeit it was a lil'
tricky.

=back

=head1 SYNOPSIS

    package Class {
    	use Moo;
    	use Whostmgr::360Monitoring::Moo;
    	has       foo => ( ;attrs )
    	conf_has  bar => ( ;attrs )
    }

=cut
