package Whostmgr::CometBackup::Logger;

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

=pod

=head1 NAME

Whostmgr::CometBackup::Logger - Logging module for Comet backup plugin

=head1 SYNOPSIS

    use Whostmgr::CometBackup::Logger ();
    
    # Create logger instance
    my $logger = Whostmgr::CometBackup::Logger->new();
    
    # Log different levels
    $logger->error("Failed to connect to Comet server");
    $logger->warn("Backup took longer than expected");
    $logger->info("Backup completed successfully");
    $logger->debug("API response", { response => $data });

=head1 DESCRIPTION

This module provides comprehensive logging functionality for the Comet backup plugin.
It logs to /var/log/comet_backup.log with automatic log rotation and structured output.

=cut

use cPstrict;
use Encode     ();
use Fcntl      qw( :flock SEEK_END );
use File::Path qw( make_path );
use POSIX      qw( strftime );

our $LOG_FILE = '/var/log/comet_backup.log';

# Log levels (higher numbers = more verbose)
our %LOG_LEVELS = (
    'error' => 1,
    'warn'  => 2,
    'info'  => 3,
    'debug' => 4,
);

=head1 METHODS

=head2 new

Creates a new Logger instance.

    my $logger = Whostmgr::CometBackup::Logger->new(
        log_level => 'info',    # Optional: error, warn, info, debug (default: info)
        max_size  => 10485760,  # Optional: max log file size in bytes (default: 10MB)
    );

=cut

sub new ( $class, %args ) {
    my $self = {
        log_level => $args{log_level} || 'info',
        max_size  => $args{max_size}  || 10_485_760,    # 10MB default
    };

    bless $self, $class;

    if ( -f '/var/cpanel/comet/verbose_logging_enabled' ) {
        $self->{log_level} = 'debug';
    }

    # Ensure log directory exists
    $self->_ensure_log_directory();

    return $self;
}

=head2 error

Logs an error message.

    $logger->error("Connection failed", { server => $server_url });

=cut

sub error ( $self, $message, $context = {} ) {
    return $self->_log( 'error', $message, $context );
}

=head2 warn

Logs a warning message.

    $logger->warn("Backup took 30 minutes", { duration => $seconds });

=cut

sub warn ( $self, $message, $context = {} ) {
    return $self->_log( 'warn', $message, $context );
}

=head2 info

Logs an informational message.

    $logger->info("Backup completed successfully");

=cut

sub info ( $self, $message, $context = {} ) {
    return $self->_log( 'info', $message, $context );
}

=head2 debug

Logs a debug message.

    $logger->debug("API response received", { response_code => 200 });

=cut

sub debug ( $self, $message, $context = {} ) {
    return $self->_log( 'debug', $message, $context );
}

=head2 set_log_level

Changes the current log level.

    $logger->set_log_level('debug');

=cut

sub set_log_level ( $self, $level ) {
    if ( !exists $LOG_LEVELS{$level} ) {
        $self->error("Invalid log level: $level");
        return;
    }

    $self->{log_level} = $level;
    return 1;
}

=head2 get_log_level

Returns the current log level.

    my $level = $logger->get_log_level();

=cut

sub get_log_level ($self) {
    return $self->{log_level};
}

# Private methods

