package Lemonldap::NG::Portal::Plugins::SingleSession;

use strict;
use Mouse;
use MIME::Base64;
use JSON qw(from_json to_json);
use Lemonldap::NG::Common::Session 'id2storage';
use Lemonldap::NG::Portal::Main::Constants qw(
  PE_OK
  PE_ERROR
  PE_NOTOKEN
  PE_TOKENEXPIRED
);

our $VERSION = '2.23.0';

extends qw(
  Lemonldap::NG::Portal::Main::Plugin
  Lemonldap::NG::Portal::Lib::OtherSessions
);

use constant endAuth => 'run';

has singleIPRule       => ( is => 'rw' );
has singleSessionRule  => ( is => 'rw' );
has singleUserByIPRule => ( is => 'rw' );
has ott => (
    is      => 'rw',
    lazy    => 1,
    default => sub {
        my $ott =
          $_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken');
        $ott->timeout( $_[0]->conf->{formTimeout} );
        return $ott;
    }
);

sub storageId {
    my ( $self, $id ) = @_;
    return $self->conf->{hashedSessionStore} ? id2storage($id) : $id;
}

sub init {
    my ($self) = @_;
    $self->addAuthRoute( removeOther => 'removeOther', ['GET'] );

    # Build triggering rules from configuration
    $self->singleIPRule(
        $self->p->buildRule( $self->conf->{singleIP}, 'singleIP' ) );
    return 0 unless $self->singleIPRule;

    $self->singleSessionRule(
        $self->p->buildRule( $self->conf->{singleSession}, 'singleSession' ) );
    return 0 unless $self->singleSessionRule;

    $self->singleUserByIPRule(
        $self->p->buildRule( $self->conf->{singleUserByIP}, 'singleUserByIP' )
    );
    return 0 unless $self->singleUserByIPRule;

    return 1;
}

sub run {
    my ( $self, $req ) = @_;
    my ( $linkedSessionId, $token, $html ) = ( '', '', '' );
    my $deleted         = [];
    my $otherSessions   = [];
    my @otherSessionsId = ();

    my $moduleOptions = $self->conf->{globalStorageOptions} || {};
    $moduleOptions->{backend} = $self->conf->{globalStorage};

    my $singleSessionRuleMatched =
      $self->singleSessionRule->( $req, $req->sessionInfo );
    my $singleIPRuleMatched = $self->singleIPRule->( $req, $req->sessionInfo );
    my $singleUserByIPRuleMatched =
      $self->singleUserByIPRule->( $req, $req->sessionInfo );

    if (   $singleSessionRuleMatched
        or $singleIPRuleMatched
        or $self->conf->{notifyOther} )
    {
        my $sessions = $self->module->searchOn(
            $moduleOptions,
            $self->conf->{whatToTrace},
            $req->{sessionInfo}->{ $self->conf->{whatToTrace} }
        );

        if ( $self->conf->{securedCookie} == 2 ) {
            $self->logger->debug("Looking for double sessions...");

            # searchOn() returns sessions indexed by their storage ID, then
            # it is required to use hashed ID
            $linkedSessionId =
              $sessions->{ $self->storageId( $req->id ) }->{_httpSession};
            my $msg =
              $linkedSessionId
              ? "Linked session found -> $linkedSessionId / " . $req->id
              : "NO linked session found!";
            $self->logger->debug($msg);
        }

        # searchOn() returns sessions indexed by their storage ID, then
        # it is required to use hashed ID
        my $storageId = $self->storageId( $req->id );
        foreach my $id ( keys %$sessions ) {
            next if $id eq $storageId;
            next
              if $linkedSessionId and $id eq $self->storageId($linkedSessionId);
            my $session = $self->p->getApacheSession( $id, hashStore => 0 )
              or next;
            if (
                $self->singleSessionRule->( $req, $req->sessionInfo )
                or (    $self->singleIPRule->( $req, $req->sessionInfo )
                    and $req->{sessionInfo}->{ipAddr} ne
                    $session->data->{ipAddr} )
              )
            {
                push @$deleted, $self->p->_sumUpSession( $session->data );

                # Trigger back-channel logout before deletion
                $self->p->_triggerBackChannelLogout( $req, $session->data );

                $self->p->_deleteSession( $req, $session, 1 );
            }
            else {
                push @$otherSessions, $self->p->_sumUpSession( $session->data );
                push @otherSessionsId, $id;
            }
        }
    }

    $token = $self->ott->createToken( {
            user     => $req->{sessionInfo}->{ $self->conf->{whatToTrace} },
            sessions => to_json( \@otherSessionsId )
        }
    ) if @otherSessionsId;

    if ($singleUserByIPRuleMatched) {
        my $sessions =
          $self->module->searchOn( $moduleOptions, 'ipAddr',
            $req->sessionInfo->{ipAddr} );

        foreach my $id ( keys %$sessions ) {
            next if $self->storageId( $req->id ) eq $id;

            # searchOn() returns sessions indexed by their storage ID, then
            # it is required to set hashStore to 0
            my $session = $self->p->getApacheSession( $id, hashStore => 0, )
              or next;
            unless ( $req->{sessionInfo}->{ $self->conf->{whatToTrace} } eq
                $session->data->{ $self->conf->{whatToTrace} } )
            {
                push @$deleted, $self->p->_sumUpSession( $session->data );

                # Trigger back-channel logout before deletion
                $self->p->_triggerBackChannelLogout( $req, $session->data );

                $self->p->_deleteSession( $req, $session, 1 );
            }
        }
    }

    $html = $self->p->mkSessionArray( $req, $deleted, 'sessionsDeleted', 1 )
      if ( $self->conf->{notifyDeleted} and @$deleted );
    $html .=
        $self->p->mkSessionArray( $req, $otherSessions, 'otherSessions', 1 )
      . $self->_mkRemoveOtherLink( $req, $token )
      if ( $self->conf->{notifyOther} and @$otherSessions );

    $req->info($html);
    return PE_OK;
}

