#!/opt/local/bin/perl5.34

use strict;

use Cwd;
use YAML qw(LoadFile);
use JSON qw(decode_json);
use LWP::UserAgent;
use HTTP::Request::Common;
use File::Basename qw(dirname);

use constant VERSION => '1.1.0';

#
# signal an error message and exit
#
sub error
{
    my ($message) = @_;

    print "Fatal Error: $message\n";
    exit 1;
}

#
# makes a request, adding any required auth info
#
sub make_request
{
    my ($req) = @_;
    my $ua = LWP::UserAgent->new;

    $req->authorization_basic(
        $ENV{'THIERIOT_USER'},
        $ENV{'THIERIOT_TOKEN'}
    );

    return $ua->request( $req );
}

#
# perform a GET and return the content
#
sub get
{
    my ($url) = @_;
    my $res = make_request(GET $url);

    return $res->is_error
        ? ''
        : $res->decoded_content;
}

#
# fetches json from the specfied url
#
sub fetch_json
{
    my ($url) = @_;
    my $data = get($url);

    return $data
        ? decode_json($data)
        : 0;
}

#
# returns the projects name
#
sub project_name
{
    my ($config) = @_;
    my $configuredName = $config->{project_name};

    return $configuredName
        ? $configuredName
        : basename(getcwd);
}

#
# creates and returns a url to jenkins
#
sub jenkins_url
{
    my ($config, $url) = @_;

    return "http://$config->{jenkins_url}$url";
}

#
# fetch the url relative to a branch's job
#
sub job_url
{
    my ($config, $branch, $url) = @_;
    my $project = project_name($config);

    return jenkins_url($config, "/job/$project-$branch$url");
}

#
# Fetches the JSON for a build number
#

sub build_json
{
    my ($config, $branch, $number) = @_;
    my $relative = sprintf('/%s/api/json', $number);
    my $url = job_url($config, $branch, $relative);

    return fetch_json($url);
}

#
# Fetch the json for the specified job
#

sub job_json
{
    my ($config, $branch) = @_;
    my $url = job_url($config, $branch, '/api/json');
    my $json = fetch_json($url);

    if (!$json) {
        error("job does not exist '$branch'");
    }

    return $json;
}

#
# returns url for a jobs config
#
sub config_url
{
    my ($config, $branch) = @_;

    return job_url($config, $branch, '/config.xml');
}

#
# fetches the projects jobs from jenkins
#
sub fetch_jobs
{
    my ($config) = @_;
    my $url = jenkins_url($config, '/api/json');
    my $json = fetch_json($url);
    my $project = project_name($config);
    my %jobs;

    foreach (@{$json->{jobs}}) {
        if ($_->{name} =~ m/^$project-(.*)/) {
            $jobs{$1} = $_;
        }
    }

    return %jobs;
}

#
# returns array of local branch names
#
sub fetch_branches
{
    my @branches;

    foreach (`git branch`) {
        push(@branches, substr($_, 2, -1));
    }
    
    return @branches;
}

#
# fetch a branch name by id
#
sub fetch_branch
{
    my ($id) = @_;
    my @branches = fetch_branches();
    my $name = ($id =~ /^\d+$/)
        ? $branches[$id - 1]
        : $id;

    if (!$name) {
        error('Invalid branch ID')
    }

    return $name;
}

#
# fetches the xml config for a branch
#
sub fetch_branch_config
{
    my ($config, $branch) = @_;
    my $url = config_url($config, $branch);

    return get($url);
}

#
# indicates if a job is currently building
#
sub is_building
{
    my ($job) = @_;

    return $job && substr($job->{color},-5) eq 'anime';
}

#
# indicates if the job is failing
#
sub is_failing
{
    my ($job) = @_;
    my $color = $job->{color};

    return $job && $color eq 'red';
}

#
# lists all branches, indicating if they have a job
#
sub branch_list
{
    my ($config) = @_;
    my %jobs = fetch_jobs($config);
    my @branches = fetch_branches();
    my $index = 1;

    foreach (@branches) {
        my $job = $jobs{$_};
        my $name = is_failing($job)
            ? "\e[0;41m$_\e[0m"
            : "\e[0;36m$_\e[0m";
        my $exists = $job
            ? "\e[0;36m✓\e[0m"
            : "\e[0;31m✘\e[0m";
        my $building = is_building($job)
            ? ' (building)'
            : '';

        print "$exists \e[0;32m[$index]\e[0m\t$name$building\n";
        $index++;
    }
}

#
# builds a branch specfied by index
#
sub branch_build
{
    my ($config, $branch) = @_;
    my $project = project_name($config);

    make_request(POST job_url($config, $branch, '/build'));
}

