# --
# 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::Ticket::Article;

use strict;
use warnings;

use parent qw(Kernel::System::EventHandler);

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

our @ObjectDependencies = (
    'Kernel::Config',
    'Kernel::System::Cache',
    'Kernel::System::CommunicationChannel',
    'Kernel::System::DB',
    'Kernel::System::Log',
    'Kernel::System::Main',
    'Kernel::System::Ticket::Article::Backend::Invalid',
    'Kernel::System::Valid',
);

=head1 NAME

Kernel::System::Ticket::Article - functions to manage ticket articles

=head1 DESCRIPTION

Since OTRS 6, article data is split in a neutral part for all articles (in the C<article> database table),
and back end specific data in custom tables (such as C<article_data_mime> for the C<MIME> based back ends).

This class only manages back end neutral article data, like listing articles with L</ArticleList()> or manipulating
article metadata like L</ArticleFlagSet()>.

For all operations involving back end specific article data (like C<ArticleCreate> and C<ArticleGet>),
please call L</BackendForArticle()> or L</BackendForChannel()> to get to the correct article back end.
See L</ArticleList()> for an example of looping over all article data of a ticket.

See L<Kernel::System::Ticket::Article::Backend::Base> for the definition of the basic interface of all
article back ends.

=head1 PUBLIC INTERFACE

=head2 new()

Don't use the constructor directly, use the ObjectManager instead:

    my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');

=cut

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

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

    # 0=off; 1=on;
    $Self->{Debug} = $Param{Debug} || 0;

    $Self->{CacheType} = 'Article';
    $Self->{CacheTTL}  = 60 * 60 * 24 * 20;

    # init of event handler
    $Self->EventHandlerInit(
        Config => 'Ticket::EventModulePost',
    );

    # TODO: check if this can be removed after the new search is implemented.
    $Self->{ArticleSearchIndexModule} = $Param{ArticleSearchIndexModule}
        || $Kernel::OM->Get('Kernel::Config')->Get('Ticket::SearchIndexModule')
        || 'Kernel::System::Ticket::ArticleSearchIndex::RuntimeDB';

    return $Self;
}

=head2 BackendForArticle()

Returns the correct back end for a given article, or the
L<Invalid|Kernel::System::Ticket::Article::Backend::Invalid> back end, so that you can always expect
a back end object instance that can be used for chain-calling.

    my $ArticleBackendObject = $ArticleObject->BackendForArticle( TicketID => 42, ArticleID => 123 );

Alternatively, you can pass in a hash with base article data as returned by L</ArticleList()>, this will avoid the
lookup for the C<CommunicationChannelID> of the article:

    my $ArticleBackendObject = $ArticleObject->BackendForArticle( %BaseArticle );

See L<Kernel::System::Ticket::Article::Backend::Base> for the definition of the basic interface of all
article back ends.

=cut

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

    for my $Needed (qw(TicketID ArticleID)) {
        if ( !defined $Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!",
            );
            return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::Invalid');
        }
    }

    if ( !$Param{CommunicationChannelID} ) {
        my @BaseArticles = $Self->ArticleList(
            TicketID  => $Param{TicketID},
            ArticleID => $Param{ArticleID},
        );
        if (@BaseArticles) {
            $Param{CommunicationChannelID} = $BaseArticles[0]->{CommunicationChannelID};
        }
    }

    if ( $Param{CommunicationChannelID} ) {
        my $ChannelObject = $Kernel::OM->Get('Kernel::System::CommunicationChannel')->ChannelObjectGet(
            ChannelID => $Param{CommunicationChannelID},
        );
        return $ChannelObject->ArticleBackend() if $ChannelObject && $ChannelObject->can('ArticleBackend');
    }

    # Fall back to the invalid back end to enable chain calling.
    return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::Invalid');
}

=head2 BackendForChannel()

Returns the correct back end for a given communication channel, or the C<Invalid> back end, so that you can always expect
a back end object instance that can be used for chain-calling.

    my $ArticleBackendObject = $ArticleObject->BackendForChannel( ChannelName => 'Email' );

See L<Kernel::System::Ticket::Article::Backend::Base> for the definition of the basic interface of all
article back ends.

=cut

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

    for my $Needed (qw(ChannelName)) {
        if ( !defined $Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!",
            );
            return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::Invalid');
        }
    }

    my $ChannelObject = $Kernel::OM->Get('Kernel::System::CommunicationChannel')->ChannelObjectGet(
        ChannelName => $Param{ChannelName},
    );
    return $ChannelObject->ArticleBackend() if $ChannelObject && $ChannelObject->can('ArticleBackend');

    return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::Invalid');
}

=head2 ArticleList()

Returns an filtered array of base article data for a ticket.

    my @Articles = $ArticleObject->ArticleList(

        TicketID               => 123,

        # Optional filters, these can be combined:

        ArticleID              => 234,                # optional, limit to one article (if present on a ticket)
        CommunicationChannel   => 'Email',            # optional, to limit to a certain CommunicationChannel
        CommunicationChannelID => 2,                  # optional, to limit to a certain CommunicationChannelID
        SenderType             => 'customer',         # optional, to limit to a certain article SenderType
        SenderTypeID           => 2,                  # optional, to limit to a certain article SenderTypeID
        IsVisibleForCustomer   => 0,                  # optional, to limit to a certain visibility

        # After filtering, you can also limit to first or last found article only:

        OnlyFirst              => 0,                  # optional, only return first match
        OnlyLast               => 0,                  # optional, only return last match
    );

