#!/usr/bin/perl

use strict;
use LWP::UserAgent;
use HTTP::Request::Common;
use Term::ReadKey "ReadMode";
use JSON;
use Data::Dumper;
use Data::UUID;
use Getopt::Long qw(:config no_ignore_case bundling);

# pre-load avatar env ---------------------------------------------------------
BEGIN {
    if (-f "/etc/avatar.conf") {
        open (ICF, "/etc/avatar.conf") || die("Cannot read avatar.conf");
        map {$ENV{$1} = $2 if /^(\w+)=(\S+)/} grep {/^\w+=\S+/} (<ICF>);
        close ICF;
        push @INC, split(':', $ENV{PERL5LIB});
    }
}

# CONS ------------------------------------------------------------------------

my $APL_VAR = $ENV{APL_VAR} || '/var/avatar';
my $APL_USR = $ENV{APL_USR} || 'apl';
my $DS_VAR = $ENV{DS_VAR} || '/var/ds';
my $LINK_VAR = '/var/vnlk';
my $LINK_CONF = "/etc/vnlk/vnlk.conf";
my $DS_LOG = "$DS_VAR/log";
my $DS_CONF = "$DS_VAR/conf/ds.conf";
my $WALL_CONF = "$DS_VAR/conf/wall.conf";
my $AV_CONF = "$APL_VAR/conf/av/conf";
my ($APL_UID, $APL_GID) = (getpwnam($APL_USR))[2,3];
my $ACTIVATE_LOGS = "$DS_VAR/log/activate";
my $LOG = "$ACTIVATE_LOGS/activate.log";
my $LOCK_FILE = "$DS_VAR/conf/lock";
my $MON_STATUS_OFFLINE = "UNPROVISIONED_OFFLINE";
my $MON_STATUS_ONLINE = "PROVISIONED_ONLINE";
my $MON_STATUS_UNPROVISIONED = "UNPROVISIONED_ONLINE";

my $RESULT="$DS_VAR/activate/result";
my $LIST_MONITORS="/opt/ds/bin/list-monitors";
my $ACTIVATED = -f "/var/ds/conf/ds.conf";

my $UBUNTU = 1 if `grep Ubuntu /etc/os-release` =~ /Ubuntu/i;

# VARS -------------------------------------------------------------------------
my $Auto;
my $DryRun;
my $Registered = -f $WALL_CONF; # 1 if DS is already registered
my $IsLink;
my %AvCfg;
my %DSCfg;
my $Endpoint;
my $DSID;
my $AvatarID;
my $WallID;
my %Monitors;

# PROC -------------------------------------------------------------------------

#-------------------------------------------------------------------------------
sub Log { # print message to log
#-------------------------------------------------------------------------------
    my $msg=shift;
    open  LOG, ">>$LOG";
    print LOG scalar localtime().'| '.$msg;
    close LOG;
}

#-------------------------------------------------------------------------------
sub prlog { # print message to screen and place in a log
#-------------------------------------------------------------------------------
    my $msg=shift;
    $msg = "[dry-run] $msg" if $DryRun;
    print $msg;
    Log $msg;
}

#-------------------------------------------------------------------------------
sub Result { # print result
#-------------------------------------------------------------------------------
    my $msg=shift;
    open  RESULT, ">$RESULT";
    print RESULT "$msg\n";
    close RESULT;
}

#-------------------------------------------------------------------------------
sub Errlog { # print error to the log and screen then die
#-------------------------------------------------------------------------------
    Result 'FAIL';
    Log "\nERROR: @_\n";
    die "\nERROR: @_\n";
}

sub read_conf {
    my $file = shift;
    open FH, $file or return undef;
    my %conf = map {/^(\w+?)\s*=\s*(\S.*)$/} grep {/^\w+\s*=/} <FH>;
    close FH;
    return \%conf;
}

sub save_conf {
    my ($file, $conf) = @_;
    if ($DryRun) {
        prlog "Save conf to file: $file\n";
        foreach my $key (sort keys %$conf) {
            prlog "$key=$conf->{$key}\n";
        }
        prlog "-------------------------\n";
    } else {
        if (open FH, '>', $file) {
            foreach my $key (sort keys %$conf) {
                print FH "$key=$conf->{$key}\n";
            }
            close FH;
        }
    }
}

