#!/usr/local/bin/perl -w

require 5.8.2;
use Getopt::Std;
use DeployUtils::Logger;

use vars qw ( $opt_l $opt_h $opt_a $opt_A $opt_d $opt_D $opt_y );

getopts('l:haAdDy');

help() if ( defined($opt_h) );
help("Must supply nodespec file & new version number as args") if ( !defined($ARGV[0]) || !defined($ARGV[1]) );
help("Cant use -a & -A together") if ( defined($opt_a) && defined($opt_A) );
help("Cant use -d & -D together") if ( defined($opt_d) && defined($opt_D) );

setLogLevel($opt_l) if ( defined($opt_l) );

my $ver = shift;
my $retval;

while ( my $nsfile = shift )
{
    $retval = pkgmnt::init($ver, $nsfile,
                            (defined($opt_d)) ? "NO" : (defined($opt_D)) ? "YES" : "ASK",
                            (defined($opt_a)) ? "NO" : (defined($opt_A)) ? "YES" : "ASK",
                            (defined($opt_y)) ? 1 : 0 );
    
    $retval = pkgmnt::getNodeSpecFromDM() if ( $retval );
    
    if ( $retval )
    {
        pkgmnt::checkForDeleted();
        pkgmnt::checkForNew();
        pkgmnt::mergeHashs();
        $retval = pkgmnt::updateNodeSpecFile();
    }

    LogError("-x", "Unable to process Nodespec file [$nsfile]") if ( ! $retval );
}



sub help
{
    my $error = shift;
    my $usage = qq(
updateNodeSpec.pl [-l N] [-d|-D] [-a|-A] [-y] NewVersion nodespecfile 
where
    NewVersion   : The new BOM version for the nodespec file.
    nodespecfile : Specifies the nodespec file to update.

Options:
    -l  : Sets log level 1-5
    
    -d  : Dont Automatically Delete packages from nodespec file if DM does not have them.
    -D  : Do Automatically Delete packages from nodespec file if DM does not have them.
        : DEFAULT if neither supplied is to interactively ask for each package.
        
    -a  : Dont Automatically Add new packages from DM into nodespec file.
    -A  : Do Automatically Add new packages from DM into nodespec file.
        : DEFAULT if neither supplied is to interactively ask for each package.
        
    -y  : Automatically answer yes to final Update nodespec file question.
    );
    
    print "$error\n" if ( defined $error );
    print $usage;
    exit 1;
}


#============================================================================
package pkgmnt;

use strict;
use DBI;
use Data::Dumper;
use DeployUtils::Logger;
use JatsRmApi;

our ($G_NodeSBOMVersion, $G_NodeName, %G_NodeProcessHash, %G_NodePkgHash, %G_NodeDmInfo);
our %newPkgs;
our ( @actionlog, $actions );

our $nodeSpecFile;
our ( $newVer, $bomver, $lifecycle );
our ( $autoDelete, $autoAdd, $autoUpdate );

