package Whostmgr::CometBackup::CometBackupAPI;

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

# This module handles all interactions with the Comet API

use cPstrict;
use LWP::UserAgent;
use Whostmgr::CometBackup::Constants ();
use Whostmgr::CometBackup::Logger    ();
use JSON::PP                         qw( encode_json decode_json );
use Data::UUID                       ();

# Global logger instance
my $logger;

# Get or create logger instance
sub _get_logger {
    return $logger //= Whostmgr::CometBackup::Logger->new( log_level => $ENV{DEBUG} ? 'debug' : 'info' );
}

# Function to make API requests to a Comet server
sub make_server_request {
    my ( $endpoint, $extra_params ) = @_;
    $extra_params ||= {};

    my $server_config = get_server_config();

    # Allow overriding server_url if provided in extra_params
    my $server_url = $extra_params->{server_url} || $server_config->{server_url};
    my $url        = $server_url . '/api/v1/' . $endpoint;

    # Build params with server config defaults, but allow overrides from extra_params
    # Priority: extra_params > server_config > defaults
    my $params = {%$extra_params};

    # Remove the override keys from params so they don't get sent as API parameters
    delete $params->{auth_type};
    delete $params->{username};
    delete $params->{password};
    delete $params->{server_url};

    # Re-add the actual auth parameters that Comet expects
    $params->{AuthType} = $extra_params->{auth_type} || $server_config->{'auth_type'} || 'Password';
    $params->{Username} = $extra_params->{username}  || $server_config->{'username'};
    $params->{Password} = $extra_params->{password}  || $server_config->{'password'};

    my $ua     = get_ua();
    my $logger = _get_logger();

    $logger->debug(
        "Making API request",
        {
            endpoint => $endpoint,
            url      => $url,
            params   => { %{$params}, Password => '[REDACTED]' }    # Don't log password
        }
    );

    my $response = $ua->post( $url, $params );

    if ( !$response->is_success ) {
        $logger->error(
            "HTTP request failed",
            {
                endpoint    => $endpoint,
                status_line => $response->status_line,
                url         => $url
            }
        );
        die "HTTP request failed: " . $response->status_line . "\n";
    }

    my $content = $response->decoded_content;
    $logger->debug(
        "Received API response",
        {
            endpoint       => $endpoint,
            status_code    => $response->code,
            status_line    => $response->status_line,
            content_length => length( $content || '' )
        }
    );

    # Always log restore responses for debugging
    if ( $endpoint =~ /restore/ ) {
        my $headers = $response->headers;
        my %header_data;
        for my $header_name ( $headers->header_field_names ) {
            $header_data{$header_name} = $headers->header($header_name);
        }

        $logger->debug(
            "Restore operation response",
            {
                endpoint        => $endpoint,
                status          => $response->status_line,
                headers         => \%header_data,
                content_preview => substr( $content || '', 0, 500 )    # First 500 chars only
            }
        );
    }

    my $json_data;
    eval { $json_data = decode_json($content); };

    if ($@) {
        $logger->error(
            "Failed to parse JSON response",
            {
                endpoint        => $endpoint,
                error           => $@,
                content_preview => substr( $content || '', 0, 200 )
            }
        );
        die "Failed to parse JSON response: $@\nResponse content: $content\n";
    }

    # Some endpoints return different structures:
    # - Most endpoints: HASH with {Status, Message, ...}
    # - get-jobs endpoint: ARRAY of job objects on success, or HASH with {Status, Message} on failure
    # - get-job-properties: HASH with job properties including Status field (job status code, not API status)
    # - get-job-log-entries: ARRAY of log entry objects

    # Check if this is an error response (HASH with Status field indicating failure)
    # Exception: get-job-properties returns job properties where Status is the job's status code (5000-5003)
    # not an API error. Job properties responses have a GUID field.
    my $is_job_properties = ( $endpoint =~ /get-job-properties/ && ref($json_data) eq 'HASH' && exists $json_data->{GUID} );

    if ( !$is_job_properties && ref($json_data) eq 'HASH' && exists $json_data->{Status} && $json_data->{Status} != 200 ) {
        $logger->error(
            "Server API error",
            {
                endpoint => $endpoint,
                status   => $json_data->{Status},
                message  => $json_data->{Message} || "Unknown error"
            }
        );
        die "Server API Error: " . ( $json_data->{Message} || "Unknown error" ) . " (Status: " . $json_data->{Status} . ")\n";
    }

    # Log successful response (handle both HASH and ARRAY responses)
    my $log_status =
      ref($json_data) eq 'HASH' && exists $json_data->{Status}
      ? $json_data->{Status}
      : 'success';

    $logger->debug(
        "API request completed successfully",
        {
            endpoint      => $endpoint,
            status        => $log_status,
            response_type => ref($json_data),
        }
    );

    return $json_data;
}

