#!/usr/bin/perl

use strict;
use warnings;

use feature qw/state/;

=head1 DESCRIPTION

=encoding utf8

    Создание дашбордов в Графане из текстовых конфигов 

    grafanerator upload -d /path/to/conf-dir

    Описание форматов конфига см. в svn+ssh://svn.yandex.ru/direct-utils/grafanerator/README
    Примеры использования: 
        svn+ssh://svn.yandex.ru/direct-utils/grafanerator-direct
        svn+ssh://svn.yandex.ru/direct-utils/grafanerator-directmod

=head1 TODO

    - создание нового дашборда (получение id)
    - ленивое обновление (получить текущее состояние, если изменений нет -- не отправлять)
    - --force

=cut

use Cwd;
use Clone qw(clone);
use Getopt::Long;
use List::MoreUtils qw/uniq/;
use JSON;
use LWP::UserAgent;
use Path::Tiny;
use Template;
use YAML;

use utf8;
use open ':std' => ':utf8';

my %FEATURES = (
    list => {
        description => qq(Список локальных и удаленных дашбордов),
        code => \&cmd_list,
    },
    upload => {
        description => qq(Обновить в Графане указанные или все дашборды),
        code => \&cmd_upload,
        params => [ qw/ argv / ],
    },
    new => {
        description => qq(Создать новый дашборд),
        code => \&cmd_new,
    },
);

my $CONF;
my $VERBOSITY = 0;

run() unless caller();

sub run 
{
    prepare_features();
    my $opt = parse_options();

    die "unknown action '$opt->{action}', stop\n" unless exists $FEATURES{$opt->{action}};
    my %params = map { $_ => $opt->{$_}  } @{$FEATURES{$opt->{action}}->{params} ||[] }, qw/ argv conf_dir /;
    $FEATURES{$opt->{action}}->{code}->(\%params );

    exit 0;
}


sub parse_options
{
    my %O = (
        conf_dir => getcwd(),
    );

    GetOptions(
        "h|help" => sub {
            system("podselect -section NAME -section DESCRIPTION -section OPTIONS -section EXAMPLES -section ACTIONS $0 | pod2text"); 
            print_actions_list();
            exit 0;
        },
        'd|dir=s' => \$O{conf_dir},
    ) or die "can't parse options, stop";

    $O{action} = shift @ARGV;

    my $main_conf = "$O{conf_dir}/grafanerator.conf.yaml";
    die "can't find config '$main_conf'" unless -f $main_conf;
    $CONF = YAML::LoadFile($main_conf);

    $O{argv} = [ @ARGV ];

    p(YAML::Dump(\%O));
    return \%O;
}

sub prepare_features
{
    return;
}


sub cmd_list
{
    my ($opt) = @_;
    my $json = grafana_get("search?tag=$CONF->{main_tag}");
    my $dashboards = decode_json($json);
    for my $d (@$dashboards){
        my $tags = join ",", @{$d->{tags}||[]};
        print join "\t\t", $d->{id}, $d->{uri}, $tags, $d->{title};
        print "\n";
    }
    return;
}

sub cmd_upload
{
    my ($opt) = @_;
    # TODO фильтрация по списку  
    for my $set ( @{$CONF->{sets}} ){
        my $templates_dir  = "$opt->{conf_dir}/$set->{templates_dir}";
        my $dashboards_dir = "$opt->{conf_dir}/$set->{dashboards_dir}";
        my $tmpl = load_templates($templates_dir);
        my @flist = @{$opt->{argv}} ? @{$opt->{argv}} : glob "$dashboards_dir/*"; 
        for my $file (@flist){
            # берем суффиксы файла ("расширения")
            $file =~ /((?:\.(?:tmpl|json|yaml|tt2))*)$/;
            my @types = grep {$_} split /\./, $1;
            die "no known suffixes found: '$file'" unless @types > 0;

            # обрабатываем содержимое файла в соответствии с расширениями, начиная с конца
            my $content = path($file)->slurp_utf8;
            for my $t (reverse @types){
                if ( $t eq "tt2" ){
                    my $t = Template->new();
                    my $res;
                    $t->process(\$content, {}, \$res) || die $t->error();
                    $content = $res;
                } elsif ( $t eq "yaml" ){
                    $content = YAML::Load($content);
                } elsif ( $t eq "json" ){
                    $content = from_json($content);
                } elsif ( $t eq "tmpl" ){
                    $content = generate_dashboard($tmpl, $content);
                }
            }
            @{$content->{tags}} = sort(uniq(@{$content->{tags}}, "auto", $CONF->{main_tag}));
            my $request = prepare_request($tmpl, $content);
            post_dashboard($request, $file);
        }
    }
    
    print "upload done\n";
}


