#!/usr/bin/perl -w

# TODO:
# implement persistent history ?
 
use strict;
use Term::ReadLine;
use File::Basename;
use Getopt::Long;
use HTTP::Status qw(:constants :is status_message);
use Text::ParseWords;
use PVE::JSONSchema;
use PVE::Cluster;
use PVE::INotify;
use PVE::RPCEnvironment;
use PVE::API2;
use JSON;

PVE::INotify::inotify_init();

my $rpcenv = PVE::RPCEnvironment->init('cli');

$rpcenv->set_language($ENV{LANG});
$rpcenv->set_user('root@pam'); 

my $basedir = '/api2/json';

my $cdir = '';

sub print_usage {
    my $msg = shift;

    print STDERR "ERROR: $msg\n" if $msg;
    print STDERR "USAGE: pvesh [verifyapi]\n";
    print STDERR "       pvesh CMD [OPTIONS]\n";

}

my $disable_proxy = 0;
my $opt_nooutput = 0;

my $cmd = shift;

my $optmatch;
do {
    $optmatch = 0;
    if ($cmd) {
	if ($cmd eq '--noproxy') {
	    $cmd = shift;
	    $disable_proxy = 1;
	    $optmatch = 1;
	} elsif ($cmd eq '--nooutput') {
	    # we use this when starting task in CLI (suppress printing upid)
	    # for example 'pvesh --nooutput create /nodes/localhost/stopall'
	    $cmd = shift;
	    $opt_nooutput = 1;
	    $optmatch = 1;
	}
    }
} while ($optmatch);

if ($cmd) {
    if ($cmd eq 'verifyapi') {
	PVE::RESTHandler::validate_method_schemas();
	exit 0;
    } elsif ($cmd eq 'ls' || $cmd eq 'get' || $cmd eq 'create' || 
	     $cmd eq 'set' || $cmd eq 'delete' ||$cmd eq 'help' ) {
	pve_command([ $cmd, @ARGV],  $opt_nooutput);
	exit(0);
    } else {
	print_usage ("unknown command '$cmd'");
	exit (-1);
    }
}

if (scalar (@ARGV) != 0) {
    print_usage ();
    exit (-1);
}

print "entering PVE shell - type 'help' for help\n";

my $term = new Term::ReadLine ('pvesh');
my $attribs = $term->Attribs;

sub complete_path {
    my($text) = @_;

    my ($dir, undef, $rest) = $text =~ m|^(.*/)?(([^/]*))?$|;
    my $path = abs_path($cdir, $dir);

    my @res = ();

    my $di = dir_info($path);
    if (my $children = $di->{children}) {
	foreach my $c (@$children) {
	    if ($c =~ /^\Q$rest/) {
		my $new =  $dir ? "$dir$c" : $c;
		push @res, $new; 
	    }
	}
    }

    if (scalar(@res) == 0) {
	return undef;
    } elsif (scalar(@res) == 1) {
	return ($res[0], $res[0], "$res[0]/");
    } 

    # lcd : lowest common denominator
    my $lcd = '';
    my $tmp = $res[0];
    for (my $i = 1; $i <= length($tmp); $i++) {
	my $found = 1;
	foreach my $p (@res) {
	    if (substr($tmp, 0, $i) ne substr($p, 0, $i)) {
		$found = 0;
		last;
	    }
	}
	if ($found) {
	    $lcd = substr($tmp, 0, $i);
	} else {
	    last;
	}
    }

    return ($lcd, @res);
};

# just to avoid an endless loop (called by attempted_completion_function)
$attribs->{completion_entry_function} = sub {
    my($text, $state) = @_;
    return undef;
};

$attribs->{attempted_completion_function} = sub {
    my ($text, $line, $start) = @_;

    my $prefix = substr($line, 0, $start);
    if ($prefix =~ /^\s*$/) { # first word (command completeion)
	$attribs->{completion_word} = [qw(help ls cd get set create delete quit)];
	return $term->completion_matches($text, $attribs->{list_completion_function});
    }

    if ($prefix =~ /^\s*\S+\s+$/) { # second word (path completion)
	return complete_path($text);
    }

    return ();   
};

sub abs_path {
    my ($current, $path) = @_;

    my $ret = $current;

    return $current if !defined($path);

    $ret = '' if $path =~ m|^\/|;

    foreach my $d (split (/\/+/ , $path)) {
	if ($d eq '.') {
	    next;
	} elsif ($d eq '..') {
	    $ret = dirname($ret);
	    $ret = '' if $ret eq '.';
	} else {
	    $ret = "$ret/$d";
	}
    }

    $ret =~ s|\/+|\/|g;
    $ret =~ s|^\/||;
    $ret =~ s|\/$||;

    return $ret;
}