# Gets and returns local credentials and server information
sub get_server_config {
    my $cred_path = $Whostmgr::CometBackup::Constants::CREDENTIALS_FILE;
    my $logger    = _get_logger();

    unless ( -f $cred_path ) {
        $logger->warn( "Credentials file not found", { path => $cred_path } );
        die "Credentials file not found at $cred_path\n";
    }

    my $data;
    eval {
        open my $fh, '<', $cred_path or die "Cannot read cache: $!";
        my $content = do { local $/; <$fh> };
        close $fh;
        $data = decode_json($content);
    };
    if ($@) {
        $logger->error(
            "Failed to read server info",
            {
                path  => $cred_path,
                error => $@
            }
        );
        die "Failed to read server configuration: $@\n";
    }

    $logger->debug(
        "Successfully loaded server info",
        {
            has_username   => defined $data->{username},
            has_device_id  => defined $data->{device_id},
            has_server_url => defined $data->{server_url},
            has_password   => defined $data->{password},
            has_auth_type  => defined $data->{auth_type},
        }
    );

    unless ( $data && ref $data eq 'HASH' ) {
        die "Server configuration is invalid - please re-register with Comet Backup";
    }

    my @missing_fields;
    for my $required_field (qw(username server_url password)) {
        unless ( $data->{$required_field} ) {
            push @missing_fields, $required_field;
        }
    }

    if (@missing_fields) {
        die "Server configuration incomplete - missing required fields: " . join( ', ', @missing_fields ) . ". Please complete server registration with Comet Backup.";
    }

    return $data;
}

# Updates the server config cache file with new data (e.g., destination_ids)
sub _update_server_config ($updates) {
    my $cred_path = $Whostmgr::CometBackup::Constants::CREDENTIALS_FILE;
    my $logger    = _get_logger();

    my $current_config = get_server_config();
    return unless $current_config;

    # Merge updates into current config
    for my $key ( keys %$updates ) {
        $current_config->{$key} = $updates->{$key};
    }

    eval {
        open my $fh, '>', $cred_path or die "Cannot write cache: $!";
        print {$fh} encode_json($current_config);
        close $fh;
    };
    if ($@) {
        $logger->error(
            "Failed to update server config",
            {
                path  => $cred_path,
                error => $@
            }
        );
        return;
    }

    $logger->debug( "Successfully updated server config", { keys => [ keys %$updates ] } );
    return 1;
}

# Gets a user agent. # TODO - remove SSL validation bypass once we confirm certs will be available all the time
sub get_ua {
    my $ua = LWP::UserAgent->new(
        timeout  => 30,
        ssl_opts => { verify_hostname => 0, SSL_verify_mode => 0 }
    );

    return $ua;
}

# Function to get entire user profile with password hashes
sub get_user_profile_and_hash {
    my $logger = _get_logger();

    $logger->debug("Attempting to retrieve profile hash for using user web API");

    my $response = make_server_request("user/web/get-user-profile-and-hash");

    if ( !$response ) {
        return undef;
    }
    return $response;
}

=head2 get_destination_ids

    my $destination_ids = Whostmgr::CometBackup::CometBackupAPI::get_destination_ids();
    # Returns: ['331f5ec2-f836-441a-947c-4005d815c433', ...]

Retrieves the list of destination IDs for the configured user. Uses cached values
from server_credentials.json if available, otherwise fetches from user profile and
caches them. Destination IDs are expected to be relatively stable.

Returns: Arrayref of destination ID strings, or undef on error

=cut

sub get_destination_ids {
    my $logger = _get_logger();

    # Check cache first
    my $server_config = get_server_config();
    if ( $server_config && $server_config->{destination_ids} && @{ $server_config->{destination_ids} } ) {
        $logger->debug( "Using cached destination IDs", { count => scalar @{ $server_config->{destination_ids} } } );
        return $server_config->{destination_ids};
    }

    # Need to fetch from user profile
    $logger->debug("Fetching destination IDs from user profile");

    my $profile = get_user_profile_and_hash();
    unless ($profile) {
        $logger->error("Failed to retrieve user profile for destination IDs");
        return undef;
    }

    # Debug: Log the structure of the profile response
    $logger->debug( "Profile response keys", { keys => join( ', ', sort keys %$profile ) } );

    # The profile data might be nested under 'Profile' key
    my $profile_data = $profile;
    if ( exists $profile->{Profile} ) {
        $logger->debug("Profile data is nested under 'Profile' key");
        $profile_data = $profile->{Profile};

        # If it's a JSON string, decode it
        if ( $profile_data && !ref($profile_data) ) {
            require Cpanel::JSON;
            eval { $profile_data = Cpanel::JSON::Load($profile_data); };
            if ($@) {
                $logger->error( "Failed to decode Profile field", { error => $@ } );
                return undef;
            }
            $logger->debug("Successfully decoded Profile JSON string");
        }
    }

    my $destinations    = $profile_data->{Destinations} || {};
    my @destination_ids = keys %$destinations;

    unless (@destination_ids) {
        $logger->warn(
            "No destinations found in user profile",
            {
                profile_data_type    => ref($profile_data),
                has_destinations_key => exists $profile_data->{Destinations} ? 1 : 0
            }
        );
        return [];
    }

    # Cache the destination IDs for future use
    _update_server_config( { destination_ids => \@destination_ids } );

    $logger->debug( "Retrieved and cached destination IDs", { count => scalar(@destination_ids) } );

    return \@destination_ids;
}