Returns a list with base article data (no back end related data included):

    (
        {
            ArticleID              => 1,
            TicketID               => 2,
            ArticleNumber          => 1,                        # sequential number of article in the ticket
            CommunicationChannelID => 1,
            SenderTypeID           => 1,
            IsVisibleForCustomer   => 0,
            CreateBy               => 1,
            CreateTime             => '2017-03-01 00:00:00',
            ChangeBy               => 1,
            ChangeTime             => '2017-03-01 00:00:00',
        },
        { ... }
    )

Please note that you need to use L<ArticleGet()|Kernel::System::Ticket::Article::Backend::Base/ArticleGet()> via the
article backend objects to access the full backend-specific article data hash for each article.

    for my $MetaArticle (@Articles) {
        my %Article = $ArticleObject->BackendForArticle( %{$MetaArticle} )->ArticleGet( %{$MetaArticle} );
    }

=cut

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

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

    if ( $Param{OnlyFirst} && $Param{OnlyLast} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'OnlyFirst and OnlyLast cannot be used together!',
        );
        return;
    }

    my @MetaArticleList = $Self->_MetaArticleList(%Param);
    return if !@MetaArticleList;

    if ( $Param{ArticleID} ) {
        @MetaArticleList = grep { $_->{ArticleID} == $Param{ArticleID} } @MetaArticleList;
    }

    if ( $Param{CommunicationChannel} || $Param{CommunicationChannelID} ) {
        my %CommunicationChannel = $Kernel::OM->Get('Kernel::System::CommunicationChannel')->ChannelGet(
            ChannelID   => scalar $Param{CommunicationChannelID},
            ChannelName => scalar $Param{CommunicationChannel},
        );
        if ( !%CommunicationChannel ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "CommunicationChannel $Param{CommunicationChannel} was not found!",
            );
            return;
        }
        @MetaArticleList = grep { $_->{CommunicationChannelID} == $CommunicationChannel{ChannelID} } @MetaArticleList;

    }

    if ( $Param{SenderType} || $Param{SenderTypeID} ) {
        my $SenderTypeID = $Param{SenderTypeID} || $Self->ArticleSenderTypeLookup( SenderType => $Param{SenderType} );
        if ( !$SenderTypeID ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Article SenderType $Param{SenderType} was not found!",
            );
            return;
        }
        @MetaArticleList = grep { $_->{SenderTypeID} == $SenderTypeID } @MetaArticleList;
    }

    if ( defined $Param{IsVisibleForCustomer} ) {
        @MetaArticleList = grep { $_->{IsVisibleForCustomer} == $Param{IsVisibleForCustomer} } @MetaArticleList;
    }

    if ( $Param{OnlyFirst} && @MetaArticleList ) {
        @MetaArticleList = ( $MetaArticleList[0] );
    }
    elsif ( $Param{OnlyLast} && @MetaArticleList ) {
        @MetaArticleList = ( $MetaArticleList[-1] );
    }

    return @MetaArticleList;
}

=head2 TicketIDLookup()

Get a ticket ID for supplied article ID.

    my $TicketID = $ArticleObject->TicketIDLookup(
        ArticleID => 123,   # required
    );

Returns ID of a ticket that article belongs to:

    $TicketID = 123;

NOTE: Usage of this lookup function is strongly discouraged, since its result is not cached.
Where possible, use C<ArticleList()> instead.

=cut

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

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

    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    return if !$DBObject->Prepare(
        SQL => '
            SELECT ticket_id
            FROM article
            WHERE id = ?
        ',
        Bind  => [ \$Param{ArticleID} ],
        Limit => 1,
    );

    my $TicketID;
    while ( my @Row = $DBObject->FetchrowArray() ) {
        $TicketID = $Row[0];
    }

    return $TicketID;
}

=head2 ArticleFlagSet()

Set article flags.

    my $Success = $ArticleObject->ArticleFlagSet(
        TicketID  => 123,
        ArticleID => 123,
        Key       => 'Seen',
        Value     => 1,
        UserID    => 123,
    );

Events:
    ArticleFlagSet

