#!/usr/bin/env perl
BEGIN {
  $ENV{SSLMAKER_DEBUG} //= grep {/^--silent/} @ARGV ? 0 : 1;
}
use Applify;
use Data::Dumper ();
use File::Spec::Functions qw(catdir);
use Path::Tiny;

option int => bits => 'SSL key bit size';
option int => days => 'Number of days the cert should be valid';
option str => home => 'sslmaker working directory';
option str => root => 'Path to root key.pem. Required for making intermediate key+cert';
option str => subject =>
  'Example: /C=US/ST=Texas/L=Dallas/O=Company/OU=Department/CN=example.com/emailAddress=admin@example.com',
  $ENV{SSLMAKER_SUBJECT};
option bool => silent => 'Only output data on failure';

documentation 'App::sslmaker';
version 'App::sslmaker';

my $wrapper = sub {
  my ($sslmaker, $method, @args) = @_;
  return $ENV{OPENSSL_CONF} ? $sslmaker->$method(@args) : $sslmaker->with_config($method, @args);
};

sub d {
  my $d = {@_};
  $_ and $_ = "$_" for values %$d;
  my $json = Data::Dumper->new([$d])->Indent(1)->Sortkeys(1)->Terse(1)->Useqq(1)->Dump;
  $json =~ s/" => /": /g;    # ugly
  $json;
}

sub action_revoke {
  my ($self, $cert) = @_;
  my $sslmaker   = App::sslmaker->new;
  my $passphrase = $self->home->child(qw(private passphrase));

  $sslmaker->$wrapper(
    revoke_cert => {
      cert       => $self->home->child(qw(certs ca.cert.pem)),
      key        => $self->home->child(qw(private ca.key.pem)),
      passphrase => -e $passphrase ? $passphrase : undef,
      revoke     => $cert,
    }
  );

  $self->_warn("Done.\n");
}

sub action_root {
  my $self = shift;
  die "--subject is required\n" unless $self->subject;

  my $args = {
    bits       => $self->bits || 8192,
    cert       => $self->_root_file('cert'),
    days       => $self->days || 365 * 30,
    home       => $self->root->parent,
    key        => $self->_root_file('key'),
    passphrase => $self->root->parent->child(qw(passphrase)),
  };

  $self->_print(d %$args);

  my $sslmaker = App::sslmaker->new;
  $sslmaker->subject($self->subject);
  $sslmaker->make_directories({home => $args->{home}, templates => 1});
  $self->run_maybe($args->{key},  sub { $sslmaker->$wrapper(make_key  => $args) });
  $self->run_maybe($args->{cert}, sub { $sslmaker->$wrapper(make_cert => $args) });
  $self->_warn("Done.\n");
}

sub action_dhparam {
  my $self = shift;
  my $path = shift || $self->home->child('dhparam.pem');
  my $bits = shift || 2048;

  $self->run_maybe($path, sub { App::sslmaker::openssl(qw(dhparam -out) => $path, $bits) });
}

sub action_generate {
  my $self = shift;
  my $cn   = shift || die "Usage: $0 generate <CN>\n";

  my $args = {
    bits    => $self->bits,
    csr     => "$cn.csr.pem",
    days    => $self->days,
    home    => $self->home,
    key     => "$cn.key.pem",
    subject => "/CN=$cn",
  };

  my $sslmaker          = App::sslmaker->new;
  my $intermediate_cert = $self->home->child(qw(certs ca.cert.pem));
  $sslmaker->subject(
    -r $intermediate_cert ? ($intermediate_cert, $self->subject) : ($self->subject));
  $self->run_maybe($args->{key}, sub { $sslmaker->make_key($args) });
  $self->run_maybe($args->{csr}, sub { $sslmaker->make_csr($args) });
  $self->_print("// Next: Need to send $args->{csr} to SSL admin for signing.\n");
  $self->_warn("Done.\n");
}