sub removeOther {
    my ( $self, $req ) = @_;
    my $res   = PE_OK;
    my $count = 0;
    $req->{urldc} = decode_base64( $req->param('url') );

    if ( my $token = $req->param('token') ) {
        if ( $token = $self->ott->getToken($token) ) {

            # Read sessions from token
            my $sessions = eval { from_json( $token->{sessions} ) };
            if ($@) {
                $self->logger->error("Bad encoding in OTT: $@");
                $res = PE_ERROR;
            }
            my $as;
            foreach (@$sessions) {

                # searchOn() returns sessions indexed by their storage ID, then
                # it is required to set hashStore to 0
                unless ( $as =
                    $self->p->getApacheSession( $_, hashStore => 0, ) )
                {
                    $self->userLogger->info(
                        "SingleSession: session $_ expired");
                    next;
                }
                my $user = $token->{user};
                if ( $req->userData->{ $self->{conf}->{whatToTrace} } eq $user )
                {
                    $self->userLogger->info("Remove \"$user\" session: $_");

                    # Trigger back-channel logout before deletion
                    $self->p->_triggerBackChannelLogout( $req, $as->data );

                    $self->p->_deleteSession( $req, $as, 1 );
                    $count++;
                }
                else {
                    $self->userLogger->warn(
                        "SingleSession called with an invalid token");
                    $res = PE_TOKENEXPIRED;
                }
            }
        }
        else {
            $self->userLogger->error(
                "SingleSession called with an expired token");
            $res = PE_TOKENEXPIRED;
        }
    }
    else {
        $self->userLogger->error('SingleSession called without token');
        $res = PE_NOTOKEN;
    }

    return $self->p->doPE( $req, $res ) if $res;
    $self->userLogger->info("$count remaining session(s) removed");
    $req->mustRedirect(1);
    return $self->p->autoRedirect($req);
}

# Build the removeOther link
# Last part of URL is built trough javascript
# @return removeOther link in HTML code
sub _mkRemoveOtherLink {
    my ( $self, $req, $token ) = @_;

    return $self->loadTemplate(
        $req,
        'removeOther',
        params => {
            link => $self->p->buildUrl(
                $req->portal, "removeOther", { token => $token }
            ),
        }
    );
}

1;
