#!/usr/bin/env raku

use HTTP::Tiny;

unit sub MAIN (
    Str  $url!         is copy,
    Int  :C(:$continue-at),     #= Resume a file transfer at the given offset
         :d(:$data),            #= Send URL encoded data
         :F(:$form),            #= Send multipart data
    Bool :f(:$fail),            #= Fail on HTTP errors
    Str  :H(:$header),          #= Set a request header
    Bool :L(:$location),        #= Follow redirects
    Str  :o(:$output),          #= Write to file instead of STDOUT
    Str  :x(:$proxy),           #= Specify the proxy to use
    Str  :r(:$range)   is copy, #= Retrieve a byte range from the server
    Int     :$retry    = 0,     #= Specify number of retries on failed requests
    Int     :$retry-delay,      #= Specify the seconds to wait between retries
    Str  :X(:$request) is copy, #= The request to make
    Str  :u(:$user)    is copy, #= Server user and password
    Str  :A(:$user-agent),      #= Set the User-Agent to send to the server
    Bool :v(:$verbose),         #= Print request and response
);

temp %*ENV<HTTP_TINY_DEBUG> = 1 if $verbose;

my %params;
for $header.List.grep: *.defined {
    my ( $key, $value ) = .split(':', 2)».trim;
    %params<headers> //= {};
    %params<headers>.append: $key.lc, $value;
}

$request ||= $data || $form ?? 'POST' !! 'GET';

$range ||= "$_-" with $continue-at;
%params<headers><range> = "bytes=$_" with $range;

if $data || $form {
    %params<headers><content-type> ||= 'application/x-www-form-urlencoded' if $data;
    %params<headers><content-type> ||= 'multipart/form-data'               if $form;

    given %params<headers><content-type> {
        when 'application/x-www-form-urlencoded' | 'multipart/form-data' {
            my %body;

            for $data.List.grep: *.defined {
               %body.append( .key, .value ) with .&parse-form-data;
            }

            for $form.List.grep: *.defined  {
               %body.append( .key, .value ) with .&parse-form-data: :strict;
            }

            %params<content> = %body;
        }
    }
}
else {
    %params<content> = $data;
}

my $ua = do {
    my %new = ( max-redirect => $location ?? 5 !! 0 );
    %new<agent> = $_ with $user-agent;
    %new<proxy> = $_ with $proxy;

    HTTP::Tiny.new: |%new;
}

if $output {
    my $fh = open $output, :w;
    END .close with $fh;

    %params<data-callback> = sub ( $blob, $resp ) { $fh.write: $blob }

    if $range {
        my $wrap-handle;
        $wrap-handle = %params<data-callback>.wrap: -> $blob, $resp {
            unless $resp<status> == 206 {
                note "HTTP server doesn't seem to support byte ranges. Cannot resume";
                exit 33;
            }

            LEAVE %params<data-callback>.unwrap: $wrap-handle;

            $fh.write: $blob;
        }
    }
}

if $user && not $user.contains: ':' {
    my $pass = prompt "Enter host password for user '$user': ";
    $user ~= ":$pass";
}

$url .= subst: /^ ( 'http' s? '://' ) /, { "$0$user@" } if $user;

my %response;
my $sleep = $retry-delay // 1;
for 0 .. $retry -> $i {
    %response = $ua.request: $request.uc, $url, |%params;

    last if %response<status> < 500;

    next unless $retry;

    note "Warning: Transient HTTP error. Will retry in $_ second"
        ~ "{ 's' if $_ != 1 }" with $sleep;

    note "Warning: $_ retr{ $_ == 1 ?? 'y' !! 'ies' } left"
        with $retry - $i;

    $sleep *= 2 unless $retry-delay;
    sleep $sleep;
}

if $fail && %response<status> >= 400 {
    note "The requested URL returned error: { .<status reason>.join: ' ' }"
        with %response;
    exit 22;
}

print .decode with %response<content>;

sub parse-form-data ( Str $data, Bool :$strict --> Pair ) {
    if $data !~~ / '=' / && $strict {
        note 'Illegaly formatted input field!';
        exit 2;
    }

    my ( $key, $value ) = $data.split: '=', 2;

    $value //= '';
    $value = $value.substr(1).IO if $value.starts-with: '@';

    $key => $value;
}
