#!perl -w

use strict;
use warnings;
use Net::Fastly;
use File::Basename;
use Data::Dumper;
use POSIX qw(strftime);
use YAML;

=head1 NAME

fastly - a command line shell for interacting with the Fastly infrastructure

=head1 USAGE

    fastly [option[s]] 

=head1 CONFIGURATION

You can either have a config file in either ~/.fastly or /etc/fastly with

    api_key = <key>
    
or a config file with

    user     = <login>
    password = <password>

Alternatively you can pass in any of those options on the command line

    fastly --api_key  <key>
    fastly --user <login> --password <password>
    
=head1 PROXYING

There are three ways to proxy:

The first method is to put a proxy option in your .fastly file (or pass it in on)

    proxy = http://localhost:8080
    
The second is to pass it in on the command line

    fastly --user <login> --password <password> --proxy http://localhost:8080

Lastly, the third method is to set your C<https_proxy> environment variable. So, in Bash

    % export https_proxy=http://localhost:8080

or in CSH or TCSH

    % setenv https_proxy=http://localhost:8080    

=head1 DESCRIPTION
    

=cut


my %opts   = Net::Fastly::get_options($ENV{HOME}."/.fastly", "/etc/fastly");
my $fastly = Net::Fastly->new(%opts);

my $customer = $fastly->current_customer;

use Term::ShellUI;
my $term = Term::ShellUI->new(app => "fastly", keep_quotes => 0,  history_file => $ENV{HOME}."/.fastly_history", prompt => "fastly> ");
          
#$term->{debug_complete}=5;
 
print 'Using '.$term->{term}->ReadLine."\n";
use Term::ANSIColor qw(:constants);

my $basecommands = { 
    "help" => {
        desc => "Print helpful information",
        args => sub { shift->help_args(undef, @_); },
        method => sub { shift->help_call(undef, @_); }
    },
    "quit" => { 
        desc => "Quit", 
        maxargs => 0,
        method => sub { shift->exit_requested(1) },
        exclude_from_history => 1,
    },
    "who" => {
        desc => "Print who is logged in",
        proc => \&who,
    }, 
    "show" => {
        desc => "Display objects",
        cmds => {
            service => {
                desc => "Show a service",
                minargs => 1,
                maxargs => 3,
                args => [\&list_service_complete, sub { ['version'] }, \&list_version],
                proc => \&show_service,
            },
            services => {
                desc => "List services",
                proc => \&show_services,
            },
            versions => {
                desc    => "Show all the versions for a service",
                cmds    => {
                    service => {
                        desc    => "The service name",
                        minargs => 1,
                        maxargs => 1,
                        args    => [\&list_service_complete ],
                        proc    => \&show_versions,
                    }
                }
            },
            stats => {
                desc => "Show stats for a service",
                cmds => {
                    service => {
                        desc    => "The service name",
                        minargs => 1,
                        args => [\&list_service_complete, sub { ['aggregate'] }, sub { ['by']}, sub { [qw(minute hour day)]}, sub { ['split'] }, sub { ['by']} , sub { ['datacenter'] }, undef],
                        proc => \&show_stats_service,
                    },
                    # all => {
                    #                    args => [sub { ['aggregate'] }, sub { ['by']}, sub { [qw(minute hour day)]}, sub { ['split'] }, sub { ['by']} , sub { ['datacenter'] }, undef],
                    #                    proc => \&show_stats_all,
                    #                    }
                }        
            },
            diff => {
                desc => "Difference between two versions",
                minargs => 5,
                maxargs => 5,
                proc => \&diff,
                args => [\&list_service_complete, sub { ['version'] }, \&list_version, sub { ['to'] }, \&list_version, undef],
            },
        }
    },
    "create" => {
        desc => "Create a new object",
        cmds => {
            service => {
                desc    => "The service name",
                minargs => 1,
                args => [\&list_service_complete, sub { ['version'] }, \&list_version, \&service_models, \&name_map_complete, \&model_args],
                proc => \&create,
            },
        },
    },
    "delete" => {
        desc => "Delete an object",
        cmds => {
            service => {
                desc    => "The service name",
                minargs => 1,
                args => [\&list_service_complete, sub { ['version'] }, \&list_version, \&service_models, \&list_objects, undef],
                proc => \&delete,
            }
        }
    },
    "set" => {
        desc => "Update an object",
        cmds => {
            service => {
                desc    => "The service name",
                minargs => 5,
                args => [\&list_service_complete, sub { ['version'] }, \&list_version, \&service_models, \&list_objects, \&model_args],
                proc => \&set,
            }
        }
    },
    "upload" => {
        desc => "Upload a custom VCL file",
        cmds => {
            service => {
                desc    => "The service name",
                minargs => 5,
                args => [\&list_service_complete, sub { ['version'] }, \&list_version, sub { ['vcl']}, \&list_objects, sub { ['from'] }, sub { ['file'] },
                        sub { shift->complete_files(@_) }, undef],
                proc => \&upload,
            }
        }
    },
    "validate" => {
        desc => "Check to see that the currently uploaded VCL is valid",
        cmds => {
            service => {
                desc    => "The service name",
                minargs => 3,
                maxargs => 3,
                args => [\&list_service_complete, sub { ['version'] }, \&list_version],
                proc => \&validate,
            }
        }
    },
    "dump" => {
        desc => "Show the generated VCL for a given service",
        cmds => {
            service => {
                desc    => "The service name",
                minargs => 3,
                maxargs => 3,
                args => [\&list_service_complete, sub { ['version'] }, \&list_version],
                proc => \&dump,
            }
        }
    },
    "activate" => {
        desc => "Activate a version for use",
        cmds => {
            service => {
                desc    => "The service name",
                minargs => 3,
                maxargs => 3,
                args => [\&list_service_complete, sub { ['version'] }, \&list_version],
                proc => \&activate,
            }
        }
    },
    "purge" => {
        desc => "Remove objects from the cache",
        cmds => {
            service => {
                desc    => "The service name",
                minargs => 1,
                maxargs => 1,
                args => [\&list_service_complete],
                proc => \&purge,
            }
        }
    },
    "clone" => {
        desc => "Clone a configuration so that the new version can be modified",
        cmds => {
            service => {
                desc    => "The service name",
                minargs => 3,
                maxargs => 3,
                args => [\&list_service_complete, sub { ['version'] }, \&list_version],
                proc => \&clone,
            }
        }
    }
};


