#!/usr/bin/perl

use strict;
use warnings;

use utf8;
use open ':std', ':encoding(UTF-8)';

=head1 DESCRIPTION

=encoding utf8

    Скрипт для выполнения разного полезного сразу во многих Директовых или Директоподобных БД

    Параметры:
     * действие (innotop, guard, pt-osc и т.п.)
     * одна или несколько баз, на которых надо выполнить команды (в стиле direct-sql: dt:ppc:1, pr:ppc:all)
     * стратегия выполнения: --show, --sequence, --tmux, --tmux-detached
     вместе с любой стратегией можно указать --dry-run/-n -- тогда выполняться ничего не будет, только напечатаются все команды
     * параметры, специфические для конкретного действия 
    
    Действие можно указывать первым параметром, а можно пользоваться симлинками dbs-<action>: dbs-innotop, dbs-sql, dbs-guard

    Список действий с примерами и дефолтными стратегиями выполнения для одной/нескольких БД, а так же список доступных БД: 
    dbs list
    dbs list-actions
    dbs list-confs

    Для отладки можно запускать с модифицированным PATH: 
    PATH=./bin:$PATH ./bin/dbs analyze-alter dt:ppc:1 'alter table campaings modify column currencyConverted enum("Yes", "No", "Maybe")'

    Особый случай: на ppcdev'ах (машинах с fqdn, начинающимся с ppcdev) для продакшен-базы (pr:/prod:/production:/sb:/sandbox:)
    для mysql по умолчанию используется пользователь direct-ro

=head1 EXAMPLES

    dbs-innotop -tmux-detached dt:ppc:{1,3,6}
    dbs-innotop -tmux dt:ppc:all
    dbs-sql {pr,sb}:ppc:all 'select now()'
    dbs-shell dev7:ppcdict

=head1 ACTIONS

=head1 TODO

 + запрос из stdin
 * алиасы для действий
 + хелпер для тестирования: список параметров, на которых можно сравнивать вывод dry-run
 + симлинки
 + разные умолчальные политики выполнения для разных действий?
 + разные умолчальные политики для одной БД и нескольких
 + отдельно show -- показать содержательные команды, отдельно настоящий dry-run -- то, что будет передано в system
 + дополнительные пользователи в db-config
 + выкинуть из direct-pt-osc логику про распараллеливание и tmux и дописать generate_cmd_pt_osc
 * тестовый db-config для тестов
 * время выполнения запросов через dbs-sql 

=cut

use feature 'state';

use Getopt::Long;
use File::Basename;
use POSIX qw(strftime);
use YAML;

use Yandex::DBShards;
use Yandex::DBTools;
use Yandex::HashUtils;
use Yandex::Shell;

use ProjectSpecific qw/get_db_conf_file get_db_conf_aliases/;

### Конфиг ###
##############

my $DRY_RUN = 0;

