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

BEGIN {
    eval {
        require LWP::UserAgent;
        LWP::UserAgent->import();
    };
    die "perl-libwww-perl rpm is missing. Please install it first\n" if $@;
    eval {
        require JSON;
        JSON->import();
    };
    die "perl-JSON rpm is missing. Please install it first\n" if $@;
    
    eval { require LWP::Protocol::https };
    die "perl-LWP-Protocol-https rpm is missing. Please install it first\n" if $@;
    
    eval {
        require DBD::Pg;
    };
    die "perl-DBD-Pg rpm is missing. Please install it first\n" if $@;
}

use File::Basename qw(dirname);
use LWP::UserAgent;
use HTTP::Request::Common qw(GET POST PUT DELETE);
use Getopt::Long qw(:config no_ignore_case bundling);
use JSON;
use lib "$ENV{HOME}/docker/current/lib";
use Overcast::Session;
use Overcast::DB;

# CONS
my $BASE_DIR = dirname(__FILE__).'/..';
my $ENV_DIR  = "$BASE_DIR/env";
my $ENV_CUR  = "~/.overcast";
my $APP_CONF = "$ENV{HOME}/docker/current/etc/env.conf";
my $DEFAULT_LOGIN_USERNAME = "realmadmin";
my $DEFAULT_LOGIN_PASSWORD = "topse";
my $USAGE = <<END;
USAGE:  realm-helper [OPTIONS] <COMMAND> [<REALM_JSON>]
        realm-helper create JSON_FILE
        realm-helper (-i|--realmid=)realmId update JSON_FILE
        realm-helper (-i|--realmid=)realmId delete
        realm-helper (-i|--realmid=)realmId get
        realm-helper (-i|--realmid=)realmId get-features
        realm-helper (-i|--realmid=)realmId set-feature NAME_1=VALUE_1 ... [NAME_N=VALUE_N] ...
        
        realm-helper setup-vmx2
        realm-helper scan-pools
        realm-helper (-i|--realmid=)realmId [--show] fix-pools
OPTIONS:
        -i, --realmid
                Realm ID
        -u, --username
                Overcast user name
        -p, --password
                Overcast user password
END

# VAR
my %Conf;
my $CurEnv;
my $EnvConf;
my $LoginUserName;
my $LoginPassword;
my $LoginHeader;
my $HttpUrl;
my $Cmd;
my $RealmId;
my $ShowOnly;
my $RealmFile;
my $dbh;
my %dbs;
my $redis;

# MAIN
# Read current env name
$CurEnv = `cat $ENV_CUR`;
chomp $CurEnv if defined $CurEnv;
die "Cannot read current env!\n" if not defined $CurEnv;

# Load environment
$EnvConf = $ENV_DIR.'/environment.'.$CurEnv;
open FH, $EnvConf or die "Cannot read $EnvConf => $!";
%Conf = map {/^(\w+)=(.*)$/} grep {/^\w+=/} <FH>;
close FH;

# Read app conf and merge with env conf
open FH, $APP_CONF or die "Cannot read $APP_CONF: $!";
my %cfg = map {/^(\w+)=(.*)$/} grep {/^\w+=/} <FH>;
close FH;
%Conf = (%Conf, %cfg);
$LoginHeader = $Conf{OVERCAST_LOGIN_HEADER};
$HttpUrl = $Conf{OVERCAST_EXT_HTTP_URL};

die "Missing Overcast HTTP URL in configuration!\n" if not $HttpUrl;

# Command line
GetOptions(
    'u|username=s' => \$LoginUserName,
    'p|password=s' => \$LoginPassword,
    'i|realmid=s'  => \$RealmId,
    'show'         => \$ShowOnly
) or die $USAGE;

$Cmd = shift @ARGV;
($RealmFile) = @ARGV;
die $USAGE if not $Cmd;
die "Realm ID must be provided!\n\n$USAGE\n" if $Cmd !~ /^(create|setup-vmx2|scan-pools)$/i and not $RealmId;

$LoginUserName = $DEFAULT_LOGIN_USERNAME if not defined $LoginUserName;
$LoginPassword = $DEFAULT_LOGIN_PASSWORD if not defined $LoginPassword;
my ($sid, $token);