sub post_dashboard
{
    my ($request, $name) = @_;
    state $ua;
    $ua ||= LWP::UserAgent->new(timeout => 10, 
			        ssl_opts => { SSL_ca_path => '/etc/ssl/certs',
			                      verify_hostname => 1,	
				            },
			       );
    #curl -k -H "Content-Type: application/json" -X POST -d @$$i https://ppcgraphite.yandex.ru/grafana/api/dashboards/db ;
    my $request_json = encode_json($request);
    my $url = "$CONF->{grafana_api_url}/dashboards/db";

    my $resp = $ua->post( $url, "Content-Type" => "application/json", Content => $request_json );
    my $status = $resp->is_success ? "ok" : "FAIL";
    print "$status $name\n";
    #print $request_json;
}


sub load_templates
{
    my ($path) = @_;
    my $dashboard_tmpl = decode_json(path("$path/grafana-dashboard.tmpl.json")->slurp);
    my $row_tmpl = decode_json(path("$path/grafana-row.tmpl.json")->slurp);
    my $panel_tmpl = decode_json(path("$path/grafana-panel.tmpl.json")->slurp);
    my $singlestat_panel_tmpl = decode_json(path("$path/grafana-panel-singlestat.tmpl.json")->slurp);
    my $request_tmpl = decode_json(path("$path/grafana-request-dashboard.json")->slurp);

    return {
        dashboard => $dashboard_tmpl,
        row => $row_tmpl, 
        panel => $panel_tmpl,
        singlestat_panel => $singlestat_panel_tmpl,
        request => $request_tmpl,
    };
}

