# --
# Copyright (C) 2001-2021 OTRS AG, https://otrs.com/
# Copyright (C) 2021 Znuny GmbH, https://znuny.org/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --

package Kernel::System::CloudService::Backend::Run;

use strict;
use warnings;

use Kernel::System::VariableCheck qw(:all);

our @ObjectDependencies = (
    'Kernel::Config',
    'Kernel::System::Cache',
    'Kernel::System::Encode',
    'Kernel::System::JSON',
    'Kernel::System::Log',
    'Kernel::System::OTRSBusiness',
    'Kernel::System::SystemData',
    'Kernel::System::WebUserAgent',
);

=head1 NAME

Kernel::System::CloudService::Backend::Run - cloud service lib

=head1 DESCRIPTION

All functions for cloud service communication.

=head1 PUBLIC INTERFACE

=head2 new()

create a CloudService object. Do not use it directly, instead use:

    my $CloudServiceObject = $Kernel::OM->Get('Kernel::System::CloudService::Backend::Run');

=cut

sub new {
    my ( $Type, %Param ) = @_;

    # allocate new hash for object
    my $Self = {};
    bless( $Self, $Type );

    # set system registration data
    %{ $Self->{RegistrationData} } =
        $Kernel::OM->Get('Kernel::System::SystemData')->SystemDataGroupGet(
        Group  => 'Registration',
        UserID => 1,
        );

    # set new cloud service url
    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
    my $Schema       = $ConfigObject->Get('PackageRepositoryURLSchema') // 'https';
    $Self->{CloudServiceURL} = $Schema . '://' . $ConfigObject->Get('CloudServiceProxyURL');

    return $Self;
}

=head2 Request()

perform a cloud service communication and return result data

    my $RequestResult = $CloudServiceObject->Request(
        OTRSIDAuth => { #  will be send encoded as JSON
            OTRSID => '',
            Password => '',
        },
        UniqueIDAuth => { #  will send encoded as JSON
            UniqueID => '',
            APIKey => '',
        },
        RequestData => { # this complex structure will be send encoded as JSON
            CloudServiceTest => [
                {
                    InstanceName = 'AnyName', # optional
                    Operation    => "ConfigurationSet",
                    Data         => {
                        # ... request operation data ...
                    },
                },
                {
                    Operation    => "SomeOperation",
                    Data         => {
                        # ... request operation data ...
                    },
                },
                # ... other entries may follow ...
            ],
            FeatureAddonManagement => [
                {
                    Operation    => "FAOListAssigned",
                    Data         => {
                        # ... request operation data ...
                    },
                },
                {
                    InstanceName = 'InstanceNameOne', # optional
                    Operation    => "FAOGet",
                    Data         => {
                        # ... request operation data ...
                    },
                },
                {
                    InstanceName = 'InstanceNameTwo', # optional
                    Operation    => "FAOGet",
                    Data         => {
                        # ... request operation data ...
                    },
                },
                # ... other entries may follow ...
            ],
            # ... other entries may follow ...
        },
        Timeout => 15,                  # optional, timeout
        Proxy   => 'proxy.example.com', # optional, proxy
    );


Returns:
    $RequestResult {
        Success      => 1,
        ErrorMessage => '...', # optional
        Results      => {
            CloudServiceTest => [
                {
                    Success      => 1, # 1 or 0
                    ErrorMessage => '...', # optional
                    InstanceName = 'AnyName', # optional
                    Operation    => "ConfigurationSet",
                    Data         => {
                        # ... response operation data ..
                    },
                },
                {
                    Success      => 0, # 1 or 0
                    ErrorMessage => '...', # optional
                    Operation    => "SomeOperation",
                    Data         => {
                        # ... response operation data ...
                    },
                },
            ],

            FeatureAddonManagement => [
                {
                    Success      => 1, # 1 or 0
                    ErrorMessage => '...', # optional
                    Operation    => "FAOListAssigned",
                    Data         => {
                        # ... response operation data ..
                    },
                },
                {
                    Success      => 1, # 1 or 0
                    ErrorMessage => '...', # optional
                    InstanceName = 'InstanceNameOne', # optional
                    Operation    => "FaoGet",
                    Data         => {
                        # ... response operation data ...
                    },
                },
                {
                    Success      => 0, # 1 or 0
                    ErrorMessage => '...', # optional
                    InstanceName = 'InstanceNameTwo', # optional
                    Operation    => "FaoGet",
                    Data         => {
                        # ... response operation data ...
                    },
                },
            ],
        },
    };

=cut