sub _log ( $self, $level, $message, $context = {} ) {

    # Check if this log level should be output
    return 1 if $LOG_LEVELS{$level} > $LOG_LEVELS{ $self->{log_level} };

    my $timestamp = strftime( '%Y-%m-%d %H:%M:%S', localtime );
    my $pid       = $$;

    # Format the log entry as plain text
    my $log_line = "[$timestamp] [$$] " . uc($level) . ": $message";

    # Add context if provided
    if ( ref $context eq 'HASH' && %{$context} ) {
        my @context_parts;
        for my $key ( sort keys %{$context} ) {

            # Skip keys with undefined length (shouldn't happen but be safe)
            next unless defined $key && length $key;

            my $value = $context->{$key};

            # Handle different value types
            if ( !defined $value ) {
                $value = 'undef';
            }
            elsif ( ref $value eq 'ARRAY' ) {
                $value = '[' . join( ', ', map { defined $_ ? $_ : 'undef' } @{$value} ) . ']';
            }
            elsif ( ref $value eq 'HASH' ) {
                my @pairs = map { "$_=" . ( defined $value->{$_} ? $value->{$_} : 'undef' ) } sort keys %{$value};
                $value = '{' . join( ', ', @pairs ) . '}';
            }
            elsif ( ref $value ) {

                # Other reference types
                $value = ref($value) . '(...)';
            }
            elsif ( $value eq '0' || $value eq '' ) {

                # Preserve false/empty string values
                $value = "'$value'";
            }

            # else: scalar value, use as-is

            push @context_parts, "$key=$value";
        }
        if (@context_parts) {
            $log_line .= " [" . join( ', ', @context_parts ) . "]";
        }
    }

    return $self->_write_to_log($log_line);
}

sub _write_to_log ( $self, $log_line ) {

    # Check if log rotation is needed
    $self->_rotate_log_if_needed();

    # Open log file for appending
    my $log_fh;
    if ( !open $log_fh, '>>', $LOG_FILE ) {
        CORE::warn "Failed to open log file $LOG_FILE: $!";
        return;
    }

    # Get exclusive lock
    if ( !flock( $log_fh, LOCK_EX ) ) {
        CORE::warn "Failed to lock log file $LOG_FILE: $!";
        close $log_fh;
        return;
    }

    # Set UTF-8 encoding on filehandle to prevent wide character warnings
    binmode( $log_fh, ':encoding(UTF-8)' );

    # Write the log entry
    print {$log_fh} $log_line . "\n";

    # Flush and close
    close $log_fh;

    return 1;
}

sub _ensure_log_directory ($self) {
    my ($log_dir) = $LOG_FILE =~ m{^(.+)/[^/]+$};

    if ( $log_dir && !-d $log_dir ) {
        eval { make_path( $log_dir, { mode => 0755 } ); };
        if ($@) {
            CORE::warn "Failed to create log directory $log_dir: $@";
        }
    }

    return 1;
}

sub _rotate_log_if_needed ($self) {
    return 1 if !-e $LOG_FILE;

    my $size = -s $LOG_FILE;
    return 1 if !$size || $size < $self->{max_size};

    # Rotate the log file
    my $backup_file = $LOG_FILE . '.old';

    # Remove old backup if it exists
    if ( -e $backup_file ) {
        unlink $backup_file or CORE::warn "Failed to remove old backup log: $!";
    }

    # Move current log to backup
    if ( !rename $LOG_FILE, $backup_file ) {
        CORE::warn "Failed to rotate log file: $!";
        return;
    }

    return 1;
}

=head1 USAGE EXAMPLES

=head2 Basic Usage

    use Whostmgr::CometBackup::Logger ();
    
    my $logger = Whostmgr::CometBackup::Logger->new();
    $logger->info("Comet backup plugin initialized");

=head2 Error Logging with Context

    my $logger = Whostmgr::CometBackup::Logger->new();
    
    eval {
        # Some risky operation
        connect_to_server($server_url);
    };
    if ($@) {
        $logger->error("Failed to connect to server", {
            server_url => $server_url,
            error      => $@,
            attempt    => $retry_count,
        });
    }

=head2 Debug Mode

    my $logger = Whostmgr::CometBackup::Logger->new(log_level => 'debug');
    
    $logger->debug("API request", {
        endpoint => '/api/v1/backup',
        params   => \%request_params,
    });

=head1 LOG FORMAT

Log entries are written in plain text format for easy reading:

    [2025-09-25 14:30:15] [12345] INFO: Backup completed successfully [duration=300, size_bytes=1048576]

The format includes:
- Timestamp in YYYY-MM-DD HH:MM:SS format
- Process ID in square brackets
- Log level (ERROR, WARN, INFO, DEBUG)
- Message text
- Optional context data in square brackets as key=value pairs

=head1 LOG ROTATION

The log file is automatically rotated when it exceeds the configured size (default: 10MB).
The old log is saved as comet_backup.log.old.

=cut

1;
