#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use Term::ANSIColor;
use POSIX qw{ strftime };

use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );

# globally allow sys_getpw* and sys_getgr* cache use
$ENV{'PW_GR_CACHE'} = 1;

my $withKeys = 1;

sub toggle_all {
    my $v = shift;
    $withKeys = $v;
    return;
}

my $remainingOptions = OVH::Bastion::Plugin::begin(

    argv    => \@ARGV,
    header  => "group info",
    options => {
        'group=s'            => \my $group,
        'all'                => \my $all,
        'with-keys'          => sub { $withKeys = 1 },
        'without-keys'       => sub { $withKeys = 0 },
        'with-everything'    => sub { toggle_all(1) },
        'without-everything' => sub { toggle_all(0) },
    },
    helptext => <<'EOF',
Print some basic information about a group

Usage: --osh SCRIPT_NAME <--group GROUP|--all> [OPTIONS]

  --group GROUP           Specify the group to display the info of
  --all                   Dump info for all groups (auditors only), use with ``--json``

  --with[out]-everything  Include or exclude all below options, including future ones
  --with[out]-keys        Whether to include the group keys list (slow-ish, default: yes)
EOF
);

my $fnret;

if (!$group && !$all) {
    help();
    osh_exit 'ERR_MISSING_PARAMETER', "Missing '--group' or '--all' parameter";
}

my $isAuditor = OVH::Bastion::is_auditor(account => $self);
if (!$isAuditor && $all) {
    osh_exit 'ERR_ACCESS_DENIED', "Only bastion auditors can use --all";
}

my @groupsToCheck;
if ($all) {
    $fnret = OVH::Bastion::get_group_list();
    $fnret or osh_exit($fnret);
    @groupsToCheck = sort keys %{$fnret->value};
    osh_info("Gathering data, this may take a few seconds...");
}
else {
    @groupsToCheck = ($group);
}

# check all groups and get the untainted data
my @groups;
foreach my $groupName (@groupsToCheck) {
    $fnret = OVH::Bastion::is_valid_group_and_existing(group => $groupName, groupType => "key");
    $fnret or osh_exit($fnret);

    # get returned untainted value
    push @groups, {group => $fnret->value->{'group'}, shortGroup => $fnret->value->{'shortGroup'}};
}

# gather this only once
$fnret = OVH::Bastion::get_bastion_ips();
my $from;
if ($fnret) {
    my @ips = @{$fnret->value};
    $from = 'from="' . join(',', @ips) . '"';
}

# big hash containing all the data we want to return
my %return;

