#!/usr/bin/perl -wT

use strict;
use warnings;

use CGI::Simple;
use CGI::Carp qw(fatalsToBrowser);
use DBI;
use Net::Amazon::EC2;
use DateTime::Format::ISO8601;
use Archive::Zip;
use MIME::Base64;
use IO::String;
use Data::Dumper;

my $root = '/var/svn/autobuild';

my %config = load_conf("$root/manage.conf");

my $cgi = new CGI::Simple;

my $user = $cgi->remote_user;
die unless $user;

my $dbh = DBI->connect("dbi:SQLite:dbname=$root/$config{database}", '', '', { RaiseError => 1 });

my $ec2 = new Net::Amazon::EC2(
    AWSAccessKeyId => $config{aws_access_key_id},
    SecretAccessKey => $config{aws_secret_access_key},
);

my @build_options = (
    { name => 'atlas', title => 'Atlas DLL' },
    { name => 'collada', title => 'Collada DLL' },
);

my $action = $cgi->url_param('action');

if (not defined $action or $action eq 'index') {
    log_action('index');
    print_index('');

} elsif ($action eq 'start') {
    die "Must be POST" unless $cgi->request_method eq 'POST';
    log_action('start');

=pod

Only one instance may run at once, and we need to prevent race conditions.
So:
* Use SQLite as a mutex, so only one CGI script can be trying to start at once
* If the last attempted start was only a few seconds ago, reject this one since
  it's probably a double-click or something
* Check the list of active machines. If it's non-empty, reject this request.
* Otherwise, start the new machine.

=cut

    $dbh->begin_work;
    die unless 1 == $dbh->do('UPDATE state SET value = ? WHERE key = ?', undef, $$, 'process_mutex');
    my ($last_start) = $dbh->selectrow_array('SELECT value FROM state WHERE key = ?', undef, 'last_start');
    my $last_start_age = time() - $last_start;
    die "Last start was only $last_start_age seconds ago - please try again later."
        if $last_start_age < 10;
    die unless 1 == $dbh->do('UPDATE state SET value = ? WHERE key = ?', undef, time(), 'last_start');

    my $instances_status = get_ec2_status_table();
    for (@$instances_status) {
        if (instance_is_autobuild($_) and $_->{instance_state} ne 'terminated') {
            die "Already got an active instance ($_->{instance_id}) - can't start another one.";
        }
    }

    # No instances are currently active, and nobody else is in this
    # instance-starting script, so it's safe to start a new one

    my $instances = start_ec2_instance();

    $dbh->commit;

    for (@$instances) {
        $dbh->do('INSERT INTO instances VALUES (?, ?)', undef, $_->{instance_id}, DateTime->now->iso8601);
    }

    print_index(generate_status_table($instances, 'Newly started instance') . '<hr>');

} elsif ($action eq 'stop') {
    die "Must be POST" unless $cgi->request_method eq 'POST';
    my $id = $cgi->url_param('instance_id');
    $id =~ /\Ai-[0-9a-f]+\z/ or die "Invalid instance_id";
    log_action('stop', $id);
    stop_ec2_instance($id);
    print_index("<strong>Stopping instance $id</strong><hr>");

} elsif ($action eq 'console') {
    my $id = $cgi->url_param('instance_id');
    $id =~ /\Ai-[0-9a-f]+\z/ or die "Invalid instance_id";
    log_action('console', $id);
    my $output = get_console_output($id);
    $output =~ s/</&lt;/g;
    print_index("<strong>Console output from $id:</strong><pre>\n$output</pre><hr>");

} elsif ($action eq 'activity') {
    my $days = int $cgi->url_param('days') || 7;
    print_activity($days);

} else {
    log_action('invalid', $action);
    die "Invalid action '$action'";
}

$dbh->disconnect;

sub instance_is_autobuild {
    my ($instance) = @_;
    return 0 if $instance->{key_name} eq 'backupserver';
    return 1;
}

sub print_index {
    my ($info) = @_;

    my $instances_status = get_ec2_status_table();
    my $got_active_instance;
    for (@$instances_status) {
        if (instance_is_autobuild($_) and $_->{instance_state} ne 'terminated') {
            $got_active_instance = $_->{instance_id} || '?';
        }
    }

    my $status = generate_status_table($instances_status, 'Current EC2 machine status');
    print <<EOF;
Pragma: no-cache
Content-Type: text/html

<!DOCTYPE html>
<title>0 A.D. autobuild manager</title>
<link rel="stylesheet" href="manage.css">
<p>Hello <i>$user</i>.</p>
<p>
 <a href="?action=index">Refresh status</a> |
 <a href="http://wfg-autobuild-logs.s3.amazonaws.com/logindex.html">View build logs</a>
</p>
<hr>
$info
$status
<p>
EOF

    if ($got_active_instance) {
        print qq{<button disabled title="Already running an instance ($got_active_instance)">Start new build</button>\n};
    } else {
        print qq{<form action="?action=start" method="post" onsubmit="return confirm('Are you sure you want to start a new build?')">\n};
        print qq{<fieldset><legend>Build options</legend>\n};
        for (@build_options) {
            print qq{<label><input type="checkbox" name="option_$_->{name}">$_->{title}</label><br>};
        }
        print qq{<button type="submit">Start new build</button>\n};
        print qq{</fieldset></form>\n};
    }
}

sub log_action {
    my ($action, $params) = @_;
    $dbh->do('INSERT INTO activity (user, ip, ua, action, params) VALUES (?, ?, ?, ?, ?)',
        undef, $user, $cgi->remote_addr, $cgi->user_agent, $action, $params);
}

