package Whostmgr::CometBackup::WebProsAuth;

#                                      Copyright 2025 WebPros International, LLC
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited.

=pod

=encoding UTF-8

=head1 NAME

Whostmgr::CometBackup::WebProsAuth

=head1 DESCRIPTION

Handles WebPros SSO authentication for Comet Backup license verification.
This module provides functionality to authenticate users via WebPros SSO
independently of WHM login, storing JWT tokens for license server verification.

=cut

use cPstrict;

use Cpanel::JSON           ();
use Cpanel::Logger         ();
use Cpanel::Exception      ();
use Cpanel::LoadModule     ();
use Cpanel::FileUtils::Write ();

my $TOKEN_FILE = $Whostmgr::CometBackup::Constants::WPA_JWT_FILE;
my $TOKEN_DIR  = '/var/cpanel/comet';

my $logger;

=head2 get_auth_status

Checks if a valid WebPros authentication token exists.

=head3 Returns

Hashref with:
  - valid: boolean indicating if token is valid
  - expires_in: seconds until expiration (if valid)
  - email: user email from token (if available)
  - reason: reason for invalidity (if not valid)

=cut

sub get_auth_status {

    # Check if token file exists
    return { valid => 0, reason => 'no_token' } unless -e $TOKEN_FILE;

    # Read token
    my $token_data = eval {
        my $json_content = do {
            local $/;
            open my $fh, '<', $TOKEN_FILE or die "Cannot read token: $!\n";
            <$fh>;
        };
        Cpanel::JSON::Load($json_content);
    };

    if ($@) {
        _logger()->warn("Failed to read token file: $@");
        return { valid => 0, reason => 'invalid_file' };
    }

    return { valid => 0, reason => 'invalid_data' } unless $token_data;

    # Check expiration
    my $token_age = time() - ( $token_data->{timestamp} || 0 );
    my $expires_in = ( $token_data->{expires_in} || 900 );

    if ( $token_age >= $expires_in ) {
        return {
            valid => 0,
            reason => 'expired',
            expired_at => $token_data->{timestamp} + $expires_in,
        };
    }

    # Token is valid
    return {
        valid      => 1,
        expires_in => $expires_in - $token_age,
        email      => _extract_email_from_token($token_data),
        token_data => $token_data,
    };
}

=head2 start_auth_flow

Initiates the WebPros SSO authentication flow.

=head3 Parameters

=over 4

=item C<$state> - Optional state data to pass through the OAuth flow

=item C<$session_id> - Optional WHM session ID to include in redirect URL

=back

=head3 Returns

Hashref with:
  - auth_url: URL to redirect user to for authentication
  - verifier: PKCE verifier (stored in session)

=cut

sub start_auth_flow ($state = undef, $session_id = undef) {

    # Session ID is required - we must have an authenticated WHM session
    die Cpanel::Exception::create( 'MissingParameter', [ name => 'session_id' ] )
        if !length $session_id;

    # Use the WebPros provider which uses the correct redirect flow
    # This provider uses https://id.webpros.com/redirect/webpros-cpanel-sign-in.html
    # which properly handles redirect-based auth instead of popup-based
    Cpanel::LoadModule::load_perl_module('Cpanel::Security::Authn::Provider::WebPros');

    my $provider = Cpanel::Security::Authn::Provider::WebPros->new(
        service_name => 'whostmgrd',
    );

    # Override the redirect URI to point directly to the Comet Backup SPA
    # The SPA's useWpaAuth hook will detect the OAuth callback params and process them
    # IMPORTANT: Include session prefix in URL path to pass cPanel security token validation
    # Add wpa_auth=1 query param to indicate we're returning from auth
    no warnings 'redefine';
    local *Cpanel::Security::Authn::Provider::WebPros::_get_redirect_uri_for_current_server_port = sub {
        my $server_port = $ENV{'SERVER_PORT'} || 2087;
        my $server_name = $ENV{'HTTP_HOST'} || $ENV{'SERVER_NAME'} || 'localhost';
        $server_name =~ s/:.*$//;  # Remove port if present

        # Always include session prefix to pass cPanel security token validation
        # Add wpa_auth=1 to indicate this is an OAuth callback
        return "https://${server_name}:${server_port}/cpsess${session_id}/cgi/comet-backup/comet-backup.cgi?wpa_auth=1";
    };

    # Add context to state
    my $auth_state = {
        purpose   => 'comet_backup_license_verification',
        timestamp => time(),
        service   => 'whostmgrd',  # Required by OpenIdConnect handler
        %{ $state || {} },
    };
    # Get authorization URL with PKCE
    # The WebPros provider handles the redirect flow properly
    my ( $auth_url, $verifier ) = $provider->start_authorize($auth_state);

    return {
        auth_url => $auth_url,
        verifier => $verifier,
    };
}

=head2 handle_callback

Handles the OAuth callback and exchanges authorization code for tokens.

=head3 Parameters

=over 4

=item C<$authz_code> - Authorization code from OAuth provider

