package CM::POETools;

use strict;
use warnings;
use POE qw(
   Component::Client::HTTP
   Component::Client::TCP
   Filter::HTTP::Parser
   Filter::SSL);
use HTTP::Response;

use JSON;
use Socket;

BEGIN {
   use Exporter;
   our @ISA = qw/Exporter/;
   our @EXPORT = qw//;
   our @EXPORT_OK = qw/requestData getSSL poe_sleep/;
}

POE::Component::Client::HTTP->spawn(
   Alias     => 'ua',                  # defaults to 'weeble'
   Timeout   => 20,                    # defaults to 180 seconds
   MaxSize   => 16384,                 # defaults to entire response
   FollowRedirects => 2                # defaults to 0 (off)
);

sub requestData {
   my $session     = shift;
   my $source      = shift;
   my $poeevent    = shift;
   my $passthrough = shift;
   my $curConfig   = shift || {};
   my $configreq   = shift;
   if ($source =~ m,^https?://,) {
      POE::Session->create(
      inline_states => {
         _start => sub {
            my ($kernel, $heap, $wheel_id, $poeevent) = @_[KERNEL, HEAP];
            $heap->{parentsession} = $_[ARG0];
            $heap->{passthrough}   = $_[ARG1];
            $heap->{poeevent}      = $_[ARG2];
            $heap->{source}        = $_[ARG3];
            $heap->{curConfig}     = $_[ARG4];
            my $request = HTTP::Request->new(($heap->{curConfig}->{method} || "GET") => $heap->{source} => $heap->{curConfig}->{header});
            $request->content(join("&", map { $_."=".$configreq->{$_} } keys %$configreq));
            $kernel->post('ua', 'request', 'on_httpresponse', $request, $heap->{passthrough});
         },
         on_httpresponse => sub {
            my ($kernel, $session, $heap, $request_packet, $response_packet) = @_[ KERNEL, SESSION, HEAP, ARG0, ARG1];
            # HTTP::Request
            my $request_object  = $request_packet->[0];
            # HTTP::Response
            my $response_object = $response_packet->[0];
            my $passthrough     = $request_packet->[1];
            $kernel->post(
               $heap->{parentsession} =>
               $heap->{poeevent} => ((
               $response_object->code == 200) ?
               $response_object->content() : undef) => $passthrough => $response_object);
         },
      }, args => [ $session, $passthrough, $poeevent, $source, $curConfig]);
   } elsif($source =~ m,^adbgui://([^\:]+)\:(\d+)\/?(.*),i) {
      my $host = $1;
      my $port = $2;
      my $cmd  = $3;
      POE::Component::Client::TCP->new(
         RemoteAddress => $host,
         RemotePort    => $port,
         Filter        => POE::Filter::Line->new(),
         Started       => sub {
            my ($kernel, $heap) = @_[KERNEL, HEAP];
            $heap->{parentsession} = $_[ARG0];
            $heap->{passthrough}   = $_[ARG1];
            $heap->{poeevent}      = $_[ARG2];
            $heap->{curConfig}     = $_[ARG3];
            $heap->{cmd}           = $_[ARG4];
            $heap->{configreq}     = $_[ARG5];
         },
         Connected => sub {
            my ($kernel, $heap) = @_[KERNEL, HEAP];
            if ($heap->{curConfig}->{auth}) {
               my $out = "AUTH ".join(" ", @{$heap->{curConfig}->{auth}});
               $_[HEAP]->{server}->put($out);
               $kernel->delay(input_timeout => $heap->{curConfig}->{adbguitimeout} || 20);
            } else {
               $kernel->yield("docmd");
            }
         },
         ConnectTimeout => $curConfig->{adbguiconnecttimeout} || $curConfig->{adbguitimeout} || 10,
         ConnectError => sub {
            my ($kernel, $heap) = @_[KERNEL, HEAP];
            $kernel->yield("term_with_error"); # => ($heap->{curConfig}->{adbguifailsleep} || 1) => "Could not connect to ADBGUI");
         },
         ServerInput => sub {
            my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
            if ($heap->{curConfig}->{auth} && !$heap->{authdone}++) {
               if ($input =~ m,^AUTH OK,) {
                  $kernel->yield("docmd");
               } else {
                  $kernel->post(
                     $heap->{parentsession} =>
                     $heap->{poeevent} => undef => $heap->{passthrough} => $input);
                  $kernel->delay(input_timeout => undef);
                  $kernel->yield("shutdown");
               }
            } else {
               $kernel->post($heap->{parentsession} => $heap->{poeevent} => $input => $heap->{passthrough});
               $kernel->delay(input_timeout => undef);
               $kernel->yield("shutdown");
            }
         },
         InlineStates => {
            input_timeout => sub {
               my ($kernel, $heap) = @_[KERNEL, HEAP];
               $kernel->delay(term_with_error => ($heap->{curConfig}->{adbguifailsleep} || 1) => "Timeout talking to ADBGUI");
            },
            term_with_error => sub {
               my ($kernel, $heap, $error, $security) = @_[KERNEL, HEAP, ARG0, ARG1];
               $kernel->post(
                  $heap->{parentsession} =>
                  $heap->{poeevent} => undef => $heap->{passthrough} => $error => $security);
               $kernel->delay(input_timeout => undef);
               $kernel->yield("shutdown");
            },
            docmd => sub {
               my ($kernel, $heap) = @_[KERNEL, HEAP];
               my $out = "JSON ".$heap->{cmd}.($heap->{configreq} ? " ".JSON->new->allow_nonref->encode($heap->{configreq}) : "");
               #print "OUT:".$out."\n";
               $_[HEAP]->{server}->put($out);
               $kernel->delay(input_timeout => $heap->{curConfig}->{adbguitimeout} || 20);
            },
         },
         Args => [ $session, $passthrough, $poeevent, $curConfig, $cmd, $configreq ],
      );
   } else {
      my $lines = undef;
      if (open(CONFIG, "<", $source)) {
         $lines = join("", <CONFIG>);
         close(CONFIG);
      }
      $poe_kernel->yield($poeevent => $lines => $passthrough => "Failed to open config filename='".$source."'" => $!);
   }
}