sub read_avatar_conf {
    my $conf_path = $IsLink ? $LINK_CONF : $AV_CONF;
    Errlog("Missing conf file $conf_path") if not -f $conf_path;
    my $cfg = read_conf $conf_path;
    Errlog("Cannot open $conf_path: $!") if not $cfg;
    %AvCfg = %$cfg;
}

sub mk_dir {
    my @dirs=@_;
    foreach my $dir (@dirs) {
        mkdir($dir,0750) if ! -d $dir;
        chown ($APL_UID,$APL_GID,$dir);
    }
}

sub ask_wall_name {
    local $| = 1;
    print "New wall name: ";
    my $name = <STDIN>;
    chomp $name;
    return $name;
}

sub vmx2_api {
    my ($http_method, $api, $data) = @_;
    
    my $url = "https://$Endpoint/api/vmx2/$api";
    my $req = HTTP::Request->new($http_method, $url);
    $req->header('Content-Type' => 'application/json');
    $req->header('X-avatarid', $AvatarID);
    $req->content(encode_json($data));
    my $lwp = LWP::UserAgent->new(requests_redirectable => ['GET', 'HEAD', 'POST', 'PUT']);
    if ($DryRun) {
        prlog "vMX2 API Request: $http_method $url. Data:\n".Dumper($data);
        return {};
    }
    my $resp = $lwp->request($req);
    if (!$resp->is_success) {
        my $msg = "Error performing vMX2 API request (HTTP error): ".$resp->code." ".$resp->message;
        my $json = eval { decode_json $resp->content };
        $msg .= "\n".$json->{error} if not $@ and $json and $json->{error};
        Errlog $msg;
    }
    my $json = decode_json($resp->content);
    if ($json->{error}) {
        Errlog "Error performing vMX2 API request (App error): ".$resp->content;
    }
    return $json;
}

sub set_var{
    mk_dir("$DS_VAR",
    "$DS_VAR/log",
    "$DS_VAR/conf");
}

sub set_conf {
    if (not -f $DS_CONF) {
        my $ug = Data::UUID->new;
        $DSID = lc $ug->create_str();
        %DSCfg = (
            OBJID => $DSID,
            AVATARID => $AvatarID,
            ENDPOINT => $Endpoint,
            NAME     => "DisplayServer $AvCfg{HOSTNAME}",
            LOCATION => "Avatar $AvCfg{HOSTNAME}"
        );
        save_conf($DS_CONF, \%DSCfg);
    } else {
        my $conf = read_conf $DS_CONF;
        $DSID = $conf->{OBJID};
        %DSCfg = %$conf;
    }
}

sub set_wall {
    if (not -f $WALL_CONF) {
        my $hostname = $AvCfg{HOSTNAME};
        my $wall_name = $Auto ? "${hostname}_wall" : "${hostname}_".ask_wall_name;
        my $wall = vmx2_api(
            'POST', 'wall',
            {
                "name" => $wall_name,
                "description" => ""
            }
        );
        $WallID = $DryRun ? "some-wall-id" : $wall->{wall_id}->{id}->{uuid};
        Errlog "No Wall ID in API call result: \n".Dumper($wall) if not $WallID;
        
        if (not $DryRun) {
            open FH, '>', $WALL_CONF;
            print FH "OBJID=$WallID\n";
            print FH "NAME=$wall_name\n";
            close FH;
        }
        prlog "New wall with GUID '$WallID' was created\n";
    } else {
        my $cfg = read_conf $WALL_CONF;
        Errlog("Unable to read wall.conf: $!") if not $cfg;
        $WallID = $cfg->{OBJID};
        prlog "Wall with GUID '$WallID' already exists\n";
    }
}