=cut

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

    for my $Needed (qw(TicketID ArticleID Key Value UserID)) {
        if ( !defined $Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    my %Flag = $Self->ArticleFlagGet(%Param);

    # check if set is needed
    return 1 if defined $Flag{ $Param{Key} } && $Flag{ $Param{Key} } eq $Param{Value};

    # get database object
    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    # set flag
    return if !$DBObject->Do(
        SQL => '
            DELETE FROM article_flag
            WHERE article_id = ?
                AND article_key = ?
                AND create_by = ?',
        Bind => [ \$Param{ArticleID}, \$Param{Key}, \$Param{UserID} ],
    );
    return if !$DBObject->Do(
        SQL => 'INSERT INTO article_flag
            (article_id, article_key, article_value, create_time, create_by)
            VALUES (?, ?, ?, current_timestamp, ?)',
        Bind => [ \$Param{ArticleID}, \$Param{Key}, \$Param{Value}, \$Param{UserID} ],
    );

    # event
    $Self->EventHandler(
        Event => 'ArticleFlagSet',
        Data  => {
            TicketID  => $Param{TicketID},
            ArticleID => $Param{ArticleID},
            Key       => $Param{Key},
            Value     => $Param{Value},
            UserID    => $Param{UserID},
        },
        UserID => $Param{UserID},
    );

    return 1;
}

=head2 ArticleFlagDelete()

Delete an article flag.

    my $Success = $ArticleObject->ArticleFlagDelete(
        TicketID  => 123,
        ArticleID => 123,
        Key       => 'seen',
        UserID    => 123,
    );

    my $Success = $ArticleObject->ArticleFlagDelete(
        TicketID  => 123,
        ArticleID => 123,
        Key       => 'seen',
        AllUsers  => 1,         # delete for all users
    );

Events:
    ArticleFlagDelete

=cut

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

    # check needed stuff
    for my $Needed (qw(TicketID ArticleID Key)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

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

    # get database object
    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    if ( $Param{AllUsers} ) {
        return if !$DBObject->Do(
            SQL => '
                DELETE FROM article_flag
                WHERE article_id = ?
                    AND article_key = ?',
            Bind => [ \$Param{ArticleID}, \$Param{Key} ],
        );
    }
    else {
        return if !$DBObject->Do(
            SQL => '
                DELETE FROM article_flag
                WHERE article_id = ?
                    AND create_by = ?
                    AND article_key = ?',
            Bind => [ \$Param{ArticleID}, \$Param{UserID}, \$Param{Key} ],
        );

        # event
        $Self->EventHandler(
            Event => 'ArticleFlagDelete',
            Data  => {
                TicketID  => $Param{TicketID},
                ArticleID => $Param{ArticleID},
                Key       => $Param{Key},
                UserID    => $Param{UserID},
            },
            UserID => $Param{UserID},
        );
    }

    return 1;
}

=head2 ArticleFlagGet()

Get article flags.

    my %Flags = $ArticleObject->ArticleFlagGet(
        ArticleID => 123,
        UserID    => 123,
    );

=cut

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

    # check needed stuff
    for my $Needed (qw(ArticleID UserID)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    # get database object
    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    # sql query
    return if !$DBObject->Prepare(
        SQL => '
            SELECT article_key, article_value
            FROM article_flag
            WHERE article_id = ?
                AND create_by = ?',
        Bind => [ \$Param{ArticleID}, \$Param{UserID} ],
    );

    my %Flag;
    while ( my @Row = $DBObject->FetchrowArray() ) {
        $Flag{ $Row[0] } = $Row[1];
    }

    return %Flag;
}

=head2 ArticleFlagsOfTicketGet()

Get all article flags of a ticket.

    my %Flags = $ArticleObject->ArticleFlagsOfTicketGet(
        TicketID  => 123,
        UserID    => 123,
    );

    returns (
        123 => {                    # ArticleID
            'Seen'  => 1,
            'Other' => 'something',
        },
    )

=cut

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

    # check needed stuff
    for my $Needed (qw(TicketID UserID)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    # get database object
    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    # sql query
    return if !$DBObject->Prepare(
        SQL => '
            SELECT article.id, article_flag.article_key, article_flag.article_value
            FROM article_flag, article
            WHERE article.id = article_flag.article_id
                AND article.ticket_id = ?
                AND article_flag.create_by = ?',
        Bind => [ \$Param{TicketID}, \$Param{UserID} ],
    );

    my %Flag;
    while ( my @Row = $DBObject->FetchrowArray() ) {
        $Flag{ $Row[0] }->{ $Row[1] } = $Row[2];
    }

    return %Flag;
}

=head2 ArticleAccountedTimeGet()

Returns the accounted time of a article.

    my $AccountedTime = $ArticleObject->ArticleAccountedTimeGet(
        ArticleID => $ArticleID,
    );

=cut

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

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

    # get database object
    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    # db query
    return if !$DBObject->Prepare(
        SQL  => 'SELECT time_unit FROM time_accounting WHERE article_id = ?',
        Bind => [ \$Param{ArticleID} ],
    );

    my $AccountedTime = 0;
    while ( my @Row = $DBObject->FetchrowArray() ) {
        $Row[0] =~ s/,/./g;
        $AccountedTime = $AccountedTime + $Row[0];
    }

    return $AccountedTime;
}

=head2 ArticleAccountedTimeDelete()

Delete accounted time of an article.

    my $Success = $ArticleObject->ArticleAccountedTimeDelete(
        ArticleID => $ArticleID,
    );

=cut

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

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

    # db query
    return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
        SQL  => 'DELETE FROM time_accounting WHERE article_id = ?',
        Bind => [ \$Param{ArticleID} ],
    );

    return 1;
}