sub who {
    if ($fastly->client->fully_authed) {
        my $user = $fastly->current_user;
        print "Logged in as ".$user->name." <".$user->login."> of ".$customer->name."\n";
    } else {
        print "Logged with api key as ".$customer->name."\n";
    } 
}

sub name_map_complete {
    my $t = shift;
    my $c = shift;
    my $commands = $fastly->commands;
    my $command  = $commands->{"$c->{args}->[3].create"};
    if ($command->{name}) {
        return (RED . "    \nNAME" . RESET);
    } else {
        return ["MAP"];
    }
}

sub show_stats_service {
    my $service_name = shift;
    my ($service)    = $fastly->search_services(name => $service_name);
    show_stats($service, @_);
}

sub show_stats {
    my $service = shift; 
    my $type = "minutely";
    if ($_[2]) {
        $type = "hourly" if ($_[2] eq 'hour');
        $type = "daily" if ($_[2] eq 'day');
    }

    my $stats = eval { $service->stats($type) }; 
    unless ($stats) {
        print BOLD, RED, "Error: ", RESET, $@,"\n";
        return;
    }

    my $split = 1;
    
    printf(BOLD."%-21s%-5s%-10s%-10s%-6s%-10s%-6s%5s   %-7s %-7s %-7s".RESET."\n", "Time", "DC", "Bytes", "Requests", "Hits", "Misses", "Pass", "%", "200", "304", "5xx");

    unless (keys %$stats) {
        print BOLD, RED, "No stats available", RESET, "\n";
        return;
    }
  
    my @stats;
    my $last;
    my $total = {start_time => time, datacenter => ''} ;
    foreach my $datacenter (keys %$stats) {
        my $stat = $stats->{$datacenter};
        next unless ref($stat);
        $stat->{datacenter} = $datacenter;
        $stat->{start_time} = $stats->{recorded};
        foreach (qw(hits requests pass uncacheable body_size header_size miss status_5xx status_200 status_304)) {
            $total->{$_} += $stat->{$_};
        }
        if ($split) {
            push @stats, $stat;
        } else {
            unless ($last) {
                $last = $stat;
                next;
            }
            if ($last->{start_time} != $stat->{start_time}) {
                push @stats, $last;
                $last = $stat;
            } else {
                foreach (qw(hits requests pass uncacheable body_size header_size miss status_5xx status_200 status_304)) {
                    $last->{$_} += $stat->{$_};
                }
            }
        }
    }
    push @stats, $last unless $split;
    push @stats, undef, $total;
    foreach my $stats (@stats) {
        unless ($stats && keys %$stats>2) {
            print "----\n";
            next;
        }
        my $ymd = strftime "%Y:%m:%d", gmtime($stats->{start_time});
        my $hms = strftime "%H:%M:%S", gmtime($stats->{start_time});
        
        my $size = $stats->{body_size} + $stats->{header_size};
        if ($size > 1024*1024*1024) {
            $size = sprintf("%.2f GB", $size / 1024 / 1024 / 1024);
        } elsif ($size > 1024*1024) {
            $size = sprintf("%.2f MB", $size / 1024 / 1024);
        } elsif ($size > 1024) {
            $size = sprintf("%.2f KB", $size / 1024);
        } else {
            $size = sprintf("%d B", $size);
        }
        my $hitp = "00.0%";
        if ($stats->{hits}) {
            $hitp = sprintf("%.1f%%", (($stats->{hits}/($stats->{requests}-$stats->{pass}-$stats->{uncacheable}))*100));
        }
        my $dc = "ALL";
        $dc = $stats->{datacenter} if ($split);
        printf("%s%-11s%-10s%s%s%-5s%s%-10s%-10s%-6s%-10s%-6s%-6s  %-7s %-7s %s%-7s%s\n", RED, $ymd, $hms, RESET, BLUE, $dc, RESET, $size, $stats->{requests}, $stats->{hits}, $stats->{miss}, $stats->{pass}, $hitp, $stats->{status_200}, $stats->{status_304}, RED, $stats->{status_5xx}, RESET);
    }
}

