#!/usr/bin/perl
#
# This script performs an analysis of staged metadata. What we want to do is:
# - determine whether we're creating new metadata, or updating existing records, and
# - whether any of these updates are safe, or whether they would break existing matches.

use strict;

use Data::Dumper;
use Getopt::Std;
use POSIX qw(:sys_wait_h :signal_h :errno_h);

use lib '/app/tools/common/lib';
use Common::Amazon::Mail;
use Common::Process;
use Common::RSApp;
use Common::Log;
use Common::Assert;
use Common::Email;
use Common::Client;
use Common::DB::Item;

use lib '/app/tools/bookpub/lib';
use BookPub::DB::Item::CatalogImport;
use BookPub::Catalog::Import::Process::Analyzer::Factory;
use BookPub::Catalog::Import::Process::Fetcher::Factory;
use BookPub::Catalog::Import::Job::Import;

# Parse the command-line options.
#
my $gVerbosityLevel = 1;
my %gOptions;
_parseCommandLine( \%gOptions );

my $gCatalogImportID = $gOptions{importID};
assert($gCatalogImportID);
my $gClientID = $gOptions{clientID};
assert($gClientID);

# Now we fork off a child process.
# This way, if the child didn't exit cleanly, we can tell.
#
my $gChildPID = fork();
if ($gChildPID) {
    _parent();
} else {
    _child();
}

sub _child {

    # Install a signal handler to catch ctrl-c
    #
    $SIG{INT} = \&CTRL_C;

    # Instantiate the Application singleton.
    #
    my $app = Common::RSApp->new( clientID => $gClientID );

    # Fetch the CatalogImport record.
    #
    my $catalogImport = BookPub::DB::Item::CatalogImport->Lookup( catalog_import_id => $gCatalogImportID );
    die "ERROR - Invalid catalog import id $gCatalogImportID" unless $catalogImport;

    # Do I want to force the state to be 'queued for analysis', or should I allow two valid states?
    die "ERROR - catalog import id $gCatalogImportID is in an invalid state"
      unless ( BookPub::DB::Item::CatalogImport::kImportStatusAnalysisReady eq $catalogImport->status() );

    $catalogImport->analysis_start_time(Common::DB::Item::kDateTimeNow);
    $catalogImport->save();

    # Kick off the analysis

    my $analyzer =
      BookPub::Catalog::Import::Process::Analyzer::Factory::NewAnalyzer( importRecord => $catalogImport, logLevel => $gVerbosityLevel );
    my $exitCode = $analyzer->run();

    # Set the state to 'import ready', which means that this Import is ready to proceed to the
    # final, import step.
    #
    $catalogImport->status(BookPub::DB::Item::CatalogImport::kImportStatusImportReady);
    $catalogImport->analysis_end_time(Common::DB::Item::kDateTimeNow);
    $catalogImport->save();

    # Queue up the import job.
    #
    my $job = BookPub::Catalog::Import::Job::Import->new( importID => $gCatalogImportID, logLevel => $gVerbosityLevel );
    $job->enqueue();

    # Record the new job id in the catalog import record
    #
    $catalogImport->import_job_id( $job->id() );
    $catalogImport->save();

    exit($exitCode);
}

my $gChildExitCode;
my $gChildIsRunning;