sub getSSL {
   my $config = shift;
   my $result = shift || \$config->{result} || \{};
   POE::Session->create(
      inline_states => {
         _start => sub {
            my ($kernel, $heap, $sender, $session, $config) = @_[KERNEL, HEAP, SENDER, SESSION, ARG0];
            $heap->{parent} = $sender->ID();
            $heap->{config} = $config;
            $heap->{wheel_server} = POE::Wheel::SocketFactory->new(
               RemoteAddress => $heap->{config}->{ip},
               RemotePort    => $heap->{config}->{port} || 443,
               SuccessEvent  => 'server_connect',
               FailureEvent  => 'server_error',
            );
            $kernel->yield("timeout");
         }, server_error => sub {
            my ($kernel, $heap, $operation, $errnum, $errstr) = @_[KERNEL, HEAP, ARG0, ARG1, ARG2];
            $kernel->yield("done" => $errnum ? ($operation." error ".$errnum." occurred: ".$errstr => {
               operation => $operation,
               errnum    => $errnum,
               errstr    => $errstr,
            }) : () );
         }, server_connect => sub {
             my ($kernel, $session, $heap, $socket) = @_[KERNEL, SESSION, HEAP, ARG0];
             my ($local_port, $local_addr) = unpack_sockaddr_in(getsockname($socket));
             $heap->{clientssl} = $heap->{config}->{nossl} ? POE::Filter::Stream->new() : POE::Filter::SSL->new(
                client   => 1,
                tls1_2   => $heap->{config}->{noforcetls} ? 0 : 1,
                cacrtmem => $heap->{config}->{ca},
             );
             $heap->{wheel_server} = POE::Wheel::ReadWrite->new(
                Handle       => $socket,
                Driver       => POE::Driver::SysRW->new,
                #Filter       => POE::Filter::Stream->new,
                InputFilter  => #(ref($heap->{config}->{data}) eq "HTTP::Request") ? POE::Filter::Stackable->new(Filters => [
                   #$heap->{clientssl},
                   #POE::Filter::HTTP::Parser->new( type => 'client' ),
                #]) :
                   $heap->{clientssl},
                OutputFilter  => (ref($heap->{config}->{data}) eq "HTTP::Request") ? POE::Filter::Stackable->new(Filters => [
                   $heap->{clientssl},
                   POE::Filter::HTTP::Parser->new( type => 'client' ),
                ]) :
                   $heap->{clientssl},
                InputEvent   => 'server_input',
                ErrorEvent   => 'server_error',
             );
             $heap->{wheel_server}->put($heap->{config}->{data});
          }, server_input => sub {
             my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
             #if (ref($heap->{config}->{data}) eq "HTTP::Request") {
             #   $heap->{data} = $input;
             #} else {
                $heap->{data} .= $input;
             #}
             $kernel->delay("done" => 0.1);
          }, timeout => sub {
            my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
            if ($heap->{count}++ < ($heap->{config}->{timeout} || $heap->{config}->{maxcount} || 10)) {
               $kernel->delay("timeout" => 1);
            } else {
               $kernel->yield("done" => "Timeout");
            }
            $heap->{config}->{onLoop}($heap->{count}, $heap->{config})
               if $heap->{config}->{onLoop};
         }, done => sub {
            my ($kernel, $heap, $error, $errordetail) = @_[KERNEL, HEAP, ARG0, ARG1];
            #print "OnDone. ".$error."\n";
            if ($error) {
               $$result->{success}     = 0;
               $$result->{error}       = $error;
               $$result->{errordetail} = $errordetail;
            } elsif(!($heap->{config}->{nosslverify} || $heap->{config}->{nossl} || $heap->{clientssl}->clientCertValid())) {
               $$result->{success} = 0;
               $$result->{clientssl} = $heap->{clientssl};
               $$result->{error} = "clientCertValid failed";
            } else {
               eval {
                  $heap->{data} = HTTP::Response->parse($heap->{data});
               };
               my $theresult = {
                  success   => 1,
                  data      => $heap->{data},
                  clientssl => $heap->{clientssl},
               };
               if (defined($heap->{data}) &&
                           $heap->{data} &&
                      (ref($heap->{data}) eq "HTTP::Response") &&
                          ($heap->{config}->{redirects}) &&
                           $heap->{data}->code &&
                         (($heap->{data}->code eq "302") ||
                          ($heap->{data}->code eq "301"))) {
                           $heap->{config}->{curredirects} ||= $heap->{config}->{redirects};
                  if ($heap->{config}->{curredirects}-- <= 0) {
                     $theresult = {
                        success => 0,
                        error   => "Too many redirects",
                        data    => $heap->{data},
                     };
                  } else {
                     my $dst = $heap->{data}->header("Location");
                     if ($dst =~ m,^\s*http(s?)://([^/]+)(/?.*)\s*$,i) {
                        my $ssl   = $1 ? 1 : 0;
                        my $host  = $2;
                        my $url   = $3;
                        #print "REDIRECT TO SSL=".$ssl." HOST=".$host." URL=".$url."\n";
                        my $request = HTTP::Request->new(GET => $url);
                        $request->protocol("HTTP/1.0");
                        $request->header("Host" => $host);
                        #die("getSSL - redirect");
                        my $redirectresult = getSSL({
                           %{$heap->{config}},
                           data => $request,
                           nossl => $ssl ? 0 : 1,
                           ip   => $host,
                           port => $ssl ? "443" : "80",
                        });
                        $theresult = {
                           %$redirectresult,
                           nextconfig => $heap->{config},
                           nextresult => $theresult,
                        };
                     } else {
                        $theresult = {
                           success => 0,
                           error   => "Server redirected to bad location '".$dst."'",
                           data    => $heap->{data},
                        };
                     }
                  }
               }
               #$$result = $theresult;
               foreach (keys %$theresult) {
                  $$result->{$_} = $theresult->{$_};
               }
            }
            $$result->{config} = $config;
            $config->{callback}($$result)
               if($config->{callback});
            $kernel->delay("timeout" => undef);
            delete $heap->{wheel_server};
         },
      },
      args => [ $config, ],
   );
   unless($config->{callback}) {
      $poe_kernel->delay("workaround_for_bug_rt_125414_2" => 99999999999999999);
      while (!exists($$result->{success})) {
         $poe_kernel->run_one_timeslice;
      };
      $poe_kernel->delay("workaround_for_bug_rt_125414_2" => undef);
   }
   return $$result;
}

sub poe_sleep {
   my $seconds = shift;
   POE::Session->create(
      inline_states => {
         _start => sub {
            $poe_kernel->delay("done" => $seconds);
         },
         done => sub {
            undef $seconds;
         },
      },
   );
   $poe_kernel->delay("workaround_for_bug_rt_125414_3" => 99999999999999999);
   while (defined($seconds)) {
      $poe_kernel->run_one_timeslice;
   };
   $poe_kernel->delay("workaround_for_bug_rt_125414_3" => undef);
}

1;
