#!/usr/bin/env perl
use strict;
use warnings;

use App::Munner;
use App::Munner::Runner;
use Cwd qw( abs_path getcwd );
use File::Temp qw( tempfile );
use Getopt::Long::Descriptive qw( describe_options );
use IPC::Signal qw( sig_num );
use List::MoreUtils qw( uniq );
use Module::Load qw( load );
use Parallel::ForkManager;
use YAML qw( Load Dump );

my $command = $ARGV[0] || "start";

my $supported_commands =
"start|duck|stop|restart|graceful|status|(access-|error-|)(logs|log)|help|doc";

if ( $command =~ /($supported_commands)/ ) {
    shift @ARGV;
}
else {
    $command = "start";
}

my $version = $App::Munner::VERSION || q{};

$ENV{PWD} ||= getcwd();

my ( $args, $usage ) = describe_options(
    "munner [$supported_commands] %o\nversion: $version",
    [
        "config|c:s" => "App runner config file ( default ./munner.yml )",
        { default => "$ENV{PWD}/munner.yml" }
    ],
    [
        "base-dir|d:s" => "Global base directory ( default ../ )"
    ],
    [ 'app|a:s@'   => "App to run",            { default => [] } ],
    [ "all|A"      => "Start All",             { default => 0 } ],
    [ "group|g=s@" => "Start a group of apps", { default => 0 } ],
);

if ( $command eq "help" ) {
    cmd_help(" ");
}

if ( $command eq "doc" ) {
    exec perldoc => "App::Munner";
}

my $config = load_config( $args->config );

my $base_dir = $args->base_dir || $config->{base_dir}
  or cmd_help("Missing base_dir");

cmd_help("base_dir is not found --> $base_dir")
  if !-d $base_dir;

if ( !$config->{apps} ) {
    config_help("Missing apps section in your config");
}

if ( !UNIVERSAL::isa( $config->{apps}, "HASH" ) ) {
    config_help("apps section of the config needs to be in hash list");
}

my %apps = %{ $config->{apps} }
  or config_help("Please specify APPs in your config");

my @apps =
  $args->all
  ? ( keys %apps )
  : @{ $args->app };

push @apps, group_of_apps( $args->group );

@apps or cmd_help("Please specify the APP you want to start");

@apps = uniq @apps;

my $forker = Parallel::ForkManager->new( scalar @apps );

foreach my $app_name (@apps) {

    my $app_config = $apps{$app_name}
      or config_help("APP $app_name config is not found");

    my $app_dir = $app_config->{dir}
      or config_help("APP $app_name has no working directory");

    my $app_wd = $app_dir =~ /^\// ? $app_dir : "$base_dir/$app_dir";

    config_help("APP $app_name working directory is not found --> $app_wd")
      if !-d $app_wd;

    $app_wd = abs_path($app_wd);

    my $run = $app_config->{run}
      or config_help("APP $app_name has no start command");

    $app_config->{carton} //= 0;

    my $exec = ( $run =~ /;/s ) ? q{} : "exec ";

    my $carton = $app_config->{carton} ? "carton exec " : q{};

    $app_config->{env} ||= [];

    $app_config->{pid} = $forker->start
      and next;

    my $env = _env( $app_config->{env} );

    my ( $fh, $script ) = tempfile(
        CLEANUP => 1,
        UNLINK  => 1,
        SUFFIX  => ".sh"
    );

    my %worker_config = ();

    if ( $app_config->{workers} ) {
        ## pass the original command to workers
        my $worker_control = $command;

        ## start worker in frontground won't work
        $worker_control = "duck"
            if $worker_control eq "start";

        ## reset the original command and let the worker munner to work
        $command = "start";

        @worker_config{qw(fh file)} = tempfile(
            CLEANUP => 1,
            UNLINK  => 1,
            SUFFIX  => ".worker.munner.yml"
        );

        _make_worker_config( $app_name, $app_wd, $app_config, %worker_config );

        $exec   = "exec ";
        $carton = q{};
        $run = "munner $worker_control -c $worker_config{file} -g $app_name";
    }

    _make_run_script(
        $app_name => ( "cd $app_wd\n" . $env . $exec . $carton . $run ) =>
          ( $script, $fh ) );

    my $runner = App::Munner::Runner->new(
        name        => $app_name,
        base_dir    => $app_wd,
        config_file => $args->config,
        command     => $script,
        app_config  => $app_config,
        todo        => $command,
    );

    if ( $command eq "start" ) {
        $runner->run;
    }
    elsif ( $command eq "duck" ) {
        $runner->run_at_bg;
    }
    elsif ( $command eq "stop" ) {
        $runner->$command;
    }
    elsif ( $command eq "restart" ) {
        $runner->$command;
    }
    elsif ( $command eq "graceful" ) {
        $runner->$command;
    }
    elsif ( $command eq "status" ) {
        $runner->$command;
    }
    elsif ( $command =~ /log/ ) {
        my $error_log  = $runner->error_log;
        my $access_log = $runner->access_log;
        if ( $command =~ /access/ ) {
            system "tail -F $access_log";
        }
        elsif ( $command =~ /error/ ) {
            system "tail -F $error_log";
        }
        else {
            system "tail -F $access_log $error_log";
        }
    }
    else {
        cmd_help("Unknown command");
    }

    sleep 1;

    $forker->finish;
}