sub clone {
    my $service_name   = shift;
    shift;
    my $version_number = shift;
    
    my ($service) = $fastly->search_services(name => $service_name);
    my $version   = $service->version($version_number);
    my $new       = eval { $version->clone };
    if ($@) {
        print BOLD, RED, "Error: ", RESET, $@,"\n";
    } else {
        print "Version $version_number cloned to ".$new->number."\n";
    }
}

sub dump {
    my $service_name   = shift;
    shift;
    my $version_number = shift;

    my ($service) = $fastly->search_services(name => $service_name);
    my $version   = $service->version($version_number);
    my $vcl       = eval { $version->generated_vcl };
    if ($@) {
        print BOLD, RED, "Error: ", RESET, $@,"\n";
    } else {
        print present({$vcl->_as_hash});
    }
}

sub validate {
    my $service_name   = shift;
    shift;
    my $version_number = shift;

    my ($service)  = $fastly->search_services(name => $service_name);
    my $version    = $service->version($version_number);
    my $valid      = eval { $version->validate };
    
    if ($@) {
        print BOLD, RED, "Error: ", RESET, $@,"\n";
    } else {
        print "Config validated\n";
    }
}

sub activate {
    my $service_name   = shift;
    shift;
    my $version_number = shift;

    my ($service)  = $fastly->search_services(name => $service_name);
    my $version    = $service->version($version_number);
    my $response   = eval { $version->activate};
    
    if ($@) {
        print BOLD, RED, "Error: ", RESET, $@,"\n";
    } else {
        print "Set $service_name version $version_number to active\n";
    }
}

sub purge {
    my $service_name = shift;
    my $version      = shift;
    my ($service)    = $fastly->search_services(name => $service_name);
    my $ok           = eval { $service->purge_all };
    if ($@) {
        print BOLD, RED, "Error: ", RESET, $@,"\n";
    } else {
        print "Purged\n";
    }
}

