package OmniPITR::Program;
use strict;
use warnings;
use English qw( -no_match_vars );

use OmniPITR::Log;
use OmniPITR::Pidfile;
use OmniPITR::Tools qw( run_command );
use File::Basename;
use File::Path qw( mkpath rmtree );
use File::Spec;
use Carp;

our $VERSION = '0.2.1';

=head1 new()

Object contstructor.

Since all OmniPITR programs are based on object, and they start with
doing the same things (namely reading and validating command line
arguments) - this is wrapped in here, to avoid code duplication.

Constructor also handles pid file creation, in case it was requested.

=cut

sub new {
    my $class = shift;
    my $self = bless {}, $class;
    $self->check_debug();
    $self->read_args();
    $self->validate_args();
    $self->{ 'pid-file' } = OmniPITR::Pidfile->new( 'pidfile' => $self->{ 'pid-file' } ) if $self->{ 'pid-file' };

    return $self;
}

=head1 check_debug()

Internal method providing --debug option handling to every omnipitr program.

If *first* argument to omnipitr program it will print to stderr all arguments, and environment variables.

=cut

sub check_debug {
    my $self = shift;
    return if 0 == scalar @ARGV;
    return unless '--debug' eq $ARGV[ 0 ];

    warn "DEBUG INFORMATION:\n";
    for my $key ( sort keys %ENV ) {
        warn sprintf( "ENV: '%s' => '%s'\n", $key, $ENV{ $key } );
    }
    warn "Command line arguments: [" . join( "] , [", @ARGV ) . "]\n";
    shift @ARGV;

    return;
}

=head1 run()

Just a stub method, that has to be overriden in subclasses.

=cut

sub run {
    my $self = shift;
    croak( "run() method in OmniPITR::Program was not overridden!" );
}

=head1 verbose()

Shortcut to make code a bit nicer.

Returns values of (command line given) verbose switch.

=cut

sub verbose { return shift->{ 'verbose' }; }

=head1 log()

Shortcut to make code a bit nicer.

Returns logger object.

=cut

sub log { return shift->{ 'log' }; }

=head1 prepare_temp_directory()

Helper function, which builds path for temp directory, and creates it.

Path is generated by using given temp-dir and 'omnipitr-backup-master' named.

For example, for temp-dir '/tmp' used temp directory would be /tmp/omnipitr-backup-master.

If any arguments are passed - they are treated as subdirectories. For example, in above example, if ("xxx", "yyy") was passed, generated directory would be /tmp/omnipitr-backup-master/xxx/yyy.
=cut

sub prepare_temp_directory {
    my $self = shift;
    return if $self->{ 'temp-dir-prepared' };
    my @sub_elements = @_;
    my $full_temp_dir = File::Spec->catfile( $self->{ 'temp-dir' }, basename( $PROGRAM_NAME ), @sub_elements );
    mkpath( $full_temp_dir );
    $self->{ 'temp-dir' }          = $full_temp_dir;
    $self->{ 'temp-dir-prepared' } = 1;
    return;
}

=head1 temp_file()

Returns full path to temp file. Name of the file is passed as argument, temp directory is created (if needed) and full path is returned.

=cut

sub temp_file {
    my $self     = shift;
    my $filename = shift;
    $self->prepare_temp_directory;
    return File::Spec->catfile( $self->{ 'temp-dir' }, $filename );
}

=head1 DESTROY()

Destructor for object - removes temp directory on program exit.

=cut

sub DESTROY {
    my $self = shift;
    if ( $self->{ 'temp-dir-prepared' } ) {
        rmtree( [ $self->{ 'temp-dir' } ], 0 );
        delete $self->{ 'temp-dir-prepared' };
    }
    return;
}

=head1 get_list_of_all_necessary_compressions()

Scans list of destinations, and gathers list of all compressions that have to be made.

This is to be able to compress file only once even when having multiple destinations that require compressed format.

This function is used by all programs that need to compress "stuff" - L<omnipitr-archive>, L<omnipitr-backup-master> and L<omnipitr-backup-slave>.

=cut

sub get_list_of_all_necessary_compressions {
    my $self = shift;

    croak 'get_list_of_all_necessary_compressions() method called, but there are no destinations?!' unless $self->{ 'destination' };

    my %compression = ();

    for my $dst_type ( qw( local remote ) ) {
        next unless my $dsts = $self->{ 'destination' }->{ $dst_type };
        for my $destination ( @{ $dsts } ) {
            $compression{ $destination->{ 'compression' } } = 1;
        }
    }

    $self->{ 'compressions' } = [ keys %compression ];

    return;
}

=head1 get_control_data()

Calls pg_controldata, and parses its output.

Verifies that output contains 2 critical pieces of information:

=over

=item * Latest checkpoint's REDO location

=item * Latest checkpoint's TimeLineID

=back

=cut

sub get_control_data {
    my $self         = shift;
    my $control_data = {};

    my $handle;
    if (   ( !defined $self->{ 'error-pgcontroldata' } )
        || ( 'break' eq $self->{ 'error-pgcontroldata' } ) )
    {
        $handle = sub {
            $self->log->fatal( @_ );
        };
    }
    elsif ( 'ignore' eq $self->{ 'error-pgcontroldata' } ) {
        $handle = sub {
            $self->log->error( @_ );
        };
    }
    else {
        $handle = sub {
            $self->log->error( @_ );
            sleep 600 while 1;
        };
    }

    $self->prepare_temp_directory();

    my $response = run_command( $self->{ 'temp-dir' }, $self->{ 'pgcontroldata-path' }, $self->{ 'data-dir' } );
    if ( $response->{ 'error_code' } ) {
        $handle->( 'Error while getting pg_controldata for %s: %s', $self->{ 'data-dir' }, $response );
        return undef;
    }

    my @lines = split( /\s*\n/, $response->{ 'stdout' } );
    for my $line ( @lines ) {
        unless ( $line =~ m{\A([^:]+):\s*(.*)\z} ) {
            $handle->( 'Pg_controldata for %s contained unparseable line: [%s]. Full response: %s', $self->{ 'data-dir' }, $line, $response );
            return undef;
        }
        $control_data->{ $1 } = $2;
    }

    unless ( $control_data->{ "Latest checkpoint's REDO location" } ) {
        $handle->( 'Pg_controldata for %s did not contain latest checkpoint redo location. Full response: %s', $self->{ 'data-dir' }, $response );
        return undef;
    }
    unless ( $control_data->{ "Latest checkpoint's TimeLineID" } ) {
        $handle->( 'Pg_controldata for %s did not contain latest checkpoint timeline ID. Full response: %s', $self->{ 'data-dir' }, $response );
        return undef;
    }

    return $control_data;
}

1;

