package IO::Iron::Common;

## no critic (Documentation::RequirePodAtEnd)
## no critic (Documentation::RequirePodSections)
## no critic (Subroutines::RequireArgUnpacking)

use 5.010_000;
use strict;
use warnings;

# Global creator
BEGIN {
    # No exports
}

# Global destructor
END {
}

# ABSTRACT: Common routines for Client Libraries to Iron services IronCache, IronMQ and IronWorker.

our $VERSION = '0.14'; # VERSION: generated by DZP::OurPkgVersion

use Path::Tiny qw{path};
use Try::Tiny;
use Log::Any qw{$log};
require JSON::MaybeXS;
use File::Spec      ();
use File::HomeDir   ();
use Hash::Util 0.06 qw{lock_keys unlock_keys};
use Carp::Assert::More;
use English '-no_match_vars';
use Params::Validate qw(:all);

sub IRON_CONFIG_KEYS {
    return (
        # Iron.io standard:
        'project_id',    # The ID of the project to use for requests.
        'token',         # The OAuth token that should be used to authenticate requests. Can be found in the HUD.
        'host',  # The domain name the API can be located at. Defaults to a product-specific value, but always using Amazon's cloud.
        'protocol'
        , # The protocol that will be used to communicate with the API. Defaults to "https", which should be sufficient for 99% of users.
        'port',    # The port to connect to the API through. Defaults to 443, which should be sufficient for 99% of users.
        'api_version'
        , # The version of the API to connect through. Defaults to the version supported by the client. End-users should probably never change this. Except: IronMQ service upgraded from v2 to v3 in 2015!
          # IO::Iron additions:
        'timeout'
        , # REST client timeout (for REST calls accessing IronMQ). N.B. This is not a IronMQ config option! It only configures client this client.
        'policies',    # Filename of JSON file containing policies.
    );
}

sub IRON_CLIENT_PARAMETERS {
    return (
        IRON_CONFIG_KEYS(),
        'config',       # The config file name.
        'connector',    # Reference to a preinitiated connector object.

        #			'policy',            # Reference to a preinitiated policy hash.
    );
}

sub get_config {    ## no critic (Subroutines::RequireArgUnpacking)
    my %params = validate(
        @_,
        {
            map { $_ => { type => SCALAR, optional => 1 }, } IRON_CONFIG_KEYS(),    ## no critic (ValuesAndExpressions::ProhibitCommaSeparatedStatements)
            'config' => { type => SCALAR, optional => 1, },
        }
    );
    $log->tracef( 'Entering get_config(%s)', \%params );
    my %config = ( map { $_ => undef } IRON_CONFIG_KEYS() );                                            ## preset config keys.
    lock_keys( %config, IRON_CONFIG_KEYS() );
    _read_iron_config_file( \%config, File::Spec->catfile( File::HomeDir->my_home, '.iron.json' ) );    # Homedir
    _read_iron_config_env_vars( \%config );                                                             # Global envs
    _read_iron_config_file( \%config, File::Spec->catfile( File::Spec->curdir(), 'iron.json' ) );       # current dir
    if ( defined $params{'config'} ) {    # config file specified when creating the class, if given.
        _read_iron_config_file( \%config,
            File::Spec->file_name_is_absolute( $params{'config'} )
            ? $params{'config'}
            : File::Spec->catfile( File::Spec->curdir(), $params{'config'} ) );
    }

    # The parameters given when the object was created, except 'config'
    my @copy_param_keys = grep { !/^config$/msx } keys %params;
    @config{@copy_param_keys} = @params{@copy_param_keys};

    $log->tracef( 'Exiting get_config: %s', \%config );
    return \%config;
}

# Replace the existing values in $config if new environment variables found.
#	Vars:
#	$config->{'project_id'}  = $ENV{'IRON_PROJECT_ID'}
#	$config->{'token'}       = $ENV{'IRON_TOKEN'}
#	$config->{'host'}        = $ENV{'IRON_HOST'}
#	$config->{'protocol'}    = $ENV{'IRON_PROTOCOL'}
#	$config->{'port'}        = $ENV{'IRON_PORT'}
#	$config->{'api_version'} = $ENV{'IRON_API_VERSION'}
#	$config->{'timeout'}     = $ENV{'IRON_TIMEOUT'}
sub _read_iron_config_env_vars {
    my ($config) = @_;
    $log->tracef( 'Entering _read_iron_config_env_vars(%s)', $config );
    foreach my $config_key ( keys %{$config} ) {
        if ( defined $ENV{ 'IRON_' . uc $config_key } ) {
            $config->{$config_key} = $ENV{ 'IRON_' . uc $config_key };
        }
    }
    $log->tracef( 'Exiting _read_iron_config_env_vars: %s', $config );
    return $config;
}