sub init
{
    ( $newVer, $nodeSpecFile, $autoDelete, $autoAdd, $autoUpdate ) = @_;

    LogNorm("");
    LogNorm("");
    LogNorm("Upgrading NodeSpec file [$nodeSpecFile] to $newVer");

    if ( ! -f $nodeSpecFile )
    {
        LogError("-x", "Nodespec file [$nodeSpecFile] does not exist");
        return 0;
    }
    
    # version must start & end with numbers and contain numbers & '.' in between
    if ( $newVer =~ /^[0-9]+[0-9\.]*[0-9]+$/ )
    {
        # split version number into number sections
        my @verNums = split(/\./, "$newVer");
        #last section is the lifecycle number so remove from list
        $lifecycle = pop(@verNums);
        #now join rest of numbers
        $bomver = join(".", @verNums);
    }
    else
    {
        LogError("-x", "Version number format incorrect, must contain digits & '.' eg (1.2 or 1.1.2)");
        return 0;
    }
    
    $autoDelete = uc $autoDelete;
    $autoAdd = uc $autoAdd;

    if ( $autoDelete ne "YES" && $autoDelete ne "NO" && $autoDelete ne "ASK" )
    {
        LogError("-x", "autoDelete flag must be YES, NO or ASK");
        return 0;
    }
    if ( $autoAdd ne "YES" && $autoAdd ne "NO" && $autoAdd ne "ASK" )
    {
        LogError("-x", "autoAdd flag must be YES, NO or ASK");
        return 0;
    }

    $Data::Dumper::Purity = 1;
    $Data::Dumper::Quotekeys = 0;
    $Data::Dumper::Sortkeys = 1;
    $Data::Dumper::Indent = 1;

    $G_NodeSBOMVersion = "";
    $G_NodeName = "";
    %G_NodeProcessHash = ( );
    %G_NodePkgHash = ( );
    %G_NodeDmInfo = ( );    
    %newPkgs = ( );
    $#actionlog = -1;
    $actions = 0;

    # require the nodespk file to get details
    require $nodeSpecFile;

    if ( ! defined($G_NodeDmInfo{ProjectName}) && ! defined($G_NodeDmInfo{BranchName}) &&
         ! defined($G_NodeDmInfo{BomName}) && ! defined($G_NodeDmInfo{NodeName}) &&
         ! defined($G_NodeDmInfo{OsName}) )
    {
        LogError("-x", "G_NodeDmInfo hash in nodespec does not have enough info");
        return 0;
    }

    LogInfo("ProjectName : $G_NodeDmInfo{ProjectName}");
    LogInfo("BranchName  : $G_NodeDmInfo{BranchName}");
    LogInfo("BomName     : $G_NodeDmInfo{BomName}");
    LogInfo("NodeName    : $G_NodeDmInfo{NodeName}");
    LogInfo("OsName      : $G_NodeDmInfo{OsName}");
        
    return 1;
}



sub getNodeSpecFromDM
{
    my $DM;
    connectDM( \$DM );

    my ($m_sqlstr ) = qq /
SELECT oc.prod_id, pkg.pkg_name, pv.pkg_version, pv.pv_description, pv.v_ext, pv.is_patch
FROM deployment_manager.dm_projects dm_,
     deployment_manager.branches branches,
     deployment_manager.boms boms,
     deployment_manager.bom_names bom,
     deployment_manager.network_nodes net,
     deployment_manager.bom_contents bomcont,
     deployment_manager.operating_systems ope,
     deployment_manager.os_contents oc,
     release_manager.package_versions pv,
     release_manager.PACKAGES pkg
WHERE ( (dm_.proj_id = branches.proj_id) AND (branches.branch_id = boms.branch_id) AND
        (bom.bom_name_id = boms.bom_name_id) AND (boms.bom_id = bomcont.bom_id) AND
        (net.node_id = bomcont.node_id) AND (net.node_id = ope.node_id) AND
        (ope.os_id = oc.os_id) AND (oc.prod_id = pv.pv_id) AND (pv.pkg_id = pkg.pkg_id) AND
        (dm_.proj_name = '$G_NodeDmInfo{ProjectName}') AND
        (branches.branch_name = '$G_NodeDmInfo{BranchName}') AND
        (bom.bom_name = '$G_NodeDmInfo{BomName}') AND
        (boms.bom_version = '$bomver') AND
        (boms.bom_lifecycle = '$lifecycle') AND
        (net.node_name = '$G_NodeDmInfo{NodeName}') AND
        (ope.os_name = '$G_NodeDmInfo{OsName}')
       )
    /;
    my ($sth )  = $DM->prepare($m_sqlstr);
    my @row;
    my $found = 0;
    
    LogDebug("Executing SQL statement\n$m_sqlstr");
    if ( defined($sth) )
    {
        if ( $sth->execute() )
        {
            if ( $sth->rows )
            {
                while ( @row = $sth->fetchrow_array )
                {
                    # Skip Patches
                    if ( !defined($row[5]) || $row[5] !~ /y/i )
                    {
                        # if extension defined
                        if ( defined($row[4]) )
                        {
                            # remove dot from start of extension
                            $row[4] =~ s/^\.//;
                            # remove extension from version
                            if ( $row[2] =~ /\.$row[4]$/ )
                            {
                                $row[2] =~ s/\.$row[4]$//;
                            }
                            else
                            {
                                LogWarn("Pkg [$row[1]] version [$row[2]] & extension [$row[4]] do not match");
                            }
                        }
                        else
                        {
                            $row[4] = "";
                        }
                        $row[3] = "" if ( ! defined($row[3]) );
                        chomp $row[3];
                        
                        # if pkg version defined in _Forceversion then we  force the version
                        if ( defined($G_NodeDmInfo{_ForceVersion}{$row[1]}) && defined($G_NodeDmInfo{_ForceVersion}{$row[1]}{$row[2]}))
                        {
                            LogDebug("Forcing pkg [$row[1]] from version [$row[2]] to [$G_NodeDmInfo{_ForceVersion}{$row[1]}{$row[2]}]");
                            $newPkgs{$row[1]}{PkgVersion}   = $G_NodeDmInfo{_ForceVersion}{$row[1]}{$row[2]};
                        }
                        else
                        {
                            $newPkgs{$row[1]}{PkgVersion}   = $row[2];
                        }
                        $newPkgs{$row[1]}{PkgProjAcronym}   = $row[4];
                        $newPkgs{$row[1]}{PkgDesc}          = $row[3];
                        $found = 1;
                        LogDebug("Found DM Pkg [$row[1]], ver [$row[2]], proj [$row[4]], desc [$row[3]]");
                    }
                }
            }
            if ( ! $found )
            {
                LogError("-x", "Found No Information about node in Deployment Manager");
                return 0;
            }            
            $sth->finish();
        }
        else
        {
            LogError("-x", "Unable to execute SQL statement in DBI");
            return 0;
        }
    }
    else
    {
        LogError("-x", "Unable to prepare SQL statement in DBI");
        return 0;
    }

    disconnectDM(\$DM);
    return 1;
}


