package RDAS::Fetcher::Mechanize::Authenticated;

use Data::Dumper;

use lib '/app/tools/common/lib';
use Common::Assert;
use Common::RSApp;

use lib '/app/tools/rdas/lib';
use base 'RDAS::Fetcher::Mechanize';

# Firefox must be configured to save downloads here (it's not magic)
# Other configuration considerations include setting it to download
# .csv and other target files automatically.
# And accepting security issues like bad certs.
# In short, there a lot of Firefoxy things that can stall mechanization
# Thankfully, they're relatively easy to fix (usually)
use constant kDownloadDir     => '/app/data/';
use constant kDownloadTimeout => 300;            # in seconds

sub fetch {
    my $self     = shift;
    my %args     = @_;
    my $form     = $args{form};
    my $click    = $args{click};
    my $download = $args{download};

    assert( $self->url, "URL Required" );

    $self->{_content}   = undef;
    $self->{_success}   = undef;
    $self->{_errorCode} = undef;

    my $mech = $self->_getMechObject();
    $mech->agent( $self->userAgent() );

    Log->info( "Fetching content from: " . $self->url );

    # 404 throws an exception, but all other non 200s don't.  weird
    eval { $mech->get( $self->url ) };
    if ($@) {
        Log->error("Failed get: $@");
        die $@;
    }

    if ( !$@ && $form ) {
        Log->info("Submitting form.");

        # the click parameter specifies which button to click and
        # lets javascript work its "magic" (?)
        if ($click) {
            assert( $mech->isa('WWW::Mechanize::Firefox'), 'Mechanize must be firefoxed for clicking' );
            my $clicked = 0;

            # fill in the form fields
            foreach my $field ( keys(%$form) ) {
                $mech->field( $field => $form->{$field} );
            }

            if ($download) {
                $self->_preDownload($download);
            }

            # we'll actually execute any javascript associated with the clickable thingy
            # right now, we don't have any need for onwhatever methods beyond that
            foreach my $clicker ( $mech->clickables() ) {
                if ( $clicker->{textContent} eq $click ) {
                    my $synchronize = $download ? 0 : 1;
                    $mech->click( { dom => $clicker, synchronize => $synchronize } );
                    $clicked = 1;
                    last;
                }
            }

            return if ( !$clicked );
        } else {

            # this will just submit the good old standard Mechanize way (i.e. POST)
            my $request = $mech->submit_form( fields => $form );
        }
    } elsif ($form) {
        Log->error("Fetch Fail: $@");
    }

    if ($download) {
        $self->{_content} = $self->_waitForDownload($download);
        $self->{_filename} = $download if ( $self->{_content} );
        return $self->{_content};
    } else {
        return $self->_response($mech);
    }
}

sub links {
    my $self  = shift;
    my $mech  = $self->_getMechObject();
    my $links = $mech->links();

    return $links;
}

sub form {
    my $self = shift;
    my $formIndex = shift || 1;

    my $mech = $self->_getMechObject();
    my $form = $mech->form_number($formIndex);

    return $form;
}

sub submit {
    my $self = shift;
    my %args = @_;

    my $form       = $args{form}        || die "form required";
    my $formNumber = $args{form_number} || 1;
    my $submitButton = $args{submit};

    Log->debug( "Submit form #$formNumber with args \n" . Dumper $form);
    my $mech = $self->_getMechObject();

    $mech->submit_form( fields => $form, form_number => $formNumber, button => $submitButton );

    $self->{_content} = $mech->content();
    $self->{_success} = 1;
}

sub click {
    my $self  = shift;
    my $click = shift;

    my $mech = $self->_getMechObject();
    assert( $mech->isa('WWW::Mechanize::Firefox'), 'Mechanize must be firefoxed for clicking' );

    foreach my $clicker ( $mech->clickables() ) {
        if ( $clicker->{textContent} eq $click ) {
            $mech->click($clicker);
            return $self->_response($mech);
        }
    }

    return;
}

sub _preDownload {
    my $self     = shift;
    my $filename = shift;

    $filename = kDownloadDir . $filename;

    # since Firefox is "helping" us with downloads, let's first get rid of any leftover download
    if ( -e $filename ) {
        unlink($filename) || die 'unable to delete previous download file';
    }

    # any partial too
    if ( -e $filename . '.part' ) {
        unlink( $filename . '.part' ) || die 'unable to delete partial download file';
    }
}