foreach my $groupData (@groups) {
    $group = $groupData->{'group'};
    my $shortGroup = $groupData->{'shortGroup'};

    # get the member list of each system group mapped to one of the roles of the group
    my %roles;
    foreach my $role (qw{ member aclkeeper gatekeeper owner }) {
        $fnret = OVH::Bastion::is_group_existing(group => $group . ($role eq 'member' ? '' : "-$role"));
        if (!$fnret) {
            osh_exit($fnret) if $role eq 'member';    # if this happens, we really have a problem here
            $roles{$role} = [];
        }
        else {
            $roles{$role} = [grep { $_ ne 'allowkeeper' } @{$fnret->value->{'members'} || []}];
        }
    }

    # data that anybody can view:
    my %ret = (
        group       => $shortGroup,
        owners      => $roles{'owner'},
        gatekeepers => $roles{'gatekeeper'}
    );

    if (   $isAuditor
        || OVH::Bastion::is_group_owner(group => $shortGroup, account => $self, superowner => 1)
        || OVH::Bastion::is_group_gatekeeper(group => $shortGroup, account => $self)
        || OVH::Bastion::is_group_aclkeeper(group => $shortGroup, account => $self)
        || OVH::Bastion::is_group_member(group => $shortGroup, account => $self))
    {
        # members, aclkeepers, gatekeepers, owners and auditors can get the aclkeepers list
        $ret{'aclkeepers'} = $roles{'aclkeeper'};

        # being a member of the system group corresponding to the bastion group
        # can mean either member or guest, so check this here, taking into account the
        # case of the realm accounts
        my (@members, @guests);
        foreach my $account (@{$roles{'member'}}) {
            # realm accounts
            if ($account =~ /^realm_(.+)/) {
                my $pRealm = $1;
                $fnret = OVH::Bastion::get_remote_accounts_from_realm(realm => $pRealm);
                if (!$fnret || !@{$fnret->value}) {
                    # we couldn't get the list, or the list is empty: at least show that the realm shared account is there
                    push @members, $user;
                    next;
                }
                # show remote realm accounts names, either as guests or members
                foreach my $pRemoteaccount (@{$fnret->value}) {
                    if (OVH::Bastion::is_group_guest(group => $shortGroup, account => "$pRealm/$pRemoteaccount")) {
                        push @guests, "$pRealm/$pRemoteaccount";
                    }
                    else {
                        push @members, "$pRealm/$pRemoteaccount";
                    }
                }
            }
            # normal case (non-realm accounts)
            else {
                if (OVH::Bastion::is_group_guest(account => $account, group => $shortGroup)) {
                    push @guests, $account;
                }
                else {
                    push @members, $account;
                }
            }
        }

        # for each guest, get the number of accesses they have on the group,
        # so we can show it nicely
        my %guest_nb_accesses;
        my @filtered_guests;
        foreach my $guest (sort @guests) {
            $fnret = OVH::Bastion::get_acl_way(way => 'groupguest', group => $shortGroup, account => $guest);

            # for realms, don't show remote accounts with zero accesses, this could be confusing
            next if ($guest =~ m{/} && $fnret && @{$fnret->value} == 0);

            $guest_nb_accesses{$guest} = $fnret ? scalar(@{$fnret->value}) : undef;
            push @filtered_guests, $guest;
        }

        $ret{'members'}         = \@members;
        $ret{'guests'}          = \@filtered_guests;
        $ret{'guests_accesses'} = \%guest_nb_accesses;

        # add a hint about possibly inactive members
        my @inactive;
        foreach my $account (@members) {
            if (OVH::Bastion::is_account_active(account => $account)->is_ko) {
                push @inactive, $account;
            }
        }
        $ret{'inactive'} = \@inactive;

        # policies
        $fnret = OVH::Bastion::group_config(group => $group, key => 'mfa_required');
        if ($fnret && $fnret->value ne 'none') {
            $ret{'mfa_required'} = $fnret->value;
        }

        $fnret = OVH::Bastion::group_config(group => $group, key => 'guest_ttl_limit');
        if ($fnret && defined $fnret->value && $fnret->value =~ /^\d+$/) {
            $ret{'guest_ttl_limit'} = $fnret->value;
        }

        $fnret = OVH::Bastion::group_config(group => $group, %{OVH::Bastion::OPT_GROUP_IDLE_KILL_TIMEOUT()});
        if ($fnret && defined $fnret->value && $fnret->value =~ /^-?\d+$/) {
            $ret{'idle_kill_timeout'} = $fnret->value;
        }

        $fnret = OVH::Bastion::group_config(group => $group, => %{OVH::Bastion::OPT_GROUP_IDLE_LOCK_TIMEOUT()});
        if ($fnret && defined $fnret->value && $fnret->value =~ /^-?\d+$/) {
            $ret{'idle_lock_timeout'} = $fnret->value;
        }
    }

    # group egress keys if we've been asked those
    if ($withKeys) {
        $fnret = OVH::Bastion::get_group_keys(group => $group);
        if ($fnret and $from) {
            foreach my $keyfile (@{$fnret->value->{'sortedKeys'}}) {
                my $key = $fnret->value->{'keys'}{$keyfile};
                $key->{'prefix'} = $from;
                $ret{'keys'}{$key->{'fingerprint'}} = $key;
            }
        }
    }

    $return{$shortGroup} = \%ret;

    # print all this in a human-readable format, except if we've been asked
    # to dump the data for all groups, in which case the caller will only use
    # our JSON output
    print_group_info(%ret) if !$all;
}

