package Lemonldap::NG::Portal::Issuer::CAS;

use strict;
use Mouse;
use URI;
use Lemonldap::NG::Portal::Main::Constants qw(
  PE_CAS_SERVICE_NOT_ALLOWED
  PE_CONFIRM
  PE_ERROR
  PE_LOGOUT_OK
  PE_OK
);

our $VERSION = '2.0.0';

extends 'Lemonldap::NG::Portal::Main::Issuer',
  'Lemonldap::NG::Portal::Lib::CAS';

# INTERFACE

sub beforeAuth { 'exportRequestParameters' }

# INITIALIZATION

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

    # Launch parents initialization subroutines, then launch IdP en SP lists
    my $res = $self->Lemonldap::NG::Portal::Main::Issuer::init();
    return 0 unless ( $self->loadApp );
    $self->addUnauthRoute(
        ( $self->path ) => {
            serviceValidate => 'serviceValidate',
            validate        => 'validate',
            proxyValidate   => 'proxyValidate',
            proxy           => 'proxy'
        },
        ['GET']
    );
    return $res;
}

# RUNNING METHODS

# Main method (launched only for authenticated users, see Main/Issuer)
sub run {
    my ( $self, $req, $target ) = @_;

    # CAS URL
    my $cas_login           = 'login';
    my $cas_logout          = 'logout';
    my $cas_validate        = 'validate';
    my $cas_serviceValidate = 'serviceValidate';
    my $cas_proxyValidate   = 'proxyValidate';
    my $cas_proxy           = 'proxy';

    # Called URL
    my $url = $req->uri();

    # Session ID
    my $session_id = $req->{sessionInfo}->{_session_id} || $req->{id};

    # Session creation timestamp
    my $time = $req->{sessionInfo}->{_utime} || time();

    # 1. LOGIN
    if ( $target eq $cas_login ) {

        $self->logger->debug("URL $url detected as an CAS LOGIN URL");

        # GET parameters
        my $service = $self->p->getHiddenFormValue( $req, 'service' )
          || $req->param('service');
        my $renew =
          $self->p->getHiddenFormValue( $req, 'renew' ) || $req->param('renew');
        my $gateway = $self->p->getHiddenFormValue( $req, 'gateway' )
          || $req->param('gateway');
        my $casServiceTicket;

        # Renew
        if ( $renew and $renew eq 'true' ) {

            # Authentication must be replayed
            $self->logger->debug("Authentication renew requested");
            $self->{updateSession} = 1;
            $req->steps(
                [
                    @{ $self->p->beforeAuth },
                    $self->p->authProcess,
                    @{ $self->p->betweenAuthAndDatas },
                    $self->p->sessionDatas,
                    @{ $self->p->afterDatas },
                ]
            );
            return PE_OK;
        }

        # If no service defined, exit
        unless ( defined $service ) {
            $self->logger->debug("No service defined in CAS URL");
            return PE_OK;
        }

        unless ( $service =~ m#^(https?://[^/]+)(/.*)?$# ) {
            $self->logger->error("Bad service $service");
            return PE_ERROR;
        }
        my ( $host, $uri ) = ( $1, $2 );
        my $app = $self->casAppList->{$host};

        # Check access on the service
        my $casAccessControlPolicy = $self->conf->{casAccessControlPolicy};

        if ( $casAccessControlPolicy =~ /^(error|faketicket)$/i ) {
            $self->logger->debug(
                "CAS access control requested on service $service");

            unless ($app) {
                $self->userLogger->error('CAS service not configured');
                return PE_CAS_SERVICE_NOT_ALLOWED;
            }
            if ( my $rule = $self->appRules->{$app} ) {
                if ( $rule->( $req, $req->sessionInfo ) ) {
                    $self->logger->debug("CAS service $service access allowed");
                }

                else {
                    $self->userLogger->error(
                        "CAS service $service access not allowed");

                    if ( $casAccessControlPolicy =~ /^(error)$/i ) {
                        $self->logger->debug(
"Return error instead of redirecting user on CAS service"
                        );
                        return PE_CAS_SERVICE_NOT_ALLOWED;
                    }

                    else {
                        $self->logger->debug(
                            "Redirect user on CAS service with a fake ticket");
                        $casServiceTicket = "ST-F4K3T1CK3T";
                    }
                }
            }
        }

        unless ($casServiceTicket) {

            # Check last authentication time to decide if
            # the authentication is recent or not
            my $casRenewFlag = 0;
            my $last_authn_utime = $req->{sessionInfo}->{_lastAuthnUTime} || 0;
            if (
                time() - $last_authn_utime <
                $self->conf->{portalForceAuthnInterval} )
            {
                $self->logger->debug(
                    "Authentication is recent, will set CAS renew flag to true"
                );
                $casRenewFlag = 1;
            }

            # Create a service ticket
            $self->logger->debug(
                "Create a CAS service ticket for service $service");

            my $Sinfos;
            $Sinfos->{type}    = 'casService';
            $Sinfos->{service} = $service;
            $Sinfos->{renew}   = $casRenewFlag;
            $Sinfos->{_cas_id} = $session_id;
            $Sinfos->{_utime}  = $time;
            $Sinfos->{_casApp} = $app;

            my $casServiceSession = $self->getCasSession( undef, $Sinfos );

            unless ($casServiceSession) {
                $self->logger->error("Unable to create CAS session");
                return PE_ERROR;
            }

            my $casServiceSessionID = $casServiceSession->id;
            $casServiceTicket = "ST-" . $casServiceSessionID;

            $self->logger->debug(
                "CAS service session $casServiceSessionID created");
        }

        # Redirect to service
        my $service_url = $service;
        $service_url .= (
            $service =~ /\?/
            ? '&ticket=' . $casServiceTicket
            : '?ticket=' . $casServiceTicket
        );

        $self->logger->debug("Redirect user to $service_url");

        $req->{urldc} = $service_url;

        $req->steps( [] );
        return PE_OK;
    }

    # 2. LOGOUT
    if ( $target eq $cas_logout ) {

        $self->logger->debug("URL $url detected as an CAS LOGOUT URL");

        # Disable Content-Security-Policy header since logout can be embedded
        # in a frame
        $req->frame(1);

        # GET parameters
        my $logout_url = $req->param('url');

        # Delete linked CAS sessions
        $self->deleteCasSecondarySessions($session_id);

        # Delete local session
        if ( my $session = $self->p->getApacheSession($session_id) ) {
            unless ( $self->p->_deleteSession( $req, $session ) ) {
                $self->logger->error("Fail to delete session $session_id ");
            }

            if ($logout_url) {

                # Display a link to the provided URL
                $self->logger->debug(
                    "Logout URL $logout_url will be displayed");

                $req->info(
'<h3 trmsg="back2CasUrl">The application you just logged out of has provided a link it would like you to follow</h3>'
                );
                $req->info("<p><a href=\"$logout_url\">$logout_url</a></p>");
                $req->datas->{activeTimer} = 0;

                return PE_CONFIRM;
            }
        }
        else {
            $self->logger->info("Unknown session $session_id");
        }

        return PE_LOGOUT_OK;

    }

    # 3. VALIDATE [CAS 1.0]
    if ( $target eq $cas_validate ) {

        $self->logger->debug("URL $url detected as an CAS VALIDATE URL");

        # This URL must not be called by authenticated users
        $self->userLogger->info(
            "CAS VALIDATE URL called by authenticated user, ignore it");

        return PE_OK;
    }

    # 4. SERVICE VALIDATE [CAS 2.0]
    if ( $target eq $cas_serviceValidate ) {

        $self->logger->debug(
            "URL $url detected as an CAS SERVICE VALIDATE URL");

        # This URL must not be called by authenticated users
        $self->userLogger->info(
            "CAS SERVICE VALIDATE URL called by authenticated user, ignore it");

        return PE_OK;
    }

    # 5. PROXY VALIDATE [CAS 2.0]
    if ( $target eq $cas_proxyValidate ) {

        $self->logger->debug("URL $url detected as an CAS PROXY VALIDATE URL");

        # This URL must not be called by authenticated users
        $self->userLogger->info(
            "CAS PROXY VALIDATE URL called by authenticated user, ignore it");

        return PE_OK;
    }

    # 6. PROXY [CAS 2.0]
    if ( $target eq $cas_proxy ) {

        $self->logger->debug("URL $url detected as an CAS PROXY URL");

        # This URL must not be called by authenticated users
        $self->userLogger->info(
            "CAS PROXY URL called by authenticated user, ignore it");

        return PE_OK;
    }

    return PE_OK;
}

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

    # Session ID
    my $session_id = $req->{sessionInfo}->{_session_id} || $req->{id};

    # Delete linked CAS sessions
    $self->deleteCasSecondarySessions($session_id);

    return PE_OK;
}

