package Whostmgr::CometBackup::BackupRunLogs;

#                                      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::BackupRunLogs - Management interface for Comet backup run logs

=head1 SYNOPSIS

    use Whostmgr::CometBackup::BackupRunLogs ();
    
    my $logs = Whostmgr::CometBackup::BackupRunLogs->new();
    
    # Get recent backup runs
    my @recent_runs = $logs->get_recent_runs(limit => 10);
    
    # Get runs for a specific job
    my @job_runs = $logs->get_runs_for_job($job_uuid);
    
    # Get detailed run information
    my $run_details = $logs->get_run_details($run_id);
    
    # Get run statistics
    my $stats = $logs->get_run_statistics();
    
    # Cancel a running backup
    $logs->cancel_backup_run($run_id);

=head1 DESCRIPTION

This module provides a comprehensive interface for managing and querying Comet backup
run logs. It allows retrieval of backup run history, statistics, and provides
functionality for monitoring and managing active backup operations.

The module interfaces with the SQLite database created by the backup runner to
provide detailed insights into backup performance, timing, and success rates.

=cut

use cPstrict;

use Cpanel::DBI::SQLite               ();
use Cpanel::FileUtils::TouchFile      ();
use Cpanel::JSON                      ();
use Cpanel::Mkdir                     ();
use DBD::SQLite                       ();
use Time::Piece                       ();
use Whostmgr::CometBackup::BackupJobs ();
use Whostmgr::CometBackup::Constants  ();
use Whostmgr::CometBackup::Logger     ();

=head1 METHODS

=head2 new

Creates a new BackupRunLogs instance.

    my $logs = Whostmgr::CometBackup::BackupRunLogs->new();

=cut

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

    $self->_ensure_database();
    return $self;
}

=head2 get_backup_runs

Gets backup runs from the backup_runs table with job information and aggregated size.

    my @runs = $logs->get_backup_runs(
        limit      => 50,           # Optional: limit number of results
        status     => 'completed',  # Optional: filter by status
        job_uuid   => $uuid,        # Optional: filter by job UUID
        start_date => $timestamp,   # Optional: runs after this time
        end_date   => $timestamp,   # Optional: runs before this time
    );

Returns array of run hashrefs with keys: id, run_uuid, job_uuid, start_time,
end_time, status, error_message, duration, job_name, job_description, total_size_bytes
plus formatted versions: start_time_formatted, end_time_formatted, duration_formatted.

=cut