#
# creates a new branch specified by index
#
sub branch_create
{
    my ($config, $branch) = @_;
    my $project = project_name($config);
    my $xml = fetch_branch_config($config, 'master');

    $xml =~ s!<name>master</name>!<name>$branch</name>!;

    make_request(POST jenkins_url($config, '/createItem'),
                      [ mode => 'copy',
                        from => "$project-master",
                        name => "$project-$branch" ]);

    make_request(POST config_url($config, $branch),
                      Content_Type => 'text/xml',
                      Content => $xml);

    sleep(2);

    branch_build($config, $branch);
}

#
# allows deleting a branch
#
sub branch_delete
{
    my ($config, $branch) = @_;
    my $project = project_name($config);

    if ($branch eq 'master') {
        error('You cannot delete the master build!');
    }

    make_request(POST job_url($config, $branch, '/doDelete'));
}

#
# cancel the last job for a branch
#

sub branch_cancel
{
    my ($config, $branch) = @_;
    my $job = job_json($config, $branch);
    my $number = $job->{lastBuild}->{number};
    my $url = job_url($config, $branch, sprintf('/%d/stop', $number));

    print "Cancelling job #$number for '$branch'\n";

    make_request(POST $url);
}

#
# Waits for a build to start
#

sub wait_for_job
{
    my ($config, $branch, $number) = @_;

    while (1) {
        my $build = build_json($config, $branch, $number);
        if ($build && $build->{building}) {
            return;
        }
        sleep(1);
    }
}

#
# Fetch the URL for progressive job output
#

sub follow_url
{
    my ($config, $branch, $number) = @_;
    my $relative = sprintf('/%d/logText/progressiveText', $number);

    return job_url($config, $branch, $relative); 
}

#
# Return response from the follow from the URL, with the given
# start point as the offset
#

sub follow_content
{
    my ($url, $start) = @_;

    return make_request(POST $url, [start => $start]);
}

#
# Return the current offset of the job output
#

sub follow_offset
{
    my ($config, $branch, $number) = @_;
    my $url = follow_url($config, $branch, $number);
    my $res = follow_content($url, 0);

    return $res
        ? $res->header('x-text-size')
        : 0;
}

#
# Follow the output of the job at the URL (where the URL
# is the progressive output url)
#

sub follow_job
{
    my ($config, $branch, $number, $start) = @_;
    my $url = follow_url($config, $branch, $number);

    while (1) {
        my $res = follow_content($url, $start);
        my $content = $res->decoded_content;
        if ($content) {
            print $content;
            $start = $res->header('x-text-size');
        }
        else {
            my $build = build_json($config, $branch, $number);
            if (!$build->{building}) {
                return;
            }
        }
        sleep(1);
    }
}

#
# Follow the branches build output, starting a job if one
# isn't already running.
#

sub follow_branch
{
    my ($config, $branch) = @_;
    my $job = job_json($config, $branch);
    my $lastBuild = build_json($config, $branch, $job->{lastBuild}->{number});

    if ($lastBuild->{building}) {
        my $number = $lastBuild->{number};
        my $start = follow_offset($config, $branch, $number);
        print "Following job #$number for '$branch'\n\n";
        follow_job($config, $branch, $number, $start);
    }
    else {
        my $number = $job->{nextBuildNumber};
        print "Created job #$number for '$branch'\n\n";
        branch_build($config, $branch);
        wait_for_job($config, $branch, $number);
        follow_job($config, $branch, $number, 0);
    }
}

#
# prints usage information
#
sub print_help
{
    print <<EOT;
Usage: trt ACTION ID

    create ID       Create a job for branch ID
    build ID        Build job for branch ID
    delete ID       Delete job for branch ID
    view ID         Open browser to the jobs page for the branch ID
    follow ID       Follow a build, starting if needed 
EOT

}

#
# tries to read the config file, return hash
#
sub read_config
{
    my ($dir) = @_;
    my $config = '.thieriot.yml';
    my $path = "$dir/$config";

    if (-e $path) {
        return LoadFile($path);
    }
    else {
        if ($dir eq '/') {
            error("Config file '$config' not found...");
        }
        return read_config(dirname($dir));
    }
}

#
# main
#

my($action, $id) = @ARGV;
my $config = read_config(getcwd);
my $branch = fetch_branch($id) unless !$id;

print "Thieriot " . VERSION . " (help: 'trt help')\n\n";

if ($action eq "create") {
    branch_create($config, $branch);
    print "Created branch '$branch', and now building!\n";
}

elsif ($action eq "build") {
    branch_build($config, $branch);
    print "Branch '$branch' now building!\n";
}

elsif ($action eq "delete") {
    branch_delete($config, $branch);
    print "Branch '$branch' deleted!\n";
}

elsif ($action eq "cancel") {
    branch_cancel($config, $branch);
}

elsif ($action eq "view") {
    my $url = job_url($config, $branch, '/');
    `open $url`;
    print "Browser opened.\n";
}

elsif ($action eq "follow") {
    follow_branch($config, $branch);
}

else {
    if ( $action ) {
        print_help();
    }
    else {
        branch_list($config);
    }
}

print "\n";