# Direct request from SP to IdP

sub validate {
    my ( $self, $req ) = @_;
    $self->logger->debug(
        'URL ' . $req->uri . ' detected as an CAS VALIDATE URL' );

    # GET parameters
    my $service = $req->param('service');
    my $ticket  = $req->param('ticket');
    my $renew   = $req->param('renew');

    # Required parameters: service and ticket
    unless ( $service and $ticket ) {
        $self->logger->error("Service and Ticket parameters required");
        return $self->returnCasValidateError();
    }

    $self->logger->debug(
        "Get validate request with ticket $ticket for service $service");

    unless ( $ticket =~ s/^ST-// ) {
        $self->logger->error("Provided ticket is not a service ticket (ST)");
        return $self->returnCasValidateError();
    }

    my $casServiceSession = $self->getCasSession($ticket);

    unless ($casServiceSession) {
        $self->logger->error("Service ticket session $ticket not found");
        return $self->returnCasValidateError();
    }

    $self->logger->debug("Service ticket session $ticket found");

    my $service1_uri = URI->new($service);
    my $service2_uri = URI->new( $casServiceSession->data->{service} );

    # Check service
    unless ( $service1_uri->eq($service2_uri) ) {

        # Tolerate that relative URI are the same
        if (   $service1_uri->rel($service2_uri) eq "./"
            or $service2_uri->rel($service1_uri) eq "./" )
        {
            $self->logger->notice(
"Submitted service $service1_uri does not exactly match initial service "
                  . $service2_uri
                  . ' but difference is tolerated.' );
        }
        else {
            $self->logger->error(
                "Submitted service $service does not match initial service "
                  . $casServiceSession->data->{service} );
            $self->deleteCasSession($casServiceSession);
            return $self->returnCasValidateError();
        }
    }
    else {
        $self->logger->debug("Submitted service $service math initial servce");
    }

    # Check renew
    if ( $renew and $renew eq 'true' ) {

        # We should check the ST was delivered with primary credentials
        $self->logger->debug("Renew flag detected ");

        unless ( $casServiceSession->data->{renew} ) {
            $self->logger->error(
"Authentication renew requested, but not done in former authentication process"
            );
            $self->deleteCasSession($casServiceSession);
            return $self->returnCasValidateError();
        }
    }

    # Open local session
    my $localSession =
      $self->p->getApacheSession( $casServiceSession->data->{_cas_id} );

    unless ($localSession) {
        $self->logger->warn( "Local session "
              . $casServiceSession->data->{_cas_id}
              . " notfound" );
        $self->deleteCasSession($casServiceSession);
        return $self->returnCasValidateError();
    }

    # Get username
    my $username =
      $localSession->data->{ $self->conf->{casAttr}
          || $self->conf->{whatToTrace} };

    $self->logger->debug("Get username $username");

    # Return success message
    $self->deleteCasSession($casServiceSession);
    return $self->returnCasValidateSuccess($username);
}

