# Notifications plugin.
#
# Two entry points for notifications:
#  * a new route "/notifback" for checking accepted notifications
#    (sub getNotifBack). It launch then autoRedirect() with "mustRedirect"
#    set to 1 because underlying handler has not seen user as authenticated
#    so datas are not set;
#  * a callback inserted in process steps after authentication process,
#    This callback launches checkForNotifications to get notification and
#    cipher LemonLDAP::NG cookies.

package Lemonldap::NG::Portal::Plugins::Notifications;

use strict;
use Mouse;
use XML::LibXSLT;
use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_NOTIFICATION);

our $VERSION = '2.0.0';

# Lemonldap::NG::Portal::Main::Plugin provides addAuthRoute() and
# addUnauthRoute() methods in addition of Lemonldap::NG::Common::Module.
extends 'Lemonldap::NG::Portal::Main::Plugin';

# PROPERTIES

sub parser {
    return $_[0]->notifObject->parser;
}

# XSLT document
has stylesheet => (
    is      => 'rw',
    lazy    => 1,
    builder => sub {
        my $self      = $_[0];
        my $xslt      = XML::LibXSLT->new();
        my $styleFile = (
            (
                $self->conf->{notificationXSLTfile}
                  and -e $self->conf->{notificationXSLTfile}
            )
            ? $self->conf->{notificationXSLTfile}
            : $self->conf->{templatesDir} . '/common/notification.xsl'
        );
        die "$styleFile not found" unless ( -e $styleFile );
        return $xslt->parse_stylesheet( $self->parser->parse_file($styleFile) );
    }
);

# Underlying notifications storage object (File, DBI, LDAP,...)
has notifObject => ( is => 'rw' );

# INTERFACE

# Declare additional process steps
sub afterDatas { 'checkNotifDuringAuth' }

# For now, notifications are done only during authentication process
#sub forAuthUser { 'checkNotifForAuthUser' }

# INITIALIZATION

sub init {
    my ($self) = @_;

    # Declare new route
    $self->addUnauthRoute( 'notifback' => 'getNotifBack', ['POST'] );
    $self->addAuthRoute( 'notifback' => 'getNotifBack', ['POST'] );

    # Search for configuration options
    my $type = $self->conf->{notificationStorage};
    unless ($type) {
        $self->error('notificationStorage is not defined, aborting');
        return 0;
    }

    # Initialize notifications storage object
    $type = "Lemonldap::NG::Common::Notifications::$type";
    eval "require $type";
    if ($@) {
        $self->error(
            "Unable to load Lemonldap::NG::Common::Notifications::$type: $@");
        return 0;
    }

    # TODO: use conf database?

    my $prms = {
        %{ $self->conf->{notificationStorageOptions} },
        p    => $self->p,
        conf => $self->p->conf
    };
    unless ( eval { $self->notifObject( $type->new($prms) ); } ) {
        $self->error($@);
        return 0;
    }
    1;
}

#sub checkNotifForAuthUser {
#    my ( $self, $req ) = @_;
#    if ( my $notif = $self->checkForNotifications($req) ) {
#
#        # Cipher cookies
#        return PE_NOTIFICATION;
#    }
#    else {
#        return PE_OK;
#    }
#}

# RUNNING METHODS

sub checkNotifDuringAuth {
    my ( $self, $req ) = @_;
    if ( $req->{datas}->{notification} = $self->checkForNotifications($req) ) {
        $self->rebuildCookies($req);

        # Restore and cipher cookies
        return PE_NOTIFICATION;
    }
    else {
        return PE_OK;
    }
}

