#!/usr/bin/perl

use strict;
use warnings;
use utf8;
use lib qw(@modulesdir@);
use feature "switch";
no warnings 'experimental::smartmatch';

use Data::Dumper;
use English qw(-no_match_vars);
use Getopt::Long qw(:config auto_help);
use Log::Any::Adapter;
use Pod::Usage;

use IdPAccountManager::Data::AuthenticationToken;
use IdPAccountManager::Data::AuthenticationToken::Manager;
use IdPAccountManager::Data::ServiceProvider;
use IdPAccountManager::Data::ServiceProvider::Manager;
use IdPAccountManager::Data::TestAccount;
use IdPAccountManager::Data::TestAccount::Manager;
use IdPAccountManager::Configuration;
use IdPAccountManager::SAMLMetadata;
use IdPAccountManager::Tools;

my %options;
GetOptions(
    \%options,
    'account_profile=s',
    'contacts=s',
    'delete',
    'displayname=s',
    'email_address=s',
    'filter_expired',
    'sp_entityid=s',
    'token=s',
) or pod2usage(
    -message => "unknown option, aborting\n",
    -verbose => 0
);

my $action = $ARGV[0];

pod2usage(
    -message => "no action given, aborting\n",
    -verbose => 0
) unless $action;

my $configuration = IdPAccountManager::Configuration->new(
    file => '@sysconfdir@/manager.conf'
);

IdPAccountManager::DB->register_db(
    driver   => $configuration->{database_type},
    database => $configuration->{database_name},
    host     => $configuration->{database_host},
    password => $configuration->{database_password},
    username => $configuration->{database_user}
);

my $db = IdPAccountManager::DB->new();

for ($action) {
    when ('add_account')    { add_account()           }
    when ('list_accounts')  { list_accounts()         }
    when ('add_provider')   { add_provider()       }
    when ('list_providers') { list_providers()     }
    when ('add_token')      { add_token()   }
    when ('get_token')      { get_token()   }
    when ('list_tokens')    { list_tokens() }
    when ('parse_metadata') { parse_metadata()  }
    default {
        pod2usage(
            -message => "invalid action '$action', aborting\n",
            -verbose => 0
        );
    }
}

sub add_account {
    pod2usage(
        -message => "missing account_profile option, aborting\n",
        -verbose => 0
    ) unless $options{account_profile};

    pod2usage(
        -message => "missing sp_entityid option, aborting\n",
        -verbose => 0
    ) unless $options{sp_entityid};

    my $test_account = IdPAccountManager::Data::TestAccount->new(
        db              => $db,
        account_profile => $options{account_profile},
        sp_entityid     => $options{sp_entityid}
    );

    die "Failed to create test account\n"
        unless $test_account;

    die "Failed to save test account\n"
        unless $test_account->save(
            accounts_validity_period => $configuration->{accounts_validity_period}
        );

    printf "Account created:\n\tuserid: user%d\n\tpassword: %s\n",
      $test_account->id(), $test_account->user_password();

}

sub list_accounts {
    my %args;
    if ($options{sp_entityid}) {
        push @{ $args{query} }, sp_entityid => $options{sp_entityid};
    }

    if ($options{account_profile}) {
        push @{ $args{query} }, account_profile => $options{account_profile};
    }

    if ($options{filter_expired}) {
        push @{ $args{query} }, expiration_date => { lt => time };
    }

    my $accounts =
        IdPAccountManager::Data::TestAccount::Manager->get_testaccounts(db => $db, %args);

    if (! @$accounts) {
        printf "No matching test account in DB\n";
    }

    foreach my $account (@$accounts) {
        $account->print();
    }

    if ($options{delete}) {
        foreach my $account (@$accounts) {
            $account->delete() or die "failed to delete test account\n";
        }
        printf "%d accounts removed\n", scalar @$accounts;

        $accounts =
            IdPAccountManager::Data::TestAccount::Manager->get_testaccounts(
                db => $db
            );

        eval {
            IdPAccountManager::Tools::update_ssp_authsources(
                $configuration->{templates_dir},
                $configuration->{idp_accounts_file},
                $configuration->{idp_scope},
                $accounts
            );
        };
        die "failed to update simpleSAMLphp configuration file: $EVAL_ERROR"
            if $EVAL_ERROR;

        printf "Update simpleSamlPhp configuration file...\n";
    }

}