=head2 ArticleSenderTypeList()

List all article sender types.

    my %ArticleSenderTypeList = $ArticleObject->ArticleSenderTypeList();

Returns:

    (
        1 => 'agent',
        2 => 'customer',
        3 => 'system',
    )

=cut

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

    my $CacheKey = 'ArticleSenderTypeList';

    # Is there a cached value yet?
    my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get(
        Type => $Self->{CacheType},
        Key  => $CacheKey,
    );
    return %{$Cache} if ref $Cache eq 'HASH';

    my $DBObject    = $Kernel::OM->Get('Kernel::System::DB');
    my $ValidObject = $Kernel::OM->Get('Kernel::System::Valid');

    return if !$DBObject->Prepare(
        SQL => "SELECT id, name FROM article_sender_type WHERE "
            . "valid_id IN (${\(join ', ', $ValidObject->ValidIDsGet())})",
    );

    my %Result;
    while ( my @Row = $DBObject->FetchrowArray() ) {
        $Result{ $Row[0] } = $Row[1];
    }

    $Kernel::OM->Get('Kernel::System::Cache')->Set(
        Type  => $Self->{CacheType},
        TTL   => $Self->{CacheTTL},
        Key   => $CacheKey,
        Value => \%Result,
    );
    return %Result;
}

=head2 ArticleSenderTypeLookup()

Lookup an article sender type id or name.

    my $SenderTypeID = $ArticleObject->ArticleSenderTypeLookup(
        SenderType => 'customer', # customer|system|agent
    );

    my $SenderType = $ArticleObject->ArticleSenderTypeLookup(
        SenderTypeID => 1,
    );

=cut

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

    if ( !$Param{SenderType} && !$Param{SenderTypeID} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need SenderType or SenderTypeID!',
        );
        return;
    }

    my %SenderTypes = $Self->ArticleSenderTypeList( Result => 'HASH' );

    if ( $Param{SenderTypeID} ) {
        return $SenderTypes{ $Param{SenderTypeID} };
    }
    return { reverse %SenderTypes }->{ $Param{SenderType} };
}

=head2 ArticleSearchIndexRebuildFlagSet()

Set the article flags to indicate if the article search index needs to be rebuilt.

    my $Success = $ArticleObject->ArticleSearchIndexRebuildFlagSet(
        ArticleIDs => [ 123, 234, 345 ]   # (Either 'ArticleIDs' or 'All' must be provided) The ArticleIDs to be updated.
        All        => 1,                  # (Either 'ArticleIDs' or 'All' must be provided) Set all articles to $Value. Default: 0,
        Value      => 1, # 0/1 default 0
    );

=cut

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

    if ( !defined $Param{All} && !IsArrayRefWithData( $Param{ArticleIDs} ) ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "Either ArticleIDs or All parameter must be provided!"
        );
        return;
    }

    $Param{All}        //= 0;
    $Param{ArticleIDs} //= [];
    $Param{Value} = $Param{Value} ? 1 : 0;

    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    if ( $Param{All} ) {

        return if !$DBObject->Do(
            SQL  => "UPDATE article SET search_index_needs_rebuild = ?",
            Bind => [ \$Param{Value}, ],
        );

        return 1;
    }

    my $InCondition = $DBObject->QueryInCondition(
        Key       => 'id',
        Values    => $Param{ArticleIDs},
        QuoteType => 'Integer',
    );

    return if !$DBObject->Do(
        SQL => "
            UPDATE article
            SET search_index_needs_rebuild = ?
            WHERE $InCondition",
        Bind => [ \$Param{Value}, ],
    );

    return 1;
}

=head2 ArticleSearchIndexRebuildFlagList()

Get a list of ArticleIDs and TicketIDs for a given flag (either needs rebuild or not)

    my %ArticleTicketIDs = $ArticleObject->ArticleSearchIndexRebuildFlagList(
        Value => 1,     # (optional) 0/1 default 0
        Limit => 10000, # (optional) default: 20000
    );

Returns:

    %ArticleIDs = (
        1 => 2, # ArticleID => TicketID
        3 => 4,
        5 => 6,
        ...
    );

=cut

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

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

    $Param{Value} = $Param{Value} ? 1 : 0;
    $Param{Limit} //= 20000;

    # get database object
    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    # sql query
    return if !$DBObject->Prepare(
        SQL => '
            SELECT id, ticket_id
            FROM article
            WHERE search_index_needs_rebuild = ?',
        Bind  => [ \$Param{Value}, ],
        Limit => $Param{Limit},
    );

    my %ArticleIDs;
    while ( my @Row = $DBObject->FetchrowArray() ) {
        $ArticleIDs{ $Row[0] } = $Row[1];
    }

    return %ArticleIDs;
}

=head2 ArticleSearchIndexStatus()

gets an article indexing status hash.

    my %Status = $ArticleObject->ArticleSearchIndexStatus();

Returns:

    %Status = (
        ArticlesTotal      => 443,
        ArticlesIndexed    => 420,
        ArticlesNotIndexed =>  23,
    );