for ($Cmd) {
    /^create$/i and do {
                my ($resp, $json) = request('POST');
                print "Realm created successfully\nRealmID: $json->{response}{realm}{id}\n";
                last;
    };
    /^update$/i and do {
                my $resp = request('PUT');
                print "Realm updated successfully\n";
                last;
    };
    /^get$/i    and do {
                my ($resp, $json) = request('GET');
                print to_json($json, {utf8 => 1, pretty => 1});
                last;
    };
    /^delete$/i and do {
                my $resp = request('DELETE');
                print "Realm deleted successfully\n";
                last;
    };
    /^set-feature$/i and do {
        my %feat = map {/^(.+?)=(.+)$/} grep {/^.+=/} @ARGV;
        set_feature(\%feat);
        last;
    };
    /^get-features$/i and do {
        my %feat = %{ get_features() };
        foreach my $name (keys %feat) {
            print "$name: $feat{$name}\n";
        }
    };
    /^setup-vmx2$/i and do {
        setup_vmx2();
    };
    /^scan-pools$/i and do {
        scan_pools();
    };
    /^fix-pools$/i and do {
        fix_pools();
    };
}

# SUB
sub login {
    ($sid, $token) = OpenSession($HttpUrl, $LoginUserName, $LoginPassword);
    die "Cannot obtain session\n" if not $sid;
}

sub request {
    my $method = shift;
    
    login if not $sid;
    my $ua = $Overcast::Session::UA;
    my $content;

    if ($method eq 'PUT' or $method eq 'POST') {
        die $USAGE if not defined $RealmFile;
        die "Cannot read $RealmFile: $!" if not open FH, $RealmFile;
        $content .= $_ while <FH>;
        close FH;
    }

    my $url = "https://$HttpUrl/api/realm";
    $url .= "/$RealmId" if defined $RealmId;

    no strict 'refs';
    my $req = $method->($url, Content_Type => 'application/json', Content => $content);
    $ua->timeout(30);
    my $resp = $ua->request($req);
    #print $resp->code . ": ".$resp->content;
    my $code = $resp->code;
    if (not $resp->content) {
        die "Request failed (http code ".$resp->code.")\n" if $resp->is_error;
    }
    my $json = eval { decode_json($resp->content) };
    if ($@) {
	warn "Bad response from service: $@\n";
	warn "\nResponse:\n".$resp->content ."\n";
	exit 1;
    }
    die "Request failed: $json->{error}\n" if $json->{code} !~ /^2/;

    return wantarray ? ($resp, $json) : $resp;
}

sub load_env {
    my $cluster = `cat ~/.overcast`; chomp $cluster;
    my $envfile = dirname(__FILE__).'/../env/environment.'.$cluster;
    if (open FH, $envfile) {
        my %env = map {/^(\w+)=(.+)$/} grep {/^\w+=/} <FH>;
        $ENV{$_}=$env{$_} foreach keys %env;
        close FH;
    }
}

sub db_connect {
    return if $dbh;
    load_env;
    $ENV{OVERCAST_DB_SERVICE_HOST} = $ENV{OVERCAST_DB_EXTERNAL_IP};
    $ENV{OVERCAST_DB_SERVICE_PASS} = $ENV{OVERCAST_DB_ADMIN_PASSWD};
    $ENV{OVERCAST_DB_SERVICE_USER} = $ENV{OVERCAST_DB_ADMIN_USER};
    $dbh = eval { DBConnect({RaiseError => 1, PrintError => 1}) };
    die "Unable to connect to DB: $@" if $@;
    $dbs{GET_ENTITLEMENT} = $dbh->prepare("SELECT val FROM confdb._obj_attr where obj=? and attr='ENTITLEMENT'");
    $dbs{UPDATE_ENTITLEMENT} = $dbh->prepare("UPDATE confdb._obj_attr SET val=? WHERE obj=? AND attr='ENTITLEMENT'");
    $dbs{GET_REALM_ROLES} = $dbh->prepare(
        "SELECT o1.obj, o2.name FROM confdb._objs o1 INNER JOIN confdb._objs o2 ON o1.obj=o2.realm 
        WHERE o1.type='realm' AND o2.type='role' AND o1.deleted=0 AND o2.deleted=0 AND o2.protected = 1"
    );
}

sub redis_connect {
    eval {
        require Redis;
        Redis->import();
    };
    if ($@) {
        die "perl-Redis rpm is missing. Please install it first\n" if $@;
    }
    my $host = $ENV{OVERCAST_REDIS_EXTERNAL_IP};
    my $port = $ENV{OVERCAST_REDIS_EXTERNAL_PORT} || 6379;
    $redis->quit if $redis;
    $redis = Redis->new(
        server => "$host:$port",
        cnx_timeout => 5,
        reconnect => 60, every => 1_000_000,
        name => "realm-helper/redis"
    );
}