sub Request {
    my ( $Self, %Param ) = @_;

    # create config object
    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');

    # If OTRSSTORM package is installed, system is able to do a Cloud request even if CloudService is disabled.
    if ( !$Kernel::OM->Get('Kernel::System::OTRSBusiness')->OTRSSTORMIsInstalled() ) {

        # check if cloud services are disabled
        my $CloudServicesDisabled = $ConfigObject->Get('CloudServices::Disabled');

        return if $CloudServicesDisabled;
    }

    # check needed stuff
    if ( !defined $Param{RequestData} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "Need RequestData!"
        );
        return;
    }

    # RequestData might be a hash
    if ( !IsHashRefWithData( $Param{RequestData} ) ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "Wrong structure for RequestData, it might be an hash with data!",
        );
        return;
    }

    # check in detail each request data
    for my $CloudService ( sort keys %{ $Param{RequestData} } ) {

        # check if CloudService is defined and not empty
        if ( !$CloudService ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "A CloudService name is needed!",
            );
            return;
        }

        # check if CloudService is defined and not empty
        if ( !IsArrayRefWithData( $Param{RequestData}->{$CloudService} ) ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "CloudService::$CloudService data structure is not valid!",
            );
            return;
        }

        for my $Instance ( @{ $Param{RequestData}->{$CloudService} } ) {

            # check if Operation is present
            if ( !$Instance->{Operation} ) {
                $Kernel::OM->Get('Kernel::System::Log')->Log(
                    Priority => 'error',
                    Message  => "Each operation request needs a Operation name!",
                );
                return;
            }

            # if Data is present it might be a hash
            if ( $Instance->{Data} && ref $Instance->{Data} ne 'HASH' ) {
                $Kernel::OM->Get('Kernel::System::Log')->Log(
                    Priority => 'error',
                    Message  => "Request Data needs to be a hash structure!",
                );
                return;
            }
        }
    }

    # create json object
    my $JSONObject = $Kernel::OM->Get('Kernel::System::JSON');

    # get UniqueIDAut structure if needed
    my $UniqueIDAuth;
    if (
        defined $Self->{RegistrationData}->{State}
        && $Self->{RegistrationData}->{State} eq 'registered'
        )
    {

        # create cache object
        my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');

        # check cache
        my $CacheKey = "APIKey::" . ( $Self->{RegistrationData}->{APIKey} || '' )
            . "::UniqueID::" . ( $Self->{RegistrationData}->{UniqueID} || '' );
        my $CacheContent = $CacheObject->Get(
            Type => 'RequestUniqueIDAuth',
            Key  => $CacheKey,
        );

        if ( defined $CacheContent ) {
            $UniqueIDAuth = $CacheContent;
        }
        else {
            # create JSON string
            $UniqueIDAuth = $JSONObject->Encode(
                Data => {
                    APIKey   => $Self->{RegistrationData}->{APIKey}   || '',
                    UniqueID => $Self->{RegistrationData}->{UniqueID} || '',
                },
            );
            $CacheObject->Set(
                Type  => 'RequestUniqueIDAuth',
                Key   => $CacheKey,
                Value => $UniqueIDAuth || '',
                TTL   => 3 * 24 * 60 * 60,
            );
        }
    }

    # get OTRSIDAuth structure if needed
    my $OTRSIDAuth = '';
    if ( $Param{OTRSID} && $Param{Password} ) {

        $OTRSIDAuth = $JSONObject->Encode(
            Data => {
                OTRSID   => $Param{OTRSID},
                Password => $Param{Password},
            },
        );
    }

    my $RequestTimeout = $Param{Timeout} || $ConfigObject->Get('WebUserAgent::Timeout') || 15;
    my $RequestProxy   = $Param{Proxy}   || $ConfigObject->Get('WebUserAgent::Proxy')   || '';

    # set timeout setting for packages
    if ( grep {/^Package/i} sort keys %{ $Param{RequestData} } ) {
        $RequestTimeout = $ConfigObject->Get('Package::Timeout') || 120;
    }

    # create JSON string
    my $RequestData = $JSONObject->Encode(
        Data => $Param{RequestData},
    );

    # add params to webuseragent object
    $Kernel::OM->ObjectParamAdd(
        'Kernel::System::WebUserAgent' => {
            Timeout => $RequestTimeout,
            Proxy   => $RequestProxy,
        },
    );

    # Perform web service request.
    my %Response;
    TRY:
    for my $Try ( 1 .. 3 ) {

        %Response = $Kernel::OM->Get('Kernel::System::WebUserAgent')->Request(
            Type => 'POST',
            URL  => $Self->{CloudServiceURL},
            Data => {
                Action       => 'PublicCloudService',
                RequestData  => $RequestData,
                UniqueIDAuth => $UniqueIDAuth,
                OTRSIDAuth   => $OTRSIDAuth,
            },
        );

        last TRY if %Response
            && $Response{Status} eq '200 OK'
            && $Response{Content}
            && ref $Response{Content} eq 'SCALAR';
    }

    # test if the web response was successful
    if ( !%Response || $Response{Status} ne '200 OK' ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'notice',
            Message  => "PublicCloudService - Can't connect to server - $Response{Status}",
        );
        return;
    }

    # check if we have content as a scalar ref
    if ( !$Response{Content} || ref $Response{Content} ne 'SCALAR' ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'notice',
            Message  =>
                "PublicCloudService - No content received from public cloud service. Please try again later.'",
        );
        return;
    }

    # convert internal used charset
    $Kernel::OM->Get('Kernel::System::Encode')->EncodeInput(
        $Response{Content},
    );

    # decode JSON data
    my $ResponseData = $JSONObject->Decode(
        Data => ${ $Response{Content} },
    );

    # check response structure
    if ( !IsHashRefWithData($ResponseData) ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "Can't decode JSON data: 'PublicCloudService'!",
        );
        return;
    }

    # check if we have an error
    if ( !$ResponseData->{Success} ) {
        my $ResponseErrorMessage = $ResponseData->{ErrorMessage} || 'No error message available.';
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'notice',
            Message  => "PublicCloudService - Error from server: $ResponseErrorMessage !",
        );
        return;
    }

    # return data from server if defined
    return $ResponseData->{Results} if defined $ResponseData->{Results};
    return;
}