sub read_saved_monitors {
    opendir DH, "$DS_VAR/conf";
    my @mondirs = grep {/^\w{8}-\w{4}/} readdir DH;
    closedir DH;
    
    foreach my $monid (@mondirs) {
        my $mon = read_conf "$DS_VAR/conf/$monid/conf";
        next if not $mon;
        $Monitors{$monid} = $mon;
    }
}

sub activate_monitor {
    my $mon = shift;
    $mon->{con_status} = $MON_STATUS_ONLINE;
    $mon->{WALLID} = $WallID;
}

sub deactivate_monitor {
    my $mon = shift;
    $mon->{WALLID} = "";
    $mon->{con_status} = $MON_STATUS_OFFLINE;
}

sub migrate_monitors {
    my $list = shift;
    # Migrate old-style monitor configs (with INPUT and without OUTPUT_* params)
    foreach my $mon (values %Monitors) {
        next if defined $mon->{OUTPUT_NAME};
        next if not defined $mon->{INPUT};
        prlog "Migrating monitor '$mon->{NAME}' ($mon->{INPUT}) to new format\n";
        my $input = $mon->{INPUT};
        $mon->{OUTPUT_NUMBER} = $input;
        my $monparams = $list->{$input};
        if ($monparams) {
            my ($output_name, $res, $lefttop) = @$monparams;
            my ($hres, $vres) = split /x/, $res;
            my ($left, $top) = split /,/, $lefttop;
            $mon->{OUTPUT_NAME} = $output_name;
            $mon->{OUTPUT_TOP}  = $top;
            $mon->{OUTPUT_LEFT} = $left;
            activate_monitor($mon);
        } else {
            deactivate_monitor($mon);
        }
    }
}

sub list_monitors {
    open LIST, "$LIST_MONITORS |";
    my %list = map { ($1, [$2,$3,$4]) if /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/} grep {/^\S+/} <LIST>;
    close LIST;
    foreach my $k (keys %list) {
        delete $list{$k} if not defined $list{$k};
    }
    Errlog("Unable to get monitors: empty list returned") if not %list;
    print "GOT MONITORS: ".Dumper(\%list) ."\n";

    return \%list
}

sub set_monitors {
    my %list = %{ list_monitors() };

    read_saved_monitors;

    migrate_monitors(\%list);
    
    my $i = 1;
    foreach my $output (keys %list) {
        my $found = 0;
        my $mon;

        my $monparams = $list{$output};
        if (not ref $monparams or not @$monparams) {
            prlog "Bad params for monitor $output: ".Dumper($monparams);
            next;
        }

        my ($output_name, $res, $lefttop) = @$monparams;
        my ($hres, $vres) = split /x/, $res;
        my ($left, $top) = split /,/, $lefttop;
        my $mon_num = sprintf("%02d", $i);

        foreach my $m (values %Monitors) {
            next if not defined $m->{OUTPUT_NAME};
            if ($m->{OUTPUT_NAME} eq $output_name) {
                $found = 1;
                $mon = $m;
                last;
            }
        }
        
        if (not $found) { # Save monitor
            my $mon_name = "$AvCfg{HOSTNAME}_Monitor-$mon_num";
            prlog "Saving NEW monitor $output_name ($output) as '$mon_name'\n";

            my $ug = Data::UUID->new;
            my $monid = lc $ug->create_str();
            mk_dir "$DS_VAR/conf/$monid" if not $DryRun;
            my $mon = {
                OBJID => $monid,
                DSID => $DSID,
                NAME => $mon_name,
                WIDTH => $hres,
                HEIGHT => $vres,
                OUTPUT_NAME => $output_name,
                OUTPUT_NUMBER => $output,
                OUTPUT_TOP => $top,
                OUTPUT_LEFT => $left,
                TOP => -1,
                LEFT => -1,
                con_status => $MON_STATUS_UNPROVISIONED
            };
            if (not $Registered) { # Assign all monitors to wall on first activation
                $mon->{TOP} = $top;
                $mon->{LEFT} = $left;
                activate_monitor($mon);
            }
            $Monitors{$monid} = $mon;
        } else {
            # Update existing monitor if any changes detected
            my $prevres = "$mon->{WIDTH}x$mon->{HEIGHT}";
            my $newres = "${hres}x${vres}";
            if ($prevres ne $newres) {
                prlog "Resolution for monitor $mon->{NAME} was changed from $prevres to $newres - marking as OFFLINE\n";
                deactivate_monitor($mon);
            } else {
                activate_monitor($mon);
            }
            $mon->{OUTPUT_NUMBER} = $output;
            $mon->{OUTPUT_LEFT} = $left;
            $mon->{OUTPUT_TOP} = $top;
        }
        $i++;
    }

    # Finally save all monitors to disk
    foreach my $mon (values %Monitors) {
        save_conf "$DS_VAR/conf/$mon->{OBJID}/conf", $mon;
    }
}

