package Whostmgr::CometBackup::DeletedUsers;

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

=encoding utf-8

=head1 NAME

Whostmgr::CometBackup::DeletedUsers - Track deleted cPanel users for Comet backup retention and pruning

=head1 SYNOPSIS

    use Whostmgr::CometBackup::DeletedUsers ();

    my $du = Whostmgr::CometBackup::DeletedUsers->new();

    # Record a deleted user
    $du->record_deleted_user( system_user => 'alice', estimated_usage_bytes => 12_000_000_000 );

    # List deleted users
    my @deleted = $du->list_deleted_users();

    # List snapshots for deleted users
    my @snapshots = $du->list_deleted_user_snapshots();

    # Mark pruning performed
    $du->mark_pruned('alice');

=head1 DESCRIPTION

This module uses the Protected Items database to track deleted cPanel users
by marking their protected items as terminated. Metadata such as estimated usage at
deletion time and pruning flags are stored alongside the protected item records.
Actual snapshot enumeration and restore logic are to be implemented later; current
methods return stub data structures where backend integration is pending.

=cut

use cPstrict;

use JSON::PP                                qw( decode_json );
use Whostmgr::CometBackup::CometBackupAPI   ();
use Whostmgr::CometBackup::Logger           ();
use Whostmgr::CometBackup::ProtectedItemMap ();

# Global logger instance
my $logger;

# Get or create logger instance
sub _get_logger {

    # Allow tests to set $ENV{COMET_TEST_MODE} to skip logger initialization
    return bless {}, 'Whostmgr::CometBackup::Logger' if $ENV{COMET_TEST_MODE};

    return $logger //= Whostmgr::CometBackup::Logger->new( log_level => $ENV{DEBUG} ? 'debug' : 'info' );
}

sub new ($class) {
    my $self = bless {
        map => Whostmgr::CometBackup::ProtectedItemMap->new(),
    }, $class;

    return $self;
}

sub _map ($self) {
    return $self->{map};
}

=head2 record_deleted_user

    $du->record_deleted_user( system_user => 'alice', estimated_usage_bytes => 5_000_000_000 );

Records that a system user has been deleted. If the user already exists in the table,
updates the deletion timestamp and usage (preserving earliest deletion timestamp).

Parameters:
  - system_user (required)
  - estimated_usage_bytes (optional, integer)

Returns: 1 on success; dies on error.

=cut

sub record_deleted_user ( $self, %args ) {
    my $system_user = $args{system_user} // die 'system_user parameter required';
    my $usage       = $args{estimated_usage_bytes};

    my $affected = $self->_map()->mark_user_deleted(
        system_user           => $system_user,
        estimated_usage_bytes => $usage,
    );

    if ( !$affected ) {
        _get_logger()->warn("No protected items recorded for $system_user; skipping deleted user tracking");
    }

    return 1;
}

=head2 get_deleted_user

    my $info = $du->get_deleted_user('alice');

Returns hashref of deleted user metadata or undef.

=cut

sub get_deleted_user ( $self, $system_user ) {
    die 'system_user parameter required' unless defined $system_user;
    return $self->_map()->get_deleted_user($system_user);
}

=head2 list_deleted_users

    my @users = $du->list_deleted_users();

Returns array of hashrefs for all deleted users.

=cut

sub list_deleted_users ($self) {
    return $self->_map()->get_deleted_users();
}

=head2 mark_pruned

    $du->mark_pruned('alice');

Marks that pruning has been performed (pruned_flag = 1).

=cut

sub mark_pruned ( $self, $system_user ) {
    $self->_map()->set_deleted_user_pruned_flag( system_user => $system_user, flag => 1 );
    return 1;
}

=head2 list_deleted_user_snapshots

    my $result = $du->list_deleted_user_snapshots($username);

Lists snapshots available for a specific deleted user by querying the backup run logs database.

This function retrieves snapshot information from the local backup_run_logs.db database,
which tracks all completed backups including their snapshot IDs, destinations, and metadata.

