package Cpanel::Dovecot::Solr::Utils;

# cpanel - Cpanel/Dovecot/Solr/Utils.pm
#                                                    Copyright 2017 cPanel, Inc.
#                                                           All rights Reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cpanel license. Unauthorized copying is prohibited

use strict;
use warnings;

use Cpanel::HTTP::Client                 ();
use Cpanel::Transaction::File::RawReader ();

=encoding utf-8

=head1 NAME

Cpanel::Dovecot::Solr::Utils

=head1 SYNOPSIS

use Cpanel::Dovecot::Solr::Utils ();
Cpanel::Dovecot::Solr::Utils::call_commit();

=head1 DESCRIPTION

This module contains misc methods to interact with the cPanel Dovecot Solr instance and config file.

=head1 FUNCTIONS


=cut

our $FTS_CONFIG_FILE = '/etc/dovecot/fts.conf';
use constant CONFIG_PERMS              => 0640;
use constant SOLR_PORT                 => 8984;
use constant MAX_TIME_TO_OPTIMIZE_SOLR => ( 60 * 60 * 4 );    # It can take 3 hours to update on mx1 so lets allow for 4

=head2 get_solr_credentials()

This function reads the username and password set in the fts.conf and returns them.

=head3 Returns

An array that contains the set username and the set password for the solr instance.

=head3 Exceptions

Any Cpanel::Transaction::File::RawReader may throw.

=cut

sub get_solr_credentials {

    my $trans = Cpanel::Transaction::File::RawReader->new( 'path' => $FTS_CONFIG_FILE );
    my $sr = $trans->get_data();
    return if !$sr || !length $$sr;

    my ( $current_username, $current_password ) = $$sr =~ m{url=http://([^:]+):([^\@]+)}s;

    return ( $current_username, $current_password );
}

=head2 get_solr_base_url()

This function assembles the base URL for the Solr instance.

=head3 Returns

The base URL for connecting to the Solr instance.

=head3 Exceptions

Any Cpanel::Transaction::File::RawReader may throw.

=cut

sub get_solr_base_url {
    my ( $current_username, $current_password ) = Cpanel::Dovecot::Solr::Utils::get_solr_credentials();

    return "http://$current_username:$current_password\@127.0.0.1:" . SOLR_PORT;
}

=head2 call_optimize()

This function makes an HTTP connection to the solr instance and calls the optimize function.

=head3 Returns

The function only returns 1.

=head3 Exceptions

Any Cpanel::Transaction::File::RawReader may throw.
Any Cpanel::HTTP::Client may throw.

=cut

sub call_optimize {
    my $base_url = Cpanel::Dovecot::Solr::Utils::get_solr_base_url();

    Cpanel::HTTP::Client->new( timeout => MAX_TIME_TO_OPTIMIZE_SOLR )->die_on_http_error()->get("$base_url/solr/dovecot/update?optimize=true");

    return 1;
}

=head2 call_commit()

This function makes an HTTP connection to the solr instance and calls the commit function.

=head3 Returns

The function only returns 1.

=head3 Exceptions

Any Cpanel::Transaction::File::RawReader may throw.
Any Cpanel::HTTP::Client may throw, but die_on_http_error isn't set.

=cut

sub call_commit {

    my $base_url = Cpanel::Dovecot::Solr::Utils::get_solr_base_url();

    # This will fail if there is nothing to commit
    # and thats just fine
    Cpanel::HTTP::Client->new()->get("$base_url/solr/dovecot/update?commit=true");

    return 1;
}

=head2 check_for_library_boot_path()

This function makes an HTTP connection to the solr instance and checks for the current Java runtime
boot path. It then checks to see if that directory exists on the machine. If it runs into any problems
it throws an exception/dies.

This function was created as a way to check if Java was upgraded while the service was running.

=head3 Returns

The function only returns 1 or dies.

=head3 Exceptions

Any Cpanel::Transaction::File::RawReader may throw.
Any Cpanel::HTTP::Client may throw, die_on_http_error will be set

=cut

sub check_for_library_boot_path {
    my $base_url = get_solr_base_url();

    my $ept = "/admin/info/properties";

    my $response = Cpanel::HTTP::Client->new()->die_on_http_error()->get("$base_url/solr$ept");

    my $content = $response->content();

    die "The Dovecot Solr endpoint “$ept” returned no content.\n" if !length $content;

    local $@;

    require JSON;

    my $parsed = eval { JSON::decode_json($content) } // do {
        die "The Dovecot Solr endpoint “$ept” returned invalid JSON ($@).";
    };

    my $boot_path = $parsed->{'system.properties'};
    $boot_path &&= $boot_path->{'sun.boot.library.path'};

    # NB: cf. RFC 6901, JSON Pointer
    die "The response from the Dovecot Solr endpoint “$ept” does not include a “/system.properties/sun.boot.library.path” value.\n" if !length $boot_path;

    # This will be a directory like "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-2.b16.el7_4.x86_64/jre/lib/amd64"
    if ( !-e $boot_path ) {
        die "stat($boot_path): $!" if !$!{'ENOENT'};

        die "The Java runtime library boot path, “$boot_path”, does not exist. Java may have been updated while Solr was running. Try restarting the 'cpanel-dovecot-solr' daemon.\n";
    }

    return 1;
}

#----------------------------------------------------------------------

=head2 $number = determine_index_version_or_die( $INDEX_DIR_PATH )

Examines the contents of $INDEX_DIR_PATH to determine the index’s version
number. Throws an exception if no such number can be found or if anything
else (e.g., filesystem error) prevents determination of the number.

NB: Compare to L<Lucene’s readCommit() in SegmentInfos|https://github.com/apache/lucene-solr/blob/master/lucene/core/src/java/org/apache/lucene/index/SegmentInfos.java>.

=cut

sub determine_index_version_or_die {
    my ($index_dir_path) = @_;

    opendir my $dh, $index_dir_path or die "opendir($index_dir_path): $!";

    while ( my $node = readdir $dh ) {

        # It’s unclear what exactly the names of these files can be.
        # Thus far names like segments_1, segments_mv, and segments_fknc
        # seem to be possible.
        next if $node !~ m<\Asegments_[0-9a-z_]+\z>;

        my $path = "$index_dir_path/$node";

        require File::Slurper;
        my $content = File::Slurper::read_binary($path);

        # As of this writing the segments files follow this format:
        # - 4 bytes -- assumedly the magic?
        # - The string “segments”, preceded by a length.
        # - The index version number, as a big-endian u32.
        local $@;
        my ( $magic, $segs_str, $version ) = eval { unpack( 'a4 C/a* N', $content ) };

        if ( $@ || ( $magic ne "\x3f\xd7\x6c\x17" ) || ( $segs_str ne 'segments' ) ) {
            my $msg = $@;
            $msg .= ' - ' if $msg;

            my $extract = substr( $content, 0, 48 );
            $msg .= sprintf( "Unexpected content in “%s” (%v.02x); skipping …", $path, $extract );

            warn $msg;
        }
        else {
            return $version;
        }
    }

    die "“$index_dir_path” does not contain a recognized index version number!";
}

sub service_is_enabled {

    # TODO: Deduplicate this.
    my $file = '/etc/cpanel-dovecot-solrdisable';
    return 0 if -e $file;

    warn "stat($file): $! - proceeding as though the file is absent.\n";

    return 1;
}

1;