sub register_ds {
    my $request = {
        "attributes" => \%DSCfg
    };
    my @mons;
    foreach my $mon (values %Monitors) {
        my $m = {
            mon_id            => { id   => { uuid => $mon->{OBJID} }},
            display_server_id => {           uuid => $DSID },
            width             => int($mon->{WIDTH}),
            height            => int($mon->{HEIGHT}),
            name              => $mon->{NAME}
        };
        if ($mon->{WALLID}) {
            $m->{left} = $mon->{LEFT} || 0;
            $m->{top} = $mon->{TOP} || 0;
            $m->{wall_id} = { id   => { uuid => $WallID }};
        }
        # Set connection status
        $m->{con_status} = $mon->{con_status} || 'UNKNOWN';
        push @mons, $m;
    }
    $request->{monitors} = \@mons;
    my $result = vmx2_api(
        'PUT', "ds/$DSID", $request
    );
    prlog "Display Server registered successfully\n";
}

sub unregister_ds {
    prlog "Unregistering DS objects from JetStream\n";
    my $request = {};
    $request->{wall_id} = { id => { uuid => $WallID } } if $WallID;
    my $result = vmx2_api(
        'DELETE', "ds/$DSID", $request
    );
    if (not $result) {
        prlog "Cannot remove corresponding video wall configurations from JetStream\n";
    } else {
        prlog "Display Server unregistered successfully\n";
    }
}

sub set_ds {

    eval {
        list_monitors if not $ACTIVATED;
        set_conf;
        set_wall;
        set_monitors;

        register_ds;
    };
    if ($@ and not $ACTIVATED) {
        chomp $@;
        unregister_ds if $DSID;
        prlog "Removing DS_VAR\n";
        system("rm -rf $DS_VAR") if not $DryRun;
        Errlog($@);
    }

    if (not $DryRun) {
        system("touch $LOCK_FILE");
        system("systemctl enable ds");
        system("systemctl is-active --quiet ds && systemctl restart ds || systemctl start ds");
    }
}

sub activate {
    GetOptions(
        'a|auto'  => \$Auto,
        'dry-run'  => \$DryRun
    ) or die "Invalid options\n";

    read_avatar_conf;

    Errlog("OBJID is missing in Avatar configuration") if not $AvCfg{OBJID};
    Errlog("ENDPOINT is missing in Avatar configuration") if not $AvCfg{ENDPOINT};

    $Endpoint = $AvCfg{ENDPOINT};
    $AvatarID = $AvCfg{OBJID};
    prlog "Using VSaaS: $Endpoint\n";

    set_var;

    set_ds;
}

sub check_link {
    if ($UBUNTU) {
        `dpkg-query -l videonext-link 2>/dev/null`;
    } else {
        system("rpm -q --quiet videonext-link");
    }
    $IsLink = 1 if $? == 0;
}

# MAIN =========================================================================
check_link();

mkdir $DS_VAR             if ! -d $DS_VAR;
mkdir $DS_LOG             if ! -d $DS_LOG;
mkdir $ACTIVATE_LOGS      if ! -d $ACTIVATE_LOGS;
mkdir "$DS_VAR/activate" if ! -d "$DS_VAR/activate";
Log "======================= START ======================\n";
Log ("$0  ".join(' ',@ARGV)."\n");
activate();
Log "======================== END =======================\n";

print "Display Server GUID: " . $DSID . "\n";