use File::Temp qw(tempfile);
sub diff {
    my $service_name = shift;
    shift;
    my $from_version = shift;
    shift;
    my $to_version = shift;
    my $from = eval { $fastly->search_services(name => $service_name, version => $from_version) };
    if ($@) { print BOLD, RED, "Error: ", RESET, $@,"\n"; return };
    my $to = eval { $fastly->search_services(name => $service_name, version => $to_version) };
    if ($@) { print BOLD, RED, "Error: ", RESET, $@,"\n"; return };
    
    if ($to->{type} eq 'error') { print BOLD, RED, "Error: ", RESET, $to->{data}->{error},"\n"; return };


    my ($to_fh, $to_fn) = tempfile();
    my ($from_fh, $from_fn) = tempfile();
    print $to_fh present({$to->_as_hash});
    print $from_fh present({$from->_as_hash});
    my $diff = `diff -u $from_fn $to_fn`;
    print "$diff";
    print "\n\n";

}

sub set {
    my $service_name = shift;
    shift;
    my $version      = shift;
    my $type         = shift;
    
    

    my $service  = eval { $fastly->search_services(name => $service_name ) };
    if ($@ || !$service) {
        print BOLD, RED, "Error finding service $service_name: ", RESET, $@,"\n";
        return;
    }

    my $name     = shift;
   
    my $method = "get_$type"; 
    my $obj    =  eval { $fastly->$method( service => $service->id, version => $version, name => $name) };
    if ($@ || !$obj) {
        print BOLD, RED, "Error finding $type $name: ", RESET, $@,"\n";
        return;
    }

    
    while (my $prop = shift @_) {
        my $val = shift @_ || last;
        if ($prop eq 'name') {
            print BOLD, RED, "You can't update the name on a $type", RESET,"\n";
            return
        }
        unless ($obj->can($prop)) {
            print BOLD, RED, "$type doesn't have the property '$prop' ", RESET,"\n";
            return
        }
        $obj->$prop($val);
    }
    $method = "update_$type"; 
    my $o = eval { $fastly->$method($obj) };
    if ($@) {
        print BOLD, RED, "Error updating $type: ", RESET, $@,"\n";
    } else {
        print "Update successful\n";
    }
    return;
}
# 
# # sub error_completion {
# #     my $t = shift;
# #     my $r = shift;
# #     if ($r->{type} eq 'error') {
# #         $t->completemsg(sprintf("%s%sError: %s %s\n", BOLD, RED, RESET, $r->{data}->{error}));
# #         return 1;
# #     }
# #     return 0;    
# # }
# 
# # sub error {
# #     my $r = shift;
# #     if ($r->{type} eq 'error') {
# #         print BOLD, RED, "Error: ", RESET, $r->{data}->{error},"\n";
# #         return 1;
# #     }
# #     return 0;
# # }
# 

sub upload {
    my $service_name = shift;
    shift;
    my $version_num  = shift;
    my $type    = shift;
    my $file    = shift;
    shift;
    shift;
    my $name    = shift || basename($file, ".vcl");
    
    local($/);
    open(my $fh, "<$file") || die "No file '$file'";
    my $data = <$fh>;
    close($fh);

    eval {
        my $service = $fastly->search_services(name => $service_name);
        my $version = $service->version($version_num);
        my $r       = $version->upload_vcl($name, $data);
    };
    if ($@) {
        print BOLD, RED, "Error: ", RESET, $@,"\n";
    } else {
        print "Uploaded $file as $name to service $service_name version $version_num\n";
    }
    return;
}

sub delete {
    my @args    = @_;
    my $name    = shift @args;
    my $service = eval { $fastly->search_services(name => $name) };
    if ($@) {
        print BOLD, RED, "Error: ", RESET, $@,"\n";
        return;
    }
        
    if (!@args) {
        my $r = eval { $fastly->delete_service($service) };
        if (!$r) {
            print BOLD, RED, "Error: ", RESET,  "Couldn't delete service $name\n";
        } else {
            print "Deleted service ".$service->id."\n";
        }
        return;
    }
   
    if (@args < 2) {
        print RED, "Need version", RESET, "\n";
        return;
    }
    my $version = shift @args;
    my $type    = shift @args;
    my $tname   = shift @args;
    my $meth    = "delete_$type";
    my $r       = eval { $fastly->$meth(service => $service->id, version => $version, name => $tname ) };
    if ($@) {
       print BOLD, RED, "Error: ", RESET, $@,"\n";
    } else {
       print "Deleted $type $tname\n";
    }
    return;
}

