package Thruk::BP::Utils;

use warnings;
use strict;
use Carp;
use Encode ();
use File::Copy qw/move/;
use File::Temp qw/tempfile/;

use Thruk::Action::AddDefaults ();
use Thruk::BP::Components::BP ();
use Thruk::Utils::Auth ();
use Thruk::Utils::Crypt ();
use Thruk::Utils::Log qw/:all/;

=head1 NAME

Thruk::BP::Utils - Helper for the business process addon

=head1 DESCRIPTION

Helper for the business process addon

=head1 METHODS

=cut

##########################################################

=head2 load_bp_data

    load_bp_data($c, [$options])

options => {
    edit:
        - 0/undef:    no edit mode
        - 1:          only edit mode

    drafts:
        - 0/undef:    skip drafts
        - 1:          load drafts too

    id:
        - <id>:       load obly BPs which match given id

    backend:
        - <id>:       load only BPs which match given backend

    skip_nodes:
        - 0/undef:    load node data
        - 1:          skip loading nodes

    skip_runtime:
        - 0/undef:    load runtime data
        - 1:          skip loading runtime data
}

load all or specific business process

=cut
sub load_bp_data {
    my($c, $opt) = @_;

    if(defined $opt && ref $opt ne 'HASH') {
        confess("load_bp_data options have been changed to hash.");
    }

    my $num        = $opt->{'id'};
    my $backend_id = $opt->{'backend'};

    # make sure our folders exist
    my $base_folder = bp_base_folder($c);
    Thruk::Utils::IO::mkdir_r($c->config->{'var_path'}.'/bp');
    Thruk::Utils::IO::mkdir_r($base_folder);

    my $bps       = [];
    my $pattern   = '*.tbp';
    my $svcfilter = { 'custom_variables' => { '>=' => "THRUK_BP_ID 0" }};
    if($num) {
        return($bps) unless $num =~ m/^\d+$/mx;
        $pattern   = $num.'.tbp';
        $svcfilter = { 'custom_variables' => { '=' => "THRUK_BP_ID ".$num }};
    }

    # check permissions
    my $is_admin = 0;
    my $allowed  = {};
    if($c->check_user_roles("authorized_for_business_processes")) {
        $is_admin = 1;
    } else {
        my $services = $c->db->get_services( filter => [ Thruk::Utils::Auth::get_auth_filter( $c, 'services' ), $svcfilter ], columns => ['custom_variable_names', 'custom_variable_values'] );
        for my $s (@{$services}) {
            my $vars = Thruk::Utils::get_custom_vars($c, $s);
            if($vars->{'THRUK_BP_ID'}) {
                $allowed->{$vars->{'THRUK_BP_ID'}} = 1;
            }
        }
    }

    my $numbers = {};
    my @files   = glob($base_folder.'/'.$pattern);
    for my $file (@files) {
        my $nr = $file;
        $nr =~ s|^.*/(\d+)\.tbp$|$1|mx;
        next if(!$is_admin && !$allowed->{$nr});
        my $bp = Thruk::BP::Components::BP->new($c, $file, undef, $opt->{'edit'}, $opt->{'skip_nodes'}, $opt->{'skip_runtime'});
        if($bp) {
            if($backend_id) {
                if(!$bp->{'bp_backend'} || $bp->{'bp_backend'} ne $backend_id) {
                    next;
                }
            }
            push @{$bps}, $bp;
            $bp->{'remote'} = 0;
            $numbers->{$bp->{'id'}} = 1;
        }
    }
    if($opt->{'drafts'}) {
        # load drafts too
        my @files = glob($c->config->{'var_path'}.'/bp/*.tbp.edit');
        for my $file (@files) {
            my $nr = $file;
            $nr =~ s|^.*/(\d+)\.tbp\.edit$|$1|mx;
            next if $numbers->{$nr};
            next if(!$is_admin && !$allowed->{$nr});
            next if $num && $num != $nr;
            $file  = $base_folder.'/'.$nr.'.tbp';
            my $bp = Thruk::BP::Components::BP->new($c, $file, undef, 1, $opt->{'skip_nodes'}, $opt->{'skip_runtime'});
            if($bp) {
                $bp->{'remote'} = 0;
                push @{$bps}, $bp;
                $numbers->{$bp->{'id'}} = 1;
            }
        }
    }

    # sort by name
    @{$bps} = sort { $a->{'name'} cmp $b->{'name'} } @{$bps};

    if(!$num && !$opt->{'skip_runtime'}) {
        check_update_index($c, $bps);
    }

    return($bps);
}