sub get_backup_runs ( $self, %args ) {
    my $limit      = $args{limit} || 50;
    my $status     = $args{status};
    my $job_uuid   = $args{job_uuid};
    my $start_date = $args{start_date};
    my $end_date   = $args{end_date};

    my $run_logs_dbh = Cpanel::DBI::SQLite->connect(
        {
            database   => $Whostmgr::CometBackup::Constants::RUN_LOG_DB,
            RaiseError => 1,
        }
    );

    my @where_clauses;
    my @bind_params;

    if ($status) {
        push @where_clauses, "br.status = ?";
        push @bind_params,   $status;
    }

    if ($job_uuid) {
        push @where_clauses, "br.job_uuid = ?";
        push @bind_params,   $job_uuid;
    }

    if ($start_date) {
        push @where_clauses, "br.start_time >= ?";
        push @bind_params,   $start_date;
    }

    if ($end_date) {
        push @where_clauses, "br.end_time <= ?";
        push @bind_params,   $end_date;
    }

    my $where_clause = @where_clauses ? "WHERE " . join( " AND ", @where_clauses ) : "";

    # Query backup_runs and aggregate TotalSize from all protected items for each run
    my $sql = qq{
        SELECT 
            br.id,
            br.run_uuid,
            br.job_uuid,
            br.start_time,
            br.end_time,
            br.status,
            br.error_message,
            (br.end_time - br.start_time) as duration,
            COALESCE(
                (SELECT SUM(
                    CAST(
                        json_extract(ir.job_properties, '\$.TotalSize') 
                        AS INTEGER
                    )
                ) 
                FROM item_runs ir
                JOIN user_runs ur ON ir.user_run_id = ur.id
                WHERE ur.run_id = br.id
                AND ir.status = 'completed'
                AND json_extract(ir.job_properties, '\$.TotalSize') IS NOT NULL),
                0
            ) as total_size_bytes
        FROM backup_runs br
        $where_clause
        ORDER BY br.start_time DESC 
        LIMIT ?
    };

    push @bind_params, $limit;

    my $sth = $run_logs_dbh->prepare($sql);
    $sth->execute(@bind_params);

    my @runs;
    my %job_uuids_seen;    # Track unique job UUIDs for lookup

    while ( my $row = $sth->fetchrow_hashref() ) {

        # Format timestamps
        if ( $row->{start_time} ) {
            $row->{start_time_formatted} = $self->_format_timestamp( $row->{start_time} );
        }
        if ( $row->{end_time} ) {
            $row->{end_time_formatted} = $self->_format_timestamp( $row->{end_time} );
        }
        if ( $row->{duration} ) {
            $row->{duration_formatted} = $self->_format_duration( $row->{duration} );
        }

        # Track job UUIDs for later lookup
        if ( $row->{job_uuid} ) {
            $job_uuids_seen{ $row->{job_uuid} } = 1;
        }

        push @runs, $row;
    }

    $run_logs_dbh->disconnect();

    # Lookup job names and descriptions from backup_jobs database
    # Also check deleted_backup_jobs for historical data
    my %job_info = ();    # job_uuid -> { name => ..., description => ... }

    if (%job_uuids_seen) {
        eval {
            my $jobs_dbh = Cpanel::DBI::SQLite->connect(
                {
                    database   => $Whostmgr::CometBackup::Constants::BACKUP_JOBS_DB,
                    RaiseError => 1,
                }
            );

            my @job_uuids    = keys %job_uuids_seen;
            my $placeholders = join( ',', ('?') x @job_uuids );

            # Query both active and deleted jobs using UNION
            my $job_sql = qq{
                SELECT uuid, name, description
                FROM backup_jobs
                WHERE uuid IN ($placeholders)
                UNION
                SELECT uuid, name, description
                FROM deleted_backup_jobs
                WHERE uuid IN ($placeholders)
            };

            my $job_sth = $jobs_dbh->prepare($job_sql);

            # Bind parameters twice - once for each SELECT in the UNION
            $job_sth->execute( @job_uuids, @job_uuids );

            while ( my $job_row = $job_sth->fetchrow_hashref() ) {
                $job_info{ $job_row->{uuid} } = {
                    name        => $job_row->{name}        || 'Unknown Job',
                    description => $job_row->{description} || '',
                };
            }

            $jobs_dbh->disconnect();
        };

        if ($@) {
            $self->{logger}->warn("Could not retrieve job information: $@");
        }
    }

    # Add job name and description to each run
    for my $run (@runs) {
        if ( $run->{job_uuid} ) {
            my $job_data = $job_info{ $run->{job_uuid} };
            if ($job_data) {
                $run->{job_name}        = $job_data->{name};
                $run->{job_description} = $job_data->{description};
            }
            else {
                # Fallback values if job lookup failed
                $run->{job_name}        = "Job $run->{job_uuid}";
                $run->{job_description} = '';
            }
        }
        else {
            # Handle case where job_uuid is missing
            $run->{job_name}        = 'Unknown Job';
            $run->{job_description} = '';
        }
    }

    return @runs;
}

=head2 get_recent_runs

Gets recent backup runs with optional filtering.

    my @runs = $logs->get_recent_runs(
        limit      => 50,           # Optional: limit number of results
        status     => 'completed',  # Optional: filter by status
        job_uuid   => $uuid,        # Optional: filter by job UUID
        start_date => $timestamp,   # Optional: runs after this time
        end_date   => $timestamp,   # Optional: runs before this time
    );

Returns array of run hashrefs with keys: id, run_uuid, job_uuid, start_time,
end_time, status, error_message, duration, job_name, job_description, plus
formatted versions: start_time_formatted, end_time_formatted, duration_formatted.

# Note: Use C<get_backup_runs> instead for backup run history. This is for protected item-level details.
This method queries item_runs which is for individual protected items.

=cut