sub checkForDeleted
{
    foreach my $i ( keys %G_NodePkgHash )
    {
        # if pkg not defined in newPkgs from DM AND its not in the NoDelete List then we ask or process
        # that is is pkg is not in DM but it is in NoDelete list then we keep it as if it was
        if ( ! defined($newPkgs{$i}) && ! defined($G_NodeDmInfo{_NoDelete}{$i}) )
        {
            # ask if neither -d or -D set
            my $ans = "";
            $ans = askYN("Pkg [$i] is not in new list, remove") if ( $autoDelete eq "ASK" );
            
            # if -d defined or asn = N then we dont delete so just log
            if ( $autoDelete eq "NO" || $ans eq "N" )
            {
                push(@actionlog, "KEEP   Old Pkg [$i]: Version [$G_NodePkgHash{$i}{PkgVersion}] Project [$G_NodePkgHash{$i}{PkgProjAcronym}] from original Nodespec");
                # keeping an old package is not a real action, as nothing changes in nodespec
            }
            # if -D defined or ans = Y then delete
            elsif ( $autoDelete eq "YES" || $ans eq "Y" )
            {
                push(@actionlog, "DELETE Old Pkg [$i]: Version [$G_NodePkgHash{$i}{PkgVersion}] Project [$G_NodePkgHash{$i}{PkgProjAcronym}] from original Nodespec");
                delete($G_NodePkgHash{$i});
                $actions++;
            }
        }
    }
}


sub checkForNew
{
    foreach my $i ( keys %newPkgs )
    {
        if ( ! defined($G_NodePkgHash{$i}) )
        {
            # if pkg is defined in noAdd list then we just remove, otherwise process as normal
            if ( defined($G_NodeDmInfo{_NoAdd}{$i}) )
            {
                delete($newPkgs{$i});
            }
            else
            {
                # ask if neither -d or -D set
                my $ans = "";
                $ans = askYN("Pkg [$i] has been added, add") if ( $autoAdd eq "ASK" );

                # if -A defined or ans = Y then we add so just log
                if ( $autoAdd eq "YES" || $ans eq "Y" )
                {
                    push(@actionlog, "ADD    New Pkg [$i]: Version [$newPkgs{$i}{PkgVersion}] Project [$newPkgs{$i}{PkgProjAcronym}] from Deployment Manager");
                    $actions++;
                }
                # if -a defined then dont Add (remove from dm list), otherwise ask
                elsif ( $autoAdd eq "NO" || $ans eq "N" )
                {
                    push(@actionlog, "SKIP   New Pkg [$i]: Version [$newPkgs{$i}{PkgVersion}] Project [$newPkgs{$i}{PkgProjAcronym}] from Deployment Manager");
                    delete($newPkgs{$i});
                    # deleting a new package is not a real action, as nothing changes in nodespec
                }
            }
        }
    }
}