=head2 OperationResultGet()

    my $OperationResult = $CloudServiceObject->OperationResultGet(
        CloudService => 'Test',
        Operation    => 'test',
        InstanceName => 'AnyName',      # optional
        RequestResult =>  {
            Success      => 1,
            Results      => {
                Test => [
                    {
                        Success      => 1,
                        InstanceName => 'AnyName',
                        Operation    => 'Test',
                        Data         => {
                            # ... response operation data ..
                        },
                    },
                    {
                        Success      => 0,
                        ErrorMessage => 'some message',
                        Operation    => 'SomeOperation',
                        Data         => {
                            # ... response operation data ...
                        },
                    },
                ],
            },
        };
    );

Returns:

    $OperationResult {
        Success      => 1,
        ErrorMessage => 'a message',        # optional
        InstanceName => 'AnyName',
        Operation    => "Test",
        Data         => {
            # ... response operation data ..
        },
    },

=cut

sub OperationResultGet {
    my ( $Self, %Param ) = @_;

    # check needed
    for my $Needed (qw(RequestResult CloudService Operation)) {

        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!",
            );

            return {
                Success => 0,
            };
        }
    }

    # check RequestResult internals
    if ( !IsHashRefWithData( $Param{RequestResult} ) ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "The format of the request result is invalid!",
        );

        return {
            Success => 0,
        };
    }

    if ( !$Param{RequestResult}->{ $Param{CloudService} } ) {
        my $Message = "No CloudService:'$Param{CloudService}' found in the request result!";
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "$Message",
        );

        return {
            Success      => 0,
            ErrorMessage => $Message,
        };
    }

    if ( !IsArrayRefWithData( $Param{RequestResult}->{ $Param{CloudService} } ) ) {
        my $Message = "Results from CloudService:'$Param{CloudService}' are invalid!";
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "$Message",
        );

        return {
            Success      => 0,
            ErrorMessage => $Message,
        };
    }

    # create a shortcut for actual cloud-service results
    my @CloudServiceResults = @{ $Param{RequestResult}->{ $Param{CloudService} } };

    RESULT:
    for my $OperationResult (@CloudServiceResults) {

        next RESULT if $OperationResult->{Operation} ne $Param{Operation};

        if ( !$Param{InstanceName} ) {

            next RESULT if defined $OperationResult->{InstanceName};
        }
        else {

            if (
                !defined $OperationResult->{InstanceName}
                || $OperationResult->{InstanceName} ne $Param{InstanceName}
                )
            {
                next RESULT;
            }
        }

        return $OperationResult;
    }

    # if not found return an error
    my $Message = "No Results from CloudService:'$Param{CloudService}' Operation:'$Param{Operation}'";
    if ( $Param{InstanceName} ) {
        $Message .= " InstanceName:'$Param{InstanceName}'!";
    }
    else {
        $Message .= " Without InstanceName!";
    }

    $Kernel::OM->Get('Kernel::System::Log')->Log(
        Priority => 'error',
        Message  => $Message,
    );

    return {
        Success => 0,
        Message => "No results found!",
    };
}

1;

=head1 TERMS AND CONDITIONS

This software is part of the OTRS project (L<https://otrs.org/>).

This software comes with ABSOLUTELY NO WARRANTY. For details, see
the enclosed file COPYING for license information (GPL). If you
did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.

=cut