sub get_recent_runs ( $self, %args ) {
    my $limit      = $args{limit} || 50;
    my $status     = $args{status};
    my $job_uuid   = $args{job_uuid};
    my $start_date = $args{start_date};
    my $end_date   = $args{end_date};

    # If it hasn't been initialized there are no runs
    if ( !-e $Whostmgr::CometBackup::Constants::RUN_LOG_DB ) {
        return;
    }

    my $dbh = Cpanel::DBI::SQLite->connect(
        {
            database   => $Whostmgr::CometBackup::Constants::RUN_LOG_DB,
            RaiseError => 1,
        }
    );

    my @where_clauses;
    my @bind_params;

    if ($status) {
        push @where_clauses, "status = ?";
        push @bind_params,   $status;
    }

    if ($job_uuid) {
        push @where_clauses, "job_uuid = ?";
        push @bind_params,   $job_uuid;
    }

    if ($start_date) {
        push @where_clauses, "ir.start_time >= ?";
        push @bind_params,   $start_date;
    }

    if ($end_date) {
        push @where_clauses, "ir.end_time <= ?";
        push @bind_params,   $end_date;
    }

    my $where_clause = @where_clauses ? "WHERE " . join( " AND ", @where_clauses ) : "";

    # Build SELECT clause - always include job_properties which contains TotalSize
    my $select_clause = q{
            ir.id, br.run_uuid, br.job_uuid, ur.username as system_user,
            ir.item_type as protected_item_type, ir.protected_item_id as protected_item_uid, 
            ir.snapshot_id as comet_snapshot_id, ir.comet_job_id,
            ir.job_properties,
            ir.start_time, ir.end_time, 0 as size_bytes,
            0 as restored_count, NULL as last_restored_time,
            br.status as run_status
    };

    my $sql = qq{
        SELECT 
            $select_clause
        FROM item_runs ir
        JOIN user_runs ur ON ir.user_run_id = ur.id
        JOIN backup_runs br ON ur.run_id = br.id
        $where_clause
        ORDER BY ir.start_time DESC 
        LIMIT ?
    };

    push @bind_params, $limit;

    my $sth = $dbh->prepare($sql);
    $sth->execute(@bind_params);

    my @runs;
    my %job_uuids_seen;    # Track unique job UUIDs for lookup

    while ( my $row = $sth->fetchrow_hashref() ) {

        $self->{logger}->debug(
            "Row from database before JSON parsing",
            {
                comet_job_id       => $row->{comet_job_id} || '[undef/empty]',
                has_job_properties => $row->{job_properties} ? 'yes' : 'no'
            }
        );

        # Parse job_properties JSON to extract SnapshotID, DestinationGUID, GUID (job ID), and TotalSize
        if ( $row->{job_properties} ) {
            eval {
                my $job_props = Cpanel::JSON::Load( $row->{job_properties} );

                # Extract GUID (Comet Job ID) from job_properties - use it if database column is empty
                if ( $job_props->{GUID} && !$row->{comet_job_id} ) {
                    $row->{comet_job_id} = $job_props->{GUID};
                    $self->{logger}->debug( "Extracted comet_job_id from job_properties", { comet_job_id => $row->{comet_job_id} } );
                }

                # Extract SnapshotID from job_properties (most accurate)
                if ( $job_props->{SnapshotID} ) {
                    $row->{comet_snapshot_id} = $job_props->{SnapshotID};
                }

                # Extract DestinationGUID from job_properties (most accurate)
                if ( $job_props->{DestinationGUID} ) {
                    $row->{comet_destination_id} = $job_props->{DestinationGUID};
                }

                # Extract TotalSize from job_properties
                if ( defined $job_props->{TotalSize} && $job_props->{TotalSize} > 0 ) {
                    $row->{size_bytes} = $job_props->{TotalSize};
                }
            };
            if ($@) {
                $self->{logger}->warn( "Failed to parse job_properties JSON", { error => $@, row_id => $row->{id} } );
            }
        }

        # Format timestamps
        if ( $row->{start_time} ) {
            $row->{start_time_formatted} = $self->_format_timestamp( $row->{start_time} );
        }
        if ( $row->{end_time} ) {
            $row->{end_time_formatted} = $self->_format_timestamp( $row->{end_time} );
        }
        if ( $row->{duration} ) {
            $row->{duration_formatted} = $self->_format_duration( $row->{duration} );
        }

        # Track job UUIDs for later lookup
        if ( $row->{job_uuid} ) {
            $job_uuids_seen{ $row->{job_uuid} } = 1;
        }

        $self->{logger}->debug(
            "Row after all processing",
            {
                run_uuid     => $row->{run_uuid},
                comet_job_id => $row->{comet_job_id} || '[undef/empty]',
                system_user  => $row->{system_user}
            }
        );

        push @runs, $row;
    }

    $dbh->disconnect();

    # Lookup job names and descriptions for all job UUIDs found
    # Also check deleted_backup_jobs for historical data
    my %job_info = ();    # job_uuid -> { name => ..., description => ... }

    if (%job_uuids_seen) {
        eval {
            # Query backup_jobs table directly to avoid recursion through get_backup_jobs
            # which would trigger _enhance_job_data and cause infinite recursion
            my $jobs_dbh = Cpanel::DBI::SQLite->connect(
                {
                    database   => $Whostmgr::CometBackup::Constants::BACKUP_JOBS_DB,
                    RaiseError => 1,
                }
            );

            my @job_uuids    = keys %job_uuids_seen;
            my $placeholders = join( ',', ('?') x @job_uuids );

            # Query both active and deleted jobs using UNION
            my $job_sql = qq{
                SELECT uuid, name, description
                FROM backup_jobs
                WHERE uuid IN ($placeholders)
                UNION
                SELECT uuid, name, description
                FROM deleted_backup_jobs
                WHERE uuid IN ($placeholders)
            };

            my $job_sth = $jobs_dbh->prepare($job_sql);

            # Bind parameters twice - once for each SELECT in the UNION
            $job_sth->execute( @job_uuids, @job_uuids );

            while ( my $job_row = $job_sth->fetchrow_hashref() ) {
                $job_info{ $job_row->{uuid} } = {
                    name        => $job_row->{name}        || 'Unknown Job',
                    description => $job_row->{description} || '',
                };
            }

            $jobs_dbh->disconnect();
        };

        if ($@) {
            $self->{logger}->warn("Could not retrieve job information: $@");
        }
    }

    # Add job name and description to each run
    for my $run (@runs) {
        if ( $run->{job_uuid} ) {
            my $job_data = $job_info{ $run->{job_uuid} };
            if ($job_data) {
                $run->{job_name}        = $job_data->{name};
                $run->{job_description} = $job_data->{description};
            }
            else {
                # Fallback values if job lookup failed
                $run->{job_name}        = "Job $run->{job_uuid}";
                $run->{job_description} = '';
            }
        }
        else {
            # Handle case where job_uuid is missing
            $run->{job_name}        = 'Unknown Job';
            $run->{job_description} = '';
        }
    }

    return @runs;
}