=cut

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

    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    return if !$DBObject->Prepare(
        SQL => 'SELECT count(*) FROM article WHERE search_index_needs_rebuild = 0',
    );

    my %Result;
    while ( my @Row = $DBObject->FetchrowArray() ) {
        $Result{ArticlesIndexed} = $Row[0];
    }

    return if !$DBObject->Prepare(
        SQL => 'SELECT count(*) FROM article WHERE search_index_needs_rebuild = 1',
    );

    while ( my @Row = $DBObject->FetchrowArray() ) {
        $Result{ArticlesNotIndexed} = $Row[0];
    }

    $Result{ArticlesTotal} = $Result{ArticlesIndexed} + $Result{ArticlesNotIndexed};

    return %Result;
}

=head2 ArticleSearchIndexBuild()

Rebuilds the current article search index table content. Existing article entries will be replaced.

    my $Success = $ArticleObject->ArticleSearchIndexBuild(
        TicketID  => 123,
        ArticleID => 123,
        UserID    => 1,
    );

Returns:

    True if indexing process was successfully finished, False if not.

=cut

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

    return $Kernel::OM->Get( $Self->{ArticleSearchIndexModule} )->ArticleSearchIndexBuild(%Param);
}

=head2 ArticleSearchIndexDelete()

Deletes entries from the article search index table base on supplied C<ArticleID> or C<TicketID>.

    my $Success = $ArticleObject->ArticleSearchIndexDelete(
        ArticleID => 123,   # required, deletes search index for single article
                            # or
        TicketID  => 123,   # required, deletes search index for all ticket articles

        UserID    => 1,     # required
    );

Returns:

    True if delete process was successfully finished, False if not.

=cut

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

    return $Kernel::OM->Get( $Self->{ArticleSearchIndexModule} )->ArticleSearchIndexDelete(%Param);
}

=head2 ArticleSearchIndexSQLJoinNeeded()

Checks the given search parameters for used article backend fields.

    my $Needed = $ArticleObject->ArticleSearchIndexSQLJoinNeeded(
        SearchParams => {
            ...
            ConditionInline         => 1,
            ContentSearchPrefix     => '*',
            ContentSearchSuffix     => '*',
            MIMEBase_From           => '%spam@example.com%',
            MIMEBase_To             => '%service@example.com%',
            MIMEBase_Cc             => '%client@example.com%',
            MIMEBase_Subject        => '%VIRUS 32%',
            MIMEBase_Body           => '%VIRUS 32%',
            MIMEBase_AttachmentName => '%anyfile.txt%',
            Chat_ChatterName        => '%Some Chatter Name%',
            Chat_MessageText        => '%Some Message Text%'
            ...
        },
    );

Returns:

    True if article search index usage is needed, False if not.

=cut

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

    return $Kernel::OM->Get( $Self->{ArticleSearchIndexModule} )->ArticleSearchIndexSQLJoinNeeded(%Param);
}

=head2 ArticleSearchIndexSQLJoin()

Generates SQL string extensions, including the needed table joins for the article index search.

    my $SQLExtenion = $ArticleObject->ArticleSearchIndexSQLJoin(
        SearchParams => {
            ...
            ConditionInline         => 1,
            ContentSearchPrefix     => '*',
            ContentSearchSuffix     => '*',
            MIMEBase_From           => '%spam@example.com%',
            MIMEBase_To             => '%service@example.com%',
            MIMEBase_Cc             => '%client@example.com%',
            MIMEBase_Subject        => '%VIRUS 32%',
            MIMEBase_Body           => '%VIRUS 32%',
            MIMEBase_AttachmentName => '%anyfile.txt%',
            Chat_ChatterName        => '%Some Chatter Name%',
            Chat_MessageText        => '%Some Message Text%'
            ...
        },
    );

Returns:

    $SQLExtension = 'LEFT JOIN article_search_index ArticleFulltext ON art.id = ArticleFulltext.article_id ';

=cut

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

    return $Kernel::OM->Get( $Self->{ArticleSearchIndexModule} )->ArticleSearchIndexSQLJoin(%Param);
}

=head2 ArticleSearchIndexWhereCondition()

Generates SQL query conditions for the used article fields, that may be used in the WHERE clauses of main
SQL queries to the database.

    my $SQLExtenion = $ArticleObject->ArticleSearchIndexWhereCondition(
        SearchParams => {
            ...
            ConditionInline         => 1,
            ContentSearchPrefix     => '*',
            ContentSearchSuffix     => '*',
            MIMEBase_From           => '%spam@example.com%',
            MIMEBase_To             => '%service@example.com%',
            MIMEBase_Cc             => '%client@example.com%',
            MIMEBase_Subject        => '%VIRUS 32%',
            MIMEBase_Body           => '%VIRUS 32%',
            MIMEBase_AttachmentName => '%anyfile.txt%',
            Chat_ChatterName        => '%Some Chatter Name%',
            Chat_MessageText        => '%Some Message Text%'
            ...
        },
    );

Returns:

    $SQLConditions = " AND (MIMEBase_From.article_value LIKE '%spam@example.com%') ";