sub get_entitlement {
    db_connect;
    my $row = eval { $dbs{GET_ENTITLEMENT}->execute($RealmId); $dbs{GET_ENTITLEMENT}->fetchrow_arrayref };
    die "DB: $@" if $@;
    die "No entitlement found for specified Realm ID\n" if not $row->[0];
    my $json = eval { decode_json $row->[0] };
    die "Error parsing entitlement: $@" if $@;
    return $json;
}

sub set_feature {
    my $feats_new = shift;
    die "No features specified" if not $feats_new or not %$feats_new;
    die "Must provide Realm ID" if not $RealmId;
    
    my $ent = get_entitlement;
    my $feats_cur = $ent->{overcast}{features};
    foreach my $fnew (keys %$feats_new) {
        my $exists = 0;
        foreach my $fcur (@$feats_cur) {
            if ($fcur->{feature} eq $fnew) {
                $exists = 1;
                $fcur->{unitsSubscribed} = $feats_new->{$fnew};
                last;
            }
        }
        push @$feats_cur, {
            feature => $fnew,
            unitsSubscribed => $feats_new->{$fnew}
        } if not $exists;
    }
    
    my $new_ent = JSON->new->utf8->pretty(0)->encode($ent);
    eval {
        $dbs{UPDATE_ENTITLEMENT}->execute($new_ent, $RealmId);
    };
    die "DB: $@\n" if $@;
}

sub get_features {
    my $ent = get_entitlement;
    
    my %feat;
    foreach my $feat (@{$ent->{overcast}->{features}}) {
        my $name = $feat->{feature};
        my $val = $feat->{unitsSubscribed};
        $feat{$name} = $val;
    }
    return \%feat;
}

sub setup_vmx2 {
    db_connect;
    my $rows = eval { $dbs{GET_REALM_ROLES}->execute; $dbs{GET_REALM_ROLES}->fetchall_arrayref };
    die "DB: $@" if $@;
    my %roles;
    my @realms2fix;
    foreach my $row (@$rows) {
        my ($realm, $role) = @$row;
        $roles{$realm} = [] if not $roles{$realm};
        push @{$roles{$realm}}, $role;
    }
    foreach my $realm (keys %roles) {
        my $roles = $roles{$realm};
        next if not $roles or not ref $roles or ref $roles ne 'ARRAY';
        my $found = 0;
        foreach my $role (@$roles) {
            $found = 1, last if $role eq 'vMX2-DS Role';
        }
        next if $found;
        push @realms2fix, $realm;
    }
    
    my $api = `kubectl get pods -n overcast | grep api-php | head -1 | cut -f 1 -d' '`; chomp $api;
    if (not @realms2fix) {
        print "All realms are vMX2 ready\n";
        return;
    }
    foreach my $realm (@realms2fix) {
        print "setup vMX2 for realm $realm\n";
        `kubectl exec $api -- curl -s -X POST -H \"X-session: $LoginHeader\" -H \"Content-Type: application/json\" -d \"{\\\"realmid\\\":\\\"$realm\\\"}\" http://localhost:8000/api/call/createVmxObjects`;
    }
}

