#!/usr/bin/perl

use strict;
use warnings;
use utf8;

use Mojo::File qw(curfile);
use lib curfile()->dirname()->sibling('lib')->to_string;

use Config::Tiny;
use Data::Dumper;
use DateTime;
use English qw(-no_match_vars);
use Getopt::Long qw(:config auto_help);
use Pod::Usage;

use AccountManager::Account;
use AccountManager::Metadata;
use AccountManager::Entity;
use AccountManager::Token;
use AccountManager::Tools;

my %options;
GetOptions(
    \%options,
    'profile=s',
    'configuration=s',
    'contacts=s',
    'delete',
    'displayname=s',
    'email_address=s',
    'expired',
    '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_file = $ENV{MOJO_CONFIG} || 'conf/manager.conf';
my $configuration = Config::Tiny->read($configuration_file);
if (!$configuration) {
    die Config::Tiny->errstr() . "\n";
}

AccountManager::DB->register_db(
    driver   => $configuration->{database}->{type},
    database => $configuration->{database}->{name},
    host     => $configuration->{database}->{host},
    password => $configuration->{database}->{password},
    username => $configuration->{database}->{username},
    options  => [ split(/, */, $configuration->{database}->{options}) ]
);

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

SWITCH: {
    if ($action eq 'add_account')    { add_account();    last SWITCH; }
    if ($action eq 'list_accounts')  { list_accounts();  last SWITCH; }
    if ($action eq 'add_service')    { add_service();    last SWITCH; }
    if ($action eq 'list_services')  { list_services();  last SWITCH; }
    if ($action eq 'add_token')      { add_token();      last SWITCH; }
    if ($action eq 'get_token')      { get_token();      last SWITCH; }
    if ($action eq 'list_tokens')    { list_tokens();    last SWITCH; }
    if ($action eq 'parse_metadata') { parse_metadata(); last SWITCH; }
    pod2usage(
        -message => "invalid action '$action', aborting\n",
        -verbose => 0
    );
}

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

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

    my $entity = $options{entityid};
    my $validity_period =
        $configuration->{$entity}->{account_validity_period} ||
        $configuration->{service}->{account_validity_period};
    my $password = AccountManager::Tools::generate_password();

    my $account = AccountManager::Account->new(
        db              => $db,
        profile         => $options{profile},
        entityid     => $options{entityid},
        scope           => $configuration->{idp}->{scope},
        password        => $password,
        password_hash   => AccountManager::Tools::sha256_hash($password),
        creation_date   => DateTime->now(),
        expiration_date => DateTime->now()->add(days => $validity_period)
    );

    die "Failed to save test account\n"
        unless $account->save();

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

}

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

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

    if ($options{expired}) {
        push @{ $args{query} }, expiration_date => { lt => DateTime->now() };
    }

    my $accounts =
        AccountManager::Account->get_accounts(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 = AccountManager::Account->get_accounts(
            db => $db
        );

        eval {
            AccountManager::Tools::update_ssp_authsources(
                $configuration->{setup}->{templates_dir},
                $configuration->{setup}->{accounts_file},
                $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 = AccountManager::Metadata->new(
            file => $configuration->{setup}->{federation_metadata_file}
        );
    };
    die "unable to load federation metadata: $EVAL_ERROR" if $EVAL_ERROR;

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

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

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

sub add_service {

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

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

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

        $provider->contacts($options{contacts});
        $provider->displayname($options{displayname}) if $options{displayname};
    } else {
        $provider = AccountManager::Entity->new(
            db          => $db,
            entityid    => $options{entityid},
            contacts    => $options{contacts},
            displayname => $options{displayname}
        );
    }

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

    printf "Service Provider created\n";

}

sub list_services {
    my %args;

    my $providers = AccountManager::Entity->get_entities(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{entityid}) {
        push @{ $args{query} }, entityid => $options{entityid};
    }
    if ($options{token}) {
        push @{ $args{query} }, token => $options{token};
    }
    if ($options{expired}) {
        push @{ $args{query} }, expiration_date => { lt => DateTime->now() };
    }

    my $tokens = AccountManager::Token->get_tokens(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 = AccountManager::Token->new(db => $db, %args);

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

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

    $token->print();

}

sub add_token {

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

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

    # delete any previous token for the same email/service couple
    my $old_token = AccountManager::Token->new(
        db            => $db,
        email_address => $options{email_address},
        entityid   => $options{entityid}
    );

    if ($old_token->load(speculative => 1)) {
        $old_token->delete() or die "failed to delete authentication token\n";
    }

    # compute a new token
    my $validity_period = $configuration->{service}->{tokens_validity_period};
    my $token = AccountManager::Token->new(
        db              => $db,
        email_address   => $options{email_address},
        entityid     => $options{entityid},
        creation_date   => DateTime->now(),
        expiration_date => DateTime->now()->add(hours => $validity_period),
        token           => AccountManager::Tools::generate_token()
    );

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

    printf "Authentication token created:\n\token %s\n", $token->token();

}

__END__

=head1 NAME

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

=head1 SYNOPSIS

access-check-manager.pl [options] add_account

  Options:
    --profile <string>
    --entityid <string>

access-check-manager.pl [options] list_accounts

  Options:
    --profile <string>
    --entityid <string>
    --expired
    --delete

access-check-manager.pl [options] parse_metadata

  Options:
    --entityid <string>

access-check-manager.pl [options] add_service

  Options:
    --entityid <string>
    --contact <string>
    --displayname <string>

access-check-manager.pl [options] list_services

  Options:
    --delete

access-check-manager.pl [options] list_tokens

  Options:
    --entityid <string>
    --token <string>
    --expired
    --delete

access-check-manager.pl [options] get_token

  Options:
    --entityid <string>
    --token <string>

access-check-manager.pl [options] add_token

  Options:
    --entityid <string>
    --email_address <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

    $> access-check-manager.pl add_account \
    --entityid https://test.federation.renater.fr/test/ressource \
    --profile student1

Adds a new test account.

    $> access-check-manager.pl list_accounts \
    --entityid https://test.federation.renater.fr/test/ressource \
    --profile student1

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

    $> access-check-manager.pl list_accounts --expired

List all expired test accounts.

    $> access-check-manager.pl list_accounts --expired --delete

Remove all expired test accounts from DB.

    $> access-check-manager.pl parse_metadata

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

    $> access-check-manager.pl list_tokens \
    --entityid https://test.federation.renater.fr/test/ressource \
    --token dhj67sjJ

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

    $> access-check-manager.pl list_tokens --expired

List all expired authentication tokens.

    $> access-check-manager.pl list_tokens --expired --delete

Remove all expired authentication tokens from DB.

    $> access-check-manager.pl get_token --token dhj67sjJ

Get informations on a token.

    $> access-check-manager.pl add_token --email_address john@my.fqdn \
    --entityid https://test.federation.renater.fr/test/ressource

Adds a new test account.

    $> access-check-manager.pl add_service \
    --entityid https://test.federation.renater.fr/test/ressource \
    --displayname 'Test SP' --contacts email1@dom,email2@dom

Adds a new service provider