sub generate_dashboard
{
    my ($t, $conf) = @_;

    my $dashboard = clone($t->{dashboard});
    $dashboard->{title} = $conf->{title} || 'new';
    $dashboard->{tags} = $conf->{tags} || [];
    $dashboard->{templating} = $conf->{templating} if exists $conf->{templating};

    my $res = $dashboard;
    $dashboard->{id} = $conf->{id} + 0;
    if (0) {
        my $request = clone($t->{request});
        $request->{dashboard} = $dashboard;
        $dashboard->{id} = $conf->{id} + 0;
        $res = $request;
    }

    if (exists $conf->{time}) {
        for my $field (qw/from to/) {
            $dashboard->{time}->{$field} = $conf->{time}->{$field} if exists $conf->{time}->{$field};
        }
    }
    $dashboard->{refresh} = $conf->{refresh} if exists $conf->{refresh};

    my $panel_id = 1; 
    for my $row_conf (@{$conf->{rows}}){
        my $row = clone($t->{row});
        push @{$dashboard->{rows}}, $row;

        my $row_panels;
        if (ref $row_conf eq 'HASH') {
            # новый формат с возможностью уточнять настройки "строк"
            $row_panels = $row_conf->{rows};
            if (exists $row_conf->{title}) {
                $row->{title} = $row_conf->{title};
                $row->{showTitle} = JSON::true,
            }
            $row->{height} = $row_conf->{height} if $row_conf->{height};
        } else {
            # для обратной совместимости, когда внутри массива строк сразу идет массив панелей
            $row_panels = $row_conf;
        }

        my $panels_count = scalar @{$row_panels};
        for my $panel_conf ( @{$row_panels} ){
            my $panel_type = $panel_conf->{type} || 'graph';
            my $panel;
            if ($panel_type eq 'graph') {
                $panel = clone($t->{panel});
            } elsif ($panel_type eq 'singlestat') {
                $panel = clone($t->{singlestat_panel});
            }
            push @{$row->{panels}}, $panel;

            my @targets;
            if (exists $panel_conf->{targets}) {
                @targets = @{$panel_conf->{targets}};
            } else {
                my $targets_conf = ref $panel_conf->{target} ? $panel_conf->{target} : [$panel_conf->{target}];
                push @targets, {target => $_} for @{$targets_conf};
            }
            for my $target (@targets) {
                $target->{hide} = JSON::true if $target->{hide};
            }
            $panel->{targets} = \@targets;

            if ($panel_type eq 'graph') {
                if ($panel_conf->{legend}) {
                    for my $lp (keys %{$panel_conf->{legend}}) {
                        $panel->{legend}->{$lp} = $panel_conf->{legend}->{$lp} eq 'true' ? JSON::true : JSON::false;
                    }
                }
                if ($panel_conf->{grid}) {
                    $panel->{grid}->{$_} = $panel_conf->{grid}->{$_} for keys %{$panel_conf->{grid}};
                }
                if ($panel_conf->{yaxes}) {
                    # yaxes: хеш - одинаковые настрйоки для двух осей, массив хешей - разные
                    for my $axis_num (0, 1) {
                        my $axis_conf = ref $panel_conf->{yaxes} eq 'ARRAY' ? $panel_conf->{yaxes}->[$axis_num] : $panel_conf->{yaxes};
                        next unless $axis_conf;
                        for my $axis_param (qw/min max logBase/) {
                            next unless exists $axis_conf->{$axis_param};
                            $panel->{yaxes}->[$axis_num]->{$axis_param} = int($axis_conf->{$axis_param});
                        }
                        if ($axis_conf->{format}) {
                            $panel->{yaxes}->[$axis_num]->{format} = $axis_conf->{format};
                        }
                    }
                }
                if ($panel_conf->{aliasColors}) {
                    $panel->{aliasColors} = $panel_conf->{aliasColors};
                }
                if ($panel_conf->{seriesOverrides}) {
                    $panel->{seriesOverrides} = $panel_conf->{seriesOverrides};
                    for my $override (@{$panel->{seriesOverrides}}) {
                        $override->{stack} = JSON::true if $override->{stack};
                    }
                }
                for my $panel_param (qw/fill linewidth/) {
                    next unless exists $panel_conf->{$panel_param};
                    $panel->{$panel_param} = int($panel_conf->{$panel_param});
                }
                $panel->{stack} = JSON::true if $panel_conf->{stack};
            } elsif ($panel_type eq 'singlestat') {
                $panel->{valueName} = $panel_conf->{valueName} if exists $panel_conf->{valueName};
                if ($panel_conf->{sparkline}) {
                    for my $gp (qw/full show/) {
                        next unless exists $panel_conf->{sparkline}->{$gp};
                        $panel->{sparkline}->{$gp} = $panel_conf->{sparkline}->{$gp} eq 'true' ? JSON::true : JSON::false;
                    }
                }
            }

            $panel->{title} = $panel_conf->{title};
            $panel->{span} = int($panel_conf->{span} || 12/$panels_count);
            $panel->{id} = $panel_id++;
        }
    }

    return $res;
}


sub prepare_request
{
    my ($t, $dashboard) = @_;

    my $request = clone($t->{request});
    $request->{dashboard} = clone($dashboard);
    return $request;
}


sub cmd_new
{
    die "not implemented";
}


sub grafana_get
{
    my ($url_rel) = @_;
    my $ua = LWP::UserAgent->new( ssl_opts => { verify_hostname => 1,
		                                SSL_ca_path => '/etc/ssl/certs' },
			          timeout => 10,
			        );
    my $url = "$CONF->{grafana_api_url}/$url_rel";
    my $resp = $ua->get($url);
    die "can't get '$url'" unless $resp->is_success;
    return $resp->decoded_content;
}

sub print_actions_list
{
    print "...\n";
}

sub p
{
    my ($message) = @_;
    return unless $VERBOSITY;
    print "$message\n";
}

