package Lemonldap::NG::Portal::Lib::Choice;

use strict;
use Mouse;
use Safe;

extends 'Lemonldap::NG::Portal::Lib::Wrapper';
with 'Lemonldap::NG::Portal::Lib::OverConf';

our $VERSION = '2.17.0';

has modules    => ( is => 'rw', default => sub { {} } );
has rules      => ( is => 'rw', default => sub { {} } );
has type       => ( is => 'rw' );
has catch      => ( is => 'rw', default => sub { {} } );
has sessionKey => ( is => 'ro', default => '_choice' );

my $_choiceRules;

# INITIALIZATION

# init() must be called by module::init() with a number:
#  - 0 for auth
#  - 1 for userDB
#  - 2 for passwordDB ?
sub init {
    my ( $self, $type ) = @_;
    $self->type($type);

    unless ( $self->conf->{authChoiceModules}
        and %{ $self->conf->{authChoiceModules} } )
    {
        $self->error("'authChoiceModules' is empty");
        return 0;
    }

    foreach my $name ( keys %{ $self->conf->{authChoiceModules} } ) {
        my @mods =
          split( /;\s*/, $self->conf->{authChoiceModules}->{$name} );
        my $module = '::'
          . [ 'Auth', 'UserDB', 'Password' ]->[$type] . '::'
          . $mods[$type];
        my $over;
        if ( $mods[5] ) {
            eval { $over = JSON::from_json( $mods[5] ) };
            if ($@) {
                $self->logger->error("Bad over value ($@), skipped");
            }
        }
        if ( $module = $self->loadModule( $module, $over ) ) {
            $self->modules->{$name} = $module;
            $self->logger->debug(
                [qw(Authentication User Password)]->[$type]
                  . " module $name selected" );
        }
        else {
            $self->logger->error(
                "Choice: unable to load $name, disabling it: " . $self->error );
            $self->error('');
        }

        # Test if auth module wants to catch some path
        unless ($type) {
            if ( $module->can('catch') ) {
                $self->catch->{$name} = $module->catch;
            }
        }

        # Display conditions
        my $safe = Safe->new;
        my $cond = $mods[4];
        if ( defined $cond and $cond !~ /^$/ ) {
            $self->logger->debug("Found rule $cond for $name");
            $_choiceRules->{$name} =
              $safe->reval("sub{my(\$env)=\@_;return ($cond)}");
            if ($@) {
                $self->logger->error("Bad condition $cond: $@");
                return 0;
            }
        }
        else {
            $self->logger->debug("No rule for $name");
            $_choiceRules->{$name} = sub { 1 };
        }
    }
    unless ( keys %{ $self->modules } ) {
        $self->error('Choice: no available modules found, aborting');
        return 0;
    }
    return 1;
}

# RUNNING METHODS

sub initDisplay {
    my ($self, $req, $module) = @_;

    my $mod = $self->modules->{$module};
    if ( $mod->{AjaxInitScript} ) {
        $self->logger->debug( 'Append ' . $mod->{Name} . ' init/script' )
        if $mod->{Name};
        $req->data->{customScript} .= $mod->AjaxInitScript;
    }
    if ( $mod->{InitCmd} ) {
        $self->logger->debug( 'Launch ' . $mod->{Name} . ' init command' )
        if $mod->{Name};
        my $res = eval( $mod->{InitCmd} );
        if ($@) {
            die "Error running InitCmd: $@";
        }
    }

}

sub checkChoice {
    my ( $self, $req ) = @_;
    my $name;

    # Check Choice from pdata
    if ( defined $req->pdata->{_choice} ) {
        $name = $req->pdata->{_choice};
        $self->logger->debug("Choice $name selected from pdata");
    }

    unless ($name) {

        # Check with catch method
        foreach ( keys %{ $self->catch } ) {
            if ( $req->path_info =~ $self->catch->{$_} ) {
                $name = $_;
                $self->logger->debug(
                    "Choice $name selected from " . $req->path_info );
                last;
            }
        }
    }

    unless ($name) {

        # Set by OAuth Resource Owner grant // RESTServer pwdCheck
        if ( $req->data->{_pwdCheck} and $self->{conf}->{authChoiceAuthBasic} )
        {
            $name = $self->{conf}->{authChoiceAuthBasic};
        }
    }

    unless ($name) {

        # Check with other methods
        $name ||=
             $req->data->{findUserChoice}
          || $req->param( $self->conf->{authChoiceParam} )
          || $req->userData->{_choice}
          || $req->sessionInfo->{_choice}
          or return 0;
        my $from =
            $req->data->{findUserChoice}                  ? 'findUser'
          : $req->param( $self->conf->{authChoiceParam} ) ? 'param'
          : $req->userData->{_choice}                     ? 'userData'
          :                                                 'sessionInfo';
        $self->logger->debug("Choice $name selected from $from");
    }

    unless ( defined $self->modules->{$name} ) {
        $self->logger->error("Unknown choice '$name'");
        return 0;
    }

    unless ( $req->data->{findUserChoice} ) {

        # Store choice if module loops
        $req->pdata->{_choice}       = $name;
        $req->data->{_authChoice}    = $name;
        $req->sessionInfo->{_choice} = $name;
        $self->p->_authentication->authnLevel("${name}AuthnLevel");
    }

    return $name if ( $req->data->{ "enabledMods" . $self->type } );
    $req->data->{ "enabledMods" . $self->type } =
      [ $self->modules->{$name} ];
    return $name;
}