##########################################################

=head2 get_bp_ids

    get_bp_ids($c, [$id])

returns list of bp ids

=cut
sub get_bp_ids {
    my($c, $id) = @_;
    die("requires admin permissions") unless $c->check_user_roles("authorized_for_business_processes");

    my $base_folder = bp_base_folder($c);
    my $bps       = [];
    my $pattern   = '*.tbp';
    if($id) {
        return($bps) unless $id =~ m/^\d+$/mx;
        $pattern   = $id.'.tbp';
    }

    my $ids = [];
    my @files   = glob($base_folder.'/'.$pattern);
    for my $file (@files) {
        my $nr = $file;
        $nr =~ s|^.*/(\d+)\.tbp$|$1|mx;
        push @{$ids}, $nr;
    }

    return($ids);
}

##########################################################

=head2 next_free_bp_file

    next_free_bp_file($c, [$id])

return next free bp file

=cut
sub next_free_bp_file {
    my($c, $id) = @_;
    my $num = $id // 1;
    my $base_folder = bp_base_folder($c);
    Thruk::Utils::IO::mkdir_r($c->config->{'var_path'}.'/bp');
    Thruk::Utils::IO::mkdir_r($base_folder);
    while(-e $base_folder.'/'.$num.'.tbp' || -e $c->config->{'var_path'}.'/bp/'.$num.'.tbp.edit') {
        $num++;
    }
    return($base_folder.'/'.$num.'.tbp', $num);
}

##########################################################

=head2 make_uniq_label

    make_uniq_label($c, $label, $bp_id)

returns new uniq label

=cut
sub make_uniq_label {
    my($c, $label, $bp_id) = @_;

    # gather names of all BPs and editBPs
    my $names = {};
    my @files = glob(bp_base_folder($c).'/*.tbp '.$c->config->{'var_path'}.'/bp/*.tbp.edit');
    for my $file (@files) {
        next if $bp_id and $file =~ m#/$bp_id\.tbp(.edit|)$#mx;
        my $data = Thruk::Utils::IO::json_lock_retrieve($file);
        $names->{$data->{'name'}} = 1;
    }

    my $num = 2;
    my $testlabel = $label;
    while(defined $names->{$testlabel}) {
        $testlabel = $label.' '.$num++;
    }

    return $testlabel;
}

##########################################################

=head2 update_bp_status

    update_bp_status($c, $bps)

update status of all given business processes

=cut
sub update_bp_status {
    my($c, $bps) = @_;
    for my $bp (@{$bps}) {
        $bp->update_status($c);
    }
    return;
}

##########################################################

=head2 save_bp_objects

    save_bp_objects($c, $bps, [$skip_reload])

save business processes objects to object file