sub parse_metadata {
    my $federation_metadata;

    eval {
        $federation_metadata = IdPAccountManager::SAMLMetadata->new(
            file => $configuration->{federation_metadata_file}
        );
    };
    die "unable to load federation metadata: $EVAL_ERROR" if $EVAL_ERROR;

    my $data = $federation_metadata->parse(id => $options{sp_entityid});

    printf "Document %s parsed\n",
      $configuration->{federation_metadata_file};

    ## List SAML entities
    printf "Hashref representing the metadata:\n";
    print Data::Dumper->Dump($data);
}

sub add_provider {

    pod2usage(
        -message => "missing sp_entityid option, aborting\n",
        -verbose => 0
    ) unless $options{sp_entityid};

    pod2usage(
        -message => "missing contacts option, aborting\n",
        -verbose => 0
    ) unless $options{contacts};

    ## Check if entry already exists in DB first
    my $provider = IdPAccountManager::Data::ServiceProvider->new(
        db       => $db,
        entityid => $options{sp_entityid}
    );
    if ($provider->load(speculative => 1)) {
        printf "Entry for %s already in DB; update it with new data\n",
          $options{sp_entityid};

        $provider->contacts($options{contacts});
        $provider->displayname($options{displayname}) if $options{displayname};
    } else {
        $provider = IdPAccountManager::Data::ServiceProvider->new(
            db          => $db,
            entityid    => $options{sp_entityid},
            contacts    => $options{contacts},
            displayname => $options{displayname}
        );
        die "failed to create service provider\n"
            unless $provider;
    }

    $provider->save() or die "failed to save service provider";

    printf "Service Provider created\n";

}

sub list_providers {
    my %args;

    my $providers = IdPAccountManager::Data::ServiceProvider::Manager->get_serviceproviders(db => $db, %args);

    if (! @$providers) {
        printf "No service provider in DB\n";
    }

    foreach my $provider (@$providers) {
        $provider->print();
    }

    if ($options{delete}) {
        foreach my $provider (@$providers) {
            $provider->delete() or die "failed to delete authentication token\n";
        }
        printf "%d providers removed\n", scalar @$providers;
    }
}

sub list_tokens {

    my %args;
    if ($options{sp_entityid}) {
        push @{ $args{query} }, sp_entityid => $options{sp_entityid};
    }
    if ($options{token}) {
        push @{ $args{query} }, token => $options{token};
    }
    if ($options{filter_expired}) {
        push @{ $args{query} }, creation_date =>
          { lt => time - ($configuration->{tokens_validity_period} * 3600) };
    }

    my $tokens =
      IdPAccountManager::Data::AuthenticationToken::Manager->get_authenticationtokens(db => $db, %args);

    if (!@$tokens) {
        printf "No corresponding token found in DB\n";
    }

    foreach my $token (@$tokens) {
        $token->print();
    }

    if ($options{delete}) {
        foreach my $token (@$tokens) {
            $token->delete() or die "failed to delete authentication token\n";
        }
        printf "%d tokens removed\n", scalar @$tokens;

    }

}

sub get_token {

    my %args;
    if ($options{token}) {
        $args{token} = $options{token};
    }

    my $token =
      IdPAccountManager::AuthenticationToken->new(db => $db, %args);

    die "No corresponding token found in DB\n"
        unless $token->load();

    if ($options{sp_entityid}) {
        die "Authentication token cannot be used for this SP\n"
            unless $token->get('sp_entityid') eq $options{sp_entityid};
    }

    $token->print();

}