Returns a hashref with username, snapshots array, and count, or an error hashref if the
operation fails.

=cut

sub list_deleted_user_snapshots ( $self, $username ) {
    my $logger = _get_logger();

    die 'username parameter required' unless defined $username;

    $logger->debug("Listing snapshots for deleted user: $username");

    # Verify user is tracked as deleted
    my $user_data = $self->get_deleted_user($username);
    unless ($user_data) {
        $logger->error("User $username not tracked in deleted user metadata");
        return { error => "User $username not tracked in deleted user metadata" };
    }

    # Query the backup_run_logs database for snapshots belonging to this user
    my $run_log_db = $Whostmgr::CometBackup::Constants::RUN_LOG_DB;

    unless ( -f $run_log_db ) {
        $logger->warn("Backup run logs database not found: $run_log_db");
        return {
            error     => "Backup run logs database not found",
            username  => $username,
            snapshots => [],
            count     => 0
        };
    }

    require DBI;
    my $dbh;
    eval {
        $dbh = DBI->connect(
            "dbi:SQLite:dbname=$run_log_db",
            "", "",
            {
                RaiseError => 1,
                AutoCommit => 1,
                PrintError => 0,
            }
        );
    };
    if ($@) {
        $logger->error("Failed to connect to backup run logs database: $@");
        return {
            error     => "Failed to connect to backup run logs database: $@",
            username  => $username,
            snapshots => [],
            count     => 0
        };
    }

    my @snapshots;
    eval {
        # Query for all completed item runs for this user that have snapshot IDs
        my $sth = $dbh->prepare(
            q{
            SELECT 
                ir.snapshot_id,
                ir.protected_item_id,
                ir.item_type,
                ir.destination_id,
                ir.start_time,
                ir.end_time,
                ir.job_properties
            FROM item_runs ir
            JOIN user_runs ur ON ir.user_run_id = ur.id
            WHERE ur.username = ?
                AND ir.status = 'completed'
                AND ir.snapshot_id IS NOT NULL
                AND ir.snapshot_id != ''
            ORDER BY ir.end_time DESC
        }
        );

        $sth->execute($username);

        while ( my $row = $sth->fetchrow_hashref() ) {
            my $job_properties = {};
            if ( $row->{job_properties} ) {
                eval {
                    require Cpanel::JSON;
                    $job_properties = Cpanel::JSON::Load( $row->{job_properties} );
                };
                if ($@) {
                    $logger->debug("Failed to decode job_properties for snapshot $row->{snapshot_id}: $@");
                }
            }

            # Calculate size from job properties if available
            my $size = 'Unknown';
            if ( $job_properties->{TotalSize} ) {
                $size = $job_properties->{TotalSize};
            }

            push @snapshots, {
                snapshot_uuid     => $row->{snapshot_id},
                snapshot_id       => $row->{snapshot_id},
                source_id         => $row->{protected_item_id},
                protected_item_id => $row->{protected_item_id},
                item_type         => $row->{item_type} || 'Unknown',
                destination_id    => $row->{destination_id},
                created_at        => $row->{end_time} || $row->{start_time},
                size              => $size,
                type              => $row->{item_type} || 'Unknown',
                status            => 'Completed',
            };
        }

        $sth->finish();
    };

    my $eval_error = $@;
    $dbh->disconnect() if $dbh;

    if ($eval_error) {
        $logger->error("Failed to query backup run logs: $eval_error");
        return {
            error     => "Failed to query backup run logs: $eval_error",
            username  => $username,
            snapshots => [],
            count     => 0
        };
    }

    $logger->debug( "Found " . scalar(@snapshots) . " snapshots for deleted user: $username" );

    return {
        username  => $username,
        snapshots => \@snapshots,
        count     => scalar(@snapshots)
    };

}

=head2 prune_single_deleted_user

    my $result = $du->prune_single_deleted_user($username);
    my $result = $du->prune_single_deleted_user($username, dry_run => 1);

