#!/usr/bin/perl
use strict;
my($app_svnid) = '$HeadURL$ $LastChangedRevision$';
#  Allow bare words, so &ade_err_error() calls look nicer.
no strict 'subs';
use lib substr `ade-config ade_include_prefix`,0,-1;
use ADE;
use lib substr `js-config js_include_prefix`,0,-1;

my(%jss_defined_errors) = (
    jss_err_misc         => { fmt => "%s" },
);

#  Options
my(%opt_resources);

#  Other globals

sub jss
{
    my($errstack_ref) = @_;
    my($rc, @select_results, $job_id, $dbh, $command, $i, $sql_statement, $resource_id);
    my(@local_errstack);
    my($db_file, $jsd_pid, $running_flag);
    my($lock_file);

    #  Set ADE options
    &ade_err_registerdefderrs(\%jss_defined_errors);
    &ade_err_resetstack($errstack_ref, dumpall=>0);

    #  Defaults for options 
    #  (identical line in jsd, jss, jsq).

    #  Register options
    if (($rc=&ade_opt_register($errstack_ref, "r:s", "resources:s", "main::jss_opt_handler_%s")) != $ade_err_ok) {
        return($rc);
    }

    #  Register handler functions
    if (($rc=&ade_msg_register($errstack_ref, \&jss_usage, \&jss_version, \&jss_listpaths)) != $ade_err_ok) {
        return($rc);
    } 

    #  Process options
    if (($rc=&ade_opt_process($errstack_ref)) != $ade_err_ok) {
        return($rc);
    } 

    #  Process arguments
    &ade_msg_usage($errstack_ref, \&app_usage, 1) if ($#ARGV+1 < 1);
    $command = join(' ', @ARGV);
    &ade_err_debug($errstack_ref, 10, "jss: command=\"$command\"");

    #  Sanity checks and derivations
    #  Check resources
    if (scalar(keys(%opt_resources)) == 0) {
        &ade_err_error($errstack_ref, jss_err_misc, "no resources specified");
        return($ade_err_fail);
    }
    foreach $resource_id (keys(%opt_resources)) {
       if ($resource_id !~ /^[a-z][a-z0-9_]*$/) {
           &ade_err_error($errstack_ref, jsd_err_misc, "$resource_id: invalid resource name (hint: it should be word-like)");
           return($ade_err_fail);
       }
       &ade_err_debug($errstack_ref, 10, "jsd: resource_id=$resource_id, quantity_wanted=$opt_resources{$resource_id}{quantity_wanted}");
       if (!defined($opt_resources{$resource_id}{quantity_wanted}) or $opt_resources{$resource_id}{quantity_wanted} !~ /^[1-9][0-9]*$/) {
           &ade_err_error($errstack_ref, jsd_err_misc, (defined($opt_resources{$resource_id}{quantity_wanted}) ? $opt_resources{$resource_id}{quantity_wanted} : "") . ": invalid quantity (hint: should be an integer)");
           return($ade_err_fail);
       }
    }
    $lock_file = $ENV{JSD_LOCKFILE} ? $ENV{JSD_LOCKFILE} : "$ade_tmp_dir/jsd-$ENV{'LOGNAME'}.lock";
    if (($rc=&ade_lck_checklock($errstack_ref, $lock_file, \$jsd_pid, \$running_flag)) != $ade_err_ok) {
        &ade_err_debug($errstack_ref, 20, "ade_lck_lock: ade_lck_checklock failed");
        return($rc);
    }
    if (!$running_flag) {
        &ade_err_error($errstack_ref, jss_err_misc, "jsd: not running");
        return($ade_err_fail);
    }
    #  Predicable path to database. Tests should not normally need to access the database.
    $db_file = "/var/tmp/jsd-$jsd_pid.sqlite";

    #  Initialise database
    if (! -w $db_file) {
        &ade_err_error($errstack_ref, jss_err_misc, "$db_file: not accessible");
        return($ade_err_fail);
    }
    my %attr = ( RaiseError => 1, HandleError => sub { return 1; } );
    if (!defined($dbh = DBI->connect("dbi:SQLite:dbname=$db_file","","", \%attr))) {
        &ade_err_error($errstack_ref, jss_err_misc, $DBI::errstr);
        return($ade_err_fail);
    }

    #  Enforce foreign key constraints
    $dbh->do("PRAGMA foreign_keys = ON");
    $dbh->sqlite_busy_timeout(10000);

    #  Check the list of all provided resources matches the list of all resources.
    $sql_statement = "SELECT resource_id FROM resources;";
    if (($rc=&ade_sql_select($errstack_ref, $dbh, \@select_results, $sql_statement)) != $ade_err_ok) {
        return($rc);
    }
    #  Simple but expensive array comparison. Compare number of elements first ...
    if ($#select_results+1 != scalar(keys(%opt_resources))) {
        &ade_err_error($errstack_ref, jss_err_misc, "resource mismatch (\$#select_results+1=" . sprintf("%d",$#select_results+1) . ", scalar(keys(%opt_resources))=" . sprintf("%d",scalar(keys(%opt_resources))));
        return($ade_err_fail);
    }
    #  ... then compare successive elements of sorted arrays.
    for ($i=0; $i<$#select_results+1; $i++) {
        if ((sort(map { ${$select_results[$_]}[0] } (0..$#select_results)))[$i] ne (sort(keys(%opt_resources)))[$i]) {
            &ade_err_error($errstack_ref, jss_err_misc, sprintf("resource mismatch (\$i=%d, (sort(map { \${\$select_results[\$_]}[0] } (0..$#select_results)))[$i]=%s, (sort(keys(%opt_resources)))[$i]=%s", $i, (sort(map { ${$select_results[$_]}[0] } (0..$#select_results)))[$i], (sort(keys(%opt_resources)))[$i]));
            return($ade_err_fail);
        }
    }

    #  Let the database decide the job id but do the whole thing in one transaction
    $sql_statement = "begin transaction;";
    if (($rc=&ade_sql_execute($errstack_ref, $dbh, $sql_statement)) != $ade_err_ok) {
        return($rc);
    }
    #  Escape single quotes in $command else the quoting in the next line doesn't work.
    $command =~ s/'/''/g;

    $sql_statement = "INSERT INTO jobs (command) VALUES('$command');";
    if (($rc=&ade_sql_execute($errstack_ref, $dbh, $sql_statement)) != $ade_err_ok) {
        $sql_statement = "rollback transaction;";
        &ade_sql_execute(\@local_errstack, $dbh, $sql_statement);
        return($rc);
    }
    $job_id = $dbh->last_insert_id("","","","");
    &ade_err_debug($errstack_ref, 10, "main: \$job_id=$job_id");

    foreach $resource_id (keys(%opt_resources)) {
        $sql_statement = "INSERT INTO pending_job_resources VALUES ($job_id, '$resource_id', $opt_resources{$resource_id}{quantity_wanted});";
        if (($rc=&ade_sql_execute($errstack_ref, $dbh, $sql_statement)) != $ade_err_ok) {
            $sql_statement = "rollback transaction;";
            &ade_sql_execute(\@local_errstack, $dbh, $sql_statement);
            return($rc);
        }
    }
    undef %opt_resources;			#  not needed any more

    #$sql_statement = "INSERT INTO commands (command) VALUES ('scan_jobs');";
    #if (($rc=&ade_sql_execute($errstack_ref, $dbh, $sql_statement)) != $ade_err_ok) {
    #    $sql_statement = "rollback transaction;";
    #    &ade_sql_execute(\@local_errstack, $dbh, $sql_statement);
    #    return($rc);
    #}

    $sql_statement = "end transaction;";
    if (($rc=&ade_sql_execute($errstack_ref, $dbh, $sql_statement)) != $ade_err_ok) {
        return($rc);
    }
    &ade_err_info($errstack_ref, "'$command' submitted with job id $job_id"); 

    #  Tell daemon something is in command queue.
    &ade_err_debug($errstack_ref, 10, "jss: waking jsd ...");
    if (kill(USR1,$jsd_pid) != 1) {
        &ade_err_error($errstack_ref, jss_err_misc, "failed to nudge process $jsd_pid");
        return($ade_err_fail);
    }

    #  Tidy up
    $dbh->disconnect;

    return($ade_err_ok);
}

sub jss_opt_handler_r
{
    my($errstack_ref, $resource_specs_string) = @_;
    my($resource_spec, $resource_id, $quantity_wanted);

    foreach $resource_spec (split(/,/, $resource_specs_string)) {
        ($resource_id, $quantity_wanted) = split(/:/, $resource_spec);
        $opt_resources{$resource_id}{quantity_wanted} = $quantity_wanted;
    }

    return $ade_err_ok;
} 

sub jss_opt_handler_resources
{
    return(&jss_opt_handler_r(@_));
}

sub jss_version
{
    my($errstack_ref, $version_text_ref) = @_;

    return(&ade_smf_extractversionfromsvnstring($errstack_ref, $app_svnid, $version_text_ref));
}

sub jss_listpaths
{
    my($errstack_ref, $pathlist_text_ref) = @_;
    my($rc);

    #${$pathlist_text_ref} = "";

    return($ade_err_ok);
}

sub jss_usage
{
    my($errstack_ref, $usage_text_ref, $passno) = @_;

    if ($passno == 1) {
        ${$usage_text_ref} = "\n";
    } elsif ($passno == 2) {
        ${$usage_text_ref} = "         -r ...    | --resources=...         set system resources\n";
    }

    return($ade_err_ok);
}

&ade_gep_main(\&jss);
