#                                      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.

package Whostmgr::CometBackup::Setup;

=encoding utf8

=head1 NAME

Whostmgr::CometBackup::Setup

=head1 DESCRIPTION

Setup related functionality for Comet Backup.

=cut

use cPstrict;
use Cpanel::AppConfig                                ();
use Cpanel::TaskQueue::Reader                        ();
use Cpanel::Exception                                ();
use Cpanel::Plugins::Components::Rules               ();
use Cpanel::Plugins::Components::Rules::Util::NVData ();
use Cpanel::Plugins::Components::Rules::CompanyId    ();
use Cpanel::PwCache                                  ();
use Whostmgr::ACLS                                   ();
use Whostmgr::CometBackup::Config                    ();
use Whostmgr::CometBackup::AgentManager              ();
use Whostmgr::CometBackup::Constants                 ();
use POSIX                                            ();
use Cpanel::Imports;

our $DEBUG = 0;

=head1 FUNCTIONS

=cut

sub is_setup {
    my $comet_agent_status = is_comet_agent_running();
    return $comet_agent_status;

    # TODO - re-enable when we know if using feature flags or Leika
    # my $default_plugin_status = eval { are_comet_default_plugins_enabled(); };
    # if ( my $exception = $@ ) {
    #     logger()->warn("The are_comet_default_plugins_enabled failed due to the following exception: $exception.");
    #     return 0;
    # }
    # return $default_plugin_status;
}

=head2 initialize

Initialize the server with Comet Backup, including setting up the recovery_data Protected Item.

This function is idempotent - it checks the current configuration status and only performs
the steps that are actually needed. It can be safely called multiple times without doing
redundant work like re-downloading or re-installing an already installed agent.

=cut

sub initialize {
    my $logger = Whostmgr::CometBackup::Logger->new();
    $logger->info("Initializing the server.");

    # Get current configuration status to determine which steps are needed
    my $config_status = _get_configuration_status();

    # If already fully configured, nothing to do
    if ( $config_status->{status} eq 'configured' ) {
        $logger->debug("Server is already fully configured with Comet Backup, nothing to do");
        return '';
    }

    # Step 1: Check license - if unlicensed, we cannot proceed
    unless ( $config_status->{has_license} ) {
        $logger->error("Comet Backup license is not present or invalid");
        die "Comet Backup license is not present or invalid";
    }
    $logger->debug("Comet Backup license is valid");

    # Step 2: Install the agent if not already installed
    unless ( $config_status->{agent_installed} ) {
        $logger->debug("Agent not installed, downloading and installing...");

        my $agent_file = _download_agent();
        unless ($agent_file) {
            $logger->error("Failed to download Comet Backup agent");
            die "Failed to download Comet Backup agent";
        }
        $logger->debug("Agent downloaded successfully: $agent_file");

        my $install_success = install_comet_agent($agent_file);
        unless ($install_success) {
            $logger->error("Failed to install Comet Backup agent");
            die "Failed to install Comet Backup agent";
        }
        $logger->debug("Agent installed successfully");

        # Enable the agent via systemd after fresh install
        $logger->debug("Enabling Comet Backup agent service...");
        my $enable_success = _enable_agent();
        unless ($enable_success) {
            $logger->warn("Failed to enable agent service, but continuing");
        }
    }
    else {
        $logger->debug("Agent already installed, skipping download and installation");
    }

    # Step 3: Start the agent if not already running
    unless ( $config_status->{agent_running} ) {
        $logger->debug("Agent not running, starting...");
        my ( $start_status, $start_message ) = Whostmgr::CometBackup::AgentManager::start();
        unless ($start_status) {
            $logger->error("Failed to start Comet Backup agent: $start_message");
            die "Failed to start Comet Backup agent: $start_message";
        }
        $logger->debug("Agent started successfully");
    }
    else {
        $logger->debug("Agent already running, skipping start");
    }

    # Step 4: Check for credentials before attempting login
    unless ( $config_status->{has_credentials} ) {
        $logger->debug("Server credentials not configured, skipping agent login");
        return '';
    }

    # Step 5: Setup and login the agent to the Comet server if not already logged in
    unless ( $config_status->{agent_logged_in} ) {
        $logger->debug("Agent not logged in, setting up Comet Backup agent...");
        my $login_success = setup_comet_agent();
        if ( $login_success == 1 ) {
            $logger->debug("Agent setup and login completed successfully");
        }
        elsif ( $login_success == 0 ) {
            $logger->debug("Agent login skipped - server credentials may not be available yet");
        }
        else {
            $logger->warn("Agent login failed");
        }
    }
    else {
        $logger->debug("Agent already logged in, skipping login setup");
    }

    # Ensure recovery_data Protected Item is created after server setup
    eval { Whostmgr::CometBackup::Setup::ensure_recovery_data_protected_item(); };
    if ($@) {
        $logger->warn("Failed to create recovery_data Protected Item during server registration: $@");

        # Don't fail the entire registration, just log the warning
    }

    return '';
}

=head2 _get_configuration_status