sub _parent {

    # Install a 'reaper' signal handler.  This is a callback that gets invoked
    # when the child process exits.
    #
    local $SIG{CHLD} = \&REAPER;

    # We will install a signal handler in the _child_ to catch SIGINT (i.e ctrl-c), so
    # we will want to ignore that signal here in the parent.
    #
    local $SIG{INT} = 'IGNORE';
    $gChildIsRunning = 1;
    while ($gChildIsRunning) {

        # Basically, we just wait around until the child exits.
        # !!! Is it possible to use sleep(0) here?   Will the kernel wake us up (i.e. cause
        # sleep to return) if we catch a signal?  That would be better...
        #
        sleep(2);
    }

    # The Process object is generally responsible for updating the Import::Record state.
    # However, if something odd happened, we'll have a chance here to _attempt_ to record that state.
    #
    if ( 0 != $gChildExitCode ) {
        my $status;
        if ( 1 == $gChildExitCode ) {
            $status = BookPub::DB::Item::CatalogImport::kImportStatusAborted;
        } else {
            $status = BookPub::DB::Item::CatalogImport::kImportStatusErrorAnalysis;
        }

        #  Note that we wait until the last moment here to instantiate a Singleton.  That's by design -
        #  in most cases the parent process doesn't _need_ a singleton, and it gets weird and confusing to
        #  have two global application objects around.  But in this case, the child is dead anyway, so it's
        #  not going to cause any weird issues.
        #
        my $app = Common::RSApp->new( clientID => $gClientID );
        my $catalogImport = BookPub::DB::Item::CatalogImport->Lookup( catalog_import_id => $gCatalogImportID );
        die "ERROR - Invalid catalog import id $gCatalogImportID" unless $catalogImport;

        $catalogImport->status($status);
        $catalogImport->analysis_end_time(Common::DB::Item::kDateTimeNow);
        $catalogImport->save();

        # Send an email reporting the problem.
        #
        if ( Common::RSApp::IsProductionServer() ) {
            my $client = Common::Client->new( clientID => $gClientID );
            my $clientName = $client->ClientName();
            my $emailTo = Common::RSApp::GetConfig( 'bookpub', 'error_report_email' );
            my $subject = "FATAL Error Report";
            my $body = "Client ID $gClientID ( $clientName ), Catalog Import ID $gCatalogImportID : Analysis Failed";

            my $fetchProcess = BookPub::Catalog::Import::Process::Fetcher::Factory::NewFetcher( importRecord => $catalogImport, logLevel => $gVerbosityLevel );
            my $errorFilePath = $fetchProcess->errorFilePath();

            my $analyzeLogPath = $errorFilePath;
            $analyzeLogPath =~ s/ERROR_LOG/analyze_log/;

            my $mail = Common::Amazon::Mail->new;
            $mail->to( $emailTo );
            $mail->subject($subject);
            $mail->body($body);

            if ( -f $errorFilePath ) {
                $mail->sendit( [$errorFilePath] );
            } elsif ( -f $analyzeLogPath ) {
                $mail->sendit( [$analyzeLogPath] );
            } else {
                $mail->sendit();
            }
        }
    }

    exit(0);
}

sub REAPER {

    # -1 means 'any child process'
    # WNOHANG means to not block until something exits - waitpid will return immediately.
    #
    my $deadChildPID = waitpid( -1, &WNOHANG );

    if ( -1 == $deadChildPID ) {

        # just kidding... ignore this.
        #
    } elsif ( WIFEXITED($?) ) {

        # Fetch the exit status code.  This requires an 8 bit shift...
        #
        $gChildExitCode = $? >> 8;

        # Set the flag which tells the parent the child is done.
        #
        $gChildIsRunning = 0;
    }

    # In some systems, once a signal handler is invoked, we need to re-install it.
    #
    $SIG{CHLD} = \&REAPER;
}

sub CTRL_C {
    Common::Log::Print("caught a ctrl-c, aborting");
    exit(1);
}

sub _parseCommandLine {
    my ($settings) = @_;

    my %opt;
    getopts( 'i:c:V:', \%opt );

    if ( !$opt{i} || !$opt{c} ) {
        _usage();
        exit(1);
    }
    $settings->{importID} = $opt{i};
    $settings->{clientID} = $opt{c};

    if ( defined $opt{V} ) {
        $gVerbosityLevel = $opt{V};
    }
}

sub _usage {
    print "\nusage: $0 -c client_id -f path_to_XML_file [-V n]\n";
    print "\n";
    print "Arguments:\n";
    print "\t-c <client_id>\t\tThe client_id of the client to process\n";
    print "\t-i <catalog_import_id>\tThe ID of the catalog import record we're processing.\n";
    print "\t-V <N>\t\t\tVerbosity level - 0 means no output\n";
}