=head2 get_runs_for_job

Gets all runs for a specific backup job.

    my @runs = $logs->get_runs_for_job($job_uuid, limit => 10);

=cut

sub get_runs_for_job ( $self, $job_uuid, %args ) {
    return $self->get_recent_runs(
        job_uuid => $job_uuid,
        %args
    );
}

=head2 get_run_details

Gets detailed information about a specific backup run, including user and item details.

    my $details = $logs->get_run_details($run_id);

Returns hashref with run info, user runs, and protected item runs.

=cut

sub get_run_details ( $self, $run_id ) {
    my $dbh = Cpanel::DBI::SQLite->connect(
        {
            database   => $Whostmgr::CometBackup::Constants::RUN_LOG_DB,
            RaiseError => 1,
        }
    );

    # Get run information
    my $run_sth = $dbh->prepare(<<~'SQL');
        SELECT 
            id,
            run_uuid,
            job_uuid,
            start_time,
            end_time,
            status,
            error_message,
            (end_time - start_time) as duration
        FROM backup_runs 
        WHERE id = ?
        SQL

    $run_sth->execute($run_id);
    my $run_info = $run_sth->fetchrow_hashref();

    unless ($run_info) {
        return;
    }

    # Format timestamps
    if ( $run_info->{start_time} ) {
        $run_info->{start_time_formatted} = $self->_format_timestamp( $run_info->{start_time} );
    }
    if ( $run_info->{end_time} ) {
        $run_info->{end_time_formatted} = $self->_format_timestamp( $run_info->{end_time} );
    }
    if ( $run_info->{duration} ) {
        $run_info->{duration_formatted} = $self->_format_duration( $run_info->{duration} );
    }

    # Get user runs
    my $user_sth = $dbh->prepare(<<~'SQL');
        SELECT 
            id,
            username,
            start_time,
            end_time,
            status,
            error_message,
            (end_time - start_time) as duration
        FROM user_runs 
        WHERE run_id = ?
        ORDER BY start_time
        SQL

    $user_sth->execute($run_id);

    my @user_runs;
    while ( my $user_row = $user_sth->fetchrow_hashref() ) {

        # Format timestamps
        if ( $user_row->{start_time} ) {
            $user_row->{start_time_formatted} = $self->_format_timestamp( $user_row->{start_time} );
        }
        if ( $user_row->{end_time} ) {
            $user_row->{end_time_formatted} = $self->_format_timestamp( $user_row->{end_time} );
        }
        if ( $user_row->{duration} ) {
            $user_row->{duration_formatted} = $self->_format_duration( $user_row->{duration} );
        }

        # Get protected items for this user
        my $item_sth = $dbh->prepare(<<~'SQL');
            SELECT 
                id,
                item_name,
                item_type,
                start_time,
                end_time,
                status,
                error_message,
                (end_time - start_time) as duration
            FROM item_runs 
            WHERE user_run_id = ?
            ORDER BY start_time
            SQL

        $item_sth->execute( $user_row->{id} );

        my @item_runs;
        while ( my $item_row = $item_sth->fetchrow_hashref() ) {

            # Format timestamps
            if ( $item_row->{start_time} ) {
                $item_row->{start_time_formatted} = $self->_format_timestamp( $item_row->{start_time} );
            }
            if ( $item_row->{end_time} ) {
                $item_row->{end_time_formatted} = $self->_format_timestamp( $item_row->{end_time} );
            }
            if ( $item_row->{duration} ) {
                $item_row->{duration_formatted} = $self->_format_duration( $item_row->{duration} );
            }

            push @item_runs, $item_row;
        }

        $user_row->{items} = \@item_runs;
        push @user_runs, $user_row;
    }

    $run_info->{users} = \@user_runs;

    $dbh->disconnect();
    return $run_info;
}

=head2 get_run_statistics

Gets comprehensive statistics about backup runs.

    my $stats = $logs->get_run_statistics(%args);

Arguments:
- days: Number of days to include in statistics (default: 30)
- job_uuid: Optional job UUID to filter statistics

Returns hashref with various statistics.