sub scan_pools {
    db_connect;
    my $rows = eval {
        $dbh->selectall_arrayref("
            select o.realm,o.obj,o.name,a1.val as bucket,a2.val as ttl 
            from confdb._objs o inner join confdb._obj_attr a1 on o.obj=a1.obj 
                inner join confdb._obj_attr a2 on o.obj=a2.obj 
            where o.type='storagepool' and o.deleted=0 and a1.attr='STORAGE_BUCKET' and a2.attr='TTL'")
    };
    foreach my $row (@$rows) {
        my ($realm, $obj, $name, $bucket, $ttl) = @$row;
        my $bucket_days = $1 if $bucket =~ /vasm-(\d+)$/;
        next if not $bucket_days;
        my $bucket_hours = $bucket_days * 24;
        if ($bucket_hours < $ttl) {
            print "Misconfigured pool: realm=$realm  [$obj] $name => expected_ttl=$ttl; current_ttl=$bucket_hours\n";
        }
    }
}

sub fix_pools {
    db_connect;
    #redis_connect;
    
    print "================================ POOLS ==========================\n";
    my $rows = eval {
        $dbh->selectall_arrayref("
            select o.realm,o.obj,o.name,a1.val as bucket,a2.val as ttl 
            from confdb._objs o inner join confdb._obj_attr a1 on o.obj=a1.obj 
                inner join confdb._obj_attr a2 on o.obj=a2.obj 
            where o.type='storagepool' and o.realm='$RealmId' and o.deleted=0 and a1.attr='STORAGE_BUCKET' and a2.attr='TTL'")
    };
    foreach my $row (@$rows) {
        my ($realm, $obj, $name, $bucket, $ttl) = @$row;
        my $bucket_days = $2 if $bucket =~ /^(.+)-vasm-(\d+)$/;
        my $project_id = $1;
        next if not $bucket_days;
        my $bucket_hours = $bucket_days * 24;
        my $fix_bucket_days = int($ttl / 24);
        if ($ttl % 24 != 0) {
            $fix_bucket_days++;
        }
        my $fix_bucket = "${project_id}-vasm-${fix_bucket_days}";
        next if $bucket_hours >= $ttl;
        print "Misconfigured pool: realm=$realm  [$obj] $name => expected_ttl=$ttl; current_ttl=$bucket_hours; fix=$fix_bucket\n";
        next if $ShowOnly;
        $dbh->do("UPDATE confdb._obj_attr SET val='$fix_bucket' WHERE obj='$obj' AND attr='STORAGE_BUCKET'");
    }

    print "================================ CAMERAS ========================\n";
    my $cams;
    $rows = eval {
        $dbh->selectall_arrayref("
            select o.realm,o.obj,o.name,a1.val as bucket,a2.val as hist 
            from confdb._objs o inner join confdb._obj_attr a1 on o.obj=a1.obj 
                inner join confdb._obj_attr a2 on o.obj=a2.obj 
            where o.type='camera' and o.deleted=0 and a1.attr='STORAGE_BUCKET' and a2.attr='STORAGE_BUCKETS' and o.realm='$RealmId'")
    };
    my $project_id;
    foreach my $row (@$rows) {
        my ($realm, $obj, $name, $bucket, $hist) = @$row;
        $hist = eval { decode_json $hist };
        warn("Error parsing STORAGE_BUCKETS for camera [$obj] '$name' : $@"), next if $@;
        warn("Empty pool history for camera [$obj] '$name'"), next if not $hist or ref($hist) ne 'ARRAY' or not  @$hist;
        my $ttl = $hist->[-1]->{ttlHrs};
        my $cam_bucket = $hist->[-1]->{bucket};
        my $poolid = $hist->[-1]->{poolid};
        warn("Empty/zero ttlHrs in STORAGE_BUCKETS for camera [$obj] '$name' : $@"), next if not $ttl;
        warn("Empty bucket in STORAGE_BUCKETS for camera [$obj] '$name' : $@"), next if not $cam_bucket;
        warn("Empty poolid in STORAGE_BUCKETS for camera [$obj] '$name' : $@"), next if not $poolid;
        my $bucket_days = $2 if $cam_bucket =~ /^(.+)-vasm-(\d+)$/;
        warn("Incorrect bucket name '$bucket' in STORAGE_BUCKETS for camera [$obj] '$name' : $@"), next if not $bucket_days;
        $project_id = $1 if $1 and not $project_id;
        my $bucket_hours = $bucket_days * 24;
        next if $bucket_hours >= $ttl; # OK
        
        my $fix_bucket_days = int($ttl / 24);
        if ($ttl % 24 != 0) {
            $fix_bucket_days++;
        }
        my $fix_bucket = "$project_id-vasm-$fix_bucket_days";
        push @$hist, {
            ttlHrs => "$ttl",
            poolid => "$poolid",
            bucket => "$fix_bucket",
            assigned => time
        };
        $hist = encode_json($hist);
        print "Need2Fix: camera [$obj] '$name' => ttl=$ttl; bucket_hours=$bucket_hours; fix=$fix_bucket\n";
        next if $ShowOnly;
        eval {
            $dbh->do("UPDATE confdb._obj_attr SET val='$fix_bucket' WHERE obj='$obj' AND attr='STORAGE_BUCKET'");
            $dbh->do("UPDATE confdb._obj_attr SET val='$hist' WHERE obj='$obj' AND attr='STORAGE_BUCKETS'");
        };
    }
}

END {
    CloseSession;
    foreach my $st (keys %dbs) {
        eval { $dbs{$st}->finish };
    }
    eval { $dbh->disconnect } if $dbh;
}