load "sigtrap", handler => \&killer, "INT";
load "sigtrap", handler => \&killer, "STOP";
load "sigtrap", handler => \&killer, "QUIT";

END { killer() }

$forker->wait_all_children;

exit;

sub load_config {
    my $file = shift;
    open FILE, "<", $file
      or config_help("Unable to load config file $file");
    local $/;
    my $config = Load(<FILE>);
    close FILE;
    return $config;
}

sub cmd_help {
    my $message = shift || q{};
    print "$message\n\n" . $usage->text;
    print "\n\n";
    exit
      if $message;
}

sub _env {
    my $list = shift
      or return q{};

    return config_help("env need to be in list")
      if ref $list ne "ARRAY";

    my $env = q{};

    foreach my $pair (@$list) {
        next
          if !$pair;

        next
          if ref $pair ne "HASH";

        my ( $key, $val ) = %$pair;

        $env .= join "=", quotemeta($key), $val;
        $env .= " \\\n";
    }

    return $env;
}

sub _inject_env {
    my $worker     = shift;
    my $log_dir    = { LOG_DIR => "/tmp" };
    my $pid_file   = { PID_FILE => "/tmp/$worker.pid" };
    my $access_log = { ACCESS_LOG => "/tmp/$worker.access.log" };
    my $error_log  = { ERROR_LOG => "/tmp/$worker.error.log" };
    my @envs       = @_
      or return ( $log_dir );
    foreach my $env(@envs) {
        my ($key, $value) = %$env;
        if ( $key eq "ERROR_LOG" ) {
            $value =~s/\.error/$worker.error/
                or $value =~s/\.log/$worker.log/;
        }
        elsif ( $key eq "ACCESS_LOG" ) {
            $value =~s/\.access/$worker.access/
                or $value =~s/\.log/$worker.log/;
        }
        elsif ( $key eq "PID_FILE" ) {
            $value =~s/\.pid/$worker.pid/;
        }
        $env = { $key => $value };
    }
    return @envs;
}

sub _make_run_script {
    my $app_name = shift;
    my $command  = shift
      or die "$app_name is MISSING RUN COMMAND.";

    my $filename = shift;
    my $fh       = shift;
    print $fh "#!/bin/sh\n";
    print $fh $command;
    close $fh;
    chmod 0700, $filename;
}