sub create {
    my @args = @_;
    my $name = shift @args;
    if (!@args) {
        my $service = eval { $fastly->create_service(name => $name ) };
        if ($@) {
            print BOLD, RED, "Error: ", RESET, $@,"\n";
        } else {
            print "Created service ".$service->id."\n";
        }
        return;
    }
    
    if (@args < 2) {
        print RED, "Need version", RESET, "\n";
        return;
    }

    my $service = eval { $fastly->search_services(name => $name) };

    #my $service_name = shift @args;
    shift @args;
    my $version = shift @args;
    my $type = shift @args;

    my $commands = $fastly->commands;
    my $cmd = $commands->{"${type}.create"};

    my %args;
    if ($cmd->{name}) {
        push @args, "name", shift @args 
    } else {
        shift @args;
    }

    my $meth = "create_$type"; 
    my $obj  = eval { $fastly->$meth(service => $service->id, version => $version, @args) };
    if ($@) {
        print BOLD, RED, "Error: ", RESET, $@,"\n";
    } else {
        warn present({$obj->_as_hash});
    }
}

sub list_objects {
    no strict 'refs';
    my $t    = shift;
    my $c    = shift; 
    my @args = @{$c->{args}};
    my $service_name = $args[0];
    my $version_num  = $args[2];
    my $type         = $args[3];
    my ($service)    = $fastly->search_services(name => $service_name, version => $version_num);
    my $meth         = "list_$type";
    my @objects      = $fastly->$meth( service => $service->id );
    return [map { $_->name } @objects];
}


sub model_args {
    my $t = shift;
    my $c = shift; 
    my $commands = $fastly->commands;
    my $cmd      = $commands->{"$c->{args}->[3].create"};
    my @args     = @{$c->{args}};

    my %seen;
    my $i = 0;
    foreach my $arg (@args) {
        $i++;
        next if($i < 6);
        $seen{$arg}++ if ($i % 2 == 0);
    }

    my $offset;
    if (@args % 2 == 0 && $args[-1] && $cmd->{$args[-1]}) {
        $offset = -1;
    }
    if (@args % 2 == 1) {
        $offset = -2;
    }    
    if ($offset) {
        if ($cmd->{$args[$offset]}->{type} =~/heavenly::(.*)/) {
            my $service   = $fastly->search_services(name => $args[0], version => $args[2]);
            my ($version) = $service->versions; 
            my $x = [$version->{$1} ];
            return $x if @$x;
        }
        return sprintf("   (%s%s%s)", RED, $cmd->{$args[$offset]}->{desc}, RESET);
    }

    if ($c->{twice}) {
        $t->completemsg("\n\n");
        foreach my $c (sort {$a cmp $b} keys %$cmd) {
            next if ($c eq 'name' || $c eq 'service' || $c eq 'version');
            next if ($seen{$c});
            if (exists $cmd->{$c}->{default}) {
                $t->completemsg(sprintf("%s%-25s%s  %s%s%s  (%s)\n",BOLD,$c,RESET, BLUE,$cmd->{$c}->{desc},RESET, $cmd->{$c}->{default} || ""));
            } else {
                $t->completemsg(sprintf("%s%s%-25s%s  %s%s%s\n",BOLD, RED,$c,RESET, BLUE,$cmd->{$c}->{desc},RESET));
            }
        } 
    }
    my @r;
    foreach my $c (sort {$a cmp $b} keys %$cmd) {
        next if ($c eq 'name' || $c eq 'service' || $c eq 'version');
        next if ($seen{$c});
        push @r, $c;
    }
    push @r, "" if(@r == 1);
    return \@r;
}


sub service_models {
    my $t = shift;
    my $c = shift; 
    my $commands = eval { $fastly->commands };
    if ($@) {
        $t->completemsg(sprintf("\n%s%s%s\n", RED, $@, RESET));
        return [];
    }
    my @options;
    foreach my $command (keys %$commands) {
        if ($command =~/(\w+).create/) { 
            next if ($1 eq 'service');
            next if ($1 eq 'version');
            push @options, $1;
        }
    }
    return \@options;
}


sub show_service {
    my $service_name    = shift;
    shift;
    my $version_number  = shift;
    my ($service)       = $fastly->search_services(name => $service_name, version => $version_number);
    print present({$service->_as_hash});
}