Internal function to get the current Comet Backup configuration status.
Returns a hashref with the same fields as the get_comet_backup_configuration_status API.

This allows initialize() to determine which steps need to be performed.

=cut

sub _get_configuration_status {
    require Cpanel::Server::Type;

    my %status = (
        status          => 'not_started',
        has_license     => 0,
        has_credentials => 0,
        agent_installed => 0,
        agent_running   => 0,
        agent_logged_in => 0,
    );

    # Check license
    # Support dev sandbox override for testing
    if ( -f '/var/cpanel/dev_sandbox' && -f '/var/cpanel/comet/license' ) {
        $status{has_license} = 1;
    }
    else {
        my @products = Cpanel::Server::Type::get_products();
        if ( grep { $_ eq 'comet_backup' } @products ) {
            $status{has_license} = 1;
        }
    }

    unless ( $status{has_license} ) {
        $status{status} = 'unlicensed';
        return \%status;
    }

    # Check credentials
    eval {
        require Whostmgr::CometBackup::CometBackupAPI;
        my $server_config = Whostmgr::CometBackup::CometBackupAPI::get_server_config();
        if ( $server_config && $server_config->{server_url} && $server_config->{username} && $server_config->{password} ) {
            $status{has_credentials} = 1;
        }
    };

    # Check if agent is installed
    $status{agent_installed} = is_comet_agent_installed();
    unless ( $status{agent_installed} ) {
        $status{status} = 'agent_not_installed';
        return \%status;
    }

    # Check if agent is running
    my ( $running_status, $agent_state ) = Whostmgr::CometBackup::AgentManager::get_status();
    if ( $running_status && $agent_state eq 'active' ) {
        $status{agent_running} = 1;
    }
    unless ( $status{agent_running} ) {
        $status{status} = 'agent_not_running';
        return \%status;
    }

    # Check if agent is logged in
    $status{agent_logged_in} = is_agent_logged_in();
    unless ( $status{agent_logged_in} ) {
        $status{status} = 'agent_not_logged_in';
        return \%status;
    }

    # All checks passed
    $status{status} = 'configured';
    return \%status;
}

=head2 get_allowed_region_hints

Fetches the list of allowed region hints for cPanel Hosted Comet licenses from the Comet Account Portal.
Reads the WebPros Account JWT id_token from the stored JWT file for authentication.

Parameters:
    None - the id_token is read from the WPA JWT file

Returns a hashref on success with the following structure:
    {

        { id => 'region-id', description => 'Region Description' },
        ...

    }

Dies with a descriptive error message on failure.

=cut