sub action_intermediate {
  my $self = shift;

  my $home = $self->home;
  my $args = {
    bits       => $self->bits,
    ca_cert    => $self->_root_file('cert'),
    ca_key     => $self->_root_file('key'),
    cert       => $home->child(qw(certs ca.cert.pem)),
    csr        => $home->child(qw(certs ca.csr.pem)),
    days       => $self->days || 365 * 28,
    extensions => 'v3_ca',
    home       => $home,
    key        => $home->child(qw(private ca.key.pem)),
    passphrase => $home->child(qw(private passphrase)),
  };

  $self->_print(d %$args);

  my $sslmaker = App::sslmaker->new;
  $sslmaker->subject($args->{ca_cert}, $self->subject);
  $sslmaker->make_directories({home => $home, templates => 1});
  $self->run_maybe($args->{key}, sub { $sslmaker->$wrapper(make_key => $args) });
  $self->run_maybe($args->{csr}, sub { $sslmaker->$wrapper(make_csr => $args) });

  $args->{home}       = $self->root->parent;
  $args->{passphrase} = $self->root->parent->child(qw(passphrase));
  $self->run_maybe($args->{cert}, sub { $sslmaker->$wrapper(sign_csr => $args) });

  $args->{chain_cert} = $home->child(qw(certs ca-chain.cert.pem));
  $sslmaker->_cat(@$args{qw( cert ca_cert chain_cert )});
  $self->_print("// Generated $args->{chain_cert} from CA and intermediate certificate\n");

  $sslmaker->openssl(
    verify => -CAfile => @$args{qw( ca_cert cert )},
    sub {
      my ($sslmaker, $output) = @_;
      die $output if $output =~ /error/;
    }
  );

  $self->_warn("Done.\n");
}

sub action_nginx {
  my $self   = shift;
  my $domain = shift || die "Usage: $0 nginx <domain>\n";

  $self->_print(App::sslmaker->_render_template(
    'nginx.config',
    {
      domain  => $domain,
      key     => "/etc/nginx/ssl/$domain.key.pem",
      cert    => "/etc/nginx/ssl/$domain.cert.pem",
      ca_cert => $self->home->child(qw(certs ca-chain.cert.pem)),
    },
  ));
}

sub action_man {
  exec perldoc => 'App::sslmaker';
}

sub action_sign {
  my ($self, $csr, $cert) = @_;
  my $sslmaker = App::sslmaker->new;
  my $home     = $self->home;

  unless ($cert) {
    $cert = $csr;
    $cert =~ s!(\.csr)?\.pem$!\.cert.pem!;
  }

  $sslmaker->$wrapper(
    sign_csr => {
      home       => $home,
      ca_cert    => $home->child(qw(certs ca.cert.pem)),
      ca_key     => $home->child(qw(private ca.key.pem)),
      cert       => $cert,
      csr        => $csr,
      extensions => 'usr_cert',
      passphrase => $home->child(qw(private passphrase)),
    }
  );

  $self->_print("// Generated $cert\n");
  $self->_warn("Done.\n");
  $self->_warn("Run this command for more details: openssl x509 -in $cert -noout -text\n");
}

sub catch {
  my $self   = shift;
  my $errno  = $!;
  my $errstr = $@;

  # remove stacktrace
  $errstr =~ s!\sat\s\S+\sline.*!!s;

  # rewrite invalid input
  $errstr =~ s!"subject"!--subject!s;

  # parse openssl exception
  if ($errstr =~ s!\sFAIL\s\((\d+)\)\s\((.*)\)$!!s) {
    $errno  = $1;
    $errstr = $2;
  }

  $! = $errno;
  die $errstr;
}

sub run_maybe {
  my ($self, $file, $cb) = @_;

  if (-e $file) {
    $self->_print("// File $file exists.\n");
  }
  else {
    $self->$cb;
    $self->_print("// Generated $_[1]\n");
  }
}

sub _build_home {
  my $self = shift;

  my $path = $self->home || $ENV{SSLMAKER_HOME};
  return $path if $path;

  for (qw(pki ssl)) {
    my $path = "/etc/$_";
    return "$path/sslmaker" if -w $path;
  }

  die "Cannot detect default --home. Maybe you have to run as root?\n";
}

sub _root_file {
  my ($self, $ext) = @_;
  my $base = $self->root->basename;
  $base =~ s!\b(cert|key)\b!$ext!;
  return $self->root->parent->child($base);
}

sub _print { shift->silent or print @_ }
sub _warn  { shift->silent or warn @_ }

app {
  my ($self, $action, @args) = @_;
  $action ||= 'man';
  $action = 'man' if grep { $action eq $_ } qw(help pod);

  if ($action ne 'man') {
    $self->home(Path::Tiny->new($self->_build_home)->absolute);
    $self->root(Path::Tiny->new($self->root || $self->home->child(qw(root ca.key.pem)))->absolute);
  }

  unless ($action and $self->can("action_$action")) {
    $self->_script->print_help;
    return 0;
  }

  eval {
    require App::sslmaker;
    $self->can("action_$action")->($self, @args);
    1;
  } or $self->catch;

  return 0;
};