sub proxy {
    my ( $self, $req ) = @_;
}

sub serviceValidate {
    my ( $self, $req ) = @_;
    return $self->_validate2( 'SERVICE', $req );
}

sub proxyValidate {
    my ( $self, $req ) = @_;
    return $self->_validate2( 'PROXY', $req );
}

# INTERNAL METHODS

sub _validate2 {
    my ( $self, $urlType, $req ) = @_;
    $self->logger->debug(
        'URL ' . $req->uri . " detected as an CAS $urlType VALIDATE URL" );

    # GET parameters
    my $service = $req->param('service');
    my $ticket  = $req->param('ticket');
    my $pgtUrl  = $req->param('pgtUrl');
    my $renew   = $req->param('renew') // 'false';

    # PGTIOU
    my $casProxyGrantingTicketIOU;

    # Required parameters: service and ticket
    unless ( $service and $ticket ) {
        $self->logger->error("Service and Ticket parameters required");
        return $self->returnCasServiceValidateError( 'INVALID_REQUEST',
            'Missing mandatory parameters (service, ticket)' );
    }

    $self->logger->debug( "Get "
          . lc($urlType)
          . " validate request with ticket $ticket for service $service" );

    # Get CAS session corresponding to ticket
    if ( $urlType eq 'SERVICE' and !( $ticket =~ s/^ST-// ) ) {
        $self->logger->error("Provided ticket is not a service ticket (ST)");
        return $self->returnCasServiceValidateError( 'INVALID_TICKET',
            'Provided ticket is not a service ticket' );
    }
    elsif ( $urlType eq 'PROXY' and !( $ticket =~ s/^(P|S)T-// ) ) {
        $self->userLogger->error(
            "Provided ticket is not a service or proxy ticket ($1T)");
        return $self->returnCasServiceValidateError( 'INVALID_TICKET',
            'Provided ticket is not a service or proxy ticket' );
    }

    my $casServiceSession = $self->getCasSession($ticket);

    unless ($casServiceSession) {
        $self->logger->error("$urlType ticket session $ticket not found");
        return $self->returnCasServiceValidateError( 'INVALID_TICKET',
            'Ticket not found' );
    }
    my $app = $casServiceSession->data->{_casApp};

    $self->logger->debug("$urlType ticket session $ticket found");

    my $service1_uri = URI->new($service);
    my $service2_uri = URI->new( $casServiceSession->data->{service} );

    # Check service
    unless ( $service1_uri->eq($service2_uri) ) {

        # Tolerate that relative URI are the same
        if (   $service1_uri->rel($service2_uri) eq "./"
            or $service2_uri->rel($service1_uri) eq "./" )
        {
            $self->logger->notice(
"Submitted service $service1_uri does not exactly match initial service "
                  . $service2_uri
                  . ' but difference is tolerated.' );
        }
        else {
            $self->userLogger->error(
                "Submitted service $service does not match initial service "
                  . $casServiceSession->data->{service} );
            $self->deleteCasSession($casServiceSession);
            return $self->returnCasServiceValidateError( 'INVALID_SERVICE',
                'Submitted service does not match initial service' );
        }
    }
    else {
        $self->logger->debug(
            "Submitted service $service match initial service");
    }

    # Check renew
    if ( $renew and $renew eq 'true' ) {

        # We should check the ST was delivered with primary credentials
        $self->logger->debug("Renew flag detected ");

        unless ( $casServiceSession->data->{renew} ) {
            $self->logger->error(
"Authentication renew requested, but not done in former authentication process"
            );
            $self->deleteCasSession($casServiceSession);
            return $self->returnCasValidateError();
        }

    }

    # Proxies (for PROXY VALIDATE only)
    my $proxies = $casServiceSession->data->{proxies};

    # Proxy granting ticket
    if ($pgtUrl) {

        # Create a proxy granting ticket
        $self->logger->debug(
            "Create a CAS proxy granting ticket for service $service");

        my $PGinfos;

        # PGT session
        $PGinfos->{type}    = 'casProxyGranting';
        $PGinfos->{service} = $service;
        $PGinfos->{_cas_id} = $casServiceSession->data->{_cas_id};
        $PGinfos->{_utime}  = $casServiceSession->data->{_utime};
        $PGinfos->{_casApp} = $app;

        # Trace proxies
        $PGinfos->{proxies} = (
              $proxies
            ? $proxies . $self->{multiValuesSeparator} . $pgtUrl
            : $pgtUrl
        );

        my $casProxyGrantingSession = $self->getCasSession( undef, $PGinfos );

        if ($casProxyGrantingSession) {

            my $casProxyGrantingSessionID = $casProxyGrantingSession->id;
            my $casProxyGrantingTicket    = "PGT-" . $casProxyGrantingSessionID;

            $self->logger->debug(
                "CAS proxy granting session $casProxyGrantingSessionID created"
            );

            # Generate the proxy granting ticket IOU
            my $tmpCasSession = $self->getCasSession();

            if ($tmpCasSession) {

                $casProxyGrantingTicketIOU = "PGTIOU-" . $tmpCasSession->id;
                $self->deleteCasSession($tmpCasSession);
                $self->logger->debug(
"Generate proxy granting ticket IOU $casProxyGrantingTicketIOU"
                );

                # Request pgtUrl
                if (
                    $self->callPgtUrl(
                        $pgtUrl, $casProxyGrantingTicketIOU,
                        $casProxyGrantingTicket
                    )
                  )
                {
                    $self->logger->debug(
                        "Proxy granting URL $pgtUrl called with success");
                }
                else {
                    $self->logger->error(
                        "Error calling proxy granting URL $pgtUrl");
                    $casProxyGrantingTicketIOU = undef;
                }
            }

        }
        else {
            $self->logger->warn(
                "Error in proxy granting ticket management, bypass it");
        }
    }

    # Open local session
    my $localSession =
      $self->p->getApacheSession( $casServiceSession->data->{_cas_id} );

    unless ($localSession) {
        $self->userLogger->error( "Local session "
              . $casServiceSession->data->{_cas_id}
              . " notfound" );
        $self->deleteCasSession($casServiceSession);
        return $self->returnCasServiceValidateError( 'INTERNAL_ERROR',
            'No session associated to ticket' );
    }

    # Get username
    my $username =
      $localSession->data->{ $self->conf->{casAttr}
          || $self->conf->{whatToTrace} };

    $self->logger->debug("Get username $username");

    # Get attributes [CAS 3.0]
    my $attributes = {};
    my $ev =
      ( $app and $self->conf->{casAppMetaDataExportedVars}->{$app} )
      ? $self->conf->{casAppMetaDataExportedVars}->{$app}
      : {};
    unless (%$ev) {
        $ev = $self->conf->{casAttributes} || {};
    }

    foreach my $casAttribute ( keys %$ev ) {
        my $localSessionValue = $localSession->data->{ $ev->{$casAttribute} };
        $attributes->{$casAttribute} = $localSessionValue
          if defined $localSessionValue;
    }

    # Return success message
    $self->deleteCasSession($casServiceSession);
    return $self->returnCasServiceValidateSuccess( $req, $username,
        $casProxyGrantingTicketIOU, $proxies, $attributes );
}

# Store request parameters in %ENV
sub exportRequestParameters {
    my ( $self, $req ) = @_;

    foreach my $param (qw/service renew gateway/) {
        if ( $req->param($param) ) {
            $req->env->{ "llng_cas_" . $param } = $req->param($param);
        }
    }

    return PE_OK;
}

1;