sub show_versions {
    my $service_name    = shift;
    my ($service)       = $fastly->search_services(name => $service_name);
    my @versions        = $service->versions;
    foreach my $version (@versions) {
        print $version->number."\t\t".$version->updated_at." ".($version->comment || "")."\n";
    }
}

sub show_services {
    my @services = $fastly->list_services;
    
    foreach my $service (sort { $a->name cmp $b->name } @services) {
        print $service->id."\t\t".$service->name."\n";
    }
}

sub list_service_complete {
    my $t = shift;
    my $c = shift; 

    my @services = $fastly->list_services;

    my $match  = lc($c->{str});
    return [ grep { lc($_) =~ /^$match/ } map { $_->name } @services ];

    #my $substr = 0;
    #if ($c->{str}) {
    #    $substr = index($c->{args}->[0], $c->{str});
    #} else {
    #    $substr = $c->{tokoff};
    #}
}

sub list_version {
    my $t = shift;
    my $c = shift;
    my $service_name    = $c->{args}->[0];
    my ($service)       = $fastly->search_services(name => $service_name);
    return sort { $a->number <=> $b->number } $service->versions;
}

sub present {
    my $obj = shift;
    Dump($obj);
}


$term->commands($basecommands);
$term->run(@ARGV);

=head1 COMMANDS

=head2 help

Display a help message with available commands.

=head2 show

Display various thing. The sub commands are

=head3 services

List all the services you have access to with their ids and names.

    fastly> show services

might show

    KXKPV9svJFuPapAMjzxgP       FooCorp
    6g2rQokiwAGSRdGYhCY76v      Example-Service
    Y9puwhPNS5Y1tAjUbxp7Z       Test

=head3 service

Display the information from one particular service including all 
backends, directors, domains and origins.

    fastly> show service <service name>
    
=head3 versions

Show the creation date of all the versions for a service.

    fastly> show versions <service name>

=head3 diff

Display the diff between two different versions

    fastly> show diff <service name> version <version number> to <version number>

=head3 stats

Display the stats for a service. Default last argument is minutely. 

    fastly> show stats <service name> [all|minutely|hourly|daily]

=head2 create

Create a new object.

    fastly> create service <service name>
    fastly> create service <service name> version <version number> backend  <name> [options[s]]
    fastly> create service <service name> version <version number> director <name> [options[s]]
    fastly> create service <service name> version <version number> domain   <name> [options[s]]
    fastly> create service <service name> version <version number> origin   <name> [options[s]]

Options look like

    fastly> create service <service name> version <version number> backend  <name> ipv4 <ip address>

=head2 set

Update an object. A note - you cannot change the names of things.

 fastly> set service <service name> version <version number> backend  <name> [options[s]]
 fastly> set service <service name> version <version number> director <name> [options[s]]
 fastly> set service <service name> version <version number> domain   <name> [options[s]]
 fastly> set service <service name> version <version number> origin   <name> [options[s]]

Agagin, like create, options look like

    fastly> set service <service name> version <version number> director <name> retries <retries>

=head2 delete

Delete an object from a configuration.

    fastly> delete service <service name>
    fastly> delete service <service name> version <version number>
    fastly> delete service <service name> version <version number> backend  <backend name>
    fastly> delete service <service name> version <version number> director <director name>
    fastly> delete service <service name> version <version number> domain   <domain name>
    fastly> delete service <service name> version <version number> origin   <origin name>
    
=head2 clone

Clone a configuration so that the new version can be modified.

    fastly> clone service <service name> version <version number>

=head2 activate

Activate a version for use - this will lock it and prevent any further modification.

    fastly> activate service <service name> version <version number>
    
=head2 purge

Remove objects from the cache.

    fastly> purge service <service name>
    
    fastly> purge <url>

=head2 upload

Upload a custom VCL file

    fastly> upload service <service name> version <version number> from <file> [as <vcl name>]

=head2 validate

Check to see that the currently uploaded VCL is valid.

    fastly> validate service <service name> version <version number>

=head2 dump

Show the generated VCL for a given service.

fastly> dump service <service name> version <version number>

=head2 quit

Exit the Fastly shell.

=head1 COPYRIGHT

Copyright 2011 - Fastly Inc

Mail support at fastly dot com if you have problems.

=head1 DEVELOPERS

http://github.com/fastly/fastly-perl

http://www.fastly.com/documentation

=cut

