#
# (c) Jan Gehring <jan.gehring@gmail.com>
#
# vim: set ts=2 sw=2 tw=0:
# vim: set expandtab:

package Rex::Repositorio::Server::Docker::Image;

use Mojo::Base 'Mojolicious::Controller';
use Data::Dumper;
use File::Spec;
use File::Path 'make_path', 'remove_tree';
use File::Basename 'basename';
require IO::All;
use JSON::XS;
use MIME::Base64;

our $VERSION = '1.2.1'; # VERSION

sub put_image {
  my ($self) = @_;

  my $repo_dir = $self->app->get_repo_dir( repo => $self->repo->{name} );
  my $image_dir =
    File::Spec->catdir( $repo_dir, "images", $self->param("name") );
  my $image_json = File::Spec->catfile( $image_dir, "image.json" );

  make_path $image_dir;

  open( my $fh, ">", $image_json ) or die($!);
  print $fh $self->req->body;
  close($fh);

  eval {
    my $ref = decode_json $self->req->body;
    if ( exists $ref->{parent} ) {
      symlink File::Spec->catfile( $repo_dir, "images", $ref->{parent} ),
        File::Spec->catfile( $image_dir, 'parent' );
    }
    1;
  } or do {
    print STDERR ">> ERR> $@\n";
  };

  $self->render( text => "true" );
}

sub put_image_layer {
  my ($self) = @_;

  my $repo_dir = $self->app->get_repo_dir( repo => $self->repo->{name} );
  my $image_dir =
    File::Spec->catdir( $repo_dir, "images", $self->param("name") );
  my $image_layer = File::Spec->catfile( $image_dir, "image.layer" );

  my $chunk = $self->req->body;

  open( my $fh, ">", $image_layer ) or die($!);
  print $fh $chunk;
  close($fh);

  $self->render( text => 'true' );
}

# TODO: implement checksuming
sub put_image_checksum {
  my ($self) = @_;

  my $name = $self->param("name");

  my $repo_dir = $self->app->get_repo_dir( repo => $self->repo->{name} );
  my $image_dir =
    File::Spec->catdir( $repo_dir, "images", $self->param("name") );
  my $chksum_file = File::Spec->catfile( $image_dir, "payload.sha256" );

  #print STDERR "($name) got chksum> "
  #  . $self->req->headers->header('X-Docker-Checksum') . "\n";
  #print STDERR "($name) got chksum payload> "
  #  . $self->req->headers->header('X-Docker-Checksum-Payload') . "\n";

  open( my $fh, ">", $chksum_file ) or die($!);
  print $fh $self->req->headers->header('X-Docker-Checksum-Payload');
  close($fh);

  $self->render( text => 'true' );
}

sub get_image_ancestry {
  my ($self) = @_;

  my $requested_file = $self->req->url;
  $self->app->log->debug("Requested-File: $requested_file");
  my $orig_url = $self->repo->{url};
  $orig_url =~ s/\/$//;
  $self->app->log->debug("Upstream-URL: $orig_url");

  $requested_file =~ s/^\///;
  my $orig_file_url = $orig_url . "/" . $requested_file;
  $self->app->log->debug("Orig-File-URL: $orig_file_url");

  my $repo_dir = $self->app->get_repo_dir( repo => $self->repo->{name} );
  my $image_dir =
    File::Spec->catdir( $repo_dir, "images", $self->param("name") );

  my @ids = ( $self->param("name") );
  my $parent_id_file = File::Spec->catfile( $image_dir, "parent" );
  if ( -l $parent_id_file ) {
    my $parent = readlink $parent_id_file;
    while ($parent) {
      my $parent_id = basename $parent;
      push @ids, $parent_id;
      $parent =
        readlink File::Spec->catfile( $image_dir, "..", $parent_id, "parent" );
    }

    $self->render( json => \@ids );
  }
  else {
    $self->_auth_upstream();
    my $upstream_file = File::Spec->catfile( $image_dir, "endpoint.data" );
    $self->app->log->debug("Looking for upstream file: $upstream_file");
    if ( -f $upstream_file ) {
      my ($upstream_host) = eval { local (@ARGV) = ($upstream_file); <>; };
      $orig_file_url =~ s"^(http|https)://([^/]+)/"$1://$upstream_host/";
      $self->app->log->debug("Rewrite Upstream-URL: $orig_file_url");
    }

    $self->proxy_to(
      $orig_file_url,
      sub {
        my ( $c, $tx ) = @_;
        $c->app->log->debug("Got data from upstream...");
        $c->app->log->debug("Writing ancestry (parent) file: $parent_id_file");
        my $ref = $tx->res->json;
        my @ids = @{$ref};
        shift @ids; # first one is the directory itself.
        my $child_dir = File::Spec->catfile( $image_dir, "parent" );
        for my $parent_id (@ids) {
          my $link_target =
            File::Spec->catdir( $repo_dir, "images", $parent_id );
          my $link_name = $child_dir;
          symlink $link_target, $link_name;

          $child_dir = File::Spec->catfile( $link_target, "parent" );
        }
      },
      sub {
        my ( $c, $tx ) = @_;
        $self->_fix_docker_headers($tx);
      },
      {
        'Authorization' => 'Token ' . $self->stash('upstream_docker_token'),
      }
    );
  }
}

