package AppUser::Login::LoginForm;
use strict;
use warnings;
use POSIX;

use lib '/app/tools/common/lib';
use Common::RSApp;
use RSApache::RSWebApp;
use Common::DB::Item::Client;
use Common::Consts;

use lib '/app/tools/appuser/lib';
use AppUser::DB::Item::User;
use AppUser::DB::Item::UserAccess;
use AppUser::Login::Command::Select;

use lib '/app/tools/api/lib';
use API::Session;

use lib '/app/tools/rps/lib';
use Common::FormObject;

use base 'Common::FormObject';

use constant kLoginBaseURL => 'http://login.royaltyshare.com';

use constant kLoginValid           => 1;
use constant kLoginValidButExpired => 2;
use constant kLoginValidButLocked  => 3;
use constant kMFALoginValid        => 4;
use constant kMFALoginWithToken    => 5;
use constant kMFALoginWithInvalidToken => 6;

use constant kEmptyTimestamp        => '0000-00-00 00:00:00';
use constant kMaxPasswordAge        => 90;
use constant kMinimumLockoutMinutes => 30;
use constant kMaxFailedAttempts     => 10;

sub _init {
    my ( $self, %args ) = @_;

    $self->SUPER::_init(%args);

    # pre-fill email address from existing cookie
    #
    my $email;
    my $masterUserID = Common::RSApp::GetMasterUserID();
    my $userObj;
    if ($masterUserID) {
        $userObj = AppUser::DB::Item::User->Lookup( user_id => $masterUserID );
        if ( defined $userObj ) {
            $email = $userObj->email if ( $userObj->email );
            $self->{DateLocked} = Common::FormObject::Scalar::String->new( value => $userObj->date_locked );
        }
    }

    $self->{Email} = Common::FormObject::Scalar::EmailAddress->new( value => $email, required => 1 );
    $self->{Password} = Common::FormObject::Scalar::String->new( required => 1 );
    $self->{Redirect} = Common::FormObject::Scalar::String->new( value    => $args{redirect} );

    # a place holder for the "Select" page a user will select their client destination
    $self->{ClientID} = Common::FormObject::Scalar->new();

    return $self;
}

