package AccessCheck::Tools;

use Mojo::Base -strict;

use Crypt::Bcrypt;
use Crypt::OpenSSL::Random ();
use Encode;
use English qw(-no_match_vars);
use List::Util qw(shuffle);
use List::MoreUtils qw(pairwise);
use MIME::Base64;
use Template;
use Template::Constants qw(:chomp);

use AccessCheck::Template::Plugin::Quote;

use constant {
    SIG_BCRYPT                       => '2y',
    PASSWORD_BCRYPT_DEFAULT_COST     => 10,
    PASSWORD_BCRYPT_MAX_PASSWORD_LEN => 72,
};

sub encrypt {
    my ($string, $key) = @_;

    my @string_chars = split(//, $string);
    my @key_chars    = split(//, $key);

    return encode_base64(otp(\@string_chars, \@key_chars));
}

sub decrypt {
    my ($string, $key) = @_;

    my @string_chars = split(//, decode_base64($string));
    my @key_chars = split(//, $key);

    return otp(\@string_chars, \@key_chars);
}

sub otp {
    my ($string, $key) = @_;

    my @chars =
        pairwise { chr(ord($a) ^ ord($b)) }
        @$string,
        @$key;

    return join('', @chars);
}

# shamelessly stolen from PHP::Functions::Password
# https://metacpan.org/dist/PHP-Functions-Password/source/lib/PHP/Functions/Password.pm
sub hash {
    my ($string) = @_;

    my $salt = Crypt::OpenSSL::Random::random_bytes(16);
    my $cost = PASSWORD_BCRYPT_DEFAULT_COST;

    # Treat passwords as strings of bytes
    # "\x{100}"  becomes "\xc4\x80"; preferred equivalent of Encode::is_utf8($string) && Encode::_utf8_off($password);
    utf8::is_utf8($string) && utf8::encode($string);

    # Everything beyond the max password length in bytes for bcrypt is silently ignored.
    require bytes;
    if (bytes::length($string) > PASSWORD_BCRYPT_MAX_PASSWORD_LEN) {
        # $string is already bytes, so the bytes:: prefix is redundant here
        $string = substr($string, 0, PASSWORD_BCRYPT_MAX_PASSWORD_LEN);
    }

    return Crypt::Bcrypt::bcrypt($string, SIG_BCRYPT, $cost, $salt);
}

sub generate_password {
    my ($size) = @_;

    # define alphabet
    my @uppers       = ('A' .. 'N', 'P' .. 'Z');
    my @lowers       = ('a' .. 'k', 'm' .. 'z');
    my @punctuations = (':', '!', '?', '&', '$', '=', '-', '#');
    my @numerics     = ('0' .. '9');
    my @all          = (@uppers, @lowers, @punctuations, @numerics);

    # start with a random character of each class
    my @chars = (
        $uppers[ rand @uppers ],
        $lowers[ rand @lowers ],
        $punctuations[ rand @punctuations ],
        $numerics[ rand @numerics ]
    );

    # complete with additional characters
    for my $i (1 .. $size - 4) {
        push(@chars, $all[ rand @all ]);
    }

    return join('', shuffle(@chars));
}

sub generate_secret {
    my ($size) = @_;

    # define alphabet
    my @lowers       = ('a' .. 'k', 'm' .. 'z');
    my @numerics     = ('0' .. '9');
    my @all          = (@lowers, @numerics);

    # fill characters list
    my @chars;
    for my $i (1 .. $size) {
        push(@chars, $all[ rand @all ]);
    }

    return join('', shuffle(@chars));
}

## Updates simpleSamlPhp authsources.php configuration file
sub update_ssp_authsources {
    my ($templates_dir, $output, $accounts) = @_;

    my $tt2 = Template->new({
        ENCODING     => 'utf8',
        PRE_CHOMP    => CHOMP_ONE,
        INCLUDE_PATH => [
            sprintf("%s/other", $templates_dir),
            sprintf("%s/accounts", $templates_dir),
        ],
    });

    $tt2->process(
        'accounts.php.tt2',
        { accounts => $accounts},
        $output,
        { binmode => ':utf8' }
    ) or die $tt2->error();
}

1;
__END__

=head1 NAME

AccessCheck::Tools - Set of subroutines usefull for the Test Account manager

=head1 DESCRIPTION

The Test Account manager instanciates test accounts associated to a SAML Identity Provider.
This module gathers a set of usefull subroutines.

=head1 FUNCTIONS

=over

=item generate_password()

Returns a random password following some security guidelines.

=item update_ssp_authsources()

Update simpleSAMLphp authsources.php configuration file with the currently valid test accounts.

=back