sub get_image_layer {
  my ($self) = @_;

  my $requested_file = $self->req->url;
  $self->app->log->debug("Requested-File: $requested_file");
  my $orig_url = $self->repo->{url};
  $orig_url =~ s/\/$//;
  $self->app->log->debug("Upstream-URL: $orig_url");

  $requested_file =~ s/^\///;
  my $orig_file_url = $orig_url . "/" . $requested_file;
  $self->app->log->debug("Orig-File-URL: $orig_file_url");

  my $repo_dir = $self->app->get_repo_dir( repo => $self->repo->{name} );
  my $image_dir =
    File::Spec->catdir( $repo_dir, "images", $self->param("name") );
  my $layer_file = File::Spec->catfile( $image_dir, "image.layer" );
  $self->app->log->debug("Layer-File: $layer_file");

  if ( -f $layer_file ) {
    $self->res->headers->add( 'Content-Type', 'application/octet-stream' );

    #$self->render(data => IO::All->new($layer_file)->slurp);
    $self->render_file( filepath => $layer_file );
  }
  else {
    $self->_auth_upstream();
    my $upstream_file = File::Spec->catfile( $image_dir, "endpoint.data" );
    $self->app->log->debug("Looking for upstream file: $upstream_file");
    if ( -f $upstream_file ) {
      my ($upstream_host) = eval { local (@ARGV) = ($upstream_file); <>; };
      $orig_file_url =~ s"^(http|https)://([^/]+)/"$1://$upstream_host/";
      $self->app->log->debug("Rewrite Upstream-URL: $orig_file_url");
    }

    $self->proxy_to(
      $orig_file_url,
      sub {
        my ( $c, $tx ) = @_;
        $c->app->log->debug("Got data from upstream...");
        $c->app->log->debug("Writing layer file: $layer_file");
        open my $fh, '>', $layer_file or die($!);
        binmode $fh;
        print $fh $tx->res->body;
        close $fh;
      },
      sub {
        my ( $c, $tx ) = @_;
        $self->_fix_docker_headers($tx);
      },
      {
        'Authorization' => 'Token ' . $self->stash('upstream_docker_token'),
      }
    );
  }
}

sub get_image {
  my ($self) = @_;

  my $requested_file = $self->req->url;
  $self->app->log->debug("Requested-File: $requested_file");
  my $orig_url = $self->repo->{url};
  $orig_url =~ s/\/$//;
  $self->app->log->debug("Upstream-URL: $orig_url");

  $requested_file =~ s/^\///;
  my $orig_file_url = $orig_url . "/" . $requested_file;
  $self->app->log->debug("Orig-File-URL: $orig_file_url");

  my $repo_dir = $self->app->get_repo_dir( repo => $self->repo->{name} );
  my $image_dir =
    File::Spec->catdir( $repo_dir, "images", $self->param("name") );
  $self->app->log->debug("Found image directory: $image_dir");

  my $image_json  = File::Spec->catfile( $image_dir, "image.json" );
  my $chksum_file = File::Spec->catfile( $image_dir, "payload.sha256" );

  if ( -f $image_json ) {
    my $content = IO::All->new($image_json)->slurp;
    my $chksum  = IO::All->new($chksum_file)->slurp;
    $self->res->headers->add( 'X-Docker-Payload-Checksum', $chksum );
    $self->res->headers->add( 'Content-Type',              'application/json' );

    return $self->render( text => $content );
  }
  else {
    $self->_auth_upstream();
    my $upstream_file = File::Spec->catfile( $image_dir, "endpoint.data" );
    $self->app->log->debug("Looking for upstream file: $upstream_file");
    if ( -f $upstream_file ) {
      my ($upstream_host) = eval { local (@ARGV) = ($upstream_file); <>; };
      $orig_file_url =~ s"^(http|https)://([^/]+)/"$1://$upstream_host/";
      $self->app->log->debug("Rewrite Upstream-URL: $orig_file_url");
    }

    $self->proxy_to(
      $orig_file_url,
      sub {
        my ( $c, $tx ) = @_;
        $c->app->log->debug("Got data from upstream...");
        open my $fh, '>', $image_json or die($!);
        print $fh $tx->res->body;
        close $fh;

        open my $fh_p, '>', $chksum_file or die($!);
        print $fh_p $tx->res->headers->header('X-Docker-Payload-Checksum');
        close $fh_p;
      },
      sub {
        my ( $c, $tx ) = @_;
        $self->_fix_docker_headers($tx);
      },
      {
        'Authorization' => 'Token ' . $self->stash('upstream_docker_token'),
      }
    );
  }

 #return $self->render( json => { error => "Image not found" }, status => 404 );
}