Prunes (deletes) backup data for a specific deleted user by:
1. Listing all snapshots for the user via C<list_deleted_user_snapshots>
2. Deleting each snapshot using the Comet API endpoint
   C<POST /api/v1/user/web/dispatcher/delete-snapshots>

Marks the user as pruned in the database upon success.

=cut

sub prune_single_deleted_user ( $self, $username, %args ) {
    my $logger  = _get_logger();
    my $dry_run = $args{dry_run} // 0;

    die 'username parameter required' unless defined $username;

    if ($dry_run) {
        $logger->debug("[DRY RUN] Would prune deleted user: $username");
    }
    else {
        $logger->debug("Pruning deleted user: $username");
    }

    # Verify user is tracked as deleted
    my $user_data = $self->get_deleted_user($username);
    unless ($user_data) {
        $logger->error("User $username not tracked in deleted user metadata");
        return { error => "User $username not tracked in deleted user metadata" };
    }

    # Verify user does not exist locally
    if ( getpwnam($username) ) {
        $logger->error("User $username still exists locally, cannot prune");
        return { error => "User $username still exists locally" };
    }

    # Check if Comet server is configured
    my $server_config = eval { Whostmgr::CometBackup::CometBackupAPI::get_server_config() };
    if ( $@ || !$server_config || $server_config == -1 ) {
        my $error = $@ || 'Server config returned invalid value';
        $logger->warn("Comet server not configured, cannot prune user: $error");
        return { error => "Comet server not configured. Please register the server first." };
    }

    # Get all snapshots for this deleted user
    my $snapshot_result = $self->list_deleted_user_snapshots($username);

    if ( $snapshot_result->{error} ) {
        $logger->error("Failed to list snapshots for user $username: $snapshot_result->{error}");
        return { error => "Failed to list snapshots: $snapshot_result->{error}" };
    }

    my $snapshots      = $snapshot_result->{snapshots} || [];
    my $snapshot_count = scalar(@$snapshots);

    $logger->debug("Found $snapshot_count snapshot(s) for deleted user $username");

    # If dry-run, return success without performing actions
    if ($dry_run) {
        $logger->debug("[DRY RUN] Would delete $snapshot_count snapshot(s) for user $username");
        return {
            success         => 1,
            message         => "Would delete $snapshot_count snapshot(s) for user $username",
            snapshots_count => $snapshot_count
        };
    }

    # If no snapshots, mark as pruned and return success
    if ( $snapshot_count == 0 ) {
        $self->mark_pruned($username);
        $logger->debug("No snapshots to delete for user $username, marked as pruned");
        return {
            success           => 1,
            message           => "No snapshots to delete for user $username",
            snapshots_deleted => 0
        };
    }

    my $connection_id = 'Unknown';
    eval { $connection_id = Whostmgr::CometBackup::CometBackupAPI::_get_user_connection_id() || die 'Unable to get 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,
        };
    }

    my $total_deleted = 0;
    my @errors;

    # Delete each snapshot individually using its active connection
    for my $snapshot (@$snapshots) {
        my $snapshot_id = $snapshot->{snapshot_id};
        my $dest_id     = $snapshot->{destination_id};

        eval {
            my $response = Whostmgr::CometBackup::CometBackupAPI::make_server_request(
                'user/web/dispatcher/delete-snapshot',
                {
                    TargetID      => $connection_id,
                    DestinationID => $dest_id,
                    SnapshotID    => $snapshot_id,
                }
            );

            if ( $response && ref($response) eq 'HASH' && ( $response->{Status} // 0 ) == 200 ) {
                $total_deleted++;
                $logger->debug("Deleted snapshot $snapshot_id from destination $dest_id");
            }
            else {
                my $error_msg = $response->{Message} || "Unknown error";
                $logger->warn("Failed to delete snapshot $snapshot_id: $error_msg");
                push @errors, "Snapshot $snapshot_id: $error_msg";
            }
        };
        if ($@) {
            $logger->error("Failed to delete snapshot $snapshot_id: $@");
            push @errors, "Snapshot $snapshot_id: $@";
        }
    }

    # If all snapshots were deleted successfully, mark user as pruned
    if ( $total_deleted == $snapshot_count ) {
        $self->mark_pruned($username);
        $logger->debug("Successfully deleted all $total_deleted snapshot(s) for user $username");
        return {
            success           => 1,
            message           => "Successfully deleted $total_deleted snapshot(s) for user $username",
            snapshots_deleted => $total_deleted
        };
    }
    elsif ( $total_deleted > 0 ) {

        # Partial success
        $logger->warn("Partially deleted snapshots for user $username: $total_deleted of $snapshot_count deleted");
        return {
            success           => 0,
            error             => "Partially deleted snapshots: " . join( "; ", @errors ),
            snapshots_deleted => $total_deleted,
            snapshots_total   => $snapshot_count
        };
    }
    else {
        # Complete failure
        $logger->error("Failed to delete any snapshots for user $username");
        return {
            error             => "Failed to delete snapshots: " . join( "; ", @errors ),
            snapshots_deleted => 0,
            snapshots_total   => $snapshot_count
        };
    }
}