sub mergeHashs
{
    my $str;
    my $updated;
    foreach my $i ( keys %newPkgs )
    {
        if ( defined($G_NodePkgHash{$i}) )
        {
            $str = "UPDATE Pkg [$i]: ";
            $updated = 0;
            if ( $newPkgs{$i}{PkgVersion} ne $G_NodePkgHash{$i}{PkgVersion} )
            {
                $str .= "Version [$G_NodePkgHash{$i}{PkgVersion}]=>[$newPkgs{$i}{PkgVersion}] ";
                $updated = 1;
                $G_NodePkgHash{$i}{PkgVersion} = $newPkgs{$i}{PkgVersion};
            }

            if ( ($newPkgs{$i}{PkgProjAcronym} || '') ne ($G_NodePkgHash{$i}{PkgProjAcronym} || '') )
            {
                $str .= "Project [$G_NodePkgHash{$i}{PkgProjAcronym}]=>[$newPkgs{$i}{PkgProjAcronym}] ";
                $updated = 1;
                if ( $newPkgs{$i}{PkgProjAcronym} )
                {
                    $G_NodePkgHash{$i}{PkgProjAcronym} = $newPkgs{$i}{PkgProjAcronym};
                }
                else
                {
                    delete $G_NodePkgHash{$i}{PkgProjAcronym};
                }
            }

            if ( $newPkgs{$i}{PkgDesc} ne $G_NodePkgHash{$i}{PkgDesc} )
            {
                $str .= "Description [$G_NodePkgHash{$i}{PkgDesc}]=>[$newPkgs{$i}{PkgDesc}] ";
                $updated = 1;
                $G_NodePkgHash{$i}{PkgDesc} = $newPkgs{$i}{PkgDesc};
            }
            if ( $updated )
            {
                push(@actionlog, $str);
                $actions++;
            }
        }
        else
        {
            $G_NodePkgHash{$i}{PkgVersion} = $newPkgs{$i}{PkgVersion};
            $G_NodePkgHash{$i}{PkgProjAcronym} = $newPkgs{$i}{PkgProjAcronym};
            $G_NodePkgHash{$i}{PkgDesc} = $newPkgs{$i}{PkgDesc};
            $G_NodePkgHash{$i}{ERGPkgFlag} = "T";
            $G_NodePkgHash{$i}{CheckVersionFlag} = "T";
            # no need to log as its already done
        }
    }
}
 
 
 
