#!/usr/bin/perl -w

=head1 NAME

    zk-db-config - быстрое обновление конфига соединений с БД (json формат Yandex::DBTools)
    посредством подсистемы zk-delivery

=head1 SINOPSYS

    # два обязательных параметра - конфиг zk-delivery и путь к ноде в zookeeper

    # важный параметр --type - определяет тип конфига, используется для проверок
    # по-умолчанию dbtools, из коробки поддерживается ini и base(без проверок)
    
    Можно передать внешнюю проверку. 
    Конфиг передается в stdin проверке. 
    Проверяется код возврата, и если он не 0, то вывод (stdout+stderr) 
    используется в качестве сообщения об ошибке. 
    --type external --external-checker /path/to/checker
    
    Если передаются дополнительные аргументы командной строки - пытаемся применить эти изменения автоматически,
    если это определено для указанного типа.
    Для типа external можно передать внешнюю модифицирующую программу:
    zk-db-config -c ... -n ... -t external --external-checker json_xs --external-editor perl -- -pe 's/ppcheavy01g/ppcheavy01e/g'
    После вызова external-editor результат проверяется external-checker-ом


    # стоит делать по-проектные алиасы
    alias direct-db-config='zk-db-config -c /etc/zk-delivery/ppc.cfg -n /direct/db-config.json'

    # посмотреть настройки текущего коннекта
    direct-db-config ppc
    # посмотреть конфиг целиком
    direct-db-config

    # переключиться
    direct-db-config ppc ppcheavy01d.yandex.ru
    # переключить только порт
    direct-db-config ppc :3406
    # переключить всё
    direct-db-config ppc ppcheavy01d.yandex.ru:3406

    # переключить ppclog
    direct-db-config ppclog [ppclogs01d.yandex.ru,ppclogs01e.yandex.ru]

    # переключить ppc:bs
    direct-db-config ppc:bs ppcheavy01d.yandex.ru

    # отредактировать конфиг (и значит сразу обновить на всех серверах)
    direct-db-config --edit

=cut

use strict;
use warnings;

use Getopt::Long;
use Text::Diff;
use JSON;

use Yandex::DBTools;
use Yandex::Shell;
use Yandex::Interactive;

my ($CONFIG, $NODE);
my $DEBUG = 0;
my $TYPE = 'dbtools';
my $EDIT;
my $EXTERNAL_CHECKER = '';
my $EXTERNAL_EDITOR = '';
GetOptions(
    "config=s" => \$CONFIG,
    "node=s" => \$NODE,

    "type=s" => \$TYPE,
    "external-checker=s" => \$EXTERNAL_CHECKER,
    "external-editor=s" => \$EXTERNAL_EDITOR,

    "help" => \&usage_full,
    "debug" => \$DEBUG,
    "e|edit" => \$EDIT,
    );

# проверяем параметры
if (!defined $CONFIG) {
    die "Config not defined";
} elsif (!-f $CONFIG) {
    die "Incorrect config defined: $CONFIG"
}
if (!defined $NODE) {
    die "Zookeeper node not defined";
}

if (!defined $TYPE) {
    die "Undefined type";
} elsif ($TYPE !~ /^[a-z0-9_]+(::[a-z0-9_]+)*$/) {
    die "Incorrect type format";
} else {
    eval "require ZkDbConfig::type::$TYPE";
    die "Incorrect type: '$TYPE': $@" if $@;
}
my %checker_params;
if ( $TYPE eq "external" ){
    die "--external-checker <executable file> required, stop" if $EXTERNAL_CHECKER =~ /^\s*$/;
    die "bad external checker $EXTERNAL_CHECKER, stop" if $EXTERNAL_CHECKER !~ /^[\/a-zA-Z0-9_\.-]+$/;
    %checker_params = ( external_checker => $EXTERNAL_CHECKER );
    if ($EXTERNAL_EDITOR) {
        die "bad external editor $EXTERNAL_EDITOR, stop" if $EXTERNAL_EDITOR !~ /^[\/a-zA-Z0-9_\.-]+$/;        
        $checker_params{external_editor} = $EXTERNAL_EDITOR;
    }
}
my $checker = "ZkDbConfig::type::$TYPE"->new(%checker_params);

# получаем текущий конфиг
my $cont = yash_qx("zk-delivery-get", "-c", $CONFIG, $NODE);

if (my @errors = $checker->check($cont)) {
    print STDERR "Current config have errors!\n";
    print STDERR map {"  $_\n"} @errors;
}

if (!@ARGV) {
    # просмотр или редактирование
    if (!$EDIT) {
        print $cont;
    } else {
        my $new_cont = $cont;
        while(1) {
            $new_cont = Yandex::Interactive::edit($new_cont);
            if ($cont ne $new_cont) {
                if (my @errors = $checker->check($new_cont)) {
                    print STDERR "ERROR:\n";
                    print STDERR map {"  $_\n"} @errors;
                    my $res = lc Yandex::Interactive::prompt("Edit again? (Please enter y/n/f=force save)", {"Edit again? (Please enter y/n/f=force save): " => qr/^(y|n|f)$/i});
                    if ($res eq 'y') {
                        next;
                    } elsif ($res eq 'n') {
                        last;
                    } elsif ($res eq 'f') {
                        # pass to saving
                    } else {
                        die "Incorrect answer";
                    }
                }
                my $diff = diff(\$cont, \$new_cont);
                print $diff, "\n";
                if (Yandex::Interactive::prompt_yn("Save config? ")) {
                    my_system("zk-delivery-set", "-c", $CONFIG, $NODE, \$new_cont);
                    print "Config saved.\n";
                }
                last;
            } else {
                print "No changes.\n";
                last;
            }
        }
    }
} else {
    if (my @todo = $checker->args_edit($cont, \@ARGV)) {
        my_system("zk-delivery-set", "-c" => $CONFIG, $NODE, @todo);
    }
}

sub my_system {
    my @cmd = @_;
    if ($DEBUG) {
        print join(" ", @cmd)."\n";
    } else {
        yash_system(@cmd);
    }
}

sub usage_full {
    system("pod2text-utf8 <$0 | less -F");
    exit(0);
}