=cut

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

    return $Kernel::OM->Get( $Self->{ArticleSearchIndexModule} )->ArticleSearchIndexWhereCondition(%Param);
}

=head2 SearchStringStopWordsFind()

Find stop words within given search string.

    my $StopWords = $ArticleObject->SearchStringStopWordsFind(
        SearchStrings => {
            'Fulltext'      => '(this AND is) OR test',
            'MIMEBase_From' => 'myself',
        },
    );

    Returns Hashref with found stop words.

=cut

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

    return $Kernel::OM->Get( $Self->{ArticleSearchIndexModule} )->SearchStringStopWordsFind(%Param);
}

=head2 SearchStringStopWordsUsageWarningActive()

Checks if warnings for stop words in search strings are active or not.

    my $WarningActive = $ArticleObject->SearchStringStopWordsUsageWarningActive();

=cut

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

    return $Kernel::OM->Get( $Self->{ArticleSearchIndexModule} )->SearchStringStopWordsUsageWarningActive(%Param);
}

=head2 ArticleSearchableFieldsList()

Get list of searchable fields across all article backends.

    my %SearchableFields = $ArticleObject->ArticleSearchableFieldsList();

Returns:

    %SearchableFields = (
        'MIMEBase_Body' => {
            Filterable => 1,
            Key        => 'MIMEBase_Body',
            Label      => 'Body',
            Type       => 'Text',
        },
        'MIMEBase_Subject' => {
            Filterable => 1,
            Key        => 'MIMEBase_Subject',
            Label      => 'Subject',
            Type       => 'Text',
        },
        ...
    );

=cut

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

    my @CommunicationChannels = $Kernel::OM->Get('Kernel::System::CommunicationChannel')->ChannelList(
        ValidID => 1,
    );

    my %SearchableFields;

    for my $Channel (@CommunicationChannels) {

        my $CurrentArticleBackendObject = $Self->BackendForChannel(
            ChannelName => $Channel->{ChannelName},
        );

        my %BackendSearchableFields = $CurrentArticleBackendObject->BackendSearchableFieldsGet();

        %SearchableFields = ( %SearchableFields, %BackendSearchableFields );
    }

    return %SearchableFields;
}

=head1 PRIVATE FUNCTIONS

=head2 _MetaArticleList()

Returns an array-hash with the meta articles of the current ticket.

    my @MetaArticles = $ArticleObject->_MetaArticleList(
        TicketID => 123,
    );

Returns:

    (
        {
            ArticleID              => 1,
            TicketID               => 2,
            ArticleNumber          => 1,                        # sequential number of article in the ticket
            CommunicationChannelID => 1,
            SenderTypeID           => 1,
            IsVisibleForCustomer   => 0,
            CreateBy               => 1,
            CreateTime             => '2017-03-01 00:00:00',
            ChangeBy               => 1,
            ChangeTime             => '2017-03-01 00:00:00',
        },
        { ... },
    )


=cut

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

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

    my $CacheKey = '_MetaArticleList::' . $Param{TicketID};

    my $Cached = $Kernel::OM->Get('Kernel::System::Cache')->Get(
        Type => $Self->{CacheType},
        Key  => $CacheKey,
    );

    if ( ref $Cached eq 'ARRAY' ) {
        return @{$Cached};
    }

    # get database object
    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    return if !$DBObject->Prepare(
        SQL => '
            SELECT id, ticket_id, communication_channel_id, article_sender_type_id, is_visible_for_customer,
                        create_by, create_time, change_by, change_time
            FROM article
            WHERE ticket_id = ?
            ORDER BY id ASC',
        Bind => [ \$Param{TicketID} ],
    );

    my @Index;
    my $Count;
    while ( my @Row = $DBObject->FetchrowArray() ) {
        my %Result;
        $Result{ArticleID}              = $Row[0];
        $Result{TicketID}               = $Row[1];
        $Result{CommunicationChannelID} = $Row[2];
        $Result{SenderTypeID}           = $Row[3];
        $Result{IsVisibleForCustomer}   = $Row[4];
        $Result{CreateBy}               = $Row[5];
        $Result{CreateTime}             = $Row[6];
        $Result{ChangeBy}               = $Row[7];
        $Result{ChangeTime}             = $Row[8];
        $Result{ArticleNumber}          = ++$Count;
        push @Index, \%Result;
    }

    $Kernel::OM->Get('Kernel::System::Cache')->Set(
        Type  => $Self->{CacheType},
        TTL   => $Self->{CacheTTL},
        Key   => $CacheKey,
        Value => \@Index,
    );

    return @Index;
}

=head2 _ArticleCacheClear()

Removes all article caches related to specified ticket.

    my $Success = $ArticleObject->_ArticleCacheClear(
        TicketID => 123,
    );