sub updateNodeSpecFile
{
    my ( $buffer, $line );
    LogNorm("--------------------------------------------------------------------");

    if ( $G_NodeSBOMVersion eq $newVer && $actions == 0 )
    {
        LogNorm("NO ACTIONS will be performed on the nodespec file [$nodeSpecFile]");
        LogNorm("--------------------------------------------------------------------");
    }
    else
    {
        LogNorm("The Following actions will be performed on the nodespec file [$nodeSpecFile]");
        LogNorm("UPDATE BOM version from $G_NodeSBOMVersion to $newVer");
        foreach my $i ( sort @actionlog )
        {
            LogNorm($i);
        }
        LogNorm("--------------------------------------------------------------------");
        if ( $autoUpdate || askYN("Update nodespec file as described ") eq "Y" )
        {
            # read all comment lines up to first comment line
            if ( ! open(NODESPEC, "<$nodeSpecFile") )
            {
                LogError("-x", "Unable to open Node Spec File to update");
                return 0;
            }
            do
            {
                $line = <NODESPEC>;
                $line =~ s/\r\n/\n/;
                $buffer .= $line if ( $line =~ /^#/ );
            }
            while ( $line =~ /^#/ );
            close NODESPEC;
        
            # now lets update the file
            if ( ! open(NODESPEC, ">$nodeSpecFile") ) 
            {
                LogError("-x", "Unable to open Node Spec File to update");
                return 0;
            }
            binmode(NODESPEC);
        
            print NODESPEC "$buffer\n";

            # Dumper prints hashes as references ie $hash = { .... };
            # Where the original nodespec files were real hashes ie %hash = ( .... );
            # So we dump hashes to buffer and replace the first line with real hash & (
            # and replace last line }; with );
        
            $buffer = Data::Dumper->Dump([ \%G_NodeDmInfo ], [ "pkgmnt::G_NodeDmInfo" ]);
            $buffer =~ s/^\$(pkgmnt::G_NodeDmInfo *= *)\{(.*)\}(.*)$/\%$1\($2\)$3/ms;
            print NODESPEC "$buffer\n";
        
            print NODESPEC Data::Dumper->Dump([ $newVer ], [ "pkgmnt::G_NodeSBOMVersion" ]) . "\n";

            print NODESPEC Data::Dumper->Dump([ $G_NodeName ], [ "pkgmnt::G_NodeName" ]) . "\n";

            $buffer = Data::Dumper->Dump([ \%G_NodeProcessHash], [ "pkgmnt::G_NodeProcessHash" ]);
            $buffer =~ s/^\$(pkgmnt::G_NodeProcessHash *= *)\{(.*)\}(.*)$/\%$1\($2\)$3/ms;
            print NODESPEC "$buffer\n";
    
            $buffer = Data::Dumper->Dump([ \%G_NodePkgHash ], [ "pkgmnt::G_NodePkgHash" ]);
            $buffer =~ s/^\$(pkgmnt::G_NodePkgHash *= *)\{(.*)\}(.*)$/\%$1\($2\)$3/ms;
            print NODESPEC "$buffer\n1;\n";
    
            close NODESPEC;
        
            LogNorm("Updated nodespec file $nodeSpecFile");
        }
    }
    return 1;
}

    
        
sub askYN
{
    use Term::ReadKey;
    my $msg = shift;
    $msg .= " [ynq] ? ";
    my $ch = "";
    my $rMode = 0;
    while ( $ch ne "Y" && $ch ne "N" )
    {
        LogNorm("-n", $msg);
        #$ch = uc HotKey::readkey();
        eval { ReadMode('cbreak'); $rMode = 1 }; # do in eval because it barfs on cygwin term
        $ch = uc ReadKey(0);
        eval { ReadMode('normal') };
        chomp $ch;
        if ( $rMode )
        {
            print "\n";
        }
        else
        {
            # if not rMode then a CR needs to be entered so lets remove it.
            my $a = <STDIN>;
        }
        LogError("Quitting application") if ( $ch eq "Q" );
    }
    return $ch;
}


1;


#============================================================================
# HotKey.pm
#package HotKey;

#use strict;
#use POSIX qw(:termios_h);
#my ($term, $oterm, $echo, $noecho, $fd_stdin);

#BEGIN {
#$fd_stdin = fileno(STDIN);
#$term     = POSIX::Termios->new();
#$term->getattr($fd_stdin);
#$oterm     = $term->getlflag();

#$echo     = ECHO | ECHOK | ICANON;
#$noecho   = $oterm & ~$echo;
#}

#sub cbreak {
#    $term->setlflag($noecho);  # ok, so i don't want echo either
#    $term->setcc(VTIME, 1);
#    $term->setattr($fd_stdin, TCSANOW);
#}

#sub cooked {
#    $term->setlflag($oterm);
#    $term->setcc(VTIME, 0);
#   $term->setattr($fd_stdin, TCSANOW);
#}

#sub readkey {
#    my $key = '';
#    cbreak();
#    sysread(STDIN, $key, 1);
#    cooked();
#    return $key;
#}

#END { cooked() }
#1;