=cut
sub save_bp_objects {
    my($c, $bps, $skip_reload) = @_;

    my $file   = $c->config->{'Thruk::Plugin::BP'}->{'objects_save_file'};
    my $format = $c->config->{'Thruk::Plugin::BP'}->{'objects_save_format'} || 'nagios';
    if($format ne 'icinga2') { $format = 'nagios'; }
    return(0, 'no \'objects_save_file\' set') unless $file;

    my($rc, $msg) = (0, 'reload ok');
    my $obj = {'hosts' => {}, 'services' => {}};
    for my $bp (@{$bps}) {
        my $data = $bp->get_objects_conf();
        merge_obj_hash($obj, $data);
    }

    my $text = "";
    $text .= "########################\n";
    $text .= "# thruk: readonly\n";
    $text .= "# don't change, file is generated by thruk and will be overwritten.\n";
    $text .= "########################\n\n\n";
    if($format eq 'nagios') {
        $text .= _get_nagios_objects($c, $obj);
    }
    elsif($format eq 'icinga2') {
        $text .= _get_icinga2_objects($c, $obj);
    }

    # save file to disk
    my $config_backend = $c->config->{'Thruk::Plugin::BP'}->{'config_backend'};
    if($config_backend) {
        # store on remote backend
        my $peer = $c->db->get_peer_by_name($config_backend);
        die("no such config_backend: ".$config_backend) unless $peer;
        die("config_backend must be of type http") unless $peer->{'type'} eq 'http';
        my $rc = $peer->rpc($c, 'Thruk::Utils::IO::write', $file, $text);
        die("write to config_backend failed") unless $rc;
    } else {
        my($fh, $filename) = tempfile();
        _debug(sprintf("writing objects to %s", $filename));
        Thruk::Utils::IO::write($filename, Encode::encode_utf8($text));

        my $new_hex = Thruk::Utils::Crypt::hexdigest(Thruk::Utils::IO::read($filename));
        my $old_hex = -f $file ? Thruk::Utils::Crypt::hexdigest(Thruk::Utils::IO::read($file)) : '';

        # check if something changed
        if($new_hex eq $old_hex) {
            _debug(sprintf("no differences in %s and %s", $filename, $file));
            # discard file
            unlink($filename);
            return(0, "no changes, no reload required");
        }

        _debug(sprintf("moving %s to %s", $filename, $file));
        if(!move($filename, $file)) {
            return(1, 'move '.$filename.' to '.$file.' failed: '.$!);
        }
    }

    my $result_backend = $c->config->{'Thruk::Plugin::BP'}->{'result_backend'};
    if(!$result_backend) {
        my $peer_key = $c->db->peer_order->[0];
        $result_backend = $c->db->get_peer_by_key($peer_key)->peer_name;
    }

    if($skip_reload) {
        return(0, "bp objects written, reload skipped.");
    }

    # and reload
    my $pkey;
    if($result_backend) {
        my $peer = $c->db->get_peer_by_key($result_backend);
        if($peer) {
            $pkey = $peer->peer_key();
            if(!$c->stash->{'has_proc_info'} || !$c->stash->{'backend_detail'}->{$pkey}) {
                Thruk::Action::AddDefaults::add_defaults($c, Thruk::Constants::ADD_SAFE_DEFAULTS);
            }
            if(!defined $c->stash->{'backend_detail'}->{$pkey} || !$c->stash->{'backend_detail'}->{$pkey}->{'running'}) {
                return(0, "reload skipped, backend offline");
            }
        }
    }

    my $last_reload = time();
    if($pkey) {
        $last_reload = $c->stash->{'pi_detail'}->{$pkey}->{'program_start'};
        if(!$last_reload) {
            my $processinfo = $c->db->get_processinfo(backends => $pkey);
            $last_reload = ($processinfo->{$pkey} && $processinfo->{$pkey}->{'program_start'}) || (time());
        }
        sleep(1) if int($last_reload) == int(time());
    }

    my $cmd = $c->config->{'Thruk::Plugin::BP'}->{'objects_reload_cmd'};
    my $reloaded = 0;
    if($cmd) {
        ($rc, $msg) = Thruk::Utils::IO::cmd($cmd." 2>&1");
        $reloaded = 1;
    }
    elsif($result_backend) {
        # restart by livestatus
        die("no backend found by name ".$result_backend) unless $pkey;
        my $options = {
            'command' => sprintf("COMMAND [%d] RESTART_PROCESS", time()),
            'backend' => [ $pkey ],
        };
        $c->db->send_command( %{$options} );
        ($rc, $msg) = (0, 'business process saved and core restarted');
        $reloaded = 1;
    }
    if($rc == 0 && $reloaded) {
        my $core_reloaded = Thruk::Utils::wait_after_reload($c, $pkey, $last_reload);
        if(!$core_reloaded) {
            ($rc, $msg) = (1, 'business process saved but core failed to restart');
        }
    }

    return($rc, $msg);
}