=head2 get_device_id

    my $device_id = Whostmgr::CometBackup::CometBackupAPI::get_device_id();

Retrieves the device ID for the configured user. Uses cached value from
server_credentials.json if available, otherwise fetches from user profile
and caches it.

Returns: Device ID string, or dies on error

=cut

sub get_device_id {
    my $logger = _get_logger();

    # Check cache first
    my $server_config = get_server_config();
    if ( $server_config && $server_config->{device_id} ) {
        $logger->debug( "Using cached device ID", { device_id => $server_config->{device_id} } );
        return $server_config->{device_id};
    }

    # Need to fetch from user profile
    $logger->debug("Fetching device ID from user profile");

    my $profile = get_user_profile_and_hash();
    unless ($profile) {
        die "Failed to retrieve user profile for device ID\n";
    }

    # Extract device ID from profile
    my $profile_data = $profile->{Profile};

    # Decode if it's a JSON string
    if ( $profile_data && !ref($profile_data) ) {
        require Cpanel::JSON;
        eval { $profile_data = Cpanel::JSON::Load($profile_data); };
        if ($@) {
            $logger->error( "Failed to decode profile to get device ID", { error => $@ } );
            die "Failed to decode user profile: $@\n";
        }
    }

    my $devices    = $profile_data->{Devices} || {};
    my @device_ids = keys %$devices;

    unless (@device_ids) {
        die "No devices found in user profile\n";
    }

    my $device_id = $device_ids[0];    # Use first device

    # Cache the device ID for future use
    _update_server_config( { device_id => $device_id } );

    $logger->debug( "Retrieved and cached device ID", { device_id => $device_id } );

    return $device_id;
}

=head2 _get_user_connection_id

    my $connection_id = _get_user_connection_id();

Internal helper function to get the first available active connection ID for the
configured user. Connection IDs are temporary and only exist while a device is
actively connected to the Comet server.

Returns: Connection ID string, or undef if no active connections

=cut

sub _get_user_connection_id {
    my $logger = _get_logger();

    $logger->debug("Fetching active connection IDs from dispatcher");

    my $result = make_server_request("user/web/dispatcher/list-active");

    # Get the first connection ID
    if ( ref($result) eq 'HASH' ) {
        my @connection_ids = keys %$result;
        if (@connection_ids) {
            $logger->debug( "Found active connections", { count => scalar(@connection_ids), first_connection => $connection_ids[0] } );
            return $connection_ids[0];    # Return the first connection ID
        }
    }

    $logger->warn("No active connections found");
    return;
}

=head2 get_source_ids_from_profile

    my $source_ids = get_source_ids_from_profile();
    # Returns: ['44B77664-AF78-11F0-B45D-A8900677DFF5', ...]

Retrieves all Source IDs (Protected Items) from the user's profile on the Comet server.
These are the actual IDs that the Comet server recognizes and can be used for backups.

Returns: Arrayref of Source ID strings, or empty arrayref if none found

=cut

sub get_source_ids_from_profile {
    my $logger = _get_logger();

    $logger->debug("Fetching Source IDs from user profile");

    my $profile = get_user_profile_and_hash();
    unless ($profile) {
        $logger->error("Failed to retrieve user profile for Source IDs");
        return [];
    }

    # Extract profile data
    my $profile_data = $profile->{Profile};
    if ( $profile_data && !ref($profile_data) ) {
        require Cpanel::JSON;
        eval { $profile_data = Cpanel::JSON::Load($profile_data); };
        if ($@) {
            $logger->error( "Failed to decode profile for Source IDs", { error => $@ } );
            return [];
        }
    }

    my $sources    = $profile_data->{Sources} || {};
    my @source_ids = keys %$sources;

    $logger->debug( "Retrieved Source IDs from profile", { count => scalar(@source_ids) } );

    return \@source_ids;
}

=head1 PROTECTED ITEM MANAGEMENT

The following methods provide batch-oriented Protected Item management for Comet
servers, designed to efficiently handle multiple Protected Items per user profile.

See L<Whostmgr::CometBackup::CometBackupAPI::ProtectedItemBatch> for full batch functionality.

=head2 create_protected_item

    my ($success, $item_uid) = Whostmgr::CometBackup::CometBackupAPI::create_protected_item(
        item_type     => 'homedir',
        system_user   => 'tau',
        configuration => { ... }
    );
    
    # Or explicitly specify target_user:
    my ($success, $item_uid) = Whostmgr::CometBackup::CometBackupAPI::create_protected_item(
        target_user   => 'custom-user',
        item_type     => 'homedir',
        system_user   => 'tau',
        configuration => { ... }
    );