=cut

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

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

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

    # MetaArticleIndex()
    $CacheObject->Delete(
        Type => $Self->{CacheType},
        Key  => '_MetaArticleList::' . $Param{TicketID},
    );

    return 1;
}

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

    my $ArticleBackendObject = $Self->BackendForChannel( ChannelName => $Param{ChannelName} );
    return if !$ArticleBackendObject;

    my $ArticleID = $ArticleBackendObject->ArticleCreate(%Param);
    return $ArticleID;
}

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

    my $ArticleBackendObject = $Self->BackendForArticle(
        TicketID  => $Param{TicketID},
        ArticleID => $Param{ArticleID}
    );
    return if !$ArticleBackendObject;

    my %Article = $ArticleBackendObject->ArticleGet(%Param);
    return %Article;
}

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

    my $ArticleBackendObject = $Self->BackendForArticle(
        TicketID  => $Param{TicketID},
        ArticleID => $Param{ArticleID}
    );
    return if !$ArticleBackendObject;

    my $ArticleUpdated = $ArticleBackendObject->ArticleUpdate(%Param);
    return $ArticleUpdated;
}

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

    my $ArticleBackendObject = $Self->BackendForChannel( ChannelName => 'Email' );
    return if !$ArticleBackendObject;

    my $ArticleID = $ArticleBackendObject->ArticleSend(%Param);
    return $ArticleID;
}

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

    my $ArticleBackendObject = $Self->BackendForArticle(
        TicketID  => $Param{TicketID},
        ArticleID => $Param{ArticleID}
    );
    return if !$ArticleBackendObject;

    my $ArticleBounced = $ArticleBackendObject->ArticleBounce(%Param);
    return $ArticleBounced;
}

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

    my $ArticleBackendObject = $Self->BackendForChannel( ChannelName => 'Email' );
    return if !$ArticleBackendObject;

    my $ArticleID = $ArticleBackendObject->SendAutoResponse(%Param);
    return $ArticleID;
}

=head2 ArticleIndex()

returns an array with article IDs

    my @ArticleIDs = $ArticleObject->ArticleIndex(
        TicketID => 123,
    );

    my @ArticleIDs = $ArticleObject->ArticleIndex(
        SenderType => 'customer',                   # optional, to limit to a certain sender type
        TicketID   => 123,
    );

=cut

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

    my @Articles = $Self->ArticleList(
        TicketID   => $Param{TicketID},
        SenderType => $Param{SenderType},
    );

    my @ArticleIDs;
    return @ArticleIDs if !@Articles;

    @ArticleIDs = map { $_->{ArticleID} } @Articles;
    return @ArticleIDs;
}

=head2 ArticleAttachmentIndex()

returns an array with article IDs

    my %AttachmentIndex = $ArticleObject->ArticleAttachmentIndex(
        TicketID         => 123,
        ArticleID        => 123,
        ExcludePlainText => 1,       # (optional) Exclude plain text attachment
        ExcludeHTMLBody  => 1,       # (optional) Exclude HTML body attachment
        ExcludeInline    => 1,       # (optional) Exclude inline attachments
        OnlyHTMLBody     => 1,       # (optional) Return only HTML body attachment, return nothing if not found
    );

Returns:

    my %AttachmentIndex = (
        '1' => {
            'FilesizeRaw'        => '804764',
            'Disposition'        => 'attachment',
            'ContentType'        => 'image/jpeg',
            'ContentAlternative' => '',
            'Filename'           => 'blub.jpg',
            'ContentID'          => ''
        },
        # ...
    );

=cut

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

    my $ArticleBackendObject = $Self->BackendForArticle(
        TicketID  => $Param{TicketID},
        ArticleID => $Param{ArticleID}
    );
    return if !$ArticleBackendObject;

    my %AttachmentIndex = $ArticleBackendObject->ArticleAttachmentIndex(
        %Param,
    );

    return %AttachmentIndex;
}

=head2 ArticleAttachment()

Get article attachment from storage. This is a delegate method from active backend.

    my %Attachment = $ArticleObject->ArticleAttachment(
        TicketID  => 123,
        ArticleID => 123,
        FileID    => 1,   # as returned by ArticleAttachmentIndex
    );

Returns:

    %Attachment = (
        Content            => 'xxxx',     # actual attachment contents
        ContentAlternative => '',
        ContentID          => '',
        ContentType        => 'application/pdf',
        Filename           => 'StdAttachment-Test1.pdf',
        FilesizeRaw        => 4722,
        Disposition        => 'attachment',
    );

=cut

sub ArticleAttachment {    ## no critic;
    my ( $Self, %Param ) = @_;

    my $ArticleBackendObject = $Self->BackendForArticle(
        TicketID  => $Param{TicketID},
        ArticleID => $Param{ArticleID}
    );
    return if !$ArticleBackendObject;

    my %Attachment = $ArticleBackendObject->ArticleAttachment(
        %Param,
    );

    return %Attachment;
}

=head2 ArticleWriteAttachment()

Write an article attachment to storage.

    my $Success = $ArticleBackendObject->ArticleWriteAttachment(
        TicketID           => 503,
        Content            => $ContentAsString,
        ContentType        => 'text/html; charset="iso-8859-15"',
        Filename           => 'lala.html',
        ContentID          => 'cid-1234',   # optional
        ContentAlternative => 0,            # optional, alternative content to shown as body
        Disposition        => 'attachment', # or 'inline'
        ArticleID          => 123,
        UserID             => 123,
    );