# Try to read the file given as second parameter. (if undef, fail).
# If fails, gracefully return 0; if succeed, change configuration (first parameter) and return 1.
sub _read_iron_config_file {
    my ( $config, $full_path_name ) = @_;
    $log->tracef( 'Entering _read_iron_config_file(%s, %s)', $full_path_name, $config );

    assert_nonblank( $full_path_name, 'full_path_name is not defined or is blank.' );

    my $read_config;
    my $rval;
    my $file = path($full_path_name);
    if ( $file->is_file ) {
        $log->tracef( 'File %s exists', $full_path_name );
        my $file_contents;
        try { $file_contents = $file->slurp_utf8 };
        if ($file_contents) {
            $log->tracef( 'Slurped file %s', $full_path_name );
            my $json = JSON::MaybeXS->new( utf8 => 1, pretty => 1 );
            $read_config = $json->decode($file_contents);
            foreach my $config_key ( keys %{$config} ) {
                if ( defined $read_config->{$config_key} ) {
                    $config->{$config_key} = $read_config->{$config_key};
                }
            }
            $rval = 1;
        }
        else {
            $log->debugf( 'Could not read file %s', $full_path_name );
            $rval = 0;
        }
    }
    else {
        $log->tracef( 'File %s does not exist', $full_path_name );
        $rval = 0;
    }
    $log->tracef( 'Exiting _read_iron_config_file: %s', $config );
    return $rval;
}

#my $GEN_DELIMS = q{!} . q{$} . q{&} . q{'} . q{(} . q{)}
#                  . q{*} . q{+} . q{,} . q{;} . q{=};
#my $SUB_DELIMS = q{:} . q{/} . q{?} . q{#} . q{[} . q{]} . q{@};
#my $RESERVED_CHARACTERS = $GEN_DELIMS . $SUB_DELIMS;
#my $RFC_3986_RESERVED_CHARACTERS =~ s/(.{1})/\\$1/sg; # Escape every character.
sub contains_rfc_3986_res_chars {
    my @params = validate_pos( @_, { type => SCALAR } );
    ## no critic (ValuesAndExpressions::RequireInterpolationOfMetachars)
    my $rfc_3986_reserved_characters = q{\!\$\&\'\(\)\*\+\,\;\=\:\/\?\#\[\]\@};
    ## critic (ValuesAndExpressions::RequireInterpolationOfMetachars)
    return ( $params[0] =~ m/[$rfc_3986_reserved_characters]{1,}/msx ) ? 1 : 0;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

IO::Iron::Common - Common routines for Client Libraries to Iron services IronCache, IronMQ and IronWorker.

=head1 VERSION

version 0.14

=for stopwords IronCache IronMQ IronWorker config json Mikko Koivunalho

=head1 REQUIREMENTS

=head1 FUNCTIONS

Internal functions for use in the Client objects.

=head2 IRON_CONFIG_KEYS

=head2 IRON_CLIENT_PARAMETERS

=head2 get_config

Get the config from file or from system environmental variables.
Follows the global configuration scheme as explained in http://dev.iron.io/mq/reference/configuration/.

The configuration is constructed as follows:

=over 8

=item 1. The global configuration file sets the defaults according to the file hierarchy. (F<.iron.json> in home folder)

=item 2. The global environment variables overwrite the global configuration file's values.

=item 3. The product-specific environment variables overwrite everything before them.

=item 4. The local configuration file overwrites everything before it according to the file hierarchy. (F<iron.json> in the same directory as the script being run)

=item 5. The configuration file specified when instantiating the client library overwrites everything before it according to the file hierarchy.

=item 6. The arguments passed when instantiating the client library overwrite everything before them.

=back

Return: ref to %config.

=head2 contains_rfc_3986_res_chars

Check that the string does not contain any RFC 3986 Reserved Characters:

!$&'()*+,;=:/?#[]@

Return True (1) if contains. Otherwise False (0).

=head1 AUTHOR

Mikko Koivunalho <mikko.koivunalho@iki.fi>

=head1 BUGS

Please report any bugs or feature requests to bug-io-iron@rt.cpan.org or through the web interface at:
 http://rt.cpan.org/Public/Dist/Display.html?Name=IO-Iron

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2023 by Mikko Koivunalho.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

The full text of the license can be found in the
F<LICENSE> file included with this distribution.

=cut