=cut

sub get_run_statistics ( $self, %args ) {
    my $days            = $args{days} || 30;
    my $job_uuid        = $args{job_uuid};
    my $since_timestamp = time() - ( $days * 24 * 60 * 60 );

    my $dbh = Cpanel::DBI::SQLite->connect(
        {
            database   => $Whostmgr::CometBackup::Constants::RUN_LOG_DB,
            RaiseError => 1,
        }
    );

    my @where_clauses = ("start_time >= ?");
    my @bind_params   = ($since_timestamp);

    if ($job_uuid) {
        push @where_clauses, "job_uuid = ?";
        push @bind_params,   $job_uuid;
    }

    my $where_clause = "WHERE " . join( " AND ", @where_clauses );

    # Get overall statistics
    my $overall_sth = $dbh->prepare(<<~SQL);
        SELECT 
            COUNT(*) as total_runs,
            COUNT(CASE WHEN status = 'completed' THEN 1 END) as successful_runs,
            COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed_runs,
            COUNT(CASE WHEN status = 'running' THEN 1 END) as running_runs,
            AVG(CASE WHEN end_time IS NOT NULL THEN (end_time - start_time) END) as avg_duration,
            MIN(CASE WHEN end_time IS NOT NULL THEN (end_time - start_time) END) as min_duration,
            MAX(CASE WHEN end_time IS NOT NULL THEN (end_time - start_time) END) as max_duration
        FROM backup_runs 
        $where_clause
        SQL

    $overall_sth->execute(@bind_params);
    my $overall_stats = $overall_sth->fetchrow_hashref();

    # Calculate success rate
    if ( $overall_stats->{total_runs} > 0 ) {
        $overall_stats->{success_rate} = sprintf(
            "%.1f%%",
            ( $overall_stats->{successful_runs} / $overall_stats->{total_runs} ) * 100
        );
    }
    else {
        $overall_stats->{success_rate} = "N/A";
    }

    # Format durations
    for my $key (qw(avg_duration min_duration max_duration)) {
        if ( $overall_stats->{$key} ) {
            $overall_stats->{"${key}_formatted"} = $self->_format_duration( $overall_stats->{$key} );
        }
    }

    # Get daily statistics
    my $daily_sth = $dbh->prepare(<<~SQL);
        SELECT 
            DATE(start_time, 'unixepoch', 'localtime') as date,
            COUNT(*) as runs,
            COUNT(CASE WHEN status = 'completed' THEN 1 END) as successful,
            COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed,
            AVG(CASE WHEN end_time IS NOT NULL THEN (end_time - start_time) END) as avg_duration
        FROM backup_runs 
        $where_clause
        GROUP BY DATE(start_time, 'unixepoch', 'localtime')
        ORDER BY date DESC
        LIMIT 30
        SQL

    $daily_sth->execute(@bind_params);

    my @daily_stats;
    while ( my $row = $daily_sth->fetchrow_hashref() ) {
        if ( $row->{avg_duration} ) {
            $row->{avg_duration_formatted} = $self->_format_duration( $row->{avg_duration} );
        }
        push @daily_stats, $row;
    }

    # Get top slowest runs
    my $slow_sth = $dbh->prepare(<<~SQL);
        SELECT 
            job_uuid,
            job_name,
            start_time,
            (end_time - start_time) as duration
        FROM backup_runs 
        $where_clause
        AND end_time IS NOT NULL
        ORDER BY (end_time - start_time) DESC
        LIMIT 10
        SQL

    $slow_sth->execute(@bind_params);

    my @slowest_runs;
    while ( my $row = $slow_sth->fetchrow_hashref() ) {
        $row->{duration_formatted}   = $self->_format_duration( $row->{duration} );
        $row->{start_time_formatted} = $self->_format_timestamp( $row->{start_time} );
        push @slowest_runs, $row;
    }

    $dbh->disconnect();

    return {
        overall      => $overall_stats,
        daily        => \@daily_stats,
        slowest_runs => \@slowest_runs,
        period_days  => $days,
    };
}

=head2 get_active_runs

Gets currently running backup jobs.

    my @active = $logs->get_active_runs();

=cut

sub get_active_runs ($self) {
    return $self->get_recent_runs(
        status => 'running',
        limit  => 100,
    );
}

=head2 cancel_backup_run

Attempts to cancel a running backup by creating a cancel file.

    my $success = $logs->cancel_backup_run($run_id);

Returns 1 on success, 0 if the run is not active.

=cut