Helper function to create a single Protected Item. This uses the batch system
internally to avoid code duplication, even for single items. Automatically 
generates a UUID for the item.

Parameters:
- item_type: Type of protected item (required)
- system_user: cPanel system username (required)
- configuration: Hash reference with item-specific configuration (optional)
- target_user: The Comet server username (optional, defaults to username from server config)

Returns: (success_flag, item_uid) where success_flag is 1 on success, 0 on failure
         and item_uid is the generated UUID for the Protected Item

=cut

sub create_protected_item (%args) {
    my $target_user = delete $args{target_user};

    # If target_user not provided, get it from server config
    unless ($target_user) {
        my $server_config = get_server_config();
        $target_user = $server_config->{username} or die "Unable to determine target_user from server config";
    }

    require Whostmgr::CometBackup::CometBackupAPI::ProtectedItemBatch;
    my $batch = Whostmgr::CometBackup::CometBackupAPI::ProtectedItemBatch->new( target_user => $target_user );

    my $item_uid = $batch->add_protected_item(%args);
    my $success  = $batch->submit();

    return ( $success, $item_uid );
}

=head2 backup_protected_item

    my $result = Whostmgr::CometBackup::CometBackupAPI::backup_protected_item(
        protected_item_uid => 'some-uuid-string',
    );
    
    # Or explicitly specify target_user and destination_id:
    my $result = Whostmgr::CometBackup::CometBackupAPI::backup_protected_item(
        target_user        => 'custom-user',
        protected_item_uid => 'some-uuid-string',
        destination_id     => '331f5ec2-f836-441a-947c-4005d815c433',
    );
    
    # Or specify multiple destinations:
    my $result = Whostmgr::CometBackup::CometBackupAPI::backup_protected_item(
        target_user        => 'custom-user',
        protected_item_uid => 'some-uuid-string',
        destination_ids    => ['331f5ec2-...', '8b5c15b6-...'],
    );

Initiates a backup of a specific Protected Item via the user web dispatcher API.

This function:
1. Validates the Protected Item exists in Comet server Sources
2. Gets the connection ID for the active device
3. Determines the destination ID(s) (uses all available if not specified)
4. Calls user/web/dispatcher/run-backup-custom to start the backup for each destination

Parameters:
- protected_item_uid: The Protected Item UID (required)
- target_user: The Comet server username (optional, defaults to username from server config)
- destination_id: A single storage destination ID (optional, uses all available destinations)
- destination_ids: An array ref of destination IDs (optional, overrides destination_id)
- run_sequentially: Boolean, if true only backs up to one destination and returns (for pkgacct items)

Internal caching parameters (used by backup runner for performance):
- _cached_source_ids: Pre-fetched array of source IDs (optional, avoids API call)
- _cached_connection_id: Pre-fetched connection ID (optional, avoids API call)

Returns: Hashref with:
- success: Boolean indicating if ALL backups were started successfully
- backup_jobs: Array of hashrefs with destination_id, success, backup_job_id, error_message
- failed_targets: Array of destination IDs that failed
- error_message: Aggregate error details (if any failed)

=cut

