# --
# 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::GenericInterface::Event::Handler;

use strict;
use warnings;

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

use Storable;

our @ObjectDependencies = (
    'Kernel::GenericInterface::Requester',
    'Kernel::System::Scheduler',
    'Kernel::System::GenericInterface::Webservice',
    'Kernel::System::Log',
    'Kernel::System::Event',
    'Kernel::System::Main',
    'Kernel::Config',
    'Kernel::System::DateTime',
);

=head1 NAME

Kernel::GenericInterface::Event::Handler - GenericInterface event handler

=head1 DESCRIPTION

This event handler intercepts all system events and fires connected GenericInterface
invokers.

=cut

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

    my $Self = {};
    bless( $Self, $Type );

    $Self->{JqIsAvailable} = $Self->_IsJqAvailable();

    return $Self;
}

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

    my $LogObject = $Kernel::OM->Get('Kernel::System::Log');
    for my $Needed (qw(Data Event Config)) {
        if ( !$Param{$Needed} ) {
            $LogObject->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    my $WebserviceObject = $Kernel::OM->Get('Kernel::System::GenericInterface::Webservice');
    my $SchedulerObject  = $Kernel::OM->Get('Kernel::System::Scheduler');
    my $MainObject       = $Kernel::OM->Get('Kernel::System::Main');
    my $RequesterObject  = $Kernel::OM->Get('Kernel::GenericInterface::Requester');
    my $ConfigObject     = $Kernel::OM->Get('Kernel::Config');

    my %WebserviceList   = %{ $WebserviceObject->WebserviceList( Valid => 1 ) };
    my %RegisteredEvents = $Kernel::OM->Get('Kernel::System::Event')->EventList();

    # Loop over web services.
    WEBSERVICEID:
    for my $WebserviceID ( sort keys %WebserviceList ) {

        my $WebserviceData = $WebserviceObject->WebserviceGet(
            ID => $WebserviceID,
        );

        next WEBSERVICEID if !IsHashRefWithData( $WebserviceData->{Config} );
        next WEBSERVICEID if !IsHashRefWithData( $WebserviceData->{Config}->{Requester} );
        next WEBSERVICEID if !IsHashRefWithData( $WebserviceData->{Config}->{Requester}->{Invoker} );

        # Check invokers of the web service, to see if some might be connected to this event.
        INVOKER:
        for my $Invoker ( sort keys %{ $WebserviceData->{Config}->{Requester}->{Invoker} } ) {

            my $InvokerConfig = $WebserviceData->{Config}->{Requester}->{Invoker}->{$Invoker};

            next INVOKER if ref $InvokerConfig->{Events} ne 'ARRAY';

            INVOKEREVENT:
            for my $InvokerEvent ( @{ $InvokerConfig->{Events} } ) {

                # Check if the invoker is connected to this event.
                next INVOKEREVENT if !IsHashRefWithData($InvokerEvent);
                next INVOKEREVENT if !IsStringWithData( $InvokerEvent->{Event} );
                next INVOKEREVENT if $InvokerEvent->{Event} ne $Param{Event};

                # Prepare event type.
                my $EventType;

                # Set the event type (event object like Article or Ticket) and event condition
                EVENTTYPE:
                for my $Type ( sort keys %RegisteredEvents ) {
                    my $EventFound = grep { $_ eq $InvokerEvent->{Event} } @{ $RegisteredEvents{$Type} || [] };

                    next EVENTTYPE if !$EventFound;

                    $EventType = $Type;
                    last EVENTTYPE;
                }

                if (
                    $EventType
                    && IsHashRefWithData( $InvokerEvent->{Condition} )
                    && IsHashRefWithData( $InvokerEvent->{Condition}->{Condition} )
                    )
                {

                    my $BackendObject = $Self->{EventTypeBackendObject}->{$EventType};

                    if ( !$BackendObject ) {

                        my $ObjectClass = "Kernel::GenericInterface::Event::ObjectType::$EventType";

                        my $Loaded = $MainObject->Require(
                            $ObjectClass,
                        );

                        if ( !$Loaded ) {
                            $LogObject->Log(
                                Priority => 'error',
                                Message =>
                                    "Could not load $ObjectClass, skipping condition checks for event $InvokerEvent->{Event}!",
                            );
                            next INVOKEREVENT;
                        }

                        $BackendObject = $Kernel::OM->Get($ObjectClass);

                        $Self->{EventTypeBackendObject}->{$EventType} = $BackendObject;
                    }

                    # Get object data
                    my %EventData = $BackendObject->DataGet(
                        Data        => $Param{Data},
                        Invoker     => $Invoker,
                        InvokerType => $InvokerConfig->{Type},
                    );

                    if ( IsHashRefWithData( \%EventData ) ) {
                        my %ObjectData;

                        # do not serialize config in case of available jq
                        if (
                            $Self->{JqIsAvailable}
                            && $InvokerConfig->{Type} eq 'Ticket::Generic'
                            )
                        {
                            %ObjectData = %EventData;
                        }
                        else {
                            $Self->_SerializeConfig(
                                Data  => \%EventData,
                                SHash => \%ObjectData,
                            );
                        }

                        # Check if the event condition matches.
                        my $ConditionCheckResult = $Self->_ConditionCheck(
                            %{ $InvokerEvent->{Condition} },
                            Data => \%ObjectData,
                        );

                        next INVOKEREVENT if !$ConditionCheckResult;
                    }
                }

                # create scheduler task for asynchronous tasks
                if ( $InvokerEvent->{Asynchronous} ) {

                    my $Success = $SchedulerObject->TaskAdd(
                        Type     => 'GenericInterface',
                        Name     => 'Invoker-' . $Invoker,
                        Attempts => 10,
                        Data     => {
                            WebserviceID => $WebserviceID,
                            Invoker      => $Invoker,
                            Data         => {
                                %{ $Param{Data} // {} },
                                Event => $Param{Event},
                            },
                        },
                    );
                    if ( !$Success ) {
                        $LogObject->Log(
                            Priority => 'error',
                            Message  => 'Could not schedule task for Invoker-' . $Invoker,
                        );
                    }

                    next INVOKEREVENT;

                }

                # execute synchronous tasks directly
                my $Result = $RequesterObject->Run(
                    WebserviceID => $WebserviceID,
                    Invoker      => $Invoker,
                    Data         => {
                        %{ Storable::dclone( $Param{Data} ) // {} },
                        Event => $Param{Event},
                    },
                );
                next INVOKEREVENT if $Result->{Success};

                # check if rescheduling is requested on errors
                next INVOKEREVENT if !IsHashRefWithData( $Result->{Data} );
                next INVOKEREVENT if !$Result->{Data}->{ReSchedule};

                # Use the execution time from the return data
                my $ExecutionTime = $Result->{Data}->{ExecutionTime};
                my $ExecutionDateTime;

                # Check if execution time is valid.
                if ( IsStringWithData($ExecutionTime) ) {

                    $ExecutionDateTime = $Kernel::OM->Create(
                        'Kernel::System::DateTime',
                        ObjectParams => {
                            String => $ExecutionTime,
                        },
                    );
                    if ( !$ExecutionDateTime ) {
                        my $WebServiceName = $WebserviceData->{Name} // 'N/A';
                        $LogObject->Log(
                            Priority => 'error',
                            Message =>
                                "WebService $WebServiceName, Invoker $Invoker returned invalid execution time $ExecutionTime. Falling back to default!",
                        );
                    }
                }

                # Set default execution time.
                if ( !$ExecutionTime || !$ExecutionDateTime ) {

                    # Get default time difference from config.
                    my $FutureTaskTimeDiff
                        = int( $ConfigObject->Get('Daemon::SchedulerGenericInterfaceTaskManager::FutureTaskTimeDiff') )
                        || 300;

                    $ExecutionDateTime = $Kernel::OM->Create('Kernel::System::DateTime');
                    $ExecutionDateTime->Add( Seconds => $FutureTaskTimeDiff );
                }

                # Create a new task that will be executed in the future.
                my $Success = $SchedulerObject->TaskAdd(
                    ExecutionTime => $ExecutionDateTime->ToString(),
                    Type          => 'GenericInterface',
                    Name          => 'Invoker-' . $Invoker,
                    Attempts      => 10,
                    Data          => {
                        Data => {
                            %{ $Param{Data} // {} },
                            Event => $Param{Event},
                        },
                        PastExecutionData => $Result->{Data}->{PastExecutionData},
                        WebserviceID      => $WebserviceID,
                        Invoker           => $Invoker,
                    },
                );
                if ( !$Success ) {
                    $LogObject->Log(
                        Priority => 'error',
                        Message  => 'Could not re-schedule a task in future for Invoker ' . $Invoker,
                    );
                }
            }
        }
    }

    return 1;
}

=head2 _SerializeConfig()

    returns a serialized hash/array of a given hash/array

    my $ConditionCheck = $Self->_SerializeConfig(
        Data => \%OldHash,
        SHash => \%NewHash,
    );

    Modifies NewHash (SHash):

    my %OldHash = (
        Config => {
            A => 1,
            B => 2,
            C => 3,
        },
        Config2 => 1
    );

    my %NewHash = (
        Config_A => 1,
        Config_B => 1,
        Config_C => 1,
        Config2  => 1,
    );

=cut

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

    for my $Needed (qw(Data SHash)) {
        if ( !$Param{$Needed} ) {
            print "Got no $Needed!\n";
            return;
        }
    }

    my @ConfigContainer;
    my $DataType = 'Hash';

    if ( IsHashRefWithData( $Param{Data} ) ) {
        @ConfigContainer = sort keys %{ $Param{Data} };
    }
    else {
        @ConfigContainer = @{ $Param{Data} };
        $DataType        = 'Array';
    }

    # Prepare prefix.
    my $Prefix = $Param{Prefix} || '';

    my $ArrayCount = 0;

    CONFIGITEM:
    for my $ConfigItem (@ConfigContainer) {

        next CONFIGITEM if !$ConfigItem;

        # Check if param data is a hash or an array ref.
        if ( $DataType eq 'Hash' ) {

            # We got a hash ref.
            if (
                IsHashRefWithData( $Param{Data}->{$ConfigItem} )
                || IsArrayRefWithData( $Param{Data}->{$ConfigItem} )
                )
            {
                $Self->_SerializeConfig(
                    Data   => $Param{Data}->{$ConfigItem},
                    SHash  => $Param{SHash},
                    Prefix => $Prefix . $ConfigItem . '_',
                );
            }
            else {

                $Prefix                  = $Prefix . $ConfigItem;
                $Param{SHash}->{$Prefix} = $Param{Data}->{$ConfigItem};
                $Prefix                  = $Param{Prefix} || '';
            }
        }

        # We got an array ref
        else {

            if ( IsHashRefWithData($ConfigItem) || IsArrayRefWithData($ConfigItem) ) {

                $Self->_SerializeConfig(
                    Data   => $ConfigItem,
                    SHash  => $Param{SHash},
                    Prefix => $Prefix . $ConfigItem . '_',
                );
            }
            else {

                $Prefix                  = $Prefix . $ArrayCount;
                $Param{SHash}->{$Prefix} = $ConfigItem;
                $Prefix                  = $Param{Prefix} || '';
            }

            $ArrayCount++;
        }
    }

    return 1;
}

=head2 _ConditionCheck()

    Checks if one or more conditions are true

    my $ConditionCheck = $Self->_ConditionCheck(
        ConditionLinking => 'and',
        Condition => {
            1 => {
                Type   => 'and',
                Fields => {
                    DynamicField_Make    => [ '2' ],
                    DynamicField_VWModel => {
                        Type  => 'String',
                        Match => 'Golf',
                    },
                    DynamicField_A => {
                        Type  => 'Hash',
                        Match => {
                            Value  => 1,
                        },
                    },
                    DynamicField_B => {
                        Type  => 'Regexp',
                        Match => qr{ [\n\r\f] }xms
                    },
                    DynamicField_C => {
                        Type  => 'Module',
                        Match =>
                            'Kernel::GenericInterface::Event::Validation::MyModule',
                    },
                    Queue =>  {
                        Type  => 'Array',
                        Match => [ 'Raw' ],
                    },
                    # ...
                },
            },
            # ...
        },
        Data => {
            Queue         => 'Raw',
            DynamicField1 => 'Value',
            Subject       => 'Testsubject',
            # ...
        },
    );

    Returns:

    $CheckResult = 1;   # 1 = process with Scheduler or Requester
                        # 0 = stop processing

=cut

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

    my $LogObject = $Kernel::OM->Get('Kernel::System::Log');
    for my $Needed (qw(Condition Data)) {
        if ( !defined $Param{$Needed} ) {
            $LogObject->Log(
                Priority => 'error',
                Message  => "Need $Needed!",
            );

            return;
        }
    }

    # Check if we have Data to check against Condition.
    if ( !IsHashRefWithData( $Param{Data} ) ) {
        $LogObject->Log(
            Priority => 'error',
            Message  => "Data has no values!",
        );

        return;
    }

    # Check if we have Condition to check against Data.
    if ( !IsHashRefWithData( $Param{Condition} ) ) {
        $LogObject->Log(
            Priority => 'error',
            Message  => "Condition has no values!",
        );

        return;
    }

    my $ConditionLinking = $Param{ConditionLinking} || 'and';

    # If there is something else than 'and', 'or', 'xor' log defect condition configuration
    if (
        $ConditionLinking ne 'and'
        && $ConditionLinking ne 'or'
        && $ConditionLinking ne 'xor'
        )
    {
        $LogObject->Log(
            Priority => 'error',
            Message  => "Invalid ConditionLinking!",
        );
        return;
    }
    my ( $ConditionSuccess, $ConditionFail ) = ( 0, 0 );

    # Loop through all submitted conditions
    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');

    CONDITIONNAME:
    for my $ConditionName ( sort { $a cmp $b } keys %{ $Param{Condition} } ) {

        next CONDITIONNAME if $ConditionName eq 'ConditionLinking';

        # Get the condition data.
        my $ActualCondition = $Param{Condition}->{$ConditionName};

        # Check if we have Fields in our Condition
        if ( !IsHashRefWithData( $ActualCondition->{Fields} ) )
        {
            $LogObject->Log(
                Priority => 'error',
                Message  => "No Fields in Condition->$ConditionName found!",
            );
            return;
        }

        # If we don't have a Condition->$Condition->Type, set it to 'and' by default
        my $CondType = $ActualCondition->{Type} || 'and';

        # If there is something else than 'and', 'or', 'xor' log defect condition configuration
        if ( $CondType ne 'and' && $CondType ne 'or' && $CondType ne 'xor' ) {
            $LogObject->Log(
                Priority => 'error',
                Message  => "Invalid Condition->$ConditionName->Type!",
            );
            return;
        }

        my ( $FieldSuccess, $FieldFail ) = ( 0, 0 );

        FIELDLNAME:
        for my $FieldName ( sort keys %{ $ActualCondition->{Fields} } ) {

            # If we have just a String transform it into string check condition.
            if ( ref $ActualCondition->{Fields}->{$FieldName} eq '' ) {
                $ActualCondition->{Fields}->{$FieldName} = {
                    Type  => 'String',
                    Match => $ActualCondition->{Fields}->{$FieldName},
                };
            }

            # If we have an Array ref in "Fields" we deal with just values
            #   -> transform it into a { Type => 'Array', Match => [1,2,3,4] } structure
            #   to unify testing later on.
            if ( ref $ActualCondition->{Fields}->{$FieldName} eq 'ARRAY' ) {
                $ActualCondition->{Fields}->{$FieldName} = {
                    Type  => 'Array',
                    Match => $ActualCondition->{Fields}->{$FieldName},
                };
            }

            # If we don't have a Condition->$ConditionName->Fields->Field->Type
            #   set it to 'String' by default.
            my $FieldType = $ActualCondition->{Fields}->{$FieldName}->{Type} || 'String';

            # If there is something else than 'String', 'Regexp', 'Hash', 'Array', 'Module' log
            #   defect config.
            if (
                $FieldType ne 'String'
                && $FieldType ne 'Hash'
                && $FieldType ne 'Array'
                && $FieldType ne 'Regexp'
                && $FieldType ne 'Module'
                )
            {
                $LogObject->Log(
                    Priority => 'error',
                    Message  => "Invalid Condition->Type!",
                );
                return;
            }

            if ( $ActualCondition->{Fields}->{$FieldName}->{Type} eq 'String' ) {

                # If our Check contains anything else than a string we can't check,
                #   Special Condition: if Match contains '0' we can check
                if (
                    (
                        !$ActualCondition->{Fields}->{$FieldName}->{Match}
                        && $ActualCondition->{Fields}->{$FieldName}->{Match} ne '0'
                    )
                    || ref $ActualCondition->{Fields}->{$FieldName}->{Match}
                    )
                {
                    $LogObject->Log(
                        Priority => 'error',
                        Message =>
                            "Condition->$ConditionName->Fields->$FieldName Match must"
                            . " be a String if Type is set to String!",
                    );
                    return;
                }

                # Make sure the data string is here and it isn't a ref (array or whatsoever)
                #   then compare it to our Condition configuration.
                if (
                    $FieldName =~ m{\Ajq\#(.+)}msi
                    || (
                        defined $Param{Data}->{$FieldName}
                        && defined $ActualCondition->{Fields}->{$FieldName}->{Match}
                        && ( $Param{Data}->{$FieldName} || $Param{Data}->{$FieldName} eq '0' )
                    )
                    )
                {

                    my $Match;

                    # Check if field data is a string and compare directly.
                    if (
                        defined $Param{Data}->{$FieldName}
                        && ref $Param{Data}->{$FieldName} eq ''
                        && $ActualCondition->{Fields}->{$FieldName}->{Match} eq $Param{Data}->{$FieldName}
                        )
                    {
                        $Match = 1;
                    }
                    elsif (
                        $Self->{JqIsAvailable}
                        && $FieldName =~ m{\Ajq\#(.+)}msi
                        )
                    {
                        my $JqExpression = $1;

                        my $CompareValue;
                        eval {
                            $CompareValue = Jq::jq( $JqExpression, $Param{Data} );
                        };

                        if (
                            defined $CompareValue
                            && defined $ActualCondition->{Fields}->{$FieldName}->{Match}
                            && $ActualCondition->{Fields}->{$FieldName}->{Match} eq $CompareValue
                            )
                        {
                            $Match = 1;
                        }
                    }

                    # Otherwise check if field data is and array and compare each element until
                    #   one match.
                    elsif ( ref $Param{Data}->{$FieldName} eq 'ARRAY' ) {

                        ITEM:
                        for my $Item ( @{ $Param{Data}->{$FieldName} } ) {
                            if ( $ActualCondition->{Fields}->{$FieldName}->{Match} eq $Item ) {
                                $Match = 1;
                                last ITEM;
                            }
                        }
                    }

                    if ($Match) {
                        $FieldSuccess++;

                        # Successful check if we just need one matching Condition to make this Condition valid.
                        return 1 if $ConditionLinking eq 'or' && $CondType eq 'or';

                        next CONDITIONNAME if $ConditionLinking ne 'or' && $CondType eq 'or';
                    }
                    else {
                        $FieldFail++;

                        # Failed check if we have all 'and' conditions.
                        return if $ConditionLinking eq 'and' && $CondType eq 'and';

                        # Try next Condition if all Condition Fields have to be true.
                        next CONDITIONNAME if $CondType eq 'and';
                    }
                    next FIELDLNAME;
                }

                my @ArrayFields = grep { $_ =~ m{ \A \Q$FieldName\E _ \d+ \z }xms } keys %{ $Param{Data} };

                if ( @ArrayFields && defined $ActualCondition->{Fields}->{$FieldName}->{Match} ) {
                    ARRAYFIELD:
                    for my $ArrayField (@ArrayFields) {
                        next ARRAYFIELD if ref $Param{Data}->{$ArrayField} ne '';
                        if ( $Param{Data}->{$ArrayField} ne $ActualCondition->{Fields}->{$FieldName}->{Match} ) {
                            next ARRAYFIELD;
                        }

                        $FieldSuccess++;

                        # Successful check if we just need one matching Condition to make this Condition valid.
                        return 1 if $ConditionLinking eq 'or' && $CondType eq 'or';

                        next CONDITIONNAME if $ConditionLinking ne 'or' && $CondType eq 'or';
                        next FIELDLNAME;
                    }
                }

                # No match = fail.
                $FieldFail++;

                # Failed check if we have all 'and' conditions
                return if $ConditionLinking eq 'and' && $CondType eq 'and';

                # Try next Condition if all Condition Fields have to be true
                next CONDITIONNAME if $CondType eq 'and';
                next FIELDLNAME;
            }
            elsif ( $ActualCondition->{Fields}->{$FieldName}->{Type} eq 'Array' ) {

                # 1. Go through each Condition->$ConditionName->Fields->$Field->Value (map).
                # 2. Assign the value to $CheckValue.
                # 3. Grep through $Data->{$Field} to find the "toCheck" value inside the Data->{$Field} Array
                # 4. Assign all found Values to @CheckResults.
                my $CheckValue;
                my @CheckResults =
                    map {
                    $CheckValue = $_;
                    grep { $CheckValue eq $_ } @{ $Param{Data}->{$FieldName} }
                    }
                    @{ $ActualCondition->{Fields}->{$FieldName}->{Match} };

                # If the found amount is the same as the "toCheck" amount we succeeded
                if (
                    scalar @CheckResults
                    == scalar @{ $ActualCondition->{Fields}->{$FieldName}->{Match} }
                    )
                {
                    $FieldSuccess++;

                    # Successful check if we just need one matching Condition to make this Condition valid.
                    return 1 if $ConditionLinking eq 'or' && $CondType eq 'or';

                    next CONDITIONNAME if $ConditionLinking ne 'or' && $CondType eq 'or';
                }
                else {
                    $FieldFail++;

                    # Failed check if we have all 'and' conditions.
                    return if $ConditionLinking eq 'and' && $CondType eq 'and';

                    # Try next Condition if all Condition Fields have to be true.
                    next CONDITIONNAME if $CondType eq 'and';
                }
                next FIELDLNAME;
            }
            elsif ( $ActualCondition->{Fields}->{$FieldName}->{Type} eq 'Hash' ) {

                # if our Check doesn't contain a hash.
                if ( ref $ActualCondition->{Fields}->{$FieldName}->{Match} ne 'HASH' ) {
                    $LogObject->Log(
                        Priority => 'error',
                        Message =>
                            "Condition->$ConditionName->Fields->$FieldName Match must"
                            . " be a Hash!",
                    );
                    return;
                }

                # If we have no data or Data isn't a hash, test failed.
                if (
                    !$Param{Data}->{$FieldName}
                    || ref $Param{Data}->{$FieldName} ne 'HASH'
                    )
                {
                    $FieldFail++;
                    next FIELDLNAME;
                }

                # Find all Data Hash values that equal to the Condition Match Values.
                my @CheckResults =
                    grep {
                    $Param{Data}->{$FieldName}->{$_} eq
                        $ActualCondition->{Fields}->{$FieldName}->{Match}->{$_}
                    }
                    keys %{ $ActualCondition->{Fields}->{$FieldName}->{Match} };

                # If the amount of Results equals the amount of Keys in our hash this part matched.
                if (
                    scalar @CheckResults
                    == scalar keys %{ $ActualCondition->{Fields}->{$FieldName}->{Match} }
                    )
                {

                    $FieldSuccess++;

                    # Successful check if we just need one matching Condition to make this condition valid.
                    return 1 if $ConditionLinking eq 'or' && $CondType eq 'or';

                    next CONDITIONNAME if $ConditionLinking ne 'or' && $CondType eq 'or';

                }
                else {
                    $FieldFail++;

                    # Failed check if we have all 'and' conditions.
                    return if $ConditionLinking eq 'and' && $CondType eq 'and';

                    # Try next Condition if all Condition Fields have to be true.
                    next CONDITIONNAME if $CondType eq 'and';
                }
                next FIELDLNAME;
            }
            elsif ( $ActualCondition->{Fields}->{$FieldName}->{Type} eq 'Regexp' )
            {

                # If our Check contains anything else then a string we can't check.
                if (
                    !$ActualCondition->{Fields}->{$FieldName}->{Match}
                    ||
                    (
                        ref $ActualCondition->{Fields}->{$FieldName}->{Match} ne 'Regexp'
                        && ref $ActualCondition->{Fields}->{$FieldName}->{Match} ne ''
                    )
                    )
                {
                    $LogObject->Log(
                        Priority => 'error',
                        Message =>
                            "Condition->$ConditionName->Fields->$FieldName Match must"
                            . " be a Regular expression if Type is set to Regexp!",
                    );
                    return;
                }

                # Precompile Regexp if is a string.
                if ( ref $ActualCondition->{Fields}->{$FieldName}->{Match} eq '' ) {
                    my $Match = $ActualCondition->{Fields}->{$FieldName}->{Match};

                    eval {
                        $ActualCondition->{Fields}->{$FieldName}->{Match} = qr{$Match};
                    };
                    if ($@) {
                        $LogObject->Log(
                            Priority => 'error',
                            Message  => $@,
                        );
                        return;
                    }
                }

                # Make sure there is data to compare.
                if (
                    $Param{Data}->{$FieldName}
                    || (
                        $Self->{JqIsAvailable}
                        && $FieldName =~ m{\Ajq\#(.+)}msi
                    )
                    )
                {
                    my $Match;

                    if ( $Self->{JqIsAvailable} && $FieldName =~ m{\Ajq\#(.+)}msi ) {
                        my $JqExpression = $1;

                        my $CompareValue;
                        eval {
                            $CompareValue = Jq::jq( $JqExpression, $Param{Data} );
                        };

                        if (
                            defined $CompareValue
                            && defined $ActualCondition->{Fields}->{$FieldName}->{Match}
                            && $CompareValue =~ $ActualCondition->{Fields}->{$FieldName}->{Match}
                            )
                        {
                            $Match = 1;
                        }
                    }

                    # Check if field data is a string and compare directly.
                    elsif (
                        ref $Param{Data}->{$FieldName} eq ''
                        && $Param{Data}->{$FieldName} =~ $ActualCondition->{Fields}->{$FieldName}->{Match}
                        )
                    {
                        $Match = 1;
                    }

                    # Otherwise check if field data is and array and compare each element until one match.
                    elsif ( ref $Param{Data}->{$FieldName} eq 'ARRAY' ) {

                        ITEM:
                        for my $Item ( @{ $Param{Data}->{$FieldName} } ) {
                            if ( $Item =~ $ActualCondition->{Fields}->{$FieldName}->{Match} ) {
                                $Match = 1;
                                last ITEM;
                            }
                        }
                    }

                    if ($Match) {
                        $FieldSuccess++;

                        # Successful check if we just need one matching Condition to make this Transition valid.
                        return 1 if $ConditionLinking eq 'or' && $CondType eq 'or';

                        next CONDITIONNAME if $ConditionLinking ne 'or' && $CondType eq 'or';
                    }
                    else {
                        $FieldFail++;

                        # Failed check if we have all 'and' conditions.
                        return if $ConditionLinking eq 'and' && $CondType eq 'and';

                        # Try next Condition if all Condition Fields have to be true.
                        next CONDITIONNAME if $CondType eq 'and';
                    }
                    next FIELDLNAME;
                }

                my @ArrayFields = grep { $_ =~ m{ \A \Q$FieldName\E _ \d+ \z }xms } keys %{ $Param{Data} };

                if ( @ArrayFields && defined $ActualCondition->{Fields}->{$FieldName}->{Match} ) {
                    ARRAYFIELD:
                    for my $ArrayField (@ArrayFields) {
                        next ARRAYFIELD if ref $Param{Data}->{$ArrayField} ne '';
                        if ( $Param{Data}->{$ArrayField} !~ $ActualCondition->{Fields}->{$FieldName}->{Match} ) {
                            next ARRAYFIELD;
                        }

                        $FieldSuccess++;

                        # Successful check if we just need one matching Condition to make this Condition valid.
                        return 1 if $ConditionLinking eq 'or' && $CondType eq 'or';

                        next CONDITIONNAME if $ConditionLinking ne 'or' && $CondType eq 'or';
                        next FIELDLNAME;
                    }
                }

                # No match = fail.
                $FieldFail++;

                # Failed check if we have all 'and' conditions
                return if $ConditionLinking eq 'and' && $CondType eq 'and';

                # Try next Condition if all Condition Fields have to be true
                next CONDITIONNAME if $CondType eq 'and';
                next FIELDLNAME;
            }
            elsif ( $ActualCondition->{Fields}->{$FieldName}->{Type} eq 'Module' ) {

                # Load Validation Modules. Default location for validation modules:
                #   Kernel/GenericInterface/Event/Validation/
                if (
                    !$MainObject->Require(
                        $ActualCondition->{Fields}->{$FieldName}->{Match}
                    )
                    )
                {
                    $LogObject->Log(
                        Priority => 'error',
                        Message  => "Can't load "
                            . $ActualCondition->{Fields}->{$FieldName}->{Type}
                            . "Module for Condition->$ConditionName->Fields->$FieldName validation!",
                    );
                    return;
                }

                # Create new ValidateModuleObject.
                my $ValidateModuleObject = $Kernel::OM->Get(
                    $ActualCondition->{Fields}->{$FieldName}->{Match}
                );

                # Handle "Data" Param to ValidateModule's "Validate" subroutine.
                if ( $ValidateModuleObject->Validate( Data => $Param{Data} ) ) {
                    $FieldSuccess++;

                    # Successful check if we just need one matching Condition to make this Condition valid.
                    return 1 if $ConditionLinking eq 'or' && $CondType eq 'or';

                    next CONDITIONNAME if $ConditionLinking ne 'or' && $CondType eq 'or';
                }
                else {
                    $FieldFail++;

                    # Failed check if we have all 'and' conditions.
                    return if $ConditionLinking eq 'and' && $CondType eq 'and';

                    # Try next Condition if all Condition Fields have to be true.
                    next CONDITIONNAME if $CondType eq 'and';
                }
                next FIELDLNAME;
            }
        }

        # FIELDLNAME end.
        if ( $CondType eq 'and' ) {

            # If we had no failing check this condition matched.
            if ( !$FieldFail ) {

                # Successful check if we just need one matching Condition to make this Condition valid.
                return 1 if $ConditionLinking eq 'or';
                $ConditionSuccess++;
            }
            else {
                $ConditionFail++;

                # Failed check if we have all 'and' condition.s
                return if $ConditionLinking eq 'and';
            }
        }
        elsif ( $CondType eq 'or' )
        {

            # If we had at least one successful check, this condition matched.
            if ( $FieldSuccess > 0 ) {

                # Successful check if we just need one matching Condition to make this Condition valid.
                return 1 if $ConditionLinking eq 'or';
                $ConditionSuccess++;
            }
            else {
                $ConditionFail++;

                # Failed check if we have all 'and' conditions.
                return if $ConditionLinking eq 'and';
            }
        }
        elsif ( $CondType eq 'xor' )
        {

            # If we had exactly one successful check, this condition matched.
            if ( $FieldSuccess == 1 ) {

                # Successful check if we just need one matching Condition to make this Condition valid.
                return 1 if $ConditionLinking eq 'or';
                $ConditionSuccess++;
            }
            else {
                $ConditionFail++;
            }
        }
    }

    # CONDITIONNAME end.
    if ( $ConditionLinking eq 'and' ) {

        # If we had no failing conditions this Condition matched.
        return 1 if !$ConditionFail;
    }
    elsif ( $ConditionLinking eq 'or' )
    {

        # If we had at least one successful condition, this condition matched.
        return 1 if $ConditionSuccess > 0;
    }
    elsif ( $ConditionLinking eq 'xor' )
    {

        # If we had exactly one successful condition, this condition matched.
        return 1 if $ConditionSuccess == 1;
    }

    # If no condition matched till here, we failed.
    return;
}

sub _IsJqAvailable {
    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');

    my $JqIsAvailable = $MainObject->Require(
        'Jq',
        Silent => 1,
    );

    return $JqIsAvailable;
}

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