sub _make_worker_config {
    my $app_name   = shift;
    my $base_dir   = shift;
    my $app_config = shift
      or die "$app_name is MISSING RUN COMMAND.";
    my %worker_config  = @_;
    my $filename = $worker_config{file};
    my $fh       = $worker_config{fh};

    my $num_of_workers = $app_config->{workers};

    my %workers = map {
        my $worker_name = "$app_name-worker-$_";
        (
            $worker_name => {
                dir => ".",
                env =>
                  [ _inject_env( $worker_name => @{ $app_config->{env} } ) ],
                carton => $app_config->{carton},
                run    => $app_config->{run}
            }
          )
    } ( 1 .. $num_of_workers );

    %worker_config = (
        base_dir => $base_dir,
        apps     => \%workers,
        groups   => {
            $app_name => { apps => [ sort keys %workers ] },
        },
    );

    print $fh Dump( \%worker_config );
    close $fh;
    chmod 0700, $filename;
}

sub killer {
    foreach my $app_name (@apps) {
        my $app_config = $apps{$app_name};
        my $pid        = delete $app_config->{pid}
          or next;
        kill sig_num("INT"), $pid;
    }
    exit;
}

sub group_of_apps {
    my $wanted_groups = shift
      or return ();

    return ()
      if ref $wanted_groups ne "ARRAY"
      or !@$wanted_groups;

    my $groups = $config->{groups}
      or config_help("No group is define in your config");

    config_help("Group config is missing or invalid")
      if ref $groups ne "HASH"
      or !%$groups;

    foreach my $group_name (@$wanted_groups) {
        my $group_config = $groups->{$group_name}
          or config_help("Group name $group_name is not defined in the config");

        my $apps = $group_config->{apps} || [];

        config_help("groups.$group_name.apps need to be an array")
          if ref $apps ne "ARRAY";

        push @apps, @$apps;

        my $grps = $group_config->{groups} || [];

        config_help("groups.$group_name.groups need to be an array")
          if ref $grps ne "ARRAY";

        if (@$grps) {
            push @apps, group_of_apps($grps);
        }
    }

    return uniq @apps;
}

sub config_help {
    my $message = shift || q{};
    print <<"HELP";
$message

munner.yml config template:
---------------------------
base_dir: "... base directory to find the app ..."
apps:
    web-frontend:
        dir: "... either full path or the tail part after base_dir ..."
        env:
            ## specify the username of the running process
            - USER: web
            - PID_FILE: /tmp/web.pid
            - ACCESS_LOG: /var/log/web.acc.log
            - ERROR_LOG: /var/log/web.err.log
        run: "... command ..."
        carton: 1 or 0
    db-api:
        dir: "... path cound find the command to run ..."
        env:
            - USER: db
            ## Having your own pid file and access and error log path
            - PID_FILE: /tmp/db.pid
            - ACCESS_LOG: /var/log/db.acc.log
            - ERROR_LOG: /var/log/db.err.log
            - foo: 1
            - bar: 2
        run: "... start up command ..."
    event-api:
        dir: "websrc/event-api"
        env:
            - USER: event
            ## access and error log will be stored at app dir
            ## specific your pid file path
            - PID_FILE: /var/log/event.pid
        run: bin/app.pl
        carton: 1
    login-server:
        ## Use abs path as app base path
        dir: /home/gateway/websrc/login-server
        env:
            - USER: gateway
              ## Using LOG_DIR for pid file, access log and error log
            - LOG_DIR: /var/log
        run: bin/app.pl
        carton: 1
    ssh-port-forward:
        dir: /tmp
        env:
            - USER: me
            ## Using TERMINAL to let ssh stay alive in the background
            - TERMINAL: 1
        run: ssh -L 3306:localhost:3306 db-server
groups:
    database:
        ## only start these apps
        apps:
            - login-server
            - db-api
    events:
        apps:
            - login-server
            - event-api
    website:
        ## start apps and above groups
        apps:
            - web-frontend
        groups:
            - database
            - events
    initd:
        ## add startup script in /etc/init.d folder
        ## to run "munner duck -g initd"
        groups:
            - website
            - database
            - events

HELP

    exit;
}