sub backup_protected_item (%args) {
    my $protected_item_uid   = $args{protected_item_uid} or die "protected_item_uid parameter required";
    my $target_user          = $args{target_user};
    my $destination_id       = $args{destination_id};
    my $destination_ids      = $args{destination_ids};                                                     # Optional array ref of destination IDs
    my $run_sequentially     = $args{run_sequentially} // 0;                                               # For pkgacct items with multiple destinations
    my $cached_source_ids    = $args{_cached_source_ids};                                                  # Optional cached source IDs
    my $cached_connection_id = $args{_cached_connection_id};                                               # Optional cached connection ID

    my $logger = _get_logger();

    # If destination_ids array ref is provided, use it; otherwise use single destination_id or all available
    my $dest_display = $destination_ids ? sprintf( '[%s]', join( ', ', @$destination_ids ) ) : ( $destination_id // '[all available]' );

    $logger->debug(
        "Preparing to back up Protected Item",
        {
            protected_item_uid => $protected_item_uid,
            target_user        => $target_user // '[from server config]',
            destination_id     => $dest_display,
            run_sequentially   => $run_sequentially,
            using_cached_data  => ( $cached_source_ids && $cached_connection_id ) ? 'yes' : 'no',
        }
    );

    # If target_user not provided, get it from server config
    unless ($target_user) {
        my $server_config = get_server_config();
        $target_user = $server_config->{username} or die "Unable to determine target_user from server config";
    }

    $logger->debug(
        "Starting backup of Protected Item",
        {
            target_user        => $target_user,
            protected_item_uid => $protected_item_uid,
        }
    );

    # Validate that the Protected Item exists in the Comet server's Sources
    # Use cached source IDs if provided, otherwise fetch from API
    $logger->debug("Validating Protected Item exists in Comet server Sources");
    my $source_ids = $cached_source_ids || get_source_ids_from_profile();
    unless ( $source_ids && ref($source_ids) eq 'ARRAY' ) {
        my $error_msg = "Failed to retrieve Source IDs from Comet server";
        $logger->error($error_msg);
        return {
            success       => 0,
            error_message => $error_msg,
        };
    }

    my %valid_sources = map { $_ => 1 } @$source_ids;
    unless ( $valid_sources{$protected_item_uid} ) {
        my $error_msg = "Protected Item $protected_item_uid does not exist in Comet server Sources (may be orphaned)";
        $logger->error(
            "Protected Item not found in Comet server",
            {
                protected_item_uid => $protected_item_uid,
                available_sources  => scalar(@$source_ids),
            }
        );
        return {
            success       => 0,
            error_message => $error_msg,
        };
    }

    $logger->debug("Protected Item validated successfully in Comet server Sources");

    # Get active connection ID for the dispatcher
    # Use cached connection ID if provided, otherwise fetch from API
    my $connection_id = $cached_connection_id;
    unless ($connection_id) {
        eval { $connection_id = _get_user_connection_id(); };
        if ($@) {
            my $error_msg = "Failed to get active connection ID: $@";
            $logger->error( "Active connection ID lookup failed", { error => $@ } );
            return {
                success       => 0,
                error_message => $error_msg,
            };
        }
    }

    unless ($connection_id) {
        my $error_msg = "No active connections found - device may be offline";
        $logger->error("No active connections available");
        return {
            success       => 0,
            error_message => $error_msg,
        };
    }

    $logger->debug( "Using active connection ID", { connection_id => $connection_id } );

    # Build list of destination IDs we will attempt
    my @dest_ids;
    my @remaining_dest_ids;    # Track remaining destinations for sequential processing

    if ( $destination_ids && ref($destination_ids) eq 'ARRAY' && @$destination_ids ) {

        # Use provided array of destination IDs
        if ( $run_sequentially && @$destination_ids > 1 ) {

            # For sequential mode, only process first destination
            @dest_ids           = ( $destination_ids->[0] );
            @remaining_dest_ids = @$destination_ids[ 1 .. $#{$destination_ids} ];
            $logger->info(
                "Using first destination ID (sequential mode)",
                {
                    destination_id         => $dest_ids[0],
                    remaining_destinations => \@remaining_dest_ids,
                }
            );
        }
        else {
            # Use all provided destination IDs
            @dest_ids = @$destination_ids;
            $logger->info( "Using provided destination IDs", { destination_ids => \@dest_ids } );
        }
    }
    elsif ($destination_id) {

        # Single destination ID provided
        @dest_ids = ($destination_id);
        $logger->info( "Using single destination ID", { destination_id => $destination_id } );
    }
    else {
        # No destinations specified, use all available
        $logger->debug("No destination_id or destination_ids specified, retrieving all available destinations");
        my $all = get_destination_ids();
        $logger->debug( "Retrieved destination IDs", { count => scalar @$all } );
        unless ( $all && @$all ) {
            my $error_msg = "No storage destinations available";
            $logger->error("No destinations found");
            return {
                success       => 0,
                error_message => $error_msg,
                backup_jobs   => [],
            };
        }
        @dest_ids = @$all;
        $logger->debug( "Using all available destinations", { destination_ids => \@dest_ids } );
    }

    my %results;    # dest_id => { success => 0/1, backup_job_id => ..., error_message => ... }
    for my $dest_id (@dest_ids) {
        $logger->debug(
            "Initiating backup to destination",
            {
                protected_item_uid => $protected_item_uid,
                connection_id      => $connection_id,
                destination_id     => $dest_id,
            }
        );

        my $response;
        eval {
            $response = make_server_request(
                "user/web/dispatcher/run-backup-custom",
                {
                    TargetID    => $connection_id,
                    Source      => $protected_item_uid,
                    Destination => $dest_id,
                }
            );
        };

        # Verbose logging of the full response structure for debugging
        $logger->debug(
            "Backup initiation FULL RESPONSE",
            {
                protected_item_uid => $protected_item_uid,
                destination_id     => $dest_id,
                response_keys      => $response ? [ sort keys %$response ] : [],
                full_response      => $response,
            }
        );

        $logger->debug(
            "Backup initiation response",
            {
                protected_item_uid => $protected_item_uid,
                destination_id     => $dest_id,
                response           => $response,
            }
        );
        if ($@) {
            my $error_msg = "API request failed: $@";
            $logger->error(
                "Failed to start backup",
                {
                    protected_item_uid => $protected_item_uid,
                    destination_id     => $dest_id,
                    error              => $@,
                }
            );
            $results{$dest_id} = {
                success       => 0,
                error_message => $error_msg,
            };
            next;
        }

        if ( $response && $response->{Status} == 200 ) {

            # Extract the actual job GUID from the response
            # Try multiple possible field names for the job ID
            my $job_id = $response->{JobID} // $response->{GUID} // $response->{JobGUID} // $response->{BackupJobID} // $connection_id;

            $logger->debug(
                "Successfully started backup of Protected Item",
                {
                    protected_item_uid => $protected_item_uid,
                    connection_id      => $connection_id,
                    destination_id     => $dest_id,
                    job_id             => $job_id,
                    job_id_source      => $response->{JobID} ? 'JobID' : $response->{GUID} ? 'GUID' : $response->{JobGUID} ? 'JobGUID' : $response->{BackupJobID} ? 'BackupJobID' : 'connection_id (FALLBACK)',
                    status             => $response->{Status},
                }
            );
            $results{$dest_id} = {
                success       => 1,
                backup_job_id => $job_id,
                status        => $response->{Status},
            };
        }
        else {
            my $error_msg = $response->{Message} // 'Unknown error occurred';
            $logger->error(
                "Failed to start backup of Protected Item",
                {
                    protected_item_uid => $protected_item_uid,
                    destination_id     => $dest_id,
                    error              => $error_msg,
                    status             => $response->{Status} // 'unknown',
                }
            );
            $results{$dest_id} = {
                success       => 0,
                error_message => $error_msg,
                status        => $response->{Status} // 'unknown',
            };
        }
    }

    # Assemble return payload
    my @details;
    my @failed;
    for my $dest_id (@dest_ids) {
        my $r = $results{$dest_id} || { success => 0, error_message => 'No result recorded' };
        push @details, {
            destination_id => $dest_id,
            success        => $r->{success},
            backup_job_id  => $r->{backup_job_id},
            status         => $r->{status},
            error_message  => $r->{error_message},
        };
        push @failed, $dest_id unless $r->{success};
    }

    my $overall_success = @failed ? 0 : 1;
    my $aggregate_error;
    if (@failed) {
        $aggregate_error = 'Backup failed for destinations: ' . join( ', ', @failed );
    }

    return {
        success                => $overall_success,
        backup_jobs            => \@details,
        failed_targets         => \@failed,
        error_message          => $aggregate_error,
        remaining_destinations => \@remaining_dest_ids,    # For sequential processing
    };
}

=head2 get_job_properties

    my $job_details = Whostmgr::CometBackup::CometBackupAPI::get_job_properties(
        job_id      => 'some-job-guid',
        target_user => 'username',  # optional
    );

Retrieves detailed properties and progress information for a specific backup job.

Uses the user/web/get-job-properties API endpoint to fetch comprehensive job details
including status, progress, start/end times, and statistics.

Parameters:
- job_id: The backup job GUID (required)
- target_user: The Comet server username (optional, defaults to username from server config)

Returns: Hashref with BackupJobDetail structure:
- GUID: Job identifier
- Status: Job status code (see Comet documentation for status codes)
- StartTime: Unix timestamp when job started
- EndTime: Unix timestamp when job ended (0 if still running)
- SourceGUID: The Protected Item UID
- DestinationGUID: The Storage Vault ID
- Progress: Hashref with progress information (if job is running)
  - Counter: Monotonic counter for progress updates
  - BytesDone: Bytes processed so far
  - ItemsDone: Items processed so far
  - ItemsTotal: Total items to process
- TotalFiles, TotalDirectories, TotalSize, etc.
- Returns undef on error

=cut

sub get_job_properties (%args) {
    my $job_id      = $args{job_id} or die "job_id parameter required";
    my $target_user = $args{target_user};

    my $logger = _get_logger();

    my @caller = caller();
    $logger->debug(
        "Fetching job properties for job_id $job_id called from",
        {
            package => $caller[0],
            file    => $caller[1],
            line    => $caller[2],
        }
    );

    # If target_user not provided, get it from server config
    unless ($target_user) {
        my $server_config = get_server_config();
        $target_user = $server_config->{username} or die "Unable to determine target_user from server config";
    }

    $logger->debug(
        "Fetching job properties",
        {
            job_id      => $job_id,
            target_user => $target_user,
        }
    );

    my $response;
    eval {
        $response = make_server_request(
            "user/web/get-job-properties",
            {
                TargetUser => $target_user,
                JobID      => $job_id,
            }
        );
    };
    if ($@) {
        $logger->error(
            "Failed to get job properties",
            {
                job_id => $job_id,
                error  => $@,
            }
        );
        return undef;
    }

    # Verbose logging of the full response structure for debugging
    $logger->debug(
        "Retrieved job properties - FULL RESPONSE",
        {
            job_id        => $job_id,
            response_keys => $response ? [ sort keys %$response ] : [],
            full_response => $response,
        }
    );

    $logger->debug(
        "Retrieved job properties - summary",
        {
            job_id       => $job_id,
            status       => $response->{Status}    // 'unknown',
            start_time   => $response->{StartTime} // 'unknown',
            end_time     => $response->{EndTime}   // 'unknown',
            has_progress => defined $response->{Progress} ? 'yes' : 'no',
        }
    );

    return $response;
}

=head2 get_all_jobs

    my $jobs = Whostmgr::CometBackup::CometBackupAPI::get_all_jobs(
        target_user => 'username',  # optional
    );

Retrieves a list of all backup jobs for the configured user.

Uses the user/web/get-jobs API endpoint to fetch all backup jobs, which can then
be filtered and matched to specific Protected Items by SourceGUID.

Parameters:
- target_user: The Comet server username (optional, defaults to username from server config)

Returns: Hashref where keys are job GUIDs and values are BackupJobDetail structures.
         Returns undef on error.

=cut

sub get_all_jobs (%args) {
    my $target_user = $args{target_user};

    my $logger = _get_logger();

    # If target_user not provided, get it from server config
    unless ($target_user) {
        my $server_config = get_server_config();
        $target_user = $server_config->{username} or die "Unable to determine target_user from server config";
    }

    $logger->debug(
        "Fetching all backup jobs",
        {
            target_user => $target_user,
        }
    );

    my $response;
    eval {
        $response = make_server_request(
            "user/web/get-jobs",
            {
                TargetUser => $target_user,
            }
        );
    };
    if ($@) {
        $logger->error(
            "Failed to get all jobs",
            {
                error => $@,
            }
        );
        return undef;
    }

    # The get-jobs API returns:
    # - On success: An ARRAY of BackupJobDetail objects
    # - On failure: A HASH with Status and Message fields

    # Check if response is an error (HASH with Status/Message)
    if ( ref($response) eq 'HASH' && exists $response->{Status} ) {
        $logger->error(
            "Failed to retrieve jobs",
            {
                status  => $response->{Status},
                message => $response->{Message} || 'Unknown error',
            }
        );
        return undef;
    }

    # Success case - response should be an ARRAY of jobs
    unless ( ref($response) eq 'ARRAY' ) {
        $logger->error(
            "Unexpected response type from get-jobs",
            {
                response_type => ref($response),
                expected      => 'ARRAY',
            }
        );
        return undef;
    }

    $logger->debug(
        "Retrieved all jobs",
        {
            job_count => scalar(@$response),
        }
    );

    # Convert array to hash indexed by GUID for easier lookup
    my %jobs_by_guid;
    for my $job (@$response) {
        next unless ref($job) eq 'HASH';
        next unless $job->{GUID};
        $jobs_by_guid{ $job->{GUID} } = $job;
    }

    return \%jobs_by_guid;
}

=head2 get_recent_jobs

    my $jobs = Whostmgr::CometBackup::CometBackupAPI::get_recent_jobs(
        target_user => 'username',  # optional
        hours       => 24,           # optional, defaults to 24
    );

Retrieves a list of recent backup jobs for the configured user using a custom search.

Uses the user/web/get-jobs-for-custom-search API endpoint to efficiently fetch only
recent backup jobs (default: last 24 hours) instead of all historical jobs. This is
much more efficient for active backup monitoring.

Parameters:
- target_user: The Comet server username (optional, defaults to username from server config)
- hours: Number of hours to look back (optional, defaults to 24)

Returns: Hashref where keys are job GUIDs and values are BackupJobDetail structures.
         Returns undef on error.

=cut

sub get_recent_jobs (%args) {
    my $target_user = $args{target_user};
    my $hours       = $args{hours} || 24;

    my $logger = _get_logger();

    # If target_user not provided, get it from server config
    unless ($target_user) {
        my $server_config = get_server_config();
        $target_user = $server_config->{username} or die "Unable to determine target_user from server config";
    }

    my $seconds = $hours * 3600;

    $logger->debug(
        "Fetching recent backup jobs",
        {
            target_user => $target_user,
            hours       => $hours,
            seconds     => $seconds,
        }
    );

    # Build search clause to get jobs from last N hours
    # TimeSinceStarted is in seconds
    # We use int_lt (less than) to get jobs started within the specified time window
    my $search_clause = {
        ClauseType     => 'and',
        ClauseChildren => [
            {
                ClauseType   => '',
                RuleField    => 'BackupJobDetail.TimeSinceStarted',
                RuleOperator => 'int_lt',
                RuleValue    => "$seconds",
            }
        ],
    };

    my $response;
    eval {
        $response = make_server_request(
            "user/web/get-jobs-for-custom-search",
            {
                TargetUser => $target_user,
                Query      => encode_json($search_clause),
            }
        );
    };
    if ($@) {
        $logger->error(
            "Failed to get recent jobs",
            {
                error => $@,
                hours => $hours,
            }
        );
        return undef;
    }

    # The get-jobs-for-custom-search API returns:
    # - On success: An ARRAY of BackupJobDetail objects
    # - On failure: A HASH with Status and Message fields

    # Check if response is an error (HASH with Status/Message)
    if ( ref($response) eq 'HASH' && exists $response->{Status} ) {
        $logger->error(
            "Failed to retrieve recent jobs",
            {
                status  => $response->{Status},
                message => $response->{Message} || 'Unknown error',
            }
        );
        return undef;
    }

    # Success case - response should be an ARRAY of jobs
    unless ( ref($response) eq 'ARRAY' ) {
        $logger->error(
            "Unexpected response type from get-jobs-for-custom-search",
            {
                response_type => ref($response),
                expected      => 'ARRAY',
            }
        );
        return undef;
    }

    $logger->debug(
        "Retrieved recent jobs",
        {
            job_count   => scalar(@$response),
            time_window => "$hours hours",
        }
    );

    # Convert array to hash indexed by GUID for easier lookup
    my %jobs_by_guid;
    for my $job (@$response) {
        next unless ref($job) eq 'HASH';
        next unless $job->{GUID};
        $jobs_by_guid{ $job->{GUID} } = $job;
    }

    return \%jobs_by_guid;
}

=head2 request_vault_snapshots

    my $snapshots = Whostmgr::CometBackup::CometBackupAPI::request_vault_snapshots(
        target_id      => '1458d3f9-7b9e-4957-bf85-dc51217195ff',
        destination_id => '331f5ec2-f836-441a-947c-4005d815c433',
    );

Requests a list of storage vault snapshots from a live connected device via the
user/web/dispatcher/request-vault-snapshots API endpoint.

This function communicates with an active Comet Backup device through the dispatcher
to retrieve the actual list of snapshots available in a storage vault. This is more
accurate than using cached snapshot data from the user profile, as it reflects the
current state of the storage vault.

Parameters:
- target_id: The connection ID of the active device (TargetID in API) (required)
- destination_id: The storage destination/vault ID (Destination in API) (required)

Returns: Arrayref of snapshot hashes, where each hash contains:
- Snapshot: The snapshot ID/hash (string)
- EngineType: The backup engine type (e.g., "engine1/stdout", "engine1/file")
- Source: The Protected Item UID (source GUID)
- CreateTime: Unix timestamp when snapshot was created
- HasOriginalPathInfo: Boolean indicating if original path information is available

Example return structure:
    [
        {
            Snapshot => "3900142e5260367a7fc4791783e396a34646022e4146ecb798fc67e37f2d96e8",
            EngineType => "engine1/stdout",
            Source => "45D5CB4A-B504-11F0-83B7-CE4F099E08C3",
            CreateTime => 1761771239,
            HasOriginalPathInfo => 0,
        },
        {
            Snapshot => "095c5ca5e5e36df28c25c6381b1872b5ea82ea0e7509cc27988338666cf5f028",
            EngineType => "engine1/file",
            Source => "3485034C-B504-11F0-843C-C2D09CF946DB",
            CreateTime => 1761771569,
            HasOriginalPathInfo => 1,
        },
        ...
    ]

Returns undef on error.

=cut

sub request_vault_snapshots (%args) {
    my $target_id      = $args{target_id}      or die "target_id parameter required";
    my $destination_id = $args{destination_id} or die "destination_id parameter required";

    my $logger = _get_logger();

    $logger->debug(
        "Requesting vault snapshots from live device",
        {
            target_id      => $target_id,
            destination_id => $destination_id,
        }
    );

    my $response;
    eval {
        $response = make_server_request(
            "user/web/dispatcher/request-vault-snapshots",
            {
                TargetID    => $target_id,
                Destination => $destination_id,
            }
        );
    };
    if ($@) {
        $logger->error(
            "Failed to request vault snapshots",
            {
                target_id      => $target_id,
                destination_id => $destination_id,
                error          => $@,
            }
        );
        return undef;
    }

    # The request-vault-snapshots API returns:
    # - On success: An ARRAY of snapshot objects
    # - On failure: A HASH with Status and Message fields

    # Check if response is an error (HASH with Status/Message)
    if ( ref($response) eq 'HASH' && exists $response->{Status} ) {
        $logger->error(
            "Failed to retrieve vault snapshots",
            {
                target_id      => $target_id,
                destination_id => $destination_id,
                status         => $response->{Status},
                message        => $response->{Message} || 'Unknown error',
            }
        );
        return undef;
    }

    # Success case - response should be an ARRAY of snapshots
    unless ( ref($response) eq 'ARRAY' ) {
        $logger->error(
            "Unexpected response type from request-vault-snapshots",
            {
                target_id      => $target_id,
                destination_id => $destination_id,
                response_type  => ref($response),
                expected       => 'ARRAY',
            }
        );
        return undef;
    }

    $logger->debug(
        "Retrieved vault snapshots from live device",
        {
            target_id      => $target_id,
            destination_id => $destination_id,
            snapshot_count => scalar(@$response),
        }
    );

    return $response;
}

1;