my %FEATURES = (
    guard => {
        description => qq("Охранять" alter'ы, rename'ы и т.п.: прибивать запросы, покушающиеся на те же таблицы
        dbs-guard dt:ppc:all),
        generate_commands => \&generate_cmd_guard,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi  => 'tmux_attached',
        params => [ qw/opt_check db/ ],
    },
    innotop => {
        description => qq(innotop в указанных БД
        dbs innotop ts:ppc:{1,3,8}),
        generate_commands => \&generate_cmd_innotop,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi  => 'sequence', # TODO поменять на tmux_attached
        params => [ qw/opt_c/ ],
    },
    sql => {
        description => qq(Всеядно выполнить запрос в mysql или ClH или открыть шелл
        опции: -A, -B, --quick
        dbs sql 'select now()' dt:ppc:{1,3} pr:ppchouse:logs
        ),
        generate_commands => \&generate_cmd_sql,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi  => 'sequence',
        params => [ qw/query exec_strategy opt_A opt_B skip_column_names opt_quick/ ],
        query_optional => 1,
    },
    mysql => {
        description => qq(Выполнить запрос в mysql
        опции: -B, --quick
        dbs mysql 'select now()' dt:ppc:{1,3}
        Почти никогда не нужен, пользуйтесь dbs-sql
        ),
        generate_commands => \&generate_cmd_mysql,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi  => 'sequence',
        params => [ qw/ query exec_strategy opt_A opt_B skip_column_names opt_quick/ ],
    },
    clh => {
        description => qq(Выполнить запрос в Clickhouse
        опции: -B,
        dbs clh 'select 1' dt:ppchouse:logs
        Почти никогда не нужен, пользуйтесь dbs-sql
        ),
        generate_commands => \&generate_cmd_clh,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi  => 'sequence',
        params => [ qw/ query exec_strategy opt_B skip_column_names / ],
    },
    'mysql-shell' => {
        description => qq(mysql-ный шелл
        dbs shell dt:ppc:1
        Почти никогда не нужен, пользуйтесь dbs-sql
        ),
        generate_commands => \&generate_cmd_mysql_shell,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi  => 'sequence',
    },
    'clh-shell' => {
        description => qq(clickhouse-ный шелл
        dbs shell dt:ppchouse:logs
        Почти никогда не нужен, пользуйтесь dbs-sql
        ),
        generate_commands => \&generate_cmd_clh_shell,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi  => 'sequence',
    },
    'live-alter' => {
        description => qq(Осторожно выполнить альтер: подождать, пока таблица будет не занята
dbs-live-alter 'alter table campaigns add column i int' dt:ppc:{1,3} --tmux-detached),
        generate_commands => \&generate_cmd_live_alter,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi  => 'tmux_attached',
        params => [ qw/ query / ],
    },
    'check-ints' => {
        description => qq(Проверить заполненность целочисленных полей
        dbs-check-ints dt:ppc:8 --sample 10000
        dbs-check-ints dt:ppc:8 --sample 10000 --exclude-file etc/mysql_check_ints/ppc.exclude
),
        generate_commands => \&generate_cmd_check_ints,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi  => 'sequence',
        params => [ qw/ opt_sample opt_exclude_file / ],
    },
    'pt-osc' => {
        description => qq(Выполнить альтер через pt-online-schema-change
        pt-osc dev7:ppc:12 'ALTER TABLE camp_options FORCE;' --tmux-attached
        pt-osc dev7:ppc:12 'ALTER TABLE camp_options FORCE;' --tmux-attached --no-auto-finish --recursion-method hosts
        ),
        generate_commands => \&generate_cmd_pt_osc,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi  => 'tmux_attached',
        params => [ qw/ query no_auto_finish critical_load max_load alter_foreign_keys_method max_lag check_interval recursion_method / ],
        flags  => [ qw/ no_check_alter / ],
        generate_session_name => \&generate_pt_osc_session_name,
        generate_session_name_params => [ qw/query/ ],
    },
    'analyze-alter' => {
        description => qq(Что поменяет альтер в таблице: знаковость, дефолты и т.д.
        cat alter_campaigns.sql | dbs-analyze-alter dt:ppc:1 -
        dbs-analyze-alter dt:ppc:1 'alter table campaigns modify column currencyConverted enum("Yes", "No", "Maybe")'
),
        generate_commands => \&generate_cmd_analyze_alter,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi  => 'sequence',
        params => [ qw/ query exec_strategy/ ],
    },
    'check-plan' => {
        description => qq(
        dbs check-plan pr:ppc:1
),
        generate_commands => \&generate_cmd_check_plan,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi  => 'sequence',
        params => [ qw/ exec_strategy / ],
    },
    'binlog' => {
        description => qq(Читать бинлоги. Внутри запустит утилиту mysqlbinlog с типичным набором параметров.
        Обычный порядок действий:
        dbs-sql ts:ppc:1 'show binary logs'
        выбрать файл, и:
        dbs-binlog ts:ppc:1 ppcdata1-bin.007039 |less
        Если нужны хитрые параметры для mysqlbinlog: через -n посмотреть команду, скопировать, добавить нужные параметры и запускать.),
        generate_commands => \&generate_cmd_binlog,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi => 'error',
        params => [ qw/
            query
            offset
            start-datetime start-position stop-datetime stop-position
            / ],
        # в query будет попадать имя файла с бинлогом. Отмечаем необязательным, чтобы generate_cmd_binlog могло само выводить понятное сообщение, если файл не дали
        query_optional => 1,
        flags => [ qw/debug-check debug-info disable-log-bin hexdump raw short-form stop-never
            to-last-log verify-binlog-checksum/ ],
    },
    mysqldump => {
        description => 'Запустить mysqldump для базы. Например:
        dbs-mysqldump ts:ppc:1 --tables geo_regions,currency_rates --skip-comments --no-create-info --extended-insert=OFF
        Обратите внимание, что в отличие от mysqldump список в --tables передаётся через запятую.
        Поддерживаются, скорее всего, не все опции mysqldump. Можно добавить в скрипт или запустить с ними команду из результата --dry-run.',
        generate_commands => \&generate_cmd_mysqldump,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi => 'error',
        params => [ qw/tables
        set-gtid-purged default-character-set extended-insert/],
        query_optional => 1,
        flags => [qw/skip-lock-tables single-transaction no-data compact skip-comments order-by-primary no-create-info/],
    },
    mongo => {
        description => qq(подключиться к монге
        ),
        generate_commands => \&generate_cmd_mongo,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi  => 'sequence',
        params => [ qw// ],
    },
    zkcli => {
        description => 'Подключиться к зукиперу',
        generate_commands => \&genegarate_cmd_zkcli,
        default_exec_strategy_single => 'sequence',
        default_exec_strategy_multi  => 'sequence',
        needs_custom_parse => 1,
        params => [ qw// ],
    },
);

### Конец конфига ###
#####################

run() unless caller();

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

    # что надо выполнить
    my $db_hosts = db_hosts( $opt->{conf_name}, $opt->{dbs}, hash_cut($opt, qw/mysql_user mysql_password/) );
    die "empty dbs list, stop" unless @$db_hosts > 0; 
    
    # умолчальная политика выполнения
    if (@$db_hosts == 1 ){
        $opt->{exec_strategy} //= $FEATURES{$opt->{action}}->{default_exec_strategy_single};
    } else {
        $opt->{exec_strategy} //= $FEATURES{$opt->{action}}->{default_exec_strategy_multi};
    }
    $opt->{exec_strategy} //= 'show';
    
    # "Параметры" -- это то, у чего есть еще аргументы
    my %params = map { $_ => $opt->{$_}  } @{$FEATURES{$opt->{action}}->{params} ||[] };
    # "Флаги" -- это параметры без аргументов, для них проставляем пустые строки
    # Различие между параметрами и флагами существенно для pt-osc например
    for (@{$FEATURES{$opt->{action}}->{flags} ||[] }){
        $params{$_} = '' if $opt->{$_};
    }
    my @to_execute;
    for my $db (@$db_hosts){
        push @to_execute, {
            cmds => $FEATURES{$opt->{action}}->{generate_commands}->( $db, \%params ), 
            title => $db->{title},
        };
    }

    # выполнить
    execute(\@to_execute, $opt);

    exit 0;
}


=head2 db_hosts

   составить список инстансов, для которых надо выполнить действия

=cut
sub db_hosts
{
    my ($global_conf_name, $dbs, $opt) = @_;
    my @where = ();

    # определяем, надо ли пытаться воспользоваться extra_users из конфига:
    # если передано только -u -- надо, иначе откуда пароль
    # если передано и -u, и -p -- не надо искать extra_users, надо просто использовать переданные значения
    my ($use_extra_users, $use_custom_user) = (0, 0);
    if      (   defined $opt->{mysql_user} &&   defined $opt->{mysql_password} ){
        $use_custom_user = 1;
    } elsif (   defined $opt->{mysql_user} && ! defined $opt->{mysql_password} ){
        $use_extra_users = 1;
    } elsif ( ! defined $opt->{mysql_user} &&   defined $opt->{mysql_password} ) {
        die "useless -p without -u, stop\n";
    } elsif ( ! defined $opt->{mysql_user} && ! defined $opt->{mysql_password}){
        # nothing
    } else {
        die "impossible";
    }
    
    my ($all_count, $can_clear_db_childs_cache) = (0,1);
    for my $db ( @$dbs ){
        my $conf_name = $global_conf_name;
        unless ( $conf_name ){
            $db =~ s/^([^:]+):// or die "can't parse db name $db";
            $conf_name = $1;
        }
        
        # сбрасываем кеш шардов :all
        eval{Yandex::DBShards::clear_db_childs_cache()};
        $can_clear_db_childs_cache = 0 if $@;

        local $Yandex::DBTools::CONFIG_FILE = get_db_conf_file($conf_name) || die "can't find conf file for '$conf_name'";

        my @dbs; 
        if ( $db =~ /^([^:]+):all(:.*)?$/ ){
            $all_count++;
            die "multiple :all requires Yandex::DBShards::clear_db_childs_cache (yandex-du-dbtools-perl >= 1.87)\n" if $all_count > 1 && !$can_clear_db_childs_cache;
            my ($db, $slave) = ($1, $2);
            $slave ||= '';
            @dbs = map {"$_$slave"} @{Yandex::DBShards::get_shard_dbnames($db,"", shard => "all")->{dbnames}}
        } else {
            @dbs = ($db);
        }

        for my $db ( @dbs ){
            #для конкретной БД можем захотеть использовать альтернативного пользователя даже если не было соотв. параметров
            my $use_extra_users = $use_extra_users;

            # eval для того, чтобы код мог работать с DBTools версий до 1.86 (там прототип требовал ровно один параметр у get_db_config)
            # сначала читаем конфиг без паролей -- чтобы узнать, какой там engine
            # в зависимости от engine можем переопределить пользователя и уже с ним прочитаем конфиг "начисто"
            my $cfg_wo_pass = eval q!get_db_config($db, skip_pass => 1)! || get_db_config($db);

            my $db_user = $opt->{mysql_user};

            # есть супер-особые случаи: если это продакшен-mysql, и работаем на ppcdev -- использовать специального пользователя для продакшена
            if (($cfg_wo_pass->{engine} || 'mysql') eq 'mysql'
                    && !$opt->{mysql_user}
                    && `hostname -f` =~ /^ppcdev/
                    && $conf_name =~ '^(pr|prod|production|sb|sandbox)$'
                    && $cfg_wo_pass->{user} ne 'developer') {
                $db_user = 'direct-ro';
                $use_extra_users = 1;
            }

            my $cfg = $use_extra_users ? eval q!get_db_config($db, db_user => $db_user)! : get_db_config($db);
            die "can't get_db_config (\$db=$db, \$use_extra_users=$use_extra_users): $@" unless defined $cfg;

            if ( $use_custom_user ){
                $cfg->{user} = $opt->{mysql_user};
                $cfg->{pass} = $opt->{mysql_password};
            }
            $db =~ /^([^:]+)/;
            my $host;
            if ($cfg->{engine} && ($cfg->{engine} eq "mongodb" || $cfg->{engine} eq "zookeeper")) {
                # для монги и зукипера много хостов, оставляем все
                $host = $cfg->{host};
            } else {
                # исторически была принята логика: если указано несколько хостов, то пробовать первый
                $host = ref $cfg->{host} ? $cfg->{host}->[0] : $cfg->{host};
            }
            my $instance = {
                database => $cfg->{db},
                host => $host,
                port => $cfg->{port},
                user => $cfg->{user},
                pass => $cfg->{pass},
                engine => $cfg->{engine} || 'mysql',
                title => "$conf_name:$db",
                ssl => $cfg->{ssl} || 0,
            };
            # бывают особенные параметры у особых БД, если присутствуют -- записываем
            for my $k (qw/replica_set/){
                next unless exists $cfg->{$k};
                $instance->{$k} = $cfg->{$k};
            }
            push @where, $instance;
        }
    }
    return \@where;
}


=head2 execute

    выполнить команды в соответствии с заданной политикой выполнения:
      * show -- напечатать
      * sequence -- последовательно одну за другой
      * tmux_attached -- в tmux, сразу присоединиться
      * tmux_detached -- в tmux, не присоединяться

=cut 
sub execute
{
    my ($to_execute, $O) = @_;

    if ( $O->{exec_strategy} eq 'show' ){
        print "to execute:\n".YAML::Dump($to_execute);
    } elsif ( $O->{exec_strategy} eq 'sequence' ){
        
        for my $script ( @$to_execute ){
            for my $cmd ( @{$script->{cmds}} ){
                my_system($cmd);
            }
        }

    } elsif ( $O->{exec_strategy} eq 'tmux_attached' || $O->{exec_strategy} eq 'tmux_detached' ){

        my $tmux_session;
        if (defined $FEATURES{$O->{action}}->{generate_session_name}) {
            my %params = map { $_ => $O->{$_}  } @{$FEATURES{$O->{action}}->{generate_session_name_params} ||[]};
            $tmux_session = $FEATURES{$O->{action}}->{generate_session_name}->($O->{dbs},\%params);
        } else {
            my $now = strftime("%Y%m%d-%H%M%S", localtime);
            $tmux_session = "dbs-$O->{action}-$now"; #TODO детали про action и еще что-нибудь
        }
        my $whoami = [getpwuid($<)]->[0];
        my @cmds;
        push @cmds, qq(tmux new-session -d -s $tmux_session\n);
        push @cmds, qq(tmux rename-window -t $tmux_session dummy\n);

        my $i = 1;
        for my $script (@$to_execute){
            # если актуально -- делать tee в лог-файл
            my $file = "/tmp/$tmux_session:$whoami:$i.cmd";
            open(my $fh, '>', $file);
            print $fh join "", map {"$_\n"} @{$script->{cmds}};

            push @cmds, qq[tmux new-window -t $tmux_session -n $script->{title} 'bash --init-file $file'\n];
            $i++;
        }
        push @cmds, qq(tmux kill-window -t $tmux_session:dummy\n);
        print "Команды запускаются в tmux... Присоединиться к сессии можно будет так:\ntmux attach -t $tmux_session\n";
        for my $c (@cmds){
            print "$c";
            my_system($c);
            print ( $? >> 8 == 0 ? "...success\n":"...fail\n");
        }
        if ( $O->{exec_strategy} eq 'tmux_attached' ){
            print "\nГотово, присоединяемся ...\ntmux attach -t $tmux_session\n";
            sleep 1;
            my_exec('tmux', 'attach', '-t', "$tmux_session");
        } else {
            print "\nГотово, можно присоединяться:\ntmux attach -t $tmux_session\n";
        }

    } elsif ( 0 ){

    } else {

        $O->{exec_strategy} //= '';
        die "unsupported execution strategy '$O->{exec_strategy}'";

    }
}

=head2 my_system

=cut
sub my_system
{
    if ($DRY_RUN){
        print "@_\n";
        return;
    } else {
        return system( @_ );
    }
}

=head2 my_exec

=cut
sub my_exec
{
    if ($DRY_RUN){
        print "exec: @_\n";
        exit 0;
    } else {
        exec @_;
    }
}


=head2 prepare_features

=cut
sub prepare_features
{
    for my $f ( values %FEATURES ){
        $f->{needs_query} = scalar grep { $_ eq 'query' } @{$f->{params} || []};
    }

    return 1;
}


=head2 parse_options

    Разобрать и проверить параметры

=cut
sub parse_options
{
    utf8::decode($_) for @ARGV;
    my %O = (
    );

    my $my_name = basename($0);

    GetOptions(
        "h|help" => sub {
            system("podselect -section NAME -section DESCRIPTION -section OPTIONS -section EXAMPLES -section ACTIONS $0 | pod2text"); 
            print_actions_list();
            exit 0;
        },
        'n|dry-run'          => \$DRY_RUN,
        'q|query=s'          => \$O{query},
        'db=s'               => \$O{db},
        's|seq|sequence'     => \$O{sequence},
        'show'               => \$O{show},
        'tmux-detached'      => \$O{tmux_detached},
        'tmux|tmux-attached' => \$O{tmux_attached},
        'A'                  => \$O{opt_A},
        'B'                  => \$O{opt_B},
        'skip-column-names'  => \$O{skip_column_names},
        'c=i'                => \$O{opt_c},
        'sample=i'           => \$O{opt_sample},
        'exclude-file=s'     => \$O{opt_exclude_file},
        'quick'              => \$O{opt_quick},
        'check'              => \$O{opt_check},
        'u|user=s'           => \$O{mysql_user},
        'p|pass=s'           => \$O{mysql_password},
        'no-auto-finish' => \$O{no_auto_finish},
        'critical-load=s' => \$O{critical_load},
        'max-load=s' => \$O{max_load},
        'alter-foreign-keys-method=s' => \$O{alter_foreign_keys_method},
        'max-lag=i' => \$O{max_lag},
        'check_interval=i' => \$O{check_interval},
        'no-check-alter' => \$O{no_check_alter},
        'recursion-method=s' => \$O{recursion_method},

        # options for binlog
        'base64-output=s' => \$O{'base64-output'},
        'binlog-file=s' => \$O{'binlog-file'},
        'connection-server-id=s' => \$O{'connection-server-id'},
        'debug' => \$O{'debug'},
        'exclude-gtids=s' => \$O{'exclude-gtids'},
        'include-gtids=s' => \$O{'include-gtids'},
        'offset=s' => \$O{'offset'},
        'read-from-remote-master=s' => \$O{'read-from-remote-master'},
        'result-file=s' => \$O{'result-file'},
        'rewrite-db=s' => \$O{'rewrite-db'},
        'server-id=i' => \$O{'server-id'},
        'server-id-bits=i' => \$O{'server-id-bits'},
        'start-datetime=s' => \$O{'start-datetime'},
        'start-position=s' => \$O{'start-position'},
        'stop-datetime=s' => \$O{'stop-datetime'},
        'stop-never-slave-server-id=s' => \$O{'stop-never-slave-server-id'},
        'stop-position=s' => \$O{'stop-position'},
        'verbose+' => \$O{'verbose'},
        'debug-check' => \$O{'debug-check'},
        'debug-info' => \$O{'debug-info'},
        'disable-log-bin' => \$O{'disable-log-bin'},
        'hexdump' => \$O{'hexdump'},
        'raw' => \$O{'raw'},
        'short-form' => \$O{'short-form'},
        'stop-never' => \$O{'stop-never'},
        'to-last-log' => \$O{'to-last-log'},
        'verify-binlog-checksum' => \$O{'verify-binlog-checksum'},

        # options for mysqldump
        # перечень неполный, можно дополнять по необходимости
        'tables=s'  => \$O{tables},
        'set-gtid-purged=s' => \$O{'set-gtid-purged'},
        'default-character-set=s' => \$O{'default-character-set'},
        'extended-insert=s' => \$O{'extended-insert'},
        'skip-lock-tables' => \$O{'skip-lock-tables'},
        'single-transaction' => \$O{'single-transaction'},
        'no-data' => \$O{'no-data'},
        'compact' => \$O{'compact'},
        'skip-comments' => \$O{'skip-comments'},
        'order-by-primary' => \$O{'order-by-primary'},
        'no-create-info' => \$O{'no-create-info'},
    ) or die "can't parse options, stop";

    if ( $my_name =~ /^dbs-(\S+)$/ && exists $FEATURES{$1} ){
        $O{action} = $1;
    } elsif ($my_name eq 'direct-sql') {
        $O{action} = 'sql';
    } else {
        $O{action} = shift @ARGV;
        $O{action} = '' unless $O{action};
    }

    if ( $O{action} =~ /^(?:list|ls)(?:-(actions|confs))?$/ ){
        my $what = $1 || '';
        if ($what eq 'actions') {
            print_actions_list();
        } elsif ( $what eq 'confs' ){
            print_configurations_list();
        } else {
            print_configurations_list();
            print_actions_list();
        }
        exit 0;
    } 

    die "unknown subcommand: '$O{action}'" unless exists $FEATURES{$O{action}};

    if ( $FEATURES{$O{action}}->{needs_query} ){
        # делим оставшийся @ARGV на запрос и базы
        for my $arg ( @ARGV ){
            if ( $arg =~ /^([\w:]+)$/ ){
                # выглядит как ссылка на БД
                push @{$O{dbs}}, $arg;
            } else {
                # не выглядит как ссылка на БД -- значит, это запрос
                die "too many queries: '$O{query}', '$arg'" if $O{query};
                $O{query} = $arg;
            }
        }
        $O{query} //= '';
        die "action '$O{action}' requires a query" if !$O{query} && !$FEATURES{$O{action}}->{query_optional}; 
    } elsif ($FEATURES{$O{action}}->{needs_custom_parse}) {
        $O{dbs} = [shift @ARGV];
    } else {
        $O{dbs} = [ @ARGV ];
    }

    if ( $FEATURES{$O{action}}->{needs_query} && $O{query} eq '-' ){
        $O{query} = join "\n", <STDIN>;
    }

    # определяем политику выполнения
    for my $strategy ( qw/show sequence tmux_attached tmux_detached / ){
        my $selected = delete $O{$strategy};
        next unless $selected;
        die "more than 1 execution strategy found: $O{exec_strategy} & $strategy, stop\n" if $O{exec_strategy};
        $O{exec_strategy} = $strategy;
    }

    #print YAML::Dump(\%O);

    return \%O;
}


=head2 print_actions_list

    печатает красивый список доступных действий с описаниями

=cut
sub print_actions_list
{
    print "\n### Available actions\n\n";
    for my $c ( sort keys %FEATURES ){
        $FEATURES{$c}->{description} =~ s/^\s*/    /gsm;
        $FEATURES{$c}->{description} =~ s/\s+$//gsm;
        print "$c\n";
        if ($FEATURES{$c}->{default_exec_strategy_single} || $FEATURES{$c}->{default_exec_strategy_multi}){
            print "    default: ".($FEATURES{$c}->{default_exec_strategy_single} // '-')." / ".($FEATURES{$c}->{default_exec_strategy_multi} // '-')."\n";
        }
        print "$FEATURES{$c}->{description}\n";
        print "\n";
    }
}


=head2 print_configurations_list

=cut 
sub print_configurations_list
{
    my $confs = get_db_conf_aliases();
    for my $c (keys %$confs){
        my $aliases = $confs->{$c};
        my $conf_file = get_db_conf_file($c) || "~~ UNKNOWN ~~";
        $confs->{$c} = {
            '1. conf_file' => $conf_file, 
            '2. aliases' => $aliases, 
        };
    }
    print "\n### DB configurations\n\n";
    print YAML::Dump($confs);
    return;

}


### Генерация команд для выполнения ###
#######################################

sub generate_pt_osc_session_name
{
    my ($dbs, $opt) = @_;
    my $is_ppc = 1;
    for my $db ( @$dbs ) {
        if (!($db =~ /ppc:.+/i)) {
            $is_ppc = 0;
            last;
        }
    }
    my $now = strftime("%Y%m%d-%H%M%S", localtime);
    if ($is_ppc == 1) {
        my $query = $opt->{query};
        $query =~ s/'/"/g;
        $query =~ s/\s+/ /g;
        $query =~ s/[;\s]+$//;
        unless ($query =~ /(?:alter) *table *([^ ]+) *(.*)$/si){
             die "can't parse query";
        }
        my ($table, $short_query) = ($1, $2);

        for ($table) {
            s/^`//;
            s/`$//;
        }
        
        return "dbs-pt-osc-ppc-$table-$now";
        
    } else {
        return "dbs-pt-osc-$now";
    }    
}

sub generate_cmd_guard
{
    my ($db, $opt) = @_;

    return [
        "direct-query-guard -H $db->{host} -P $db->{port} -u $db->{user} -p $db->{pass}".($opt->{opt_check} ? " --check": "").($opt->{db} ? " --db $opt->{db}": ""),
    ];
}


sub generate_cmd_innotop
{
    my ($db, $opt) = @_;

    my $count = $opt->{opt_c} || 1000;
    die "bad 'count' param, stop\n" unless $count =~ /^[0-9]+$/ && $count < 100_000;
    return [
        "innotop --user=$db->{user} --host=$db->{host} --port=$db->{port} -p$db->{pass} -d 1 --mode Q --count $count",
    ];
}

sub generate_cmd_sql
{
    my ($db, $opt) = @_;

    # в зависимости от типа базы и наличия запроса генерируем команду соответствующей процедурой
    if      ( $db->{engine} eq 'mysql' && $opt->{query} ne '' ) {
        return generate_cmd_mysql($db, $opt);
    } elsif ( $db->{engine} eq 'mysql' && $opt->{query} eq '' ) {
        return generate_cmd_mysql_shell($db, $opt);
    } elsif ( $db->{engine} eq 'clickhouse' && $opt->{query} ne '' ) {
        return generate_cmd_clh($db, $opt);
    } elsif ( $db->{engine} eq 'clickhouse' && $opt->{query} eq '' ) {
        return generate_cmd_clh_shell($db, $opt);
    } else {
        die "unexpected combination of engine ($db->{engine}) and query ($opt->{query})";
    }
}


sub generate_cmd_mysql
{
    my ($db, $opt) = @_;

    state $skip_column_names = $opt->{skip_column_names};

    my @mysql_args = ( 
        "--default-character-set=utf8",
        "--user=$db->{user}",
        "--host=$db->{host}",
        "--port=$db->{port}",
        $opt->{opt_B}      ? '--batch' : '--table',
        $opt->{opt_quick}  ? '--quick' : (),
        $skip_column_names ? '--skip-column-names' : (), 
        '--comments',
        $db->{database},

    );
    $skip_column_names = 1 if $opt->{opt_B} && $opt->{exec_strategy} eq "sequence";

    my $cmd = 
        "cat <<'EOF' |env MYSQL_PWD=".yash_quote($db->{pass})
        . ' '
        . "mysql"
        . ' '
        . join( ' ', map { yash_quote($_) } @mysql_args )
        . "\n"
        . $opt->{query}
        . "\nEOF\n";

    return [
        $opt->{opt_B} ? () : (qq!echo $db->{title}!),
        $cmd,
    ];
}

sub generate_cmd_clh
{
    my ($db, $opt) = @_;

    state $skip_column_names = $opt->{skip_column_names};

    my @clh_args = ( 
        "--user=$db->{user}",
        (($db->{pass} // '') ne '' ? "--password=$db->{pass}" : ()),
        "--host=$db->{host}",
        (($db->{ssl} // 0) == 0 ? ($db->{port} == 8123 ? "--port=9000" : "--port=$db->{port}") : "--port=9440"),
        "--database=$db->{database}",
        "--max_block_size=1024",
        (($db->{ssl} // 0) == 0 ? "" : "-s"),
        ($opt->{opt_B} ? ($skip_column_names ? '--format=TabSeparated' : '--format=TabSeparatedWithNames') : '--format=PrettyCompactMonoBlock'),
    );
    $skip_column_names = 1 if $opt->{opt_B} && $opt->{exec_strategy} eq "sequence";

    my $cmd = 
        "cat <<'EOF' |"
        . ' '
        . "clickhouse-client"
        . ' '
        . join( ' ', map { yash_quote($_) } @clh_args )
        . "\n"
        . $opt->{query}
        . "\nEOF\n";

    return [
        $opt->{opt_B} ? () : (qq!echo $db->{title}!),
        $cmd,
    ];
}


sub generate_cmd_clh_shell
{
    my ($db, $opt) = @_;

    my @clh_args = ( 
        "--user=$db->{user}",
        (($db->{pass} // '') ne '' ? "--password=$db->{pass}" : ()),
        "--host=$db->{host}",
        "--database=$db->{database}",
        "--max_block_size=1024",
        (($db->{ssl} // 0) == 0 ? ($db->{port} == 8123 ? "--port=9000" : "--port=$db->{port}") : "--port=9440"),
        (($db->{ssl} // 0) == 0 ? "" : "-s"),
    );

    my $cmd = 
        "clickhouse-client"
        . ' '
        . join( ' ', map { yash_quote($_) } @clh_args )
        . "\n";

    return [
        $cmd,
    ];
}


sub generate_cmd_mysql_shell
{
    my ($db, $opt) = @_;

    $ENV{MYSQL_PWD} = $db->{pass};

    my @mysql_args = ( 
        "--default-character-set=utf8",
        "--prompt=$db->{title}> ",
        "--user=$db->{user}",
        "--host=$db->{host}",
        "--port=$db->{port}",
        $opt->{opt_A} ? '-A' : (),
        $db->{database},
    );

    my $cmd = "env MYSQL_PWD=".yash_quote($db->{pass})
        . ' '
        . "mysql"
        . ' '
        . join( ' ', map { yash_quote($_) } @mysql_args )
        . "\n";

    return [
        $cmd,
    ];
}

sub generate_cmd_mongo
{
    my ($db, $opt) = @_;

    # mongo --ipv6 "canvas:...@man-9a83ok43msvlgbb1.db.yandex.net:27018,sas-5xglod6lhe46ei2j.db.yandex.net:27108,vla-ikuj6ebg4j9y5ntn.db.yandex.net:27018/canvas?replicaSet=rs01&authSource=canvas&w=majority&ssl=true"

    my $hosts_str = join ",", @{$db->{host}};

    my $cmd = 
        'mongo'
        . ' '
        . '--ipv6'
        . ' '
        . qq!"$db->{user}:$db->{pass}\@$hosts_str/$db->{database}?replicaSet=$db->{replica_set}&authSource=$db->{database}&w=majority&ssl=true"!;

    print $cmd;

    return [
        $cmd,
    ];
}

sub generate_cmd_live_alter
{
    my ($db, $opt) = @_;

    return [
        "dt-live-alter -H $db->{host} -P $db->{port} -u $db->{user} -p $db->{pass} '$opt->{query}' --db $db->{database}",
    ];
}

sub generate_cmd_check_ints
{
    my ($db, $opt) = @_;

    return [
        "check-mysql-int-types.pl -H $db->{host} -P $db->{port} -u $db->{user} -p $db->{pass} --db $db->{database}"
        . ( $opt->{opt_sample} ? " --sample $opt->{opt_sample}" : "")
        . ( $opt->{opt_exclude_file} ? " --exclude-file $opt->{opt_exclude_file}" : "")
    ];
}

sub generate_cmd_pt_osc
{
    my ($db, $opt) = @_;
    my $cmd = "direct-pt-osc -h $db->{host} --port $db->{port} -u $db->{user} -p $db->{pass} --db $db->{database} --execute";
    for my $param (keys $opt) {
        if (defined $opt->{$param}) {
            my $param_replace = $param;
            $param_replace =~ tr/_/-/;
            if ($param eq "query") {
                $cmd.= " '$opt->{$param}'";
            } else {
                $cmd.= " --$param_replace $opt->{$param}";
            }
        }
    }
    return [
       $cmd,
    ];
}

sub generate_cmd_analyze_alter
{
    my ($db, $opt) = @_;

    my $cmd = 
        "cat <<'EOF' |direct-analyze-alter.pl -H $db->{host} -P $db->{port} -u $db->{user} -p $db->{pass} --db $db->{database} -q -"
        . "\n"
        . $opt->{query}
        . "\nEOF\n";

    return [
        $cmd,
    ];
}

sub generate_cmd_check_plan
{
    my ($db) = @_;

    return [
        "direct-check-query-execution-plan -H $db->{host} -P $db->{port} -u $db->{user} -p $db->{pass} --db $db->{database}",
    ];
}

sub generate_cmd_binlog
{
    my ($db, $opt) = @_;

    if (not $opt->{'query'}) {
        die "Binlog file expected, stop\nList of available files: dbs-sql path:to:db 'show binary logs'\n";
    }

    $ENV{MYSQL_PWD} = $db->{pass};
    my $binlog_file = delete $opt->{'query'};
    my @mysqlbinlog_args = (
        "--user=$db->{user}",
        "--host=$db->{host}",
        "--port=$db->{port}",
        "--database=$db->{database}",
        "--read-from-remote-server",
        "-vv",
        "--base64-output=decode-rows",
    );
    push @mysqlbinlog_args, (map {"--$_=$opt->{$_}"} grep {defined $opt->{$_}} @{$FEATURES{binlog}->{params}});
    push @mysqlbinlog_args, (map {"--$_"} grep {defined $opt->{$_}} @{$FEATURES{binlog}->{flags}});
    push @mysqlbinlog_args, $binlog_file;

    my $cmd = "env MYSQL_PWD=".yash_quote($db->{pass})
        . ' '
        . "mysqlbinlog"
        . ' '
        . join( ' ', map { yash_quote($_) } @mysqlbinlog_args )
        . "\n";

    return [
        $cmd,
    ];
}

sub generate_cmd_mysqldump
{
    my ($db, $opt) = @_;

    $ENV{MYSQL_PWD} = $db->{pass};
    my @tables;
    if ($opt->{tables}) {
        @tables = split /,/, delete $opt->{tables};
    }
    my @mysqldump_args = (
        '--user', $db->{user},
        '--host', $db->{host},
        '--port', $db->{port},
    );
    push @mysqldump_args, (map {("--$_=$opt->{$_}")} grep {defined $opt->{$_}} @{$FEATURES{mysqldump}->{params}});
    push @mysqldump_args, (map {"--$_"} grep {defined $opt->{$_}} @{$FEATURES{mysqldump}->{flags}});
    push @mysqldump_args, $db->{database};
    push @mysqldump_args, @tables if @tables;

    my $cmd = "env MYSQL_PWD=".yash_quote($db->{pass})
        . ' '
        . "mysqldump"
        . ' '
        . join( ' ', map { yash_quote($_) } @mysqldump_args )
        . "\n";

    return [
        $cmd,
    ];
}

sub genegarate_cmd_zkcli
{
    my ($db, $opt) = @_;

    my $hosts_str = join ",", @{$db->{host}};
    my $cmd = "dt-zkcli -H $hosts_str @ARGV";

    return [
        $cmd,
    ];
}


### Конец генерации команд ###
##############################