sub cancel_backup_run ( $self, $run_id ) {
    my $logger = $self->{logger};

    # Check if the run is still active
    my @active_runs = $self->get_active_runs();
    my $target_run  = ( grep { $_->{id} == $run_id } @active_runs )[0];

    unless ($target_run) {
        $logger->warn(
            "Cannot cancel backup run - not currently active",
            {
                run_id => $run_id,
            }
        );
        return 0;
    }

    # Create job-specific cancel file
    my $cancel_file = Whostmgr::CometBackup::Constants::get_cancel_file_path( $target_run->{job_uuid} );
    Cpanel::FileUtils::TouchFile::touchfile($cancel_file);

    $logger->info(
        "Backup run cancellation requested",
        {
            run_id      => $run_id,
            job_uuid    => $target_run->{job_uuid},
            job_name    => $target_run->{job_name},
            cancel_file => $cancel_file,
        }
    );

    return 1;
}

=head2 cleanup_old_logs

Removes old backup run logs to prevent database bloat.

    my $deleted_count = $logs->cleanup_old_logs(
        days => 90,  # Keep logs for 90 days (default: 30)
    );

=cut

sub cleanup_old_logs ( $self, %args ) {
    my $days             = $args{days} || 30;
    my $cutoff_timestamp = time() - ( $days * 24 * 60 * 60 );

    my $logger = $self->{logger};
    my $dbh    = Cpanel::DBI::SQLite->connect(
        {
            database   => $Whostmgr::CometBackup::Constants::RUN_LOG_DB,
            RaiseError => 1,
        }
    );

    # Get count of runs to be deleted
    my $count_sth = $dbh->prepare("SELECT COUNT(*) FROM backup_runs WHERE start_time < ?");
    $count_sth->execute($cutoff_timestamp);
    my ($delete_count) = $count_sth->fetchrow_array();

    if ( $delete_count == 0 ) {
        $logger->debug("No old backup logs to clean up");
        $dbh->disconnect();
        return 0;
    }

    # Begin transaction
    $dbh->begin_work();

    eval {
        # Delete item runs first (foreign key constraint)
        $dbh->do( <<~'SQL', undef, $cutoff_timestamp );
            DELETE FROM item_runs 
            WHERE user_run_id IN (
                SELECT ur.id FROM user_runs ur
                JOIN backup_runs br ON ur.run_id = br.id
                WHERE br.start_time < ?
            )
            SQL

        # Delete user runs
        $dbh->do( <<~'SQL', undef, $cutoff_timestamp );
            DELETE FROM user_runs 
            WHERE run_id IN (
                SELECT id FROM backup_runs WHERE start_time < ?
            )
            SQL

        # Delete backup runs
        $dbh->do( "DELETE FROM backup_runs WHERE start_time < ?", undef, $cutoff_timestamp );

        $dbh->commit();

        $logger->debug(
            "Cleaned up old backup logs",
            {
                deleted_runs => $delete_count,
                days_kept    => $days,
            }
        );
    };

    if ( my $error = $@ ) {
        $dbh->rollback();
        $logger->error(
            "Failed to cleanup old backup logs",
            {
                error => $error,
            }
        );
        die $error;
    }

    $dbh->disconnect();
    return $delete_count;
}

=head2 _ensure_database

Ensures the backup run logs database and tables exist.

This method creates the database schema if it doesn't exist, preventing
errors when querying the database before any backups have run.

=cut

sub _ensure_database ($self) {
    my $db_dir = "/var/cpanel/comet";
    Cpanel::Mkdir::ensure_directory_existence_and_mode( $db_dir, 0755 ) unless -d $db_dir;

    # Database will be created by the backup runner if it doesn't exist
    return unless -f $Whostmgr::CometBackup::Constants::RUN_LOG_DB;

    # Verify database integrity and ensure backup_run_items table exists
    my $dbh = Cpanel::DBI::SQLite->connect(
        {
            database   => $Whostmgr::CometBackup::Constants::RUN_LOG_DB,
            RaiseError => 1,
        }
    );

    # Create tables if they don't exist
    # This schema matches the one in bin/comet_backup_runner
    $dbh->do(<<~'SQL');
        CREATE TABLE IF NOT EXISTS backup_runs (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            run_uuid TEXT NOT NULL UNIQUE,
            job_uuid TEXT NOT NULL,
            start_time INTEGER NOT NULL,
            end_time INTEGER,
            status TEXT DEFAULT 'running',
            error_message TEXT
        )
        SQL

    $dbh->do(<<~'SQL');
        CREATE TABLE IF NOT EXISTS user_runs (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            run_id INTEGER NOT NULL,
            username TEXT NOT NULL,
            start_time INTEGER NOT NULL,
            end_time INTEGER,
            status TEXT DEFAULT 'running',
            error_message TEXT,
            FOREIGN KEY (run_id) REFERENCES backup_runs(id)
        )
        SQL

    $dbh->do(<<~'SQL');
        CREATE TABLE IF NOT EXISTS item_runs (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_run_id INTEGER NOT NULL,
            protected_item_id TEXT NOT NULL,
            item_type TEXT NOT NULL,
            comet_job_id TEXT,
            destination_id TEXT,
            snapshot_id TEXT,
            start_time INTEGER NOT NULL,
            end_time INTEGER,
            status TEXT DEFAULT 'running',
            error_message TEXT,
            job_properties TEXT,
            last_poll_time INTEGER,
            FOREIGN KEY (user_run_id) REFERENCES user_runs(id)
        )
        SQL

    # Create indexes
    $dbh->do("CREATE INDEX IF NOT EXISTS idx_backup_runs_run_uuid ON backup_runs(run_uuid)");
    $dbh->do("CREATE INDEX IF NOT EXISTS idx_backup_runs_job_uuid ON backup_runs(job_uuid)");
    $dbh->do("CREATE INDEX IF NOT EXISTS idx_backup_runs_start_time ON backup_runs(start_time)");
    $dbh->do("CREATE INDEX IF NOT EXISTS idx_user_runs_run_id ON user_runs(run_id)");
    $dbh->do("CREATE INDEX IF NOT EXISTS idx_item_runs_user_run_id ON item_runs(user_run_id)");

    # Verify database integrity
    $dbh->do("PRAGMA integrity_check");

    $dbh->disconnect();

    return;
}