=cut

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

    my $ArticleBackendObject = $Self->BackendForArticle(
        TicketID  => $Param{TicketID},
        ArticleID => $Param{ArticleID}
    );
    return if !$ArticleBackendObject;

    my $Success = $ArticleBackendObject->ArticleWriteAttachment(%Param);
    return $Success;
}

=head2 ArticleCount()

Returns count of article.

    my $Count = $ArticleObject->ArticleCount(
        TicketID  => 123,
    );

Returns:

    my $Count = 1;

=cut

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

    my $DBObject  = $Kernel::OM->Get('Kernel::System::DB');
    my $LogObject = $Kernel::OM->Get('Kernel::System::Log');

    NEEDED:
    for my $Needed (qw(TicketID)) {

        next NEEDED if defined $Param{$Needed};

        $LogObject->Log(
            Priority => 'error',
            Message  => "Parameter '$Needed' is needed!",
        );
        return;
    }

    my $Count = 0;
    my $SQL   = '
        SELECT COUNT(*)
        FROM article
        WHERE ticket_id = ?';

    return if !$DBObject->Prepare(
        SQL  => $SQL,
        Bind => [ \$Param{TicketID} ],
    );

    while ( my @Row = $DBObject->FetchrowArray() ) {
        $Count = $Row[0];
    }

    return $Count;
}

=head2 ArticleAttachmentCount()

Returns count of article attachment.

    my $Count = $ArticleObject->ArticleAttachmentCount(
        TicketID  => 123,
        ArticleID => 123,
    );

Returns:

    my $Count = 1;

=cut

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

    my $LogObject    = $Kernel::OM->Get('Kernel::System::Log');
    my $DBObject     = $Kernel::OM->Get('Kernel::System::DB');
    my $MainObject   = $Kernel::OM->Get('Kernel::System::Main');
    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');

    NEEDED:
    for my $Needed (qw(TicketID ArticleID)) {

        next NEEDED if defined $Param{$Needed};

        $LogObject->Log(
            Priority => 'error',
            Message  => "Parameter '$Needed' is needed!",
        );
        return;
    }
    my $Count = 0;

    my $ArticleBackendObject = $Self->BackendForArticle(
        TicketID  => $Param{TicketID},
        ArticleID => $Param{ArticleID}
    );

    if (
        $ArticleBackendObject->{ArticleStorageModule} eq
        'Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB'
        )
    {

        my $SQL = '
            SELECT COUNT(*)
            FROM article_data_mime_attachment
            WHERE article_id = ?';

        return if !$DBObject->Prepare(
            SQL  => $SQL,
            Bind => [ \$Param{ArticleID} ],
        );

        while ( my @Row = $DBObject->FetchrowArray() ) {
            $Count = $Row[0];
        }

    }
    elsif (
        $ArticleBackendObject->{ArticleStorageModule} eq
        'Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS'
        )
    {

        $Self->{ArticleDataDir} = $ConfigObject->Get('Ticket::Article::Backend::MIMEBase::ArticleDataDir');

        my $ContentPath = $Self->ArticleContentPathGet(
            ArticleID => $Param{ArticleID},
        );

        my @Filenames = $MainObject->DirectoryRead(
            Directory => "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}",
            Filter    => "*",
            Silent    => 1,
        );

        FILENAME:
        for my $Filename ( sort @Filenames ) {

            # do not use control file
            next FILENAME if $Filename =~ /\.content_alternative$/;
            next FILENAME if $Filename =~ /\.content_id$/;
            next FILENAME if $Filename =~ /\.content_type$/;
            next FILENAME if $Filename =~ /\.disposition$/;
            next FILENAME if $Filename =~ /\/plain.txt$/;
            $Count++;
        }
    }

    return $Count;
}

=head2 ArticleContentPathGet()

Get the stored content path of an article.

    my $Path = $BackendObject->ArticleContentPathGet(
        ArticleID => 123,
    );

=cut

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

    my $LogObject   = $Kernel::OM->Get('Kernel::System::Log');
    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
    my $DBObject    = $Kernel::OM->Get('Kernel::System::DB');

    NEEDED:
    for my $Needed (qw(ArticleID)) {

        next NEEDED if defined $Param{$Needed};

        $LogObject->Log(
            Priority => 'error',
            Message  => "Parameter '$Needed' is needed!",
        );
        return;
    }

    my $CacheKey = 'ArticleContentPathGet::' . $Param{ArticleID};

    my $Cache = $CacheObject->Get(
        Type => $Self->{CacheType},
        Key  => $CacheKey,
    );
    return $Cache if $Cache;

    return if !$DBObject->Prepare(
        SQL  => 'SELECT content_path FROM article_data_mime WHERE article_id = ?',
        Bind => [ \$Param{ArticleID} ],
    );

    my $Result;
    while ( my @Row = $DBObject->FetchrowArray() ) {
        $Result = $Row[0];
    }

    $CacheObject->Set(
        Type  => $Self->{CacheType},
        TTL   => $Self->{CacheTTL},
        Key   => $CacheKey,
        Value => $Result,
    );

    return $Result;
}

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