sub _waitForDownload {
    my $self     = shift;
    my $filename = shift;

    $filename = kDownloadDir . $filename;

    # Firefox downloads like this (at least at this point in time):
    #   1) create empty destination file (e.g. export.csv)
    #   2) download data to new partial file (e.g. export.csv.part)
    #   3) replace destination file when finished
    #
    # so, we will wait until the destination file has a size (meaning it's finished)
    # *or* there haven't been any updates to the .part file for five minutes
    # (which also appears to be how long Firefox waits before giving up, and either way,
    # as long as we're willing to wait)

    # give it up to the download timeout period to even get started
    my $startTime = time;
    while ( !( -e $filename ) ) {
        if ( time > ( $startTime + kDownloadTimeout ) ) {
            Log->error("Download failed to initiate");
            return;
        }
        sleep 1;
    }

    # then wait until it's got data or the part file hasn't been updated
    my @dest;
    while ( ( @dest = stat($filename) ) && $dest[7] == 0 ) {
        my @part = stat( $filename . '.part' );
        if ( scalar(@part) ) {
            if ( $part[9] < time - kDownloadTimeout ) {
                Log->error("Download stalled");
                return;
            }
        }
        sleep 1;
    }

    open( my $download, '<', $filename );
    my $content = do { local $/; <$download> };
    close($download);
    return $content;

    return;
}

sub _response {
    my $self = shift;
    my $mech = shift;

    $self->{_filename} = $mech->response->filename();

    if ( $mech->status == 200 ) {
        $self->{_content} = $mech->content();
        $self->{_success} = 1;

        #Log->debug( Dumper $mech->res->request );
        return $self->{_content};
    } else {
        $self->{_redirectURL} = $mech->res->header('location');
        $self->{_errorCode}   = $mech->status();
        Log->warn( "Fetch status not 200: " . $self->errorCode );
        return;
    }
}

#  With authenticated sessions we want a persistant Mech object so it retains
#  cookies.
sub _getMechObject {
    my $self = shift;
    my %args = @_;

    my $singleton = Common::RSApp::Instance();
    my $mech      = $singleton->{wwwMechanizeObj};

    unless ($mech) {
        Log->info( "Creating " . ref($self) . " object" );
        $singleton->{wwwMechanizeObj} = $self->SUPER::_getMechObject( cookie_jar => {}, %args );
    }

    return $singleton->{wwwMechanizeObj};
}

sub ClearSingleton {
    my $singleton = Common::RSApp::Instance();
    Log->info(" **** Clearing mechanize cookies ****");
    $singleton->{wwwMechanizeObj}           = undef;
    $singleton->{wwwMechanizeAuthenticated} = undef;
    Log->notice("Logged out");
}

sub filename { shift->{_filename} }

sub post {
    my $self = shift;
    my %args = @_;

    my $form = $args{form} || die "form required";
    my $url  = $args{url}  || $self->url;

    Log->debug( "Post form data to url: $url\n with args \n" . Dumper $form);
    my $mech = $self->_getMechObject();

    my $ua = $mech->post( $url, Content => $form );

    if ( $ua->code == 200 ) {
        $self->{_content} = $mech->content();
        $self->{_success} = 1;
        return $ua;
    } else {
        $self->{_errorCode} = $mech->status();
        $self->{_success}   = undef;

        Log->warn( "Fetch status not 200: " . $self->errorCode );
        die Dumper $mech;
        return;
    }
}

sub dumpResponse {
    my $self = shift;
    Log->debug( Dumper $self->_getMechObject()->response()->request()->as_string() );
}

sub updateHTML {
    my $self = shift;
    my $html = shift || die;

    my $mech = $self->_getMechObject();
    $mech->update_html($html);
}

sub LoggedIn {
    my $singleton = Common::RSApp::Instance();

    if (@_) {
        $singleton->{wwwMechanizeAuthenticated} = shift;
        Log->notice( "Logged in: " . $singleton->{wwwMechanizeAuthenticated} );
    }

    return $singleton->{wwwMechanizeAuthenticated};
}

1;