=head2 _format_timestamp

Formats a Unix timestamp into a human-readable string.

=cut

sub _format_timestamp ( $self, $timestamp ) {
    return unless $timestamp;

    my $time = Time::Piece->new($timestamp);
    return $time->strftime('%Y-%m-%d %H:%M:%S');
}

=head2 _format_duration

Formats a duration in seconds into a human-readable string.

=cut

sub _format_duration ( $self, $seconds ) {
    return unless defined $seconds;

    my $hours = int( $seconds / 3600 );
    $seconds %= 3600;
    my $minutes = int( $seconds / 60 );
    $seconds %= 60;

    if ( $hours > 0 ) {
        return sprintf( "%dh %02dm %02ds", $hours, $minutes, $seconds );
    }
    elsif ( $minutes > 0 ) {
        return sprintf( "%dm %02ds", $minutes, $seconds );
    }
    else {
        return sprintf( "%.1fs", $seconds );
    }
}

=head2 log_protected_item_start

Logs the start of a protected item backup.

    my $item_id = $logs->log_protected_item_start(
        run_uuid            => $run_uuid,
        job_uuid            => $job_uuid,
        system_user         => 'tau',
        protected_item_type => 'databases_mysql',
        protected_item_uid  => 'comet-item-12345',
    );

Returns the database ID for the backup item record.

=cut

sub log_protected_item_start ( $self, %args ) {
    my $run_uuid            = $args{run_uuid}            or die "run_uuid parameter required";
    my $job_uuid            = $args{job_uuid}            or die "job_uuid parameter required";
    my $system_user         = $args{system_user}         or die "system_user parameter required";
    my $protected_item_type = $args{protected_item_type} or die "protected_item_type parameter required";
    my $protected_item_uid  = $args{protected_item_uid}  or die "protected_item_uid parameter required";

    my $dbh = $self->_get_database_handle();

    my $current_time = time();
    my $sth          = $dbh->prepare(
        q{
        INSERT INTO backup_run_items 
        (run_uuid, job_uuid, system_user, protected_item_type, protected_item_uid, start_time, status)
        VALUES (?, ?, ?, ?, ?, ?, 'running')
    }
    );

    $sth->execute( $run_uuid, $job_uuid, $system_user, $protected_item_type, $protected_item_uid, $current_time );

    return $dbh->last_insert_id( undef, undef, 'backup_run_items', 'id' );
}

=head2 log_protected_item_complete

Marks a protected item backup as completed.

    $logs->log_protected_item_complete(
        item_id          => $item_id,
        comet_snapshot_id => 'snapshot-xyz-789',
        size_bytes       => 1024000,
    );

=cut

sub log_protected_item_complete ( $self, %args ) {
    my $item_id           = $args{item_id} or die "item_id parameter required";
    my $comet_snapshot_id = $args{comet_snapshot_id};                             # Optional
    my $size_bytes        = $args{size_bytes};                                    # Optional

    my $dbh = $self->_get_database_handle();

    my $current_time = time();
    my $sth          = $dbh->prepare(
        q{
        UPDATE backup_run_items 
        SET end_time = ?, status = 'completed', comet_snapshot_id = ?, size_bytes = ?
        WHERE id = ?
    }
    );

    $sth->execute( $current_time, $comet_snapshot_id, $size_bytes, $item_id );

    return $sth->rows;
}

=head2 log_protected_item_failed

Marks a protected item backup as failed.

    $logs->log_protected_item_failed(
        item_id       => $item_id,
        error_message => 'Database connection failed',
    );

=cut