sub validate {
    my ($self, $token) = @_;

    my $valid = $self->SUPER::validate();

    # our base class will validate that the email and password are valid strings.
    # then it's our job to do a second level of validation here
    #
    if ($valid) {

        # is this the email from an existing user?
        #
        my $userObj = AppUser::DB::Item::User->Lookup( email => $self->Email() );
        if ( defined $userObj && $userObj->user_id ) {

            my $userData = $userObj->GetLoginData( $userObj->user_id );

            my $userToken = AppUser::DB::Item::UserToken->Lookup( email => $self->Email() );
            if ( $userToken ) {
                if ( $token && $userToken->isValidToken( $token ) ) {
                    # A token will be present if we've already done the login check. If
                    # the user is associated with multiple clients, then they'll be
                    # redirected to /rps/login (and to this form) after MFAAuth tries
                    # to accept the login.  The token is basically saying we've already
                    # validated the login info, no need to check passwords again.
                    return kMFALoginWithToken;
                } else {
                    # Token not valid
                    $userToken->delete;
                    return kMFALoginWithInvalidToken;
                }
            }

            # Email is valid, let's check if the user is locked out
            #
            my $now = strftime "%Y-%m-%d %H:%M:%S", localtime time;

            if ( !$userObj->disabled ) {

                if ( AppUser::DB::Item::User->HasPassword( $userObj->user_id, $self->Password() ) ) {
                    # password is valid

                    if ( $userObj->date_locked ne kEmptyTimestamp ) { # user locked

                        # Once locked out, user must stay locked out for 30 minutes
                        if ( $userData->{minutes_locked} > kMinimumLockoutMinutes ) {
                            $userObj->date_locked( kEmptyTimestamp ); # release the lock
                            $userObj->save;
                            $valid = kLoginValid;
                        } else {
                            $self->{Email}->setError(Common::FormObject::kErrUserLockedOut);
                            $valid = 0;
                        }

                    } elsif( $userData->{password_age} > kMaxPasswordAge || $userObj->must_change ) {
                        $self->{Password}->setError(Common::FormObject::kErrPasswordExpired);
                        $valid = kLoginValidButExpired;
                        $userObj->must_change(1);
                        $userObj->save;

                    } else {
                        $userObj->last_login($now);
                        $userObj->login_attempts(0);
                        $userObj->last_attempt(kEmptyTimestamp);
                        $userObj->save;
                        $valid = kLoginValid;

                    }

                } else {
                    # password invalid
                    # If the user is already locked output a message saying so, otherwise keep
                    # track of the failed attempts and lock if we get too many failed attempts.

                    if ( $userObj->date_locked ne kEmptyTimestamp ) { # user locked
                        # if you get here, the only way to get out is to enter a valid password or use
                        # the forgot password feature.
                        $self->{Email}->setError(Common::FormObject::kErrUserLockedOut);
                        $valid = 0;
                    } else {

                        my $failedAttempts = $userObj->login_attempts;
                        $failedAttempts++;
                        $userObj->login_attempts($failedAttempts);

                        if( $failedAttempts < kMaxFailedAttempts ) {
                            $userObj->last_attempt($now);
                            $self->{Password}->setError(Common::FormObject::kErrPasswordInvalid);
                            $valid = 0;
                        } else {
                            $userObj->date_locked($now);
                            $self->{Password}->setError(Common::FormObject::kErrTooManyFailedLogins);
                            $valid = 0;
                        }

                        $userObj->save;
                    }
                }

            } else {
                $self->{Email}->setError(Common::FormObject::kErrUserDisabled);
                $valid = 0;
            }
        } else {
            $self->{Email}->setError(Common::FormObject::kErrEmailNotFound);
            $valid = 0;
        }
    }

    return $valid;
}