=item C<$verifier> - PKCE verifier from start_auth_flow

=item C<$state> - State data from OAuth provider (for verification)

=back

=head3 Returns

Hashref with:
  - success: boolean
  - token_data: the stored token data (if successful)
  - error: error message (if failed)

=cut

sub handle_callback ($authz_code, $verifier, $state = undef) {

    my $result;
    eval {
        die Cpanel::Exception::create( 'MissingParameter', [ name => 'authz_code' ] )
            if !length $authz_code;

        die Cpanel::Exception::create( 'MissingParameter', [ name => 'verifier' ] )
            if !length $verifier;

        Cpanel::LoadModule::load_perl_module('Cpanel::Security::Authn::OpenIdConnect');

        my $provider = Cpanel::Security::Authn::OpenIdConnect::get_openid_provider(
            'whostmgrd',
            'webpros'
        );

        # Exchange authorization code for tokens
        my $access_token = $provider->callback( $authz_code, $verifier );

        if ( !$access_token ) {
            die Cpanel::Exception::create(
                'Authz::AccessTokenRetrievalError',
                'Failed to retrieve access token from WebPros'
            );
        }

        # Store the token
        _store_token($access_token);

        _logger()->info('WebPros authentication successful for Comet Backup');

        $result = {
            success    => 1,
            token_data => {
                expires_in => $access_token->{expires_in} || 900,
                email      => _extract_email_from_token($access_token),
            },
        };
    };

    if ($@) {
        my $error = Cpanel::Exception::get_string($@);
        _logger()->warn("WebPros authentication failed: $error");
        $result = {
            success => 0,
            error   => $error,
        };
    }

    return $result;
}

=head2 get_token

Retrieves a valid token, refreshing if necessary.

=head3 Returns

The ID token string, or undef if no valid token available.

=cut

sub get_token {

    my $status = get_auth_status();

    return unless $status->{valid};
    return unless $status->{token_data};

    return $status->{token_data}{id_token};
}

=head2 clear_token

Removes the stored authentication token.

=head3 Returns

Boolean indicating success.

=cut

sub clear_token {

    return 1 unless -e $TOKEN_FILE;

    if ( unlink $TOKEN_FILE ) {
        _logger()->info('Cleared WebPros authentication token for Comet Backup');
        return 1;
    }

    _logger()->warn("Failed to clear token file: $!");
    return 0;
}

#
# Private helper functions
#

sub _store_token ($access_token_obj) {

    # Ensure directory exists
    if ( !-d $TOKEN_DIR ) {
        require File::Path;
        eval {
            File::Path::make_path(
                $TOKEN_DIR,
                {
                    mode  => 0700,
                    owner => 'root',
                    group => 'root',
                }
            );
        };
        if ($@) {
            die Cpanel::Exception::create(
                'IO::DirectoryCreationFailed',
                "Failed to create directory $TOKEN_DIR: $@"
            );
        }
    }

    # Prepare token data structure
    # OIDC::Lite::Client::Token stores data as hash keys, not methods
    my $token_data = {
        access_token  => $access_token_obj->{access_token}  || '',
        refresh_token => $access_token_obj->{refresh_token} || '',
        expires_in    => $access_token_obj->{expires_in}    || 900,
        token_type    => $access_token_obj->{token_type}    || '',
        scope         => $access_token_obj->{scope}         || '',
        id_token      => $access_token_obj->{id_token}      || '',
        timestamp     => time(),
    };

    # Add any extra fields
    foreach my $key ( keys %{$access_token_obj} ) {
        if ( !exists $token_data->{$key} ) {
            $token_data->{$key} = $access_token_obj->{$key};
        }
    }

    # Write token data as JSON
    my $json_data = Cpanel::JSON::pretty_dump($token_data);

    # Write to file with secure permissions
    eval {
        Cpanel::FileUtils::Write::overwrite_no_exceptions(
            $TOKEN_FILE,
            $json_data,
            0600,
        );
        _logger()->info("Stored WebPros JWT token for Comet Backup to $TOKEN_FILE");
    };

    if ($@) {
        die Cpanel::Exception::create(
            'IO::FileWriteFailed',
            "Failed to store JWT token to $TOKEN_FILE: $@"
        );
    }

    return;
}

sub _extract_email_from_token ($token_data) {
    return unless $token_data;

    my $id_token = $token_data->{id_token} or return;

    # Decode JWT to extract email claim
    my @parts = split( /\./, $id_token );
    return unless @parts >= 2;

    require MIME::Base64;
    my $payload = $parts[1];

    # Add padding if needed
    $payload .= '=' x ( 4 - length($payload) % 4 ) if length($payload) % 4;

    my $claims = eval {
        my $decoded = MIME::Base64::decode_base64url($payload);
        Cpanel::JSON::Load($decoded);
    };

    return $claims->{email} if $claims;
    return;
}

sub _logger {
    return $logger ||= Cpanel::Logger->new();
}

1;