sub _fix_docker_headers {

  my ( $self, $tx ) = @_;

  my $docker_endpoint = $tx->res->headers->header('X-Docker-Endpoints');
  $tx->res->headers->remove('X-Docker-Endpoints');
  $tx->res->headers->remove('X-Docker-Token');
  $tx->res->headers->add( 'R-Docker-Endpoints', $docker_endpoint )
    if $docker_endpoint;

  if ( $self->res->headers->header('WWW-Authenticate') ) {
    $tx->res->headers->add( 'WWW-Authenticate',
      $self->res->headers->header('WWW-Authenticate') );
  }

  if ( $self->res->headers->header('X-Docker-Token') ) {
    $tx->res->headers->add( 'X-Docker-Token',
      $self->res->headers->header('X-Docker-Token') );
  }

  $tx->res->headers->add( 'X-Docker-Endpoints' => $self->req->headers->host );
  $tx->res->headers->add( 'Pragma'             => 'no-cache' );
  $tx->res->headers->add( 'Expires'            => '-1' );

}

sub _auth_upstream {
  my ($self) = @_;
  my $docker_token = $self->stash('upstream_docker_token');
  if ($docker_token) {
    $self->app->log->debug("Docker Token: $docker_token");
  }
  else {
    $self->app->log->debug("Docker Token: no token found in session.");
    $self->app->log->debug("Need to authenticate on upstream docker registry.");

    my $repo_dir = $self->app->get_repo_dir( repo => $self->repo->{name} );
    my $image_dir =
      File::Spec->catdir( $repo_dir, "images", $self->param("name") );
    my $library = File::Spec->catfile( $image_dir, "library.data" );
    if ( !-f $library ) {
      $self->app->log->error("Can't find upstream library url.");
      die "Error finding upstream library url.";
    }
    my $registry_url = IO::All->new($library)->slurp;
    $self->app->log->debug("Got upstream registry url: $registry_url");

    my $base64_auth_string = MIME::Base64::encode_base64(
      $self->repo->{upstream_user} . ":" . $self->repo->{upstream_password} );

 # need to create a new object, so there is no cookies from proxy requests in it
    my $ua = Mojo::UserAgent->new;
    $ua->max_redirects(5);

    if ( $self->repo->{ca} ) {
      $ua->ca( $self->repo->{ca} );
    }
    if ( $self->repo->{key} ) {
      $ua->key( $self->repo->{key} );
    }
    if ( $self->repo->{cert} ) {
      $ua->cert( $self->repo->{cert} );
    }

    my $tx = $ua->get(
      $registry_url,

      { # some custom headers to get the docker token, so that we can
         # authenticate against the image servers.
        'X-Docker-Token' => 'true',
        'Authorization'  => "Basic $base64_auth_string",
      },

    );

    if ( $tx->success ) {
      my $docker_token = $tx->res->headers->header('X-Docker-Token') || "";
      if ( !$docker_token ) {
        $self->app->log->error(
          "Authentication successfull but can't find a token.");
        $self->app->log->debug( Dumper($tx) );
      }
      $self->app->log->debug("Got my docker token: $docker_token");
      $self->stash( 'upstream_docker_token', $docker_token );
    }
    else {
      $self->app->log->error(
        "Can't authenticate to upstream docker registry: $registry_url");
    }
  }
}

1;