sub log_protected_item_failed ( $self, %args ) {
    my $item_id       = $args{item_id} or die "item_id parameter required";
    my $error_message = $args{error_message} || 'Unknown error';

    my $dbh = $self->_get_database_handle();

    my $current_time = time();
    my $sth          = $dbh->prepare(
        q{
        UPDATE backup_run_items 
        SET end_time = ?, status = 'failed', error_message = ?
        WHERE id = ?
    }
    );

    $sth->execute( $current_time, $error_message, $item_id );

    return $sth->rows;
}

=head2 get_restorable_items

Gets a list of successfully backed up items available for restoration.

    my @items = $logs->get_restorable_items(
        system_user   => 'tau',           # Optional: filter by user
        item_type     => 'databases_mysql', # Optional: filter by type  
        start_date    => $timestamp,      # Optional: items after this time
        end_date      => $timestamp,      # Optional: items before this time
        limit         => 50,              # Optional: limit results
    );

Returns array of item hashrefs with restoration details.

=cut

sub get_restorable_items ( $self, %args ) {
    my $system_user = $args{system_user};
    my $item_type   = $args{item_type};
    my $start_date  = $args{start_date};
    my $end_date    = $args{end_date};
    my $limit       = $args{limit} || 100;

    my $dbh = $self->_get_database_handle();

    # Query the actual tables populated by the backup runner
    # item_runs contains the backup completion data
    # user_runs links item_runs to system users
    # backup_runs contains run_uuid and job_uuid
    # protected_items (from ProtectedItemMap) contains comet_snapshot_id

    my @where_clauses = ("ir.status = 'completed'");
    my @bind_params;

    if ($system_user) {
        push @where_clauses, "ur.username = ?";
        push @bind_params,   $system_user;
    }

    if ($item_type) {
        push @where_clauses, "ir.item_type = ?";
        push @bind_params,   $item_type;
    }

    if ($start_date) {
        push @where_clauses, "ir.start_time >= ?";
        push @bind_params,   $start_date;
    }

    if ($end_date) {
        push @where_clauses, "ir.end_time <= ?";
        push @bind_params,   $end_date;
    }

    my $where_clause = "WHERE " . join( " AND ", @where_clauses );

    my $sql = qq{
        SELECT 
            ir.id, br.run_uuid, br.job_uuid, ur.username as system_user,
            ir.item_type as protected_item_type, ir.protected_item_id as protected_item_uid, 
            ir.snapshot_id as comet_snapshot_id,
            ir.destination_id as comet_destination_id,
            ir.comet_job_id,
            ir.job_properties,
            ir.start_time, ir.end_time,
            0 as restored_count, NULL as last_restored_time,
            br.status as run_status
        FROM item_runs ir
        JOIN user_runs ur ON ir.user_run_id = ur.id
        JOIN backup_runs br ON ur.run_id = br.id
        $where_clause
        ORDER BY ir.start_time DESC 
        LIMIT ?
    };

    push @bind_params, $limit;

    my $sth = $dbh->prepare($sql);
    $sth->execute(@bind_params);

    my @items;
    while ( my $row = $sth->fetchrow_hashref() ) {

        # Parse job_properties JSON to extract TotalSize
        my $size_bytes = 0;
        if ( $row->{job_properties} ) {
            eval {
                my $job_props = Cpanel::JSON::Load( $row->{job_properties} );

                # Extract TotalSize from job_properties
                if ( defined $job_props->{TotalSize} && $job_props->{TotalSize} > 0 ) {
                    $size_bytes = $job_props->{TotalSize};
                }
            };
            if ($@) {
                $self->{logger}->warn( "Failed to parse job_properties JSON", { error => $@, row_id => $row->{id} } );
            }
        }
        $row->{size_bytes} = $size_bytes;

        # Format timestamps
        if ( $row->{start_time} ) {
            $row->{start_time_formatted} = $self->_format_timestamp( $row->{start_time} );
        }
        if ( $row->{end_time} ) {
            $row->{end_time_formatted} = $self->_format_timestamp( $row->{end_time} );
        }
        if ( $row->{last_restored_time} ) {
            $row->{last_restored_time_formatted} = $self->_format_timestamp( $row->{last_restored_time} );
        }

        # Format size
        if ( $row->{size_bytes} ) {
            $row->{size_formatted} = $self->_format_bytes( $row->{size_bytes} );
        }

        push @items, $row;
    }

    return @items;
}

# Private methods

sub _get_database_handle ($self) {
    return Cpanel::DBI::SQLite->connect(
        {
            database   => $Whostmgr::CometBackup::Constants::RUN_LOG_DB,
            RaiseError => 1,
        }
    );
}

sub _format_bytes ( $self, $bytes ) {
    return unless defined $bytes;

    my @units      = ( 'B', 'KB', 'MB', 'GB', 'TB' );
    my $unit_index = 0;

    while ( $bytes >= 1024 && $unit_index < $#units ) {
        $bytes /= 1024;
        $unit_index++;
    }

    return sprintf( "%.1f%s", $bytes, $units[$unit_index] );
}

1;