##########################################################

=head2 clean_function_args

    clean_function_args($args)

return clean args from a string

=cut
sub clean_function_args {
    my($args) = @_;
    return([]) unless defined $args;
    my @newargs = $args =~ m/('.*?'|".*?"|\d+)/gmx;
    for my $arg (@newargs) {
        $arg =~ s/^'(.*)'$/$1/mx;
        $arg =~ s/^"(.*)"$/$1/mx;
        if($arg =~ m/^(\d+|\d+.\d+)$/mx) {
            $arg = $arg + 0; # make it a real number
        }
    }
    return(\@newargs);
}

##########################################################

=head2 clean_orphaned_edit_files

  clean_orphaned_edit_files($c, [$threshold])

remove old edit files

=cut
sub clean_orphaned_edit_files {
    my($c, $threshold) = @_;
    $threshold = 86400 unless defined $threshold;
    my $base_folder = bp_base_folder($c);
    for my $pattern (qw/edit runtime/) {
    my @files = glob($c->config->{'var_path'}.'/bp/*.tbp.'.$pattern);
        for my $file (@files) {
            $file =~ m/\/(\d+)\.tbp\.$pattern/mx;
            if($1 && !-e $base_folder.'/'.$1.'.tbp') {
                my($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($file);
                next if $mtime > (time() - $threshold);
                unlink($file);
            }
        }
    }
    return;
}

##########################################################

=head2 update_cron_file

  update_cron_file($c)

update reporting cronjobs

=cut
sub update_cron_file {
    my($c) = @_;

    my $rate = int($c->config->{'Thruk::Plugin::BP'}->{'refresh_interval'} || 1);
    if($rate <  1) { $rate =  1; }
    if($rate > 60) { $rate = 60; }

    # gather reporting send types from all reports
    my $cron_entries = [];
    my @files = glob(bp_base_folder($c).'/*.tbp');
    if(scalar @files > 0) {
        open(my $fh, '>>', $c->config->{'var_path'}.'/cron.log');
        Thruk::Utils::IO::close($fh, $c->config->{'var_path'}.'/cron.log');
        my $cmd = sprintf("cd %s && %s '%s bp all' >/dev/null 2>>%s/cron.log",
                                $c->config->{'project_root'},
                                $c->config->{'thruk_shell'},
                                $c->config->{'thruk_bin'},
                                $c->config->{'var_path'},
                        );
        push @{$cron_entries}, ['* * * * *', $cmd] if $rate == 1;
        push @{$cron_entries}, ['*/'.$rate.' * * * *', $cmd] if $rate != 1;
    }

    # disable calculations by setting refresh_interval or workers to zero
    if(defined $c->config->{'Thruk::Plugin::BP'}->{'refresh_interval'} && $c->config->{'Thruk::Plugin::BP'}->{'refresh_interval'} == 0
      || defined $c->config->{'Thruk::Plugin::BP'}->{'worker'} && $c->config->{'Thruk::Plugin::BP'}->{'worker'} == 0) {
          $cron_entries = [];
    }

    Thruk::Utils::update_cron_file($c, 'business process', $cron_entries);
    return 1;
}

##########################################################

=head2 get_custom_functions

  get_custom_functions($c)

returns list of custom functions

=cut
sub get_custom_functions {
    my($c) = @_;

    # get required files
    my $functions = [];
    my @files = glob(bp_base_folder($c).'/*.pm');
    for my $filename (@files) {
        next unless -s $filename;
        my $f = _parse_custom_functions($filename, 'function$');
        push @{$functions}, @{$f};
    }
    return $functions;
}

##########################################################

=head2 get_custom_filter

  get_custom_filter($c)

returns list of custom filter

=cut
sub get_custom_filter {
    my($c) = @_;

    # get required files
    my $functions = [];
    my @files = glob(bp_base_folder($c).'/*.pm');
    for my $filename (@files) {
        next unless -s $filename;
        my $f = _parse_custom_functions($filename, 'filter$');
        push @{$functions}, @{$f};
    }

    # sort by name
    @{$functions} = sort { $a->{'name'} cmp $b->{'name'} } @{$functions};

    return $functions;
}

##########################################################
sub _parse_custom_functions {
    my($filename, $filter) = @_;

    my $functions = [];
    my $last_help = "";
    my $last_args = [];

    open(my $fh, '<', $filename);
    while(my $line = <$fh>) {
        if($line =~ m/^\s*sub\s+([\w_]+)(\s|\{)/mx) {
            my $func = $1;
            my $name = $func;
            $last_help =~ s/^(Input|Output):\s(.*?):?$//mx;
            if($2) {
                $name = $1.": ". $2;
            }
            $name =~ s/:?\s*$//gmx;
            $last_help =~ s/^Arguments:\s$//mx;
            $last_help =~ s/\A\s*//msx;
            $last_help =~ s/\s*\Z//msx;
            if(!$filter || $func =~ m/$filter/mx) {
                push @{$functions}, { function => $func, help => $last_help, file => $filename, args => $last_args, name => $name };
            }
            $last_help = "";
            $last_args = [];
        }
        elsif($line =~ m/^\s*\#\s*arg\d+:\s*(.*)/mx) {
            my($name, $type, $args) = split(/\s*;\s*/mx,$1,3);
            if($type eq 'checkbox' or $type eq 'select') { $args = [split(/\s*;\s*/mx,$args)]; }
            push @{$last_args}, {name => $name, type => $type, args => $args};
        }
        elsif($line =~ m/^\s*\#\ ?(.*?$)/mx) {
            $last_help .= $1."\n";
        }
        elsif($line =~ m/^\s*$/mx) {
            $last_help = "";
            $last_args = [];
        }
    }
    CORE::close($fh);

    return $functions;
}

##########################################################

=head2 join_labels

    join_labels($nodes, [$state])

return string with joined labels

=cut
sub join_labels {
    my($nodes, $state) = @_;
    my @labels;
    for my $n (@{$nodes}) {
        push @labels, $n->{'label'};
    }
    my $num = scalar @labels;
    if($num == 0) {
        return('');
    }
    my $long = "";
    if($state) {
        for my $n (@{$nodes}) {
            my $firstline = "[".$n->{'label'}."] ".Thruk::Utils::Filter::state2text($state);
            $firstline   .= " - ".(split(/\n/mx, $n->{'status_text'}))[0] if $n->{'status_text'};
            $long .= "\n- ".clean_status_text($firstline);
        }
    }
    if($num == 1) {
        return($labels[0].$long);
    }
    if($num == 2) {
        return($labels[0].' and '.$labels[1].$long);
    }
    my $last = pop @labels;
    my $label = join(', ', @labels).' and '.$last;
    if(length($label) > 150) {
        $label = substr($label,0,147).'...';
    }
    return($label.$long);
}

##########################################################

=head2 join_args

    join_args($args)

return string with joined args

=cut
sub join_args {
    my($args) = @_;
    my @arg;
    for my $e (@{$args}) {
        $e = '' unless defined $e;
        if($e =~ m/^(\d+|\d+\.\d+)$/mx) {
            push @arg, $e;
        } else {
            push @arg, "'".$e."'";
        }
    }
    return(join(', ', @arg));
}

##########################################################

=head2 state2text

    state2text($state)

return string of given state

=cut
sub state2text {
    return(Thruk::Utils::Filter::state2text(@_));
}

##########################################################

=head2 hoststate2text

    hoststate2text($state)

return string of given host state

=cut
sub hoststate2text {
    return(Thruk::Utils::Filter::hoststate2text(@_));
}

##########################################################

=head2 merge_obj_hash

    merge_obj_hash($hash, $data)

merge objects hash with more objects

=cut
sub merge_obj_hash {
    my($hash, $data) = @_;

    if(defined $data->{'hosts'}) {
        for my $hostname (keys %{$data->{'hosts'}}) {
            my $host = $data->{'hosts'}->{$hostname};
            $hash->{'hosts'}->{$hostname} = $host;
        }
    }

    if(defined $data->{'services'}) {
        for my $hostname (keys %{$data->{'services'}}) {
            for my $description (keys %{$data->{'services'}->{$hostname}}) {
                my $service = $data->{'services'}->{$hostname}->{$description};
                $hash->{'services'}->{$hostname}->{$description} = $service;
            }
        }
    }
    return($hash);
}

##########################################################

=head2 clean_nasty

    clean_nasty($string)

clean nasty chars from string

=cut
sub clean_nasty {
    my($str) = @_;
    confess("nothing?") unless defined $str;
    $str =~ s#[`~!\$%^&*\|'"<>\?,\(\)=]*##gmxo;
    return($str);
}

##########################################################

=head2 bp_base_folder

    bp_base_folder($c)

return base folder of business process files

=cut
sub bp_base_folder {
    my($c) = @_;
    our $base_folder;
    return($base_folder) if $base_folder;
    $base_folder = Thruk::Utils::base_folder($c).'/bp';
    return($base_folder);
}

##########################################################
# return objects in nagios format
sub _get_nagios_objects {
    my($c, $obj) = @_;

    my $str = "";
    for my $hostname (sort keys %{$obj->{'hosts'}}) {
        $str .= 'define host {'. "\n";
        my $keys = _get_sorted_keys([keys %{$obj->{'hosts'}->{$hostname}}]);
        for my $attr (@{$keys}) {
            $str .= ' '. $attr. ' '. $obj->{'hosts'}->{$hostname}->{$attr}. "\n";
        }
        $str .= "}\n";
    }
    for my $hostname (sort keys %{$obj->{'services'}}) {
        for my $description (sort keys %{$obj->{'services'}->{$hostname}}) {
            $str .= 'define service {'. "\n";
            my $keys = _get_sorted_keys([keys %{$obj->{'services'}->{$hostname}->{$description}}]);
            for my $attr (@{$keys}) {
                $str .= ' '. $attr. ' '. $obj->{'services'}->{$hostname}->{$description}->{$attr}. "\n"
            }
            $str .= "}\n";
        }
    }

    return($str);
}

##########################################################
# return objects in icinga2 format
sub _get_icinga2_objects {
    my($c, $obj) = @_;

    my $str = "";
    for my $hostname (sort keys %{$obj->{'hosts'}}) {
        $str .= 'object Host "'.$hostname.'" {'. "\n";
        my $keys = _get_sorted_keys([keys %{$obj->{'hosts'}->{$hostname}}]);
        for my $attr (@{$keys}) {
            next if $attr eq 'host_name';
            next if $attr eq 'alias';
            $str .= _get_icinga2_object_attr('host', $attr, $obj->{'hosts'}->{$hostname}->{$attr});
        }
        $str .= "}\n";
    }
    for my $hostname (sort keys %{$obj->{'services'}}) {
        for my $description (sort keys %{$obj->{'services'}->{$hostname}}) {
            $str .= 'object Service "'.$description.'" {'. "\n";
            my $keys = _get_sorted_keys([keys %{$obj->{'services'}->{$hostname}->{$description}}]);
            for my $attr (@{$keys}) {
                next if $attr eq 'service_description';
                next if $attr eq 'alias';
                $str .= _get_icinga2_object_attr('service', $attr, $obj->{'services'}->{$hostname}->{$description}->{$attr});
            }
            $str .= "}\n";
        }
    }

    return($str);
}

##########################################################
sub _get_icinga2_object_attr {
    my($type, $attr, $val) = @_;
    my $key = $attr;
    if($attr =~ m/^_(.*)$/mx) {
        $key = 'vars.'.$1;
    }
    if($attr eq 'use') {
        my @templates = split(/\s*,\s*/mx, $val);
        my $str = "";
        for my $tpl (@templates) {
            $str .= ' import "'.$tpl. "\"\n";
        }
        return($str);
    }
    if($attr =~ m/_interval$/mx) {
        $val = $val.'m';
    }
    return(' '. $key. ' = "'.$val. "\"\n");

}

##########################################################
sub _get_sorted_keys {
    my($keys) = @_;
    eval {
        require Monitoring::Config::Object::Parent;
        require Monitoring::Config;
        Monitoring::Config::set_save_config();
        my @keys = @{Monitoring::Config::Object::Parent::get_sorted_keys(undef, $keys)};
        $keys = \@keys;
    };
    if($@) {
        # do a normal alphanumeric sort otherwise
        $keys = [sort @{$keys}];
    }
    return $keys;
}

##########################################################

=head2 get_nodes_grouped_by_state

    get_nodes_grouped_by_state($c, $nodes, $bp, $aggregation)

return nodes grouped by state, downtime and acknowledged

=cut
sub get_nodes_grouped_by_state {
    my($c, $nodes, $bp, $aggregation) = @_;

    my $groups = {};
    for my $n (@{$nodes}) {
        my $key = lc(Thruk::Utils::Filter::state2text($n->{'status'}));
        if($n->{'acknowledged'} && $n->{'status'} != 0) {
            $key = 'acknowledged_'.$key;
        }
        elsif($n->{'scheduled_downtime_depth'}) {
            $key = 'downtime_'.$key;
        }
        $groups->{$key} = [] unless defined $groups->{$key};
        push @{$groups->{$key}}, $n;
    }

    my $order;
    if($aggregation eq 'worst') {
        $order = $bp->{'default_state_order'};
    } elsif($aggregation eq 'best') {
        $order = [reverse @{$bp->{'default_state_order'}}];
    } else {
        die("unknown aggregation: ".$aggregation);
    }

    for my $state (@{$order}) {
        if($groups->{$state}) {
            my $first = $groups->{$state}->[0];
            my $extra = {};
            if(!defined $c->config->{'Thruk::Plugin::BP'}->{'sync_downtime_ack_state'} || $c->config->{'Thruk::Plugin::BP'}->{'sync_downtime_ack_state'} >= 1) {
                $extra = {
                    'acknowledged'             => $first->{'acknowledged'} // 0,
                    'scheduled_downtime_depth' => $first->{'scheduled_downtime_depth'} // 0,
                };
            }
            return($first->{'status'}, $groups->{$state}, $extra);
        }
    }

    # nothing found
    return(3, [], {});
}

##########################################################

=head2 check_update_index

    check_update_index($c, [$bps])

runs index update if necessary.

=cut
sub check_update_index {
    my($c, $bps) = @_;
    if($ENV{'THRUK_CRON'} || !-s $c->config->{'var_path'}.'/bp/.index' || (stat(_))[9] < time()-300) {
        _update_index($c, $bps);
    }
    return;
}

##########################################################
sub _update_index {
    my($c, $bps) = @_;
    $c->stats->profile(begin => "_update_index");

    my $index = {};
    $bps = load_bp_data($c) unless $bps;
    for my $bp (@{$bps}) {
        for my $n (@{$bp->{'nodes'}}) {
            if($n->{'bp_ref'}) {
                push @{$index->{$n->{'bp_ref'}}}, [$bp->{'id'}, $n->{'bp_ref_peer'}];
            }
        }
    }
    Thruk::Utils::IO::json_lock_store($c->config->{'var_path'}.'/bp/.index', $index, { pretty => 1 });

    $c->stats->profile(end => "_update_index");
    return;
}

##########################################################

=head2 clean_status_text

    clean_status_text($text)

returns text and removes performance data

=cut
sub clean_status_text {
    my($text, $keeplongoutput) = @_;
    chomp($text);
    $text =~ s/\|.*$//gmx;
    return($text // "") if $keeplongoutput;
    return((split(/\n|\\+n/mx, $text, 2))[0] // "");
}

##########################################################

1;