sub add_token {

    pod2usage(
        -message => "missing email_address option, aborting\n",
        -verbose => 0
    ) unless $options{email_address};

    pod2usage(
        -message => "missing sp_entityid option, aborting\n",
        -verbose => 0
    ) unless $options{sp_entityid};

    my $token = IdPAccountManager::Data::AuthenticationToken->new(
        db            => $db,
        email_address => $options{email_address},
        sp_entityid   => $options{sp_entityid}
    );

    die "failed to create authentication token\n"
        unless $token;

    ## First remove token if on exist for this email+SP
    if ($token->load(speculative => 1)) {
        $token->delete() or die "failed to delete authentication token\n";

        $token = IdPAccountManager::Data::AuthenticationToken->new(
            db            => $db,
            email_address => $options{email_address},
            sp_entityid   => $options{sp_entityid}
        );
        die "failed to create authentication token\n"
            unless $token;
    }

    $token->save() or die "failed to save authentication token\n";

    $token->print();

}

__END__

=head1 NAME

account-manager-client.pl - Command line client to the Test IdP Account manager

=head1 SYNOPSIS

B<account-manager-client.pl> B<add_account>
S<B<--account_profile> I<string>>
S<B<--sp_entityid> I<string>>

B<account-manager-client.pl> B<list_accounts>
S<[B<--account_profile> I<string>]>
S<[B<--sp_entityid> I<string>]>
[B<--filter_expired>]
[B<--delete>]

B<account-manager-client.pl> B<parse_metadata>
S<[B<--sp_entityid> I<string>]>

B<account-manager-client.pl> B<add_provider>
S<B<--sp_entityid> I<string>>
S<B<--contact> I<string>>
S<[B<--displayname> I<string>]>

B<account-manager-client.pl> B<list_providers>
[B<--delete>]

B<account-manager-client.pl> B<list_tokens>
S<B<--sp_entityid> I<string>>
S<[B<--token> I<string>]>
[B<--filter_expired>]
[B<--delete>]

B<account-manager-client.pl> B<get_token>
S<B<--sp_entityid> I<string>>
S<[B<--token> I<string>]>

B<account-manager-client.pl> B<add_token>
S<B<--sp_entityid> I<string>>
S<B<--email_address> I<string>>

=head1 DESCRIPTION

The Test Account manager instanciates test accounts associated to a SAML
Identity Provider. This script provides a command-line interface for most
functions.

=head1 EXAMPLES

    $> account-manager-client.pl add_account \
    --sp_entityid https://test.federation.renater.fr/test/ressource \
    --account_profile student1

Adds a new test account.

    $> account-manager-client.pl list_accounts \
    --sp_entityid https://test.federation.renater.fr/test/ressource \
    --account_profile student1

List all test accounts. Criterias can be added to filter test accounts.

    $> account-manager-client.pl list_accounts --filter_expired

List all expired test accounts.

    $> account-manager-client.pl list_accounts --filter_expired \
    --delete

Remove all expired test accounts from DB.

    $> account-manager-client.pl parse_metadata

Parses the SAML metadata file, as defined by the
C<federation_metadata_file> configuration parameter.

    $> account-manager-client.pl list_tokens \
    --sp_entityid https://test.federation.renater.fr/test/ressource \
    --token dhj67sjJ

List all authentication tokens. Criterias can be added to filter tokens.

    $> account-manager-client.pl list_tokens \
    --filter_expired

List all expired authentication tokens.

    $> account-manager-client.pl list_tokens \
    --filter_expired --delete

Remove all expired authentication tokens from DB.

    $> account-manager-client.pl get_token \
    --token dhj67sjJ

Get informations on a token.

    $> account-manager-client.pl add_token \
    --email_address john@my.fqdn \
    --sp_entityid https://test.federation.renater.fr/test/ressource

Adds a new test account.

    $> account-manager-client.pl add_provider \
    --sp_entityid https://test.federation.renater.fr/test/ressource \
    --displayname 'Test SP' --contacts email1@dom,email2@dom

Adds a new Service provider