=head2 restore_deleted_user

    my $result = $du->restore_deleted_user($username, $snapshot_uuid);
    my $result = $du->restore_deleted_user($username, $snapshot_uuid, dry_run => 1);

Restores a deleted user from a specific snapshot.

B<IMPORTANT LIMITATION>: Restoring terminated users requires user-level API access.
The relevant API endpoints for restore operations are:
- C<POST /api/v1/user/web/dispatcher/run-restore> (local restore to path)
- C<POST /api/v1/user/web/dispatcher/run-restore-custom> (custom restore options)

Both require:
- User credentials (username and password)
- C<Path>: Local path to restore to
- C<Source>: Protected Item ID
- C<Destination>: Storage Vault ID
- C<Snapshot>: Snapshot ID (optional, defaults to latest)

B<FUTURE WORK>: User restoration requires:
1. Recreation of the cPanel user account (via WHM API or /usr/local/cpanel/scripts/adduser)
2. Restoration of backup data to user's home directory
3. Recreation of user's Comet backup configuration
4. Re-registration of user's devices with Comet server

This functionality requires integration with:
- cPanel account management (Whostmgr::Accounts::Create)
- Comet device registration workflow
- Storage vault access without live device connections

Marks the user as active again in the Protected Item map upon successful restoration.

=cut