sub name {
    my ( $self, $req, $type ) = @_;
    unless ($req) {
        return 'Choice';
    }
    my $n = ref( $req->data->{ "enabledMods" . $self->type }->[0] );
    $n =~ s/^Lemonldap::NG::Portal::(?:(?:UserDB|Auth)::)?//;
    return $n;
}

package Lemonldap::NG::Portal::Main;

# Build authentication loop displayed in template
# Return authLoop array reference
sub _buildAuthLoop {
    my ( $self, $req ) = @_;
    my @authLoop;

    # Test authentication choices
    unless ( ref $self->conf->{authChoiceModules} eq 'HASH' ) {
        $self->logger->warn("No authentication choices defined");
        return [];
    }

    foreach ( sort keys %{ $self->conf->{authChoiceModules} } ) {

        my $name = $_;

        # Name can have a digit as first character
        # for sorting purpose
        # Remove it in displayed name
        $name =~ s/^(\d*)?(\s*)?//;

        # Replace also _ by space for a nice display
        $name =~ s/\_/ /g;

        # Find modules associated to authChoice
        my ( $auth, $userDB, $passwordDB, $url, $condition ) =
          split( /;\s*/, $self->conf->{authChoiceModules}->{$_} );

        unless ( $_choiceRules->{$_} ) {
            $self->logger->error("$_ has no rule");
            $_choiceRules->{$_} = sub { 1 };
        }
        unless ( $_choiceRules->{$_}->( $req->env ) ) {
            $self->logger->debug(
"Condition returns false, authentication choice $_ will not be displayed"
            );
        }
        else {
            $self->logger->debug("Displaying authentication choice $_");
            if ( $auth and $userDB and $passwordDB ) {

                # Default URL
                $req->data->{cspFormAction} ||= {};
                if (
                    defined $url
                    and not $self->checkXSSAttack( 'URI',
                        $req->env->{'REQUEST_URI'} )
                    and $url =~
                    q%^(https?://)?[^\s/.?#$].[^\s]+$% # URL must be well formatted
                  )
                {

                    my $csp_uri = $self->cspGetHost($url);
                    $req->data->{cspFormAction}->{$csp_uri} = 1;
                }
                else {
                    $url .= '#';
                }
                $self->logger->debug("Use URL $url");

                eval {
                    $self->_authentication->initDisplay($req, $_);
                };
                $self->logger->info("Unable to initialize choice $_ display: $@") if $@;


                # Options to store in the loop
                my $optionsLoop = {
                    name   => $name,
                    key    => $_,
                    module => $auth,
                    url    => $url
                };

                # Get displayType for this module
                no strict 'refs';
                my $displayType = eval {
                    $self->_authentication->modules->{$_}
                      ->can('getDisplayType')->( $self, $req );
                } || 'logo';

                $self->logger->debug(
                    "Display type $displayType for module $auth");
                $optionsLoop->{$displayType} = 1;
                my $logo = $_;

                my $foundLogo = 0;

                # If displayType is logo, check if key.png is available
                if (  -e $self->conf->{templateDir}
                    . "/../htdocs/static/common/modules/"
                    . $logo
                    . ".png" )
                {
                    $optionsLoop->{logoFile} = $logo . ".png";
                    $foundLogo = 1;
                }
                else {
                    $optionsLoop->{logoFile} = $auth . ".png";
                }

                # Compatibility, with Custom, try the module name if
                # key was not found
                if ( $auth eq 'Custom' and not $foundLogo ) {
                    $logo =
                      ( ( $self->{conf}->{customAuth} || "" ) =~ /::(\w+)$/ )
                      [0];
                    if (
                        $logo
                        and ( -e $self->conf->{templateDir}
                            . "/../htdocs/static/common/modules/"
                            . $logo
                            . ".png" )
                      )
                    {
                        $optionsLoop->{logoFile} = $logo . ".png";
                    }
                }

                # Register item in loop
                push @authLoop, $optionsLoop;

                $self->logger->debug(
                    "Authentication choice $name will be displayed");
            }
            else {
                $req->error("Authentication choice $_ value is invalid");
                return 0;
            }
        }

    }

    return \@authLoop;

}

1;