sub get_allowed_region_hints () {
    require Cpanel::HTTP::Client;
    require Cpanel::JSON;

    my $id_token = _get_id_token();
    unless ($id_token) {
        my $error_msg = "Failed to get id_token for get_allowed_region_hints";
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    my $url = $Whostmgr::CometBackup::Constants::COMET_ACCOUNT_PORTAL_URL . '/api/v1/webpros_ka_list_allowed_region_hints';

    logger()->debug("Fetching allowed region hints from Comet Account Portal");

    my $http = Cpanel::HTTP::Client->new();
    $http->set_default_header( 'Authorization', "Bearer $id_token" );

    my $response;
    eval { $response = $http->request( 'POST', $url, { headers => { 'Content-Type' => 'application/x-www-form-urlencoded' } } ); };

    if ($@) {
        my $error_msg = "HTTP request to Comet Account Portal failed: $@";
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    my $status = $response->status();
    if ( $status != 200 ) {
        my $error_msg = "Failed to fetch region hints: HTTP $status";
        if ( $response->content() ) {
            my $error_data = eval { Cpanel::JSON::Load( $response->content() ) };
            if ( $error_data && $error_data->{data} ) {
                if ( ref $error_data->{data} eq 'HASH' && $error_data->{data}{message} ) {
                    $error_msg .= " - " . $error_data->{data}{message};
                }
            }
        }
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    my $content = $response->content();
    unless ($content) {
        my $error_msg = "Empty response from Comet Account Portal";
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    my $data = eval { Cpanel::JSON::Load($content) };
    if ($@) {
        my $error_msg = "Failed to parse JSON response from Comet Account Portal: $@";
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    unless ( $data->{status} && $data->{status} eq 'ok' ) {
        my $error_msg = "Unexpected response status from Comet Account Portal: " . ( $data->{status} // 'unknown' );
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    unless ( $data->{data} && ref $data->{data} eq 'HASH' ) {
        my $error_msg = "Invalid response data structure from Comet Account Portal";
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    logger()->debug("Successfully fetched region hints from Comet Account Portal");

    # Cache the region data to file
    _cache_region_data( $data->{data} );

    # Return the data directly (contains console and storage arrays)
    return $data->{data};
}

=head2 _cache_region_data

Caches the region data to the cache file for later use by activate_license.
Ensures the cache directory exists before writing.

=cut

sub _cache_region_data ($region_data) {
    require Cpanel::JSON;
    require File::Path;

    # Ensure cache directory exists
    unless ( -d $Whostmgr::CometBackup::Constants::CACHE_DIR ) {
        eval { File::Path::make_path( $Whostmgr::CometBackup::Constants::CACHE_DIR, { mode => 0700 } ); };
        if ($@) {
            logger()->warn("Failed to create cache directory: $@");
            return;
        }
    }

    # Write region data to cache file
    my $cache_file = $Whostmgr::CometBackup::Constants::REGION_DATA_CACHE;
    eval {
        open my $fh, '>', $cache_file or die "Cannot open $cache_file: $!";
        print $fh Cpanel::JSON::Dump($region_data);
        close $fh;
    };
    if ($@) {
        logger()->warn("Failed to cache region data: $@");
        return;
    }

    logger()->debug("Region data cached to $cache_file");
    return 1;
}

=head2 _load_cached_region_data

Loads the cached region data from the cache file.

Returns the region data hashref on success, undef if cache doesn't exist or is invalid.

=cut

sub _load_cached_region_data () {
    require Cpanel::JSON;

    my $cache_file = $Whostmgr::CometBackup::Constants::REGION_DATA_CACHE;

    unless ( -f $cache_file ) {
        logger()->debug("Region data cache file does not exist: $cache_file");
        return;
    }

    my $region_data;
    eval {
        open my $fh, '<', $cache_file or die "Cannot open $cache_file: $!";
        local $/;
        my $content = <$fh>;
        close $fh;
        $region_data = Cpanel::JSON::Load($content);
    };
    if ($@) {
        logger()->warn("Failed to load cached region data: $@");
        return;
    }

    return $region_data;
}

=head2 activate_license

Activates a Comet license using the provided authorization code via the Comet Account Portal.
Requires a valid WebPros Account JWT token for authentication.

Parameters:
    $id_token            - The WebPros Account JWT id_token for Bearer authentication (used for identity verification)
    $auth_code           - The authorization code provided by KA (required)
    $storage_region_hint - Region hint for the Comet Storage Server (required)
    $console_region_hint - Region hint for the Comet Management Console API Server (required)

Note that the console region is required by the Comet Account Portal API, however we don't currently allow end users to configure it.
There are currently only 2 endpoints both in Texas, US, so we randomly choose one with a 50/50 chance.
We load the allowed regions from a cache of get_allowed_region_hints().

Returns a hashref on success with the following structure:
    {
        auth_code_consumed => 'the-consumed-auth-code',
        username           => 'comet-hosted-username',   # may be undef
        password           => 'comet-hosted-password',   # may be undef
        server_url         => 'https://server.url',      # may be undef
    }

Returns undef on failure.

=cut

sub activate_license ( $id_token, $auth_code, $storage_region_hint = undef ) {
    require Cpanel::HTTP::Client;
    require Cpanel::JSON;

    unless ($id_token) {
        logger()->error("id_token is required for activate_license");
        return;
    }

    unless ($auth_code) {
        logger()->error("Authorization code is required for activate_license");
        return;
    }

    # Load cached region data to validate storage region and select console region
    my $region_data = _load_cached_region_data();
    unless ($region_data) {
        my $error_msg = "Region data cache not found. Please call get_allowed_region_hints first to populate the cache.";
        logger()->error($error_msg);
        die "$error_msg\n";

        # Note that the cached data should always exist for
    }

    # Validate that the supplied storage_region_hint is in the allowed list
    my $console_region_hint;
    if ( defined $storage_region_hint ) {
        my @storage_regions   = @{ $region_data->{storage} // [] };
        my %valid_storage_ids = map { $_->{id} => 1 } @storage_regions;

        unless ( $valid_storage_ids{$storage_region_hint} ) {
            my $valid_list = join( ', ', sort keys %valid_storage_ids );
            my $error_msg  = "Invalid storage_region_hint '$storage_region_hint'. Valid options are: $valid_list";
            logger()->error($error_msg);
            die "$error_msg\n";
        }
    }

    # Randomly select a console region from the available options
    my @console_regions = @{ $region_data->{console} // [] };
    unless (@console_regions) {
        my $error_msg = "No console regions available in cached region data";
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    $console_region_hint = $console_regions[ int( rand( scalar @console_regions ) ) ]->{id};
    logger()->debug("Selected console region: $console_region_hint");

    my $url = $Whostmgr::CometBackup::Constants::COMET_ACCOUNT_PORTAL_URL . '/api/v1/webpros_ka_activate_license';

    logger()->info("Activating license via Comet Account Portal");

    # Development bypass for activation testing.
    if ( -f '/var/cpanel/comet_dev_bypass_activation' ) {

        # Open bypass file to read test credentials
        logger()->debug("Development bypass for activation testing enabled, skipping HTTP request.");
        if ( open( my $fh, '<', '/var/cpanel/comet_dev_bypass_activation' ) ) {
            my $line = <$fh>;
            chomp $line;
            close($fh);
            my ( $test_username, $test_password, $test_server_url ) = split( /,/, $line );

            # Simulate some API call latency
            sleep 1;

            return {
                auth_code_consumed => 'd085bad1-9241-4138-8d2e-b5f644de3aa7',    # what these usually look like, just for completeness
                username           => $test_username   // 'testuser',
                password           => $test_password   // 'testpassword',
                server_url         => $test_server_url // 'https://testserver.cometbackup.example/',
            };
        }
        else {
            logger()->error("Failed to open development bypass activation file: $!");
            die "Failed to open development bypass activation file: $!\n";
        }
    }

    my $http = Cpanel::HTTP::Client->new();
    $http->set_default_header( 'Authorization', "Bearer $id_token" );

    # Build form data
    my %form_data = ( auth_code => $auth_code );
    $form_data{storage_region_hint} = $storage_region_hint if defined $storage_region_hint;
    $form_data{console_region_hint} = $console_region_hint;

    my $response;
    eval { $response = $http->post_form( $url, \%form_data ); };

    if ($@) {
        my $error_msg = "HTTP request to Comet Account Portal failed: $@";
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    my $status = $response->status();
    if ( $status != 200 ) {
        my $error_msg = "Failed to activate license: HTTP $status";
        if ( $response->content() ) {
            my $error_data = eval { Cpanel::JSON::Load( $response->content() ) };
            if ( $error_data && $error_data->{data} ) {
                if ( ref $error_data->{data} eq 'HASH' ) {
                    if ( $error_data->{data}{message} ) {
                        $error_msg .= " - " . $error_data->{data}{message};
                    }
                    else {
                        # Handle validation errors (map of field => error)
                        my @errors;
                        for my $field ( keys %{ $error_data->{data} } ) {
                            push @errors, "$field: $error_data->{data}{$field}";
                        }
                        $error_msg .= " - " . join( ', ', @errors ) if @errors;
                    }
                }
            }
        }
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    my $content = $response->content();
    unless ($content) {
        my $error_msg = "Empty response from Comet Account Portal";
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    my $data = eval { Cpanel::JSON::Load($content) };
    if ($@) {
        my $error_msg = "Failed to parse JSON response from Comet Account Portal: $@";
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    unless ( $data->{status} && $data->{status} eq 'ok' ) {
        my $error_msg = "Unexpected response status from Comet Account Portal: " . ( $data->{status} // 'unknown' );
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    unless ( $data->{data} && ref $data->{data} eq 'HASH' && $data->{data}{auth_code_consumed} ) {
        my $error_msg = "Invalid response data structure from Comet Account Portal - missing auth_code_consumed";
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    logger()->info("Successfully activated license via Comet Account Portal");

    return {
        auth_code_consumed => $data->{data}{auth_code_consumed},
        username           => $data->{data}{username},
        password           => $data->{data}{password},
        server_url         => $data->{data}{server_url},
    };
}

=head2 _get_id_token

Reads the WebPros Account JWT from the stored file and extracts the id_token.
This is used for Bearer authentication with the Comet Account Portal.

Returns the id_token string on success, undef on failure.

=cut

sub _get_id_token () {
    require Cpanel::JSON;

    my $jwt_file = $Whostmgr::CometBackup::Constants::WPA_JWT_FILE;

    unless ( -f $jwt_file ) {
        logger()->error("WPA JWT file does not exist: $jwt_file");
        return;
    }

    open my $jwt_fh, '<', $jwt_file or do {
        logger()->error("Cannot open WPA JWT file: $!");
        return;
    };

    # Read entire file content
    my $jwt_token = do { local $/; <$jwt_fh> };
    chomp $jwt_token;
    close $jwt_fh;

    unless ($jwt_token) {
        logger()->error("WPA JWT file is empty");
        return;
    }

    my $jwt_data = eval { Cpanel::JSON::Load($jwt_token); };
    if ($@) {
        logger()->error("Failed to parse WPA JWT JSON: $@");
        return;
    }

    my $id_token = $jwt_data->{id_token};
    unless ($id_token) {
        logger()->error("WPA JWT does not contain id_token");
        return;
    }

    return $id_token;
}

=head2 get_server_credentials_from_cap

Fetches server credentials from Comet Access Portal (CAP) using the license activation code, region information and WPA JWT.
On success, saves the credentials to the credentials file.

The region_hr parameter is a hash reference containing region information for the storage server.
The management API endpoint is automatically chosen by Comet during provisioning.

Example region_hr:
{
    region_id => 'us-west-1',
}

=cut

sub get_server_credentials_from_cap {
    my ($region_hr) = @_;

    # Read license activation code from file
    open my $fh, '<', $Whostmgr::CometBackup::Constants::LICENSE_ACTIVATION_FILE or do {
        my $error_msg = "Cannot open license activation file $Whostmgr::CometBackup::Constants::LICENSE_ACTIVATION_FILE : $!";
        logger()->error($error_msg);
        die "$error_msg\n";
    };
    my $activation_code = <$fh>;
    chomp $activation_code;
    close $fh;
    unless ($activation_code) {
        my $error_msg = "License activation code is empty";
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    # Verify region information

    # Get the id_token from the stored JWT file
    my $id_token = _get_id_token();
    unless ($id_token) {
        my $error_msg = "Failed to get id_token for get_server_credentials_from_cap";
        logger()->error($error_msg);
        die "$error_msg\n";
    }

    # Send region, activation code, JWT to CAP and fetch server credentials to server defined in $Whostmgr::CometBackup::Constants::COMET_ACCOUNT_PORTAL_URL
    # activate_license will throw an exception with detailed error on failure
    my $credentials = activate_license( $id_token, $activation_code, $region_hr->{region_id} );

    # Write credentials to file
    require Cpanel::JSON;
    my $credentials_json = Cpanel::JSON::Dump($credentials);
    my $orig_umask       = umask(0077);                        # Ensure file is created with 600 permissions
    open my $cred_fh, '>', $Whostmgr::CometBackup::Constants::CREDENTIALS_FILE or do {
        my $error_msg = "Cannot open credentials file for writing: $!";
        logger()->error($error_msg);
        die "$error_msg\n";
    };
    print $cred_fh $credentials_json;
    close $cred_fh;
    umask($orig_umask);
    logger()->debug("Server credentials successfully fetched and saved");

    return ( 1, $credentials_json );
}

=head2 setup_comet_agent

Sets up the Comet Backup agent by logging it into the Comet server.
Should be called after the agent is installed and running.

Returns 1 on successful login, 0 if skipped (no credentials), undef on failure.

=cut

sub setup_comet_agent {
    require Cpanel::SafeRun::Object;
    require Whostmgr::CometBackup::CometBackupAPI;

    logger()->debug("Setting up Comet Backup agent");

    my $server_config;
    eval { $server_config = Whostmgr::CometBackup::CometBackupAPI::get_server_config(); };

    if ($@) {

        # If getting the license failed, the following will fail as well
        logger()->debug("Server credentials not available yet, skipping agent login");
        return 0;    # Skip, not an error
    }

    unless ( $server_config && $server_config->{server_url} ) {
        logger()->debug("Server URL not configured, skipping agent login");
        return 0;    # Skip, not an error
    }

    my $server_url = $server_config->{server_url};
    my $username   = $server_config->{username};
    my $password   = $server_config->{password};

    unless ( $username && $password ) {
        logger()->debug("Username or password not configured, skipping agent login");
        return 0;    # Skip, not an error
    }

    # This is very important, do not remove unless you know what you are doing
    $ENV{'OVERRIDE_INSTALL_SERVER'} = $server_url;

    logger()->debug("Logging agent into Comet server: $server_url with username: $username");

    # First, check current login status and ensure completely clean state
    my $status_check = Cpanel::SafeRun::Object->new(
        program => '/opt/CometBackup/backup-tool',
        args    => [ 'login', 'status' ],
    );

    my $current_status = $status_check->stdout();
    logger()->debug("Current login status: $current_status");

    # If logged in, logout first
    if ( $current_status =~ /Logged in/i ) {
        logger()->debug("Agent already logged in, logging out first");
        my $logout_run = Cpanel::SafeRun::Object->new(
            program => '/opt/CometBackup/backup-tool',
            args    => [ 'login', 'disconnect' ],
        );
        if ( $logout_run->CHILD_ERROR() ) {
            logger()->warn( "Failed to logout: " . $logout_run->stderr() );
        }
        else {
            logger()->debug("Successfully logged out");
        }
    }

    # Step 1: Use the backup-tool login join-lobby command to connect to the server
    my $run = Cpanel::SafeRun::Object->new(
        program => '/opt/CometBackup/backup-tool',
        args    => [ 'login', 'join-lobby', $server_url ],
    );

    if ( $run->CHILD_ERROR() ) {
        my $error = $run->stderr();

        # Check if already in lobby
        if ( $error =~ /Logged\s+out,\s+in.*lobby/i || $run->stdout() =~ /Logged\s+out,\s+in.*lobby/i ) {
            logger()->debug("Agent already in lobby, continuing");
        }
        elsif ( $error =~ /Logged in/, ) {
            logger()->debug("Agent already logged in, continuing");
        }
        else {
            logger()->error("Failed to join lobby on Comet server: $error");
            return;    # Actual error
        }
    }
    else {
        logger()->debug( "Join lobby output: " . $run->stdout() );
    }

    # Step 2: Use the backup-tool login prompt command to configure credentials
    # This command prompts for server URL, username, and password interactively
    # We use IPC::Open3 to handle the interactive prompts
    require IPC::Open3;
    require Symbol;

    my $child_in  = Symbol::gensym();
    my $child_out = Symbol::gensym();
    my $child_err = Symbol::gensym();

    logger()->debug("Starting backup-tool login prompt");

    # The backup-tool login prompt command prompts for username, password, and server URL
    my $pid = eval { IPC::Open3::open3( $child_in, $child_out, $child_err, '/opt/CometBackup/backup-tool', 'login', 'prompt' ); };

    if ($@) {
        logger()->error("Failed to start backup-tool login prompt: $@");
        return;
    }

    # Use IO::Select to wait for output
    require IO::Select;
    my $selector = IO::Select->new($child_out);

    my $stdout_output = '';
    my $stderr_output = '';
    my $prompt_buffer = '';

    # Interact with the prompts
    eval {
        my $timeout          = 30;       # Total timeout in seconds
        my $start_time       = time();
        my $prompts_answered = 0;
        my $max_prompts      = 2;        # Expect username and password

        while ( $prompts_answered <= $max_prompts && ( time() - $start_time ) < $timeout ) {

            # Wait for output to be available (2 second timeout per iteration)
            my @ready = $selector->can_read(2);

            if (@ready) {

                # Read character by character to detect prompts immediately
                my $char;
                my $bytes = sysread( $child_out, $char, 1 );

                if ( defined $bytes && $bytes > 0 ) {
                    $prompt_buffer .= $char;
                    $stdout_output .= $char;

                    # Log buffer state periodically for debugging
                    if ( length($prompt_buffer) > 0 && $prompt_buffer =~ /:/ ) {
                        logger()->debug( "Current buffer: " . substr( $prompt_buffer, -50 ) ) if length($prompt_buffer) > 10;
                    }

                    # Check if we have a complete prompt (ends with ": " or just ":")
                    # The Server URL prompt includes a default in parentheses like "Server URL (https://dal.comet.cpanel.net/):"
                    if ( $prompt_buffer =~ /(Username|Password|Server URL(?:\s*\([^)]+\))?|Comet Server|Server):\s*$/i ) {
                        my $prompt_type = $1;
                        logger()->debug("Received prompt: $prompt_type");

                        my $response;
                        if ( $prompt_type =~ /^username$/i ) {
                            $response = $username;
                            logger()->debug("Responding with username: $username");
                        }
                        elsif ( $prompt_type =~ /^password$/i ) {
                            $response = $password;
                            logger()->debug("Responding with password: [REDACTED]");
                        }
                        else {
                            logger()->debug("Received unknown prompt: $prompt_type");
                        }

                        if ($response) {

                            # Send response with newline
                            logger()->debug("Sending response to prompt");
                            my $bytes_written = syswrite( $child_in, "$response\n" );
                            logger()->debug("Wrote $bytes_written bytes");
                            $prompts_answered++;
                            logger()->debug("Prompts answered so far: $prompts_answered");
                            $prompt_buffer = '';    # Clear buffer after responding

                            # Give the process a moment to process the input
                            select( undef, undef, undef, 0.5 );
                        }
                    }
                }
                elsif ( !defined $bytes ) {

                    # Error reading
                    logger()->warn("Error reading from child process: $!");
                    last;
                }
            }
            else {
                # Timeout waiting for data
                logger()->debug("Timeout waiting for data, checking if process is still running");
            }

            # Check if process has exited
            my $kid = waitpid( $pid, POSIX::WNOHANG() );
            if ( $kid > 0 ) {
                logger()->debug("Process exited after answering $prompts_answered prompts");
                logger()->debug("Current buffer at exit: '$prompt_buffer'") if length($prompt_buffer) > 0;
                logger()->debug("Total stdout so far: '$stdout_output'");

                # Read any remaining output
                while ( my $bytes = sysread( $child_out, my $buf, 4096 ) ) {
                    $stdout_output .= $buf;
                    logger()->debug("Read additional $bytes bytes after exit");
                }
                last;
            }
        }

        logger()->debug("Finished prompt interaction loop, answered $prompts_answered prompts");

        # Close stdin to signal we're done
        close $child_in;

        # Read any remaining output with a short timeout
        while ( $selector->can_read(0.5) ) {
            my $bytes = sysread( $child_out, my $buf, 4096 );
            last unless $bytes;
            $stdout_output .= $buf;
        }
    };

    if ($@) {
        logger()->error("Failed during interactive login: $@");
        kill 'TERM', $pid;
        waitpid( $pid, 0 );
        return;
    }

    # Read stderr
    my $err_selector = IO::Select->new($child_err);
    while ( $err_selector->can_read(0.5) ) {
        my $bytes = sysread( $child_err, my $buf, 4096 );
        last unless $bytes;
        $stderr_output .= $buf;
    }

    close $child_out;
    close $child_err;

    # Wait for the process to finish if not already done
    waitpid( $pid, 0 );
    my $exit_code = $? >> 8;

    logger()->debug("Login prompt output: $stdout_output");
    logger()->debug("Login prompt errors: $stderr_output") if $stderr_output;

    if ( $exit_code != 0 ) {

        # Check if it's just a warning about already being logged in
        my $combined_output = "$stdout_output $stderr_output";
        if ( $combined_output =~ /already logged in/i || $combined_output =~ /Logged in/i ) {
            logger()->debug("Agent appears to already be logged in, continuing");
        }
        else {
            logger()->error("Failed to configure agent credentials (exit code: $exit_code): $stderr_output");
            return;    # Actual error
        }
    }

    # Step 3: Verify login status
    my $status_run = Cpanel::SafeRun::Object->new(
        program => '/opt/CometBackup/backup-tool',
        args    => [ 'login', 'status' ],
    );

    my $status_output = $status_run->stdout();
    logger()->debug("Login status: $status_output");

    if ( $status_output =~ /Logged in/i ) {
        logger()->debug("Agent successfully logged in and verified");
        return 1;
    }
    else {
        logger()->warn("Agent login command succeeded but verification failed");
        return;
    }
}

=head2 ensure_recovery_data_protected_item

Ensures the recovery_data Protected Item is created and configured correctly on the Comet server.
This special Protected Item backs up /var/cpanel/comet/ every 5 minutes for disaster recovery.

Should be called after server credentials are available. This function is automatically invoked
during server initialization and when credentials are uploaded with disaster recovery enabled.

=head3 Returns

Returns 1 on success, 0 on failure.

=head3 Example

    use Whostmgr::CometBackup::Setup;

    my $success = Whostmgr::CometBackup::Setup::ensure_recovery_data_protected_item();
    if ($success) {
        print "Recovery data backup configured successfully\n";
    }

=cut

sub ensure_recovery_data_protected_item {
    logger()->debug("Ensuring recovery_data Protected Item is configured");

    eval {
        require Whostmgr::CometBackup::CometBackupAPI;
        require Whostmgr::CometBackup::CometBackupAPI::ProtectedItemBatch;

        # Get server config to determine target user
        my $server_config = Whostmgr::CometBackup::CometBackupAPI::get_server_config();
        my $target_user   = $server_config->{username};

        unless ($target_user) {
            logger()->warn("Cannot ensure recovery_data: no server username configured");
            return 0;
        }

        # Create a batch instance
        my $batch = Whostmgr::CometBackup::CometBackupAPI::ProtectedItemBatch->new( target_user => $target_user );

        # Ensure the recovery_data Protected Item exists and is current
        my $success = $batch->ensure_recovery_data_protected_item();

        if ($success) {
            logger()->debug("recovery_data Protected Item successfully configured");
        }
        else {
            logger()->warn("Failed to configure recovery_data Protected Item");
        }

        return $success;
    };

    if ($@) {
        logger()->error("Error ensuring recovery_data Protected Item: $@");
        return 0;
    }
}

sub is_comet_backup_allowed {
    my $config = Whostmgr::CometBackup::Config::load();
    require Cpanel::Plugins::Components::Rules;
    my $rules_obj = Cpanel::Plugins::Components::Rules->new( 'config' => $config->{'enable-plugin'} );
    return $rules_obj->is_allowed();
}

sub is_comet_agent_running {
    my ( $status, $state ) = Whostmgr::CometBackup::AgentManager::get_status();
    return 0 unless $status;
    return $state eq 'active' ? 1 : 0;
}

sub _restart_agent {
    logger()->info("Restarting the Comet Backup Agent");
    my ( $status, $message ) = Whostmgr::CometBackup::AgentManager::restart();
    unless ($status) {
        logger()->error("Failed to restart agent: $message");
        return 0;
    }
    return 1;
}

sub is_comet_agent_installed {
    if ( -x $Whostmgr::CometBackup::Constants::BACKUP_TOOL_PATH ) {
        logger()->debug("Comet Backup agent is installed") if $DEBUG == 1;
        return 1;
    }
    return 0;
}

=head2 _download_agent

Downloads the appropriate Comet Backup agent package based on the OS platform.
Supports both RPM-based (Platform=23) and DEB-based (Platform=21) distributions.

Returns the path to the downloaded file on success, undef on failure.

=cut

sub _download_agent {
    require Cpanel::OS;
    require Cpanel::HTTP::Client;
    require File::Temp;

    my $base_distro = Cpanel::OS::base_distro();
    logger()->debug("Detected base distribution: $base_distro");

    # Determine platform code based on OS
    my $platform;
    if ( $base_distro eq 'rhel' ) {
        $platform = '23';    # RPM-based systems
    }
    elsif ( $base_distro eq 'debian' ) {
        $platform = '21';    # DEB-based systems
    }
    else {
        logger()->error("Unsupported OS base distribution: $base_distro");
        return;
    }

    logger()->debug("Using platform code: $platform");

    # Create temporary directory for download
    my $temp_dir = File::Temp::tempdir( CLEANUP => 0 );
    logger()->debug("Created temporary download directory: $temp_dir");

    # Download the agent using Cpanel::HTTP::Client
    # Can also use https://hou.comet.cpanel.net, consider falling back if one fails
    my $server_url   = 'https://dal.comet.cpanel.net';
    my $download_url = "$server_url/api/v1/admin/branding/generate-client/by-platform";

    logger()->debug(
        "Downloading agent from Comet server",
        {
            url      => $download_url,
            platform => $platform,
            temp_dir => $temp_dir,
        }
    );

    my $http = Cpanel::HTTP::Client->new();

    # Prepare form data for POST request
    my %form_data = (
        'SelfAddress' => $server_url,
        'Platform'    => $platform,
    );

    # Perform POST request
    my $response;
    eval { $response = $http->post_form( $download_url, \%form_data ); };

    if ($@) {
        logger()->error("HTTP request failed: $@");
        return;
    }

    unless ( $response->{success} ) {
        logger()->error( "Failed to download agent: HTTP " . $response->status() . " - " . $response->reason() );
        return;
    }

    # Get the downloaded content
    my $content = $response->content();
    unless ($content) {
        logger()->error("Downloaded content is empty");
        return;
    }

    # Determine filename from Content-Disposition header or generate one
    my $filename;
    if ( my $disposition = $response->header('Content-Disposition') ) {
        if ( $disposition =~ /filename[*]?=["']?([^"';]+)/ ) {
            $filename = $1;
        }
    }

    # Fallback filename based on platform
    $filename ||= $base_distro eq 'rhel' ? 'comet-backup-agent.rpm' : 'comet-backup-agent.deb';

    my $downloaded_file = "$temp_dir/$filename";

    # Write content to file
    open my $fh, '>', $downloaded_file or do {
        logger()->error("Cannot write to $downloaded_file: $!");
        return;
    };
    binmode $fh;
    print {$fh} $content;
    close $fh;

    logger()->debug("Downloaded agent file: $downloaded_file");

    return $downloaded_file;
}

=head2 install_comet_agent

Installs the Comet Backup agent from the downloaded package file, or will download it if not provided.
Handles both RPM and DEB package formats without user interaction.

Parameters:
- agent_file: Path to the downloaded agent package

Returns 1 on success, undef on failure.

=cut

sub install_comet_agent {
    require Cpanel::OS;
    require Cpanel::SafeRun::Object;
    my ($agent_file) = @_;

    if ( !-f $agent_file ) {
        $agent_file = _download_agent();
    }

    unless ( -f $agent_file ) {
        logger()->error("Agent file does not exist: $agent_file");
        return;
    }

    my $base_distro = Cpanel::OS::base_distro();
    logger()->debug("Installing agent for base distribution: $base_distro");

    my $run;
    if ( $base_distro eq 'rhel' ) {

        # Install RPM package
        logger()->debug("Installing RPM package: $agent_file");
        $run = Cpanel::SafeRun::Object->new(
            program => '/usr/bin/rpm',
            args    => [ '-Uvh', '--force', $agent_file ],
        );
    }
    elsif ( $base_distro eq 'debian' ) {

        # Install DEB package
        logger()->debug("Installing DEB package: $agent_file");
        $run = Cpanel::SafeRun::Object->new(
            program => '/usr/bin/dpkg',
            args    => [ '-i', $agent_file ],
        );
    }
    else {
        logger()->error("Unsupported OS for installation: $base_distro");
        return;
    }

    if ( $run->CHILD_ERROR() ) {
        my $error = $run->stderr();
        logger()->error("Failed to install agent: $error");
        return;
    }

    logger()->debug( "Installation output: " . $run->stdout() );
    logger()->debug("Agent installation completed successfully");

    return 1;
}

=head2 _enable_agent

Enables the Comet Backup agent service via systemd so it starts on boot.

Returns 1 on success, undef on failure.

=cut

sub _enable_agent {
    require Cpanel::SafeRun::Object;

    logger()->debug("Enabling backup-tool service via systemd");

    my $run = Cpanel::SafeRun::Object->new(
        program => '/usr/bin/systemctl',
        args    => [ 'enable', 'backup-tool' ],
    );

    if ( $run->CHILD_ERROR() ) {
        my $error = $run->stderr();
        logger()->error("Failed to enable agent service: $error");
        return;
    }

    logger()->debug( "Enable output: " . $run->stdout() );
    return 1;
}

=head2 is_agent_logged_in

Checks if the Comet Backup agent is currently logged into the Comet server.

Returns 1 if logged in, 0 if not logged in or agent not installed.

=cut

sub is_agent_logged_in {
    require Cpanel::SafeRun::Object;
    require Whostmgr::CometBackup::Constants;

    # Check if backup-tool binary exists
    unless ( -x $Whostmgr::CometBackup::Constants::BACKUP_TOOL_PATH ) {
        logger()->debug("backup-tool binary not found, agent not installed");
        return 0;
    }

    # Check login status
    my $run = Cpanel::SafeRun::Object->new(
        program => $Whostmgr::CometBackup::Constants::BACKUP_TOOL_PATH,
        args    => [ 'login', 'status' ],
    );

    if ( $run->CHILD_ERROR() ) {
        logger()->warn("Failed to check agent login status");
        return 0;
    }

    my $status_output = $run->stdout();

    # Check if output indicates logged in status
    # Expected output: "Logged in, locked (1)" or similar
    if ( $status_output =~ /Logged in/i ) {
        return 1;
    }

    return 0;
}

# Helper function to properly quote strings for shell execution
# Escapes single quotes and wraps the string in single quotes
sub _shell_quote {
    my ($str) = @_;

    # Replace single quotes with '\'' (end quote, escaped quote, start quote)
    $str =~ s/'/'\\''/g;

    # Wrap in single quotes
    return "'$str'";
}

1;