# Search for notifications and if any, returns HTML fragment.
# TODO: replace this by JSON+Ajax
sub checkForNotifications {
    my ( $self, $req ) = @_;

    # Look for pending notifications in database
    my $uid = $req->sessionInfo->{ $self->notifObject->notifField };
    my ( $notifs, $forUser ) = $self->notifObject->getNotifications($uid);
    my $form;
    return 0 unless ($notifs);

    # Transform notifications
    my $i = 0;    #Files count
    foreach my $file ( values %$notifs ) {
        eval {
            my $xml = $self->parser->parse_string($file);
            my $j   = 0;                                    #Notifications count
            foreach my $notif (
                $xml->documentElement->getElementsByTagName('notification') )
            {

                # Get the reference
                my $reference = $notif->getAttribute('reference');

                $self->lmLog( "Get reference $reference", 'debug' );

                # Check it in session
                if ( exists $req->{sessionInfo}->{"notification_$reference"} ) {

                    # The notification was already accepted
                    $self->lmLog(
                        "Notification $reference was already accepted",
                        'debug' );

                    # Remove it from XML
                    $notif->unbindNode();
                    next;
                }

                # Check condition if any
                my $condition = $notif->getAttribute('condition');

                if ($condition) {

                    $self->lmLog( "Get condition $condition", 'debug' );

                    unless ( $self->p->HANDLER->safe->reval($condition) ) {
                        $self->lmLog( "Notification condition not accepted",
                            'debug' );

                        # Remove it from XML
                        $notif->unbindNode();
                        next;

                    }
                }

                $j++;
            }

            # Go to next file if no notification found
            next unless $j;
            $i++;

            # Transform XML into HTML
            my $results = $self->stylesheet->transform( $xml, start => $i );
            $form .= $self->stylesheet->output_string($results);
        };
        if ($@) {
            $self->lmLog(
                "Bad XML file: a notification for $uid was not done ($@)",
                'warn' );
            return 0;
        }
    }

    # Stop here if nothing to display
    return 0 unless $i;

    # Returns HTML fragment
    $req->id( $self->p->HANDLER->tsv->{cipher}->encrypt( $req->id ) );
    return $form;
}

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

    # Look if all notifications have been accepted. If not, redirects to
    # portal

    # Search for Lemonldap::NG cookie (ciphered)
    my $id;
    unless ($id = $req->cookies
        and $id =~ s/$self->{conf}->{cookieName}=([^,; ]+)/$1/o )
    {
        return $self->p->sendError( $req, 'No cookie found', 401 );
    }
    $id = $self->p->HANDLER->tsv->{cipher}->decrypt($id)
      or return $self->sendError( $req, 'Unable to decrypt', 500 );

    # Verify that session exists
    $req->userData( $self->p->HANDLER->retrieveSession($id) )
      or return $self->sendError( $req, 'Unknown session', 401 );

    # Restore datas
    $self->p->importHandlerDatas($req);
    my $uid = $req->sessionInfo->{ $self->notifObject->notifField };

    my ( $notifs, $forUser ) = $self->notifObject->getNotifications($uid);
    if ($notifs) {

        # Get accepted notifications
        $req->parseBody;
        my ( $refs, $checks ) = ( {}, {} );
        my $prms = $req->params;
        foreach ( keys %$prms ) {
            my $v = $prms->{$_};
            if (s/^reference//) {
                $refs->{$v} = $_;
            }
            elsif ( s/^check// and /^(\d+x\d+)x(\d+)$/ and $v eq 'accepted' ) {
                push @{ $checks->{$1} }, $2;
            }
        }

        my $result = 1;
        foreach my $fileName ( keys %$notifs ) {
            my $file       = $notifs->{$fileName};
            my $fileResult = 1;
            my $xml        = $self->parser->parse_string($file);

            # Get pending notifications and verify that they have been accepted
            foreach my $notif (
                $xml->documentElement->getElementsByTagName('notification') )
            {
                my $reference = $notif->getAttribute('reference');

                # Check if this pending notification has been seen
                if ( my $refId = $refs->{$reference} ) {

                    # Verity that checkboxes have been checked
                    my @toCheck = $notif->getElementsByTagName('check');
                    if ( my $toCheckCount = @toCheck ) {
                        unless ($checks->{$refId}
                            and $toCheckCount == @{ $checks->{$refId} } )
                        {
                            $self->p->userNotice(
                                "$uid has not accepted notification $reference"
                            );
                            $result = $fileResult = 0;
                            next;
                        }
                    }
                }
                else {
                    # Current pending notification has not been found in
                    # request
                    $result = $fileResult = 0;
                    $self->lmLog(
                        'Current pending notification has not been found',
                        'debug' );
                    next;
                }

                # Register acceptation
                $self->p->userNotice(
                    "$uid has accepted notification $reference");
                $self->p->updatePersistentSession( $req,
                    { "notification_$reference" => time() } );
                $self->lmLog(
                    "Notification $reference registered in persistent session",
                    'debug'
                );
            }

        # Notifications accepted for this file, delete it unless it's a wildcard
            if ( $fileResult and exists $forUser->{$fileName} ) {
                $self->lmLog( "Notification file deleted", 'debug' );
                $self->notifObject->delete($fileName);
            }
        }
        unless ($result) {

            # One pending notification has been found and not accepted,
            # restart process to display pending notifications
            # TODO: is it a good idea to launch all 'afterDatas' subs ?
            $self->lmLog(
                'Pending notification has been found and not accepted',
                'debug' );
            return $self->p->do( $req, $self->p->afterDatas );
        }

        # All pending notifications have been accepted, restore cookies and
        # launch 'controlUrl' to restore "urldc" using do()
        $self->lmLog( 'All pending notifications have been accepted', 'debug' );
        $self->rebuildCookies($req);
        $self->p->do( $req, ['controlUrl'] );
    }
    else {
        # No notifications checked here, this entry point must not be called.
        # Redirecting to portal
        $self->lmLog( 'No notifications checked', 'debug' );
        $req->mustRedirect(1);
        $self->p->do( $req, [] );
    }
}

sub rebuildCookies {
    my ( $self, $req ) = @_;
    my @tmp;
    for ( my $i = 0 ; $i < @{ $req->{respHeaders} } ; $i += 2 ) {
        push @tmp, $req->respHeaders->[0], $req->respHeaders->[1]
          unless ( $req->respHeaders->[0] eq 'Set-Cookie' );
    }
    $req->{respHeaders} = \@tmp;
    $self->p->buildCookie($req);
}

1;