sub restore_deleted_user ( $self, $username, $snapshot_uuid, %args ) {
    my $logger  = _get_logger();
    my $dry_run = $args{dry_run} // 0;

    die 'username parameter required'      unless defined $username;
    die 'snapshot_uuid parameter required' unless defined $snapshot_uuid;

    if ($dry_run) {
        $logger->debug("[DRY RUN] Would restore deleted user: $username from snapshot: $snapshot_uuid");
    }
    else {
        $logger->debug("Restoring deleted user: $username from snapshot: $snapshot_uuid");
    }

    # Verify user is tracked as deleted
    my $user_data = $self->get_deleted_user($username);
    unless ($user_data) {
        $logger->error("User $username not tracked in deleted user metadata");
        return { error => "User $username not tracked in deleted user metadata" };
    }

    # Verify user does not exist locally
    if ( getpwnam($username) ) {
        $logger->error("User $username already exists locally, cannot restore");
        return { error => "User $username already exists locally" };
    }

    # Check if Comet server is configured
    my $server_config = eval { Whostmgr::CometBackup::CometBackupAPI::get_server_config() };
    if ( $@ || !$server_config || $server_config == -1 ) {
        my $error = $@ || 'Server config returned invalid value';
        $logger->warn("Comet server not configured, cannot restore user: $error");
        return { error => "Comet server not configured. Please register the server first." };
    }

    # If dry-run, return success without performing actions
    if ($dry_run) {
        $logger->debug("[DRY RUN] Would successfully restore user $username from snapshot $snapshot_uuid");
        return {
            success  => 1,
            message  => "User $username would be restored successfully",
            job_uuid => "dry-run-" . time()
        };
    }

    # NOTE: Restoring terminated users requires user-level API access.
    # The real endpoints would be:
    #   POST /api/v1/user/web/dispatcher/run-restore
    #   POST /api/v1/user/web/dispatcher/run-restore-custom
    # But they require:
    #   - User credentials (username and password)
    #   - Source (Protected Item ID)
    #   - Destination (Storage Vault ID)
    #   - Path (local restore path)
    #
    # Proper user restoration would require:
    # 1. Recreating the cPanel account via Whostmgr::Accounts::Create
    # 2. Restoring backup data to the new user's home directory
    # 3. Recreating the user's Comet configuration and device registration

    # TODO: Implement user-level API calls for restoration
    eval {
        # Placeholder: would use user/web/dispatcher/run-restore endpoint
        # with user credentials once implemented
        my $response = Whostmgr::CometBackup::CometBackupAPI::make_server_request(
            'user/web/restore-deleted-user',
            {
                username      => $username,
                snapshot_uuid => $snapshot_uuid
            }
        );

        if ( $response && ref($response) eq 'HASH' && ( $response->{Status} // 200 ) == 200 ) {
            $self->_map()->mark_user_reactivated($username);

            $logger->debug("Successfully restored user $username from snapshot $snapshot_uuid");
            return {
                success  => 1,
                message  => "User $username restored successfully",
                job_uuid => $response->{job_uuid}
            };
        }
        else {
            $logger->error( "Failed to restore user $username: " . ( $response->{Message} || "Unknown error" ) );
            return { error => "Failed to restore user: " . ( $response->{Message} || "Unknown error" ) };
        }
    };
    if ($@) {
        $logger->error("Failed to restore user $username: $@");
        return { error => "Failed to restore user: $@" };
    }
}

=head2 get_user_usage

    my $bytes = $du->get_user_usage('alice');

Returns stored estimated_usage_bytes (or 0 if undefined).

=cut

sub get_user_usage ( $self, $system_user ) {
    my $info = $self->get_deleted_user($system_user) || return 0;
    return $info->{estimated_usage_bytes} // 0;
}

=head2 prune_deleted_users_before

    my $count = $du->prune_deleted_users_before( cutoff_epoch => time() - 30*86400 );

Deletes deleted user records whose last_seen_deleted_ts is older than cutoff.

=cut

sub prune_deleted_users_before ( $self, %args ) {
    my $cutoff = $args{cutoff_epoch} // die 'cutoff_epoch parameter required';
    return $self->_map()->purge_deleted_users_before_epoch($cutoff);
}

=head2 prune_deleted_user_snapshots_before

    my $result = $du->prune_deleted_user_snapshots_before(
        cutoff_epoch => time() - 90*86400,
        dry_run => 0
    );

Prunes snapshot records from backup_run_logs.db for deleted users whose
snapshots are older than the specified cutoff time.

This function:
1. Identifies deleted users (users in the deleted users list who no longer exist locally)
2. Finds completed item_runs for those users with end_time before the cutoff
3. Deletes those item_run records from the database

Parameters:
  - cutoff_epoch (required): Unix timestamp cutoff
  - dry_run (optional): If true, only counts records without deleting

Returns hashref with:
  - count: Number of snapshot records deleted (or would be deleted if dry_run)
  - users_affected: Array of usernames whose snapshots were pruned
  - error: Error message if operation failed

=cut

sub prune_deleted_user_snapshots_before ( $self, %args ) {
    my $logger       = _get_logger();
    my $cutoff_epoch = $args{cutoff_epoch} // die 'cutoff_epoch parameter required';
    my $dry_run      = $args{dry_run}      // 0;
    my $users        = $args{users};    # Optional arrayref of usernames to filter by

    # Get list of deleted users
    my @deleted_users = $self->list_deleted_users();

    if ( !@deleted_users ) {
        $logger->debug("No deleted users found to prune snapshots for");
        return {
            count          => 0,
            users_affected => [],
        };
    }

    # Filter to only include users that don't exist locally anymore
    my @truly_deleted = grep {
        my $username = $_->{system_user};
        !getpwnam($username);
    } @deleted_users;

    # Further filter by specific users if requested
    if ( $users && ref($users) eq 'ARRAY' && @$users ) {
        my %target_users = map { $_ => 1 } @$users;
        @truly_deleted = grep { $target_users{ $_->{system_user} }; } @truly_deleted;
    }

    if ( !@truly_deleted ) {
        $logger->debug("No truly deleted users found to prune snapshots for");
        return {
            count          => 0,
            users_affected => [],
        };
    }

    # Access backup run logs database
    my $run_log_db = $Whostmgr::CometBackup::Constants::RUN_LOG_DB;

    unless ( -f $run_log_db ) {
        $logger->warn("Backup run logs database not found: $run_log_db");
        return {
            error          => "Backup run logs database not found",
            count          => 0,
            users_affected => [],
        };
    }

    require DBI;
    my $dbh;
    eval {
        $dbh = DBI->connect(
            "dbi:SQLite:dbname=$run_log_db",
            "", "",
            {
                RaiseError => 1,
                AutoCommit => 1,
                PrintError => 0,
            }
        );
    };
    if ($@) {
        $logger->error("Failed to connect to backup run logs database: $@");
        return {
            error          => "Failed to connect to backup run logs database: $@",
            count          => 0,
            users_affected => [],
        };
    }

    my $total_count = 0;
    my @users_affected;

    eval {
        # For each deleted user, find and delete their old snapshot records
        for my $user_info (@truly_deleted) {
            my $username = $user_info->{system_user};

            if ($dry_run) {

                # Count what would be deleted
                my $sth = $dbh->prepare(
                    q{
                    SELECT COUNT(*)
                    FROM item_runs ir
                    JOIN user_runs ur ON ir.user_run_id = ur.id
                    WHERE ur.username = ?
                        AND ir.status = 'completed'
                        AND ir.end_time IS NOT NULL
                        AND ir.end_time < ?
                }
                );
                $sth->execute( $username, $cutoff_epoch );
                my ($count) = $sth->fetchrow_array();

                if ( $count > 0 ) {
                    $total_count += $count;
                    push @users_affected, $username;
                    $logger->debug("[DRY RUN] Would delete $count snapshot records for user $username");
                }
            }
            else {
                # Actually delete the records
                my $sth = $dbh->prepare(
                    q{
                    DELETE FROM item_runs
                    WHERE id IN (
                        SELECT ir.id
                        FROM item_runs ir
                        JOIN user_runs ur ON ir.user_run_id = ur.id
                        WHERE ur.username = ?
                            AND ir.status = 'completed'
                            AND ir.end_time IS NOT NULL
                            AND ir.end_time < ?
                    )
                }
                );
                my $rows = $sth->execute( $username, $cutoff_epoch );

                if ( $rows > 0 ) {
                    $total_count += $rows;
                    push @users_affected, $username;
                    $logger->debug("Deleted $rows snapshot records for deleted user $username");
                }
            }
        }
    };

    my $eval_error = $@;
    $dbh->disconnect() if $dbh;

    if ($eval_error) {
        $logger->error("Failed to prune snapshot records: $eval_error");
        return {
            error          => "Failed to prune snapshot records: $eval_error",
            count          => 0,
            users_affected => [],
        };
    }

    $logger->debug( "Pruned $total_count snapshot records for " . scalar(@users_affected) . " deleted users" );

    return {
        count          => $total_count,
        users_affected => \@users_affected,
    };
}

1;

__END__

=head1 FUTURE WORK

Integrate real snapshot listing and pruning via Comet server API, implement restore flow
with account recreation and protected item reactivation, add configurable high-usage
threshold and retention policies.

=cut