sub acceptLogin {
    my ($self, $forceChange) = @_;

    my ( $response, $redirect );
    my $userObj = AppUser::User::User->new( email => $self->Email() );

    my $clientIDToUse = $self->ClientID();

    my $token;
    if ( $forceChange && $forceChange =~ /\?token=(\w+)/ ) {
        $token = $1;
        $forceChange = '';
    }
    # are we at login.royaltyshare.com?
    if ( Common::RSApp::GetClientID() == 0 ) {

        # Let's check for portal users right off the bat.
        if ( UserBelongsToNoClient( $userObj->UserID ) ) {
            $response = RSApache::Response::Redirect->new( kLoginBaseURL . "?portal=1" );

        # ok, we need to determine which site to send this
        # user to. We'll lookup all the clients this user
        # has access to and if there is more than one
        # we will present a landing page to choose one,
        # otherwise we will forward directly to that one client.
        #
        # if there's a selected_client that means they are coming
        # from the landing page, so send them where they want.

        # if the user is coming from the landing page then ClientID
        # will be set in this "form"
        #
        } elsif ($clientIDToUse) {

            # make sure this user belongs to this client
            #
            my $clientUser = AppUser::DB::Item::User->GetClientUser( $self->ClientID(), $self->Email() );
            if ( $clientUser->user_id == $userObj->UserID ) {
                $userObj->ClientID( $self->ClientID() );
                my $client = Common::DB::Item::Client->Lookup( client_id => $self->ClientID() );
                my $vhost = $client->web_alias();
                $vhost = $client->client_name_clean unless $vhost;

                my $url = "http://" . $vhost . ".royaltyshare.com";

                $response = RSApache::Response::Redirect->new($url);

                # Clear the user login token
                $self->_clearUserToken();

            } else {
                $clientIDToUse = 0;

                # this should only happen if someone is trying to hack their way in
                $response = RSApache::Response::Redirect->new(kLoginBaseURL);
            }
        } else {
            if ( my $clientID = UserBelongsToOneClient( $userObj->UserID ) ) {

                # user belongs to ONE client, store it here
                $clientIDToUse = $clientID;
                $self->ClientID($clientID);
                $userObj->ClientID($clientID);
                my $client = Common::DB::Item::Client->Lookup( client_id => $self->ClientID() );
                my $vhost = $client->web_alias();
                $vhost = $client->client_name_clean unless $vhost;
                my $url = "http://" . $vhost . ".royaltyshare.com";
                $response = RSApache::Response::Redirect->new($url);

                # Clear the user login token
                $self->_clearUserToken();
            } else {

                # send them to the client select page
                my %newParams = ( Email => $self->Email(), Password => $self->Password() );
                my $newCmd = AppUser::Login::Command::Select->new(%newParams);
                $newCmd->{_token} = $token;
                $response = $newCmd->execute();
            }
        }
    } elsif ( Common::RSApp::GetClientID() == API::Session::PORTAL_CLIENT_ID ) {  # portal-login

        # Clear the user login token
        $self->_clearUserToken();

        # Redirect to portal site
        my $redirect = "http://portal.royaltyshare.com";

        # Override redirect if forceChange is set
        $redirect = $forceChange if ( $forceChange );

        $response = RSApache::Response::Redirect->new($redirect);

    } else {

        # Clear the user login token
        $self->_clearUserToken();

        # we're already at the correct client domain
        # (eg. sony.royaltyshare.com) so just log them in.
        my $redirect = $self->Redirect();

        if ( !$redirect ) {
            $redirect = "/";
        }

        # Override redirect if forceChange is set
        $redirect = $forceChange if ( $forceChange );

        $response = RSApache::Response::Redirect->new($redirect);
    }

    # Both the RS_API and userId cookies are required by the Portal code.
    # Let's just set those every time.
    my $cookie_val = API::Session->_genSessionCookie( $userObj->UserID ); # RS_API

    my $cookie = CGI::Cookie->new(
        -name    => 'userId',
        -value   => $userObj->UserID,
        -domain  => '.royaltyshare.com', # works
        -expires => "+12M",
    );
    my $r = $ENV{MOD_PERL_API_VERSION} >= 2
        ? Apache2::RequestUtil->request
        : Apache->request();

    $r->headers_out->add( "Set-Cookie" => $cookie ); # XXX or err_headers_out ??

    my $session = RSApache::RSWebApp::GetSession();
    $session->Create( $userObj, $clientIDToUse );

    return $response;
}

# _clearUserToken - convenience function to clear the login token that's
# supposed to exist during the login + 2FA login flow.  This should only
# be called just before you redirect the user to a client site.
sub _clearUserToken {
    my $self = shift;
    my $userToken = AppUser::DB::Item::UserToken->Lookup( email => $self->Email() );
    $userToken->delete if ( $userToken );
}

sub UserBelongsToOneClient {
    my ($userID) = @_;
    my $clientID;

    my $userAccessColl = AppUser::DB::Item::UserAccess->GetByUserID($userID);

    # if client_id is 0 or has multiple clients, return undef
    # else return the client_id
    #
    if ($userAccessColl) {
        if ( $userAccessColl->size == 1 ) {
            my $userAccess = $userAccessColl->next;
            if ( $userAccess->client_id > 0 ) {
                $clientID = $userAccess->client_id;
            }
        }
    }

    return $clientID;
}

sub UserBelongsToNoClient {

    # In other words, is this a portal user who shouldn't be here.
    my ($userID) = @_;
    my $clientID;

    my $userAccessColl = AppUser::DB::Item::UserAccess->GetByUserID($userID);

    # If this user has access to no clients, return true.
    # Otherwise, return undef..
    #

    if ( $userAccessColl->size == 0 ) {
        return 1;
    }

    return undef;
}

1;