sub print_group_info {
    my %ret       = @_;
    my $groupName = $ret{'group'};

    osh_info("Group ${groupName}'s Owners are: "
          . colored(@{$ret{'owners'}} ? join(" ", sort @{$ret{'owners'}}) : '-', 'red'))
      if $ret{'owners'};

    osh_info("Group ${groupName}'s GateKeepers (managing the members/guests list) are: "
          . colored(@{$ret{'gatekeepers'}} ? join(" ", sort @{$ret{'gatekeepers'}}) : '-', 'red'))
      if $ret{'gatekeepers'};

    osh_info("Group ${groupName}'s ACLKeepers (managing the group servers list) are: "
          . colored(@{$ret{'aclkeepers'}} ? join(" ", sort @{$ret{'aclkeepers'}}) : '-', 'red'))
      if $ret{'aclkeepers'};

    osh_info("Group ${groupName}'s Members (with access to ALL the group servers) are: "
          . colored(@{$ret{'members'}} ? join(" ", sort @{$ret{'members'}}) : '-', 'red'))
      if $ret{'members'};

    # show guest info, along with the number of accesses each guest has
    if ($ret{'guests'}) {
        my @guest_text;
        foreach my $guest (@{$ret{'guests'}}) {
            my $nb = $ret{'guests_accesses'}{$guest};
            push @guest_text, sprintf("%s[%s]", $guest, $nb // '?');
        }
        osh_info("Group ${groupName}'s Guests (with access to SOME of the group servers) are: "
              . colored(@{$ret{'guests'}} ? join(" ", sort @guest_text) : '-', 'red'));
    }

    # current user doesn't have enough rights to get this info, tell them that
    if (!$ret{'members'}) {
        osh_info "You should ask them if you think you need access for your work tasks.";
    }

    if (@{$ret{'inactive'}}) {
        osh_info("For your information, the following accounts are inactive: "
              . colored(join(" ", @{$ret{'inactive'}}), "blue"));
    }

    if ($ret{'mfa_required'}) {
        my %mfa2text = (
            "any"      => "",
            "totp"     => " (TOTP)",
            "password" => " (password)",
        );
        osh_warn("MFA Required: when connecting to servers of this group, users will be asked for an "
              . "additional authentication factor"
              . $mfa2text{$ret{'mfa_required'}});
    }

    if ($ret{'guest_ttl_limit'}) {
        osh_warn("Guest TTL enforced: guest accesses must have a TTL with a maximum duration of "
              . OVH::Bastion::duration2human(seconds => $ret{'guest_ttl_limit'})->value->{'duration'});
    }

    if ($ret{'idle_kill_timeout'}) {
        my $action = "NOT be cut";
        if ($ret{'idle_kill_timeout'} > 0) {
            $action =
              "be cut after " . OVH::Bastion::duration2human(seconds => $ret{'idle_kill_timeout'})->value->{'duration'};
        }
        osh_warn "Specific idle kill timeout: idle sessions on servers of this group will $action";
    }

    if ($ret{'idle_lock_timeout'}) {
        my $action = "NOT be locked";
        if ($ret{'idle_lock_timeout'} > 0) {
            $action = "be locked after "
              . OVH::Bastion::duration2human(seconds => $ret{'idle_lock_timeout'})->value->{'duration'};
        }
        osh_warn "Specific idle kill timeout: idle sessions on servers of this group will $action";
    }

    if ($withKeys) {
        osh_info ' ';
        if (!%{$ret{'keys'}}) {
            osh_info "This group has no SSH egress key, the owner may use groupGenerateEgressKey to generate one.";
        }
        elsif (keys %{$ret{'keys'}} == 1) {
            osh_info "The public key of this group is:";
        }
        else {
            osh_info "The public key of this group are:";
        }
        osh_info ' ';
        my @sorted = sort { $ret{'keys'}{$a}{'mtime'} <=> $ret{'keys'}{$b}{'mtime'} } keys %{$ret{'keys'}};
        foreach my $fingerprint (@sorted) {
            OVH::Bastion::print_public_key(key => $ret{'keys'}{$fingerprint});
        }
    }

    return;
}

if (!$all) {
    # only one group, don't return a hash of hash to keep backward compat
    my @keys = keys %return;
    osh_ok $return{$keys[0]};
}
else {
    osh_info "If you're only seeing this line, you might want to use --json";
    osh_ok \%return;
}