my $read_password = sub {
    my $attribs = $term->Attribs;
    my $old = $attribs->{redisplay_function};
    $attribs->{redisplay_function} = $attribs->{shadow_redisplay};
    my $input = $term->readline('password: ');
    my $conf = $term->readline('Retype new password: ');
    $attribs->{redisplay_function} = $old;
    die "Passwords do not match.\n" if ($input ne $conf);
    return $input;
};

sub reverse_map_cmd {
    my $method = shift;

    my $mmap = {
	GET => 'get',
	PUT => 'set',
	POST => 'create',
	DELETE => 'delete',
    };

    my $cmd = $mmap->{$method};

    die "got strange value for method ('$method') - internal error" if !$cmd;

    return $cmd;
}

sub map_cmd {
    my $cmd = shift;

    my $mmap = {
	create => 'POST',
	set => 'PUT',
	get => 'GET',
	ls => 'GET',
	delete => 'DELETE',
    };

    my $method = $mmap->{$cmd};

    die "unable to map method" if !$method;

    return $method;
}

sub check_proxyto {
    my ($info, $uri_param) = @_;

    if ($info->{proxyto}) {
	my $pn = $info->{proxyto};
	my $node = $uri_param->{$pn};
	die "proxy parameter '$pn' does not exists" if !$node;

	if ($node ne 'localhost' && ($node ne PVE::INotify::nodename())) {
	    die "proxy loop detected - aborting\n" if $disable_proxy;
	    my $remip = PVE::Cluster::remote_node_ip($node);
	    return ($node, $remip);
	}
    }

    return undef;
}

sub proxy_handler {
    my ($node, $remip, $dir, $cmd, $args) = @_;

    my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip", 
	       'pvesh', '--noproxy', $cmd, $dir, @$args];

    system(@$remcmd) == 0 || die "proxy handler failed\n";
}

sub call_method {
    my ($dir, $cmd, $args, $nooutput) = @_;

    my $method = map_cmd($cmd);

    my $uri_param = {};
    my ($handler, $info) = PVE::API2->find_handler($method, $dir, $uri_param);
    if (!$handler || !$info) {
	die "no '$cmd' handler for '$dir'\n";
    }

    my ($node, $remip) = check_proxyto($info, $uri_param);
    return proxy_handler($node, $remip, $dir, $cmd, $args) if $node;

    my $data = $handler->cli_handler("$cmd $dir", $info->{name}, $args, [], $uri_param, $read_password);

    return if $nooutput;

    warn "200 OK\n"; # always print OK status if successful

    if ($info && $info->{returns} && $info->{returns}->{type}) {
	my $rtype = $info->{returns}->{type};

	return if $rtype eq 'null';

	if ($rtype eq 'string') {
	    print $data if $data;
	    return;
	}
    }

    print to_json($data, {utf8 => 1, allow_nonref => 1, canonical => 1, pretty => 1 });

    return;
}

sub find_resource_methods {
    my ($path, $ihash) = @_;

    for my $method (qw(GET POST PUT DELETE)) {
	my $uri_param = {};
	my ($handler, $info, $pm) = PVE::API2->find_handler($method, $path, $uri_param);
	if ($handler && $info && !$ihash->{$info}) {
	    $ihash->{$info} = {
		path => $pm,
		handler => $handler, 
		info => $info, 
		uri_param => $uri_param,
	    };
	}
    }
}

sub print_help {
    my ($path, $opts) = @_;

    my $ihash = {};

    find_resource_methods($path, $ihash);

    if (!scalar(keys(%$ihash))) {
	die "no such resource\n";
    }

    my $di = dir_info($path);
    if (my $children = $di->{children}) {
	foreach my $c (@$children) {
	    my $cp = abs_path($path, $c);
	    find_resource_methods($cp, $ihash);
	}
    }

    foreach my $mi (sort { $a->{path} cmp $b->{path} } values %$ihash) {
	my $method = $mi->{info}->{method};

	# we skip index methods for now.
	next if ($method eq 'GET') && PVE::JSONSchema::method_get_child_link($mi->{info});

	my $path = $mi->{path};
	$path =~ s|/+$||; # remove trailing slash

	my $cmd = reverse_map_cmd($method);

	print $mi->{handler}->usage_str($mi->{info}->{name}, "$cmd $path", [], $mi->{uri_param}, 
					$opts->{verbose} ? 'full' : 'short', 1);
	print "\n\n" if $opts->{verbose};
    }
 
};

sub resource_cap {
    my ($path) = @_;

    my $res = '';

    my ($handler, $info) = PVE::API2->find_handler('GET', $path);
    if (!($handler && $info)) {
	$res .= '--';
    } else {
	if (PVE::JSONSchema::method_get_child_link($info)) {
	    $res .= 'Dr';
	} else {
	    $res .= '-r';
	}
    }

    ($handler, $info) = PVE::API2->find_handler('PUT', $path);
    if (!($handler && $info)) {
	$res .= '-';
    } else {
	$res .= 'w';
    }

    ($handler, $info) = PVE::API2->find_handler('POST', $path);
    if (!($handler && $info)) {
	$res .= '-';
    } else {
	$res .= 'c';
    }

    ($handler, $info) = PVE::API2->find_handler('DELETE', $path);
    if (!($handler && $info)) {
	$res .= '-';
    } else {
	$res .= 'd';
    }

    return $res;
}