sub print_activity {
    my ($days) = @_;
    print <<EOF;
Content-Type: text/html

<!DOCTYPE html>
<title>0 A.D. autobuild activity log</title>
<link rel="stylesheet" href="manage.css">
<table>
<tr><th>Date<th>User<th>IP<th>Action<th>Params<th>UA
EOF

    my $sth = $dbh->prepare("SELECT * FROM activity WHERE timestamp > datetime('now', ?) ORDER BY id DESC");
    $sth->execute("-$days day");
    while (my $row = $sth->fetchrow_hashref) {
        print '<tr>';
        print '<td>'.$cgi->escapeHTML($row->{$_}) for qw(timestamp user ip action params ua);
        print "\n";
    }

print <<EOF;
</table>
EOF

}

sub generate_status_table {
    my ($instances, $caption) = @_;
    my @columns = (
        [ reservation_id => 'Reservation ID' ],
        [ instance_id => 'Instance ID' ],
        [ instance_state => 'State' ],
        [ image_id => 'Image ID '],
        [ dns_name => 'DNS name' ],
        [ launch_time => 'Launch time' ],
        [ reason => 'Last change' ],
    );
    my $count = @$instances;
    my $status = qq{<table id="status">\n<caption>$caption &mdash; $count instances</caption>\n<tr>};
    for (@columns) { $status .= qq{<th>$_->[1]}; }
    $status .= qq{\n};

    for my $item (@$instances) {
        $status .= qq{<tr>};
        for (@columns) {
            my $key = $_->[0];
            my $val = $item->{$key} // '';
            if ($key eq 'launch_time') {
                my $t = DateTime::Format::ISO8601->parse_datetime($val);
                my $now = DateTime->now();
                my $diff = $now - $t;
                my ($days, $hours, $minutes) = $diff->in_units('days', 'hours', 'minutes');
                my $age = "$minutes minutes ago";
                $age = "$hours hours, $age" if $hours;
                $age = "$days days, $age" if $days;
                $status .= qq{<td>$val ($age)};
            } else {
                $status .= qq{<td>$val};
            }
        }
        $status .= qq{<td><a href="?action=console;instance_id=$item->{instance_id}">Console output</a>\n};
        $status .= qq{<td><form action="?action=stop;instance_id=$item->{instance_id}" method="post" onsubmit="return confirm('Are you sure you want to terminate this instance?')"><button type="submit">Terminate</button></form>\n}
            if instance_is_autobuild($item);
    }

    $status .= qq{</table>};
    return $status;
}

sub flatten_instance {
    my ($reservation, $instance) = @_;
    return {
        reservation_id => $reservation->reservation_id,
        instance_id => $instance->instance_id,
        instance_state => $instance->instance_state->name,
        image_id => $instance->image_id,
        dns_name => $instance->dns_name,
        launch_time => $instance->launch_time,
        reason => $instance->reason,
        key_name => $instance->key_name,
    };
}

sub get_ec2_status_table {
#     return [ ];
#     return [ {
#         reservation_id => 'r-12345678',
#         instance_id => 'i-12345678',
#         instance_state => 'pending',
#         image_id => 'ami-12345678',
#         dns_name => '',
#         launch_time => '2008-12-30T17:14:22.000Z',
#         reason => '',
#     } ];

    my $reservations = $ec2->describe_instances();
    my @ret = ();
    for my $reservation (@$reservations) {
        push @ret, map flatten_instance($reservation, $_), @{$reservation->instances_set};
    }
    return \@ret;
}

sub get_console_output {
    my ($instance_id) = @_;
    my $output = $ec2->get_console_output(InstanceId => $instance_id);
    return "(Last updated: ".$output->timestamp.")\n".$output->output;
}

sub start_ec2_instance {
#     return [ {
#         reservation_id => 'r-12345678',
#         instance_id => 'i-12345678',
#         instance_state => 'pending',
#         image_id => 'ami-12345678',
#         dns_name => '',
#         launch_time => '2008-12-30T17:14:22.000Z',
#         reason => '',
#     } ];

    my $user_data = create_user_data();

    my $reservation = $ec2->run_instances(
        ImageId => $config{image_id},
        MinCount => 1,
        MaxCount => 1,
        KeyName => $config{key_name},
        SecurityGroup => $config{security_group},
        UserData => encode_base64($user_data),
        InstanceType => $config{instance_type},
        'Placement.AvailabilityZone' => $config{availability_zone},
    );

    if (ref $reservation eq 'Net::Amazon::EC2::Errors') {
        die "run_instances failed:\n".(Dumper $reservation);
    }

    return [ map flatten_instance($reservation, $_), @{$reservation->instances_set} ];
}

sub create_user_data {
    my @files = qw(run.pl run.conf);

    my $zip = new Archive::Zip;
    for (@files) {
        $zip->addFile("$root/$_", "$_") or die "Failed to add $root/$_ to zip";
    }

    my %options;
    for (@build_options) {
        $options{$_->{name}} = ($cgi->param('option_'.$_->{name}) ? 1 : 0);
    }
    my $options = Dumper \%options;
    $zip->addString($options, 'options.pl') or die "Failed to add options.pl to zip";

    my $fh = new IO::String;
    if ($zip->writeToFileHandle($fh) != Archive::Zip::AZ_OK) {
        die "writeToFileHandle failed";
    }
    return ${$fh->string_ref};
}

sub stop_ec2_instance {
    my ($instance_id) = @_;

#     return;

    $ec2->terminate_instances(
        InstanceId => $instance_id,
    );
}

sub load_conf {
    my ($filename) = @_;
    open my $f, '<', $filename or die "Failed to open $filename: $!";
    my %c;
    while (<$f>) {
        if (/^(.+?): (.+)/) {
            $c{$1} = $2;
        }
    }
    return %c;
}