sub extract_children {
    my ($lnk, $data) = @_;

    my $res = [];

    return $res if !($lnk && $data);

    my $href = $lnk->{href};
    if ($href =~ m/^\{(\S+)\}$/) {
	my $prop = $1;

	foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
	    next if !ref($elem);
	    my $value = $elem->{$prop};
	    push @$res, $value;
	}
    }

    return $res;
}

sub dir_info {
    my ($path) = @_;

    my $res = { path => $path };
    my $uri_param = {};
    my ($handler, $info, $pm) = PVE::API2->find_handler('GET', $path, $uri_param);
    if ($handler && $info) {
	eval {
	    my $data = $handler->handle($info, $uri_param);
	    my $lnk = PVE::JSONSchema::method_get_child_link($info);
	    $res->{children} = extract_children($lnk, $data);
	}; # ignore errors ?
    }
    return $res;
}

sub list_dir {
    my ($dir, $args) = @_;

    my $uri_param = {};
    my ($handler, $info) = PVE::API2->find_handler('GET', $dir, $uri_param);
    if (!$handler || !$info) {
	die "no such resource\n";
    }

    if (!PVE::JSONSchema::method_get_child_link($info)) {
	die "resource does not define child links\n";
    }

    my ($node, $remip) = check_proxyto($info, $uri_param);
    return proxy_handler($node, $remip, $dir, 'ls', $args) if $node;


    my $data = $handler->cli_handler("ls $dir", $info->{name}, $args, [], $uri_param, $read_password); 
    my $lnk = PVE::JSONSchema::method_get_child_link($info);
    my $children = extract_children($lnk, $data);

    foreach my $c (@$children) {
	my $cap = resource_cap(abs_path($dir, $c));
	print "$cap $c\n";
    }
}


sub pve_command {
    my ($args, $nooutput) = @_;

    PVE::Cluster::cfs_update();

    $rpcenv->init_request();

    my $cmd = shift @$args;

    if ($cmd eq 'cd') {

	my $path =  shift @$args;

	die "usage: cd [dir]\n" if scalar(@$args);

	if (!defined($path)) {
	    $cdir = '';
	    return;
	} else {
	    my $new_dir = abs_path($cdir, $path);
	    my ($handler, $info) = PVE::API2->find_handler('GET', $new_dir);
	    die "no such resource\n" if !$handler;
	    $cdir = $new_dir;
	}

    } elsif ($cmd eq 'help') {

	my $help_usage_error = sub {
	    die "usage: help [path] [--verbose]\n";
	};

	my $opts = {};

	&$help_usage_error() if !Getopt::Long::GetOptionsFromArray($args, $opts, 'verbose');

	my $path;
	if (scalar(@$args) && $args->[0] !~ m/^\-/)  {
	    $path = shift @$args;
	}

	&$help_usage_error() if scalar(@$args);

	print "help [path] [--verbose]\n";
	print "cd [path]\n";
	print "ls [path]\n\n";

	print_help(abs_path($cdir, $path), $opts);

    } elsif ($cmd eq 'ls') {
	my $path;
	if (scalar(@$args) && $args->[0] !~ m/^\-/)  {
	    $path = shift @$args;
	}

	list_dir(abs_path($cdir, $path), $args);

    } elsif ($cmd eq 'get') {

	my $path;
	if (scalar(@$args) && $args->[0] !~ m/^\-/)  {
	    $path = shift @$args;
	}

	call_method(abs_path($cdir, $path), $cmd, $args);

    } elsif ($cmd eq 'create') {

	my $path;
	if (scalar(@$args) && $args->[0] !~ m/^\-/)  {
	    $path = shift @$args;
	}

	call_method(abs_path($cdir, $path), $cmd, $args, $nooutput);

    } elsif ($cmd eq 'delete') {

	my $path = shift @$args;

	die "usage: delete [path]\n" if scalar(@$args);

	call_method(abs_path($cdir, $path), $cmd, $args);

    } elsif ($cmd eq 'set') {

	my $path;
	if (scalar(@$args) && $args->[0] !~ m/^\-/)  {
	    $path = shift @$args;
	}

	call_method(abs_path($cdir, $path), $cmd, $args);

    } else {
	die "unknown command '$cmd'\n";
    }

}

my $input;
while (defined ($input = $term->readline("pve:/$cdir> "))) {
    chomp $input;

    next if $input =~ m/^\s*$/;

    if ($input =~ m/^\s*q(uit)?\s*$/) {
	exit (0);
    }

    $term->addhistory($input);

    eval {
	my $args = [ shellwords($input) ];
	pve_command($args);
    };
    warn $@ if $@;
}
