#
#  Mchange.pm
#  --toplevel database queries for mchange
#
#  Brian Hackbarth
#  hackbart@cs.wisc.edu
#

package Mchange;

use strict;

use Mchange::CSDB;
use Mchange::Splat;
use Mchange::Machine;
use Mchange::Script;
use Mchange::RT; # bhw
use Mchange::Error;
use Data::Validate::Domain;

sub new {

  my $class = shift;
  
  my $self;

  my %args = @_;
  
  #set up the hash for all of the data
  my $eh = new Mchange::Error;

  if ( $args{error} eq 'text' ) {
    $self->{eh} = sub {$eh->text_err( @_ ) };
  }
  else {
    $self->{eh} = sub { $eh->gui_err( @_ ) };
  }

  $self->{invdb} = new Mchange::CSDB( $self->{eh} );
  $self->{splatdb} = new Mchange::Splat( $self->{eh} );
  $self->{rtdb} = new Mchange::RT; #bhw

  my $ret = bless $self, $class;
  return $ret;
  
}

sub DESTROY {

  my $self = shift;
  #print "Mchange.pm DESTROY\n";
  
}

#
#  register_machine
#  --cache the data about this machine
#

sub register_machine {

  my ( $self, $mname ) = @_;

  #if already cached, don't destroy data
  return $self->{machines}{$mname} if $self->{machines}{$mname};

  return $self->{eh}->( "Machine $mname doesn't exist" )
    unless $self->machine_exists( $mname );
    
  #set up all the parts a machine has
  my $part_ref = $self->get_machine_parts( $mname );

  #and now take care of the attributes
  my $attrib_ref = $self->get_machine_attrib( $mname );

  my $machine = new Mchange::Machine( $part_ref, $attrib_ref );
  $self->{machines}{$mname} = $machine;

  #hackity, hack, hack to get rt subject/script name
  $self->{transact}{machine} = $mname;

  return $machine;
  
}

#
#  print_info
#  --print out the current state of a machine
#

sub print_info {

  my ( $self, $machine ) = @_;

  unless ( $self->{machines}{$machine} ) {
    print "$machine is not registered--can't 'print_info'\n";
    return $self->{eh}->( "$machine is not registered--can't 'print_info'" );
  }

  $self->{machines}{$machine}->print_info;
    
}


#
#  move_part
#  --schedule a part move
#

sub move_part {

  my ( $self, $mname, $invnum ) = @_;

  return $self->{eh}->( "Ummm...you have to enter a part..." )
    unless $invnum;
     
  #check for a valid invnum first
  $invnum =~ tr/a-z/A-Z/; # convert it to upper, c000123 will work then.
  return $self->{eh}->( "$invnum is not a valid inventory number" )
    unless $invnum =~ m/\b(C|F|U|S|L)(\d|C)(\d){5}\b/;

  #check if the part is already part of a transaction
  if ( my $ref = $self->{transact}{parts}{$invnum} ) {
    return $self->{eh}->( "Part $invnum scheduled to move to $ref->{machine}--undo move first\n" );
  }

  my $part = $self->get_part_info( $invnum );

  return $self->{eh}->( "Part $invnum does not exist" )
    unless $part;
  
  return $self->{eh}->( "$invnum is already on $mname" )
    if $part->cpuname eq $mname;

  #..and flag it to as scheduled to be added
  #keep track of all the info so it's faster down the road
  $self->{transact}{parts}{$invnum} = $part;
  #add the machine to where it is being moved
  $self->{transact}{parts}{$invnum}{machine} = $mname;

  #return all the info about the part added
  return $part;
}

#
#  undo_move_part
#  --unschedule a part move
#

sub undo_move_part {

  my ( $self, $invnum ) = @_;

  return $self->{eh}->( "Ummm...you have to enter a part..." )
    unless $invnum;
  
  #see if it is a valid inventory number
  return $self->{eh}->( "$invnum is not a valid inventory number" )
    unless $invnum =~ m/\b(C|S|U|F|L)(\d{6})\b/;
                        
  #check if there has been a request already
  return $self->{eh}->( "Part $invnum never scheduled to be moved" )
    unless $self->{transact}{parts}{$invnum};
  
  #get a ref to the data
  my $part = $self->{transact}{parts}{$invnum};
  delete $self->{transact}{parts}{$invnum};

  return $part;
}

#
#  get_machine_parts
#  --return info about parts on a machine
#  

sub get_machine_parts {

  my ( $self, $mname ) = @_;

  #check if the machine is registered first
  if ( $self->{machines}{$mname} ) {
    return $self->{machines}{$mname}->get_parts;
  }

  my $ref = $self->{invdb}->query_machine_parts( $mname );

  my $part_hash;
  while ( my ( $key, $value ) = each( %$ref ) ) {

    my $part = new Mchange::Part( $value );

    #cache the list of known parts too
    $self->{parts}{$key} = $part;

    $part_hash->{$key} = $part;
    
  }
  
  return $part_hash;
}


#
#  get_part_info
#  --get information about a part based on cnum
#

sub get_part_info {

  my ( $self, $invnum ) = @_;

  if ( $self->{parts}{$invnum} ) {
    return $self->{parts}{$invnum};
  }
  
  my $part_ref = $self->{invdb}->query_part( $invnum );

  my $part = new Mchange::Part( $part_ref )
    if $part_ref;

  #cache into list of know parts
  $self->{parts}{$invnum} = $part;
  return $part;
  
}

#  set_attrib
#  --set an attrib--for now just add it to transactions
#

sub set_attrib {

  my ( $self, $mname, $attrib, $value ) = @_;

  #make sure there is no single quotes
  $value =~ s/\'/\\\'/g;

  my $mach;
  return $self->{eh}->( 'Machine needs to be registered before setting any attributes' )
    unless $mach = $self->{machines}{$mname};

  #don't care if they have the same value entered
  return if $mach->$attrib eq $value;

  #need to handle these a bit different because they need to be done at certain times
  if ( $attrib eq 'cpuname' ) {

    #check to see if the new name is valid
    return $self->{eh}->( "$value is not a valid hostname. ( [a-zA-Z0-9]+ )" )
        if !$self->is_valid_mname( $value );
    
    #see if the new name is already in use
    return $self->{eh}->( "$value already exists in CSDB.  Pick another name" )
      if !(grep { $value } keys %{$self->{transact}{names}}) && $self->machine_exists( $value );

    return $self->{eh}->( "$value already exists in Splat.  Pick another name" )
      if !(grep { $value } keys %{$self->{transact}{splat_names}}) && $self->{splatdb}->check_exists( 'hostname', $value);

    #see if we need to add the host to DNS
    #print "gethostbyname: " . (gethostbyname( $value ))[0] ."\n";
    #print "val: $value\n";
    
    unless ( (gethostbyname( $value ))[0] eq $value ) {
      $self->{transact}{dns_add}{$mname}{room} = $mach->room;
      $self->{transact}{dns_add}{$mname}{newname} = $value;
    }

    # for machines with .sp hosts in splat, we need to change its hostnames in tandem
    # to avoid orphans
    #
    # later on when the mchange script is made and Splat.pm's update_name is called, it will
    # check to see if the name is in splat and will harmlessly return if its not.
    # because of that we can be a little lazy and try to change splat .sp hosts on
    # everything
    #
    # well, no, I guess we can't be lazy.  see below comment.

    my $mname_sp;
    my $value_sp;
    if ($mname =~ /([\w-]+).(cs|stat).wisc.edu/) {
	# the current hostname is fully qualified
	$mname_sp = "$1.sp.$2.wisc.edu";
    } else {
	# current hostname is not fq
        $mname_sp = $mname . ".sp";
    }

    if ($value =~ /([\w-]+).(cs|stat).wisc.edu/) {
        # the new hostname is fully qualified
        $value_sp = "$1.sp.$2.wisc.edu";
    } else {
        # new hostname is not fq
        $value_sp = $value . ".sp";
    }

    # so we know what the sp currently should be and what it'd turn into.
    # problem is, Splat.pm's update_name sub will detach the sp host and
    # leave it disconnected unless we tell it to attach after the change.
    # the attach will error out if the .sp host doesn't exist, so that's our
    # first order of business.  
    # (this isn't the place for this in the
    # grand picture, but until the attributes screen accounts for sp hosts
    # there isn't much to be done about it.  mchange wasn't made with 
    # sp's in mind.)

    if ($self->{splatdb}->check_exists( 'hostname', $mname_sp)) {
    	# the .sp we expect to find based on the machine's hostname exists.  let's take care of it.
    	$self->{transact}{splat_names}{$mname_sp} = $value_sp;
        
	# with the hostname change pending, we also need to schedule the re-attach.
        # we need some intel to be able to do that.

        my $sp_idf;
	my $sp_jack;
	my $splat_intel = $self->{splatdb}->query_machine_attrib( $mname_sp );
	
	$sp_idf = $splat_intel->{idf};
	$sp_jack = $splat_intel->{jacknum};

	# its possible that the .sp isn't really attached, in which case we shouldn't
        # have idf/jack info
	if ($sp_idf && $sp_jack) {
		my $port = $sp_idf . "-" . $sp_jack;

		# sanity check the sp's port.  this shouldn't fail if its already attached, 
                # unless something's quite screwed up.
		return $self->{eh}->( "The splat port for the attached SP doesn't exist... this shouldn't have happened." )
		      unless $self->{splatdb}->check_port( $port );
		
		# yay! schedule the new attach.
		# we have to schedule it for the new name or else it will fail, and 
                # while Script.pm will check and fix things like this, it does it based
                # on the csdb name change, which we aren't doing for the splat-only
		# sp hosts.
		$self->{transact}{splat_attach}{$value_sp} = $port;
	}

    }
    
    
    $self->{transact}{names}{$mname} = $value;
    $self->{transact}{splat_names}{$mname} = $value;

    #update:  monitor is no longer used
    #slam the monitor type on name change ( it's in a different table )
    #$self->{transact}{attrib}{$mname}{monitor} = $mach->monitor
    #  if $mach->monitor;
        
  }
  elsif ( $attrib eq 'lock' ) {
    $value =~ s/(\D+)/\U$1/;
    $self->{transact}{attrib}{$mname}{$attrib} = $value;
  }
  elsif ( $attrib eq 'room' ) {
    $value =~ s/(\D+)/\U$1/;
    $self->{transact}{attrib}{$mname}{$attrib} = $value;
  }
  elsif ( $attrib eq 'uwoffice' ) {
    $self->{transact}{office}{$value->{user}} = $value->{office};
  }
  elsif ( $attrib eq 'prime_user' ) {
    if ( $self->{transact}{names}{$mname} ) {
      my $newname = $self->{transact}{names}{$mname};
      return $self->{eh}->( "User $value does not have an account on $newname.  Add it in CSDB first if applicable" )
        unless $self->{invdb}->check_account( $newname, $value ) or $value eq 'labacct' or $value eq 'statdept';
    }
    else {
      return $self->{eh}->( "User $value does not have an account on $mname.  Add it in CSDB first if applicable." )
        unless $self->{invdb}->check_account( $mname, $value ) or $value eq 'labacct' or $value eq 'statdept';
    }

    $self->{transact}{attrib}{$mname}{$attrib} = $value;
  }
  elsif ( $attrib eq 'validos' ) {
    my $os_list = $mach->validos;

    unless ( $os_list ) {
      my $part = $self->{invdb}->get_cpu_part( $mname );

      return $self->{eh}->( "There is no CPU part on the machine.  Cannot add valid OS" )
        unless $part;

    }
    else {
      #see if the machine already has the validos
      map { return $self->{eh}->( "$value already is a valid OS for $mname" ) if $_ =~ /^$value$/ } @$os_list;
    }
    
    push @{$self->{transact}{validos}{$mname}}, $value;
    push( @$os_list, $value );
    $mach->validos( $os_list );
    return;
  }
  elsif ( $attrib eq 'mnotes' ) {
    print "Mnote: $value\n";
    return $self->{eh}->( "Mnote can only be 80 characters long" )
      if length( $value ) > 80;

    $self->{transact}{attrib}{$mname}{$attrib} = $value;
  }
  elsif ( $attrib eq 'os' ) {
    #need to check if it is a validos
    my $list;

    if ( $self->{machines}{$mname} ) {
      $list = $self->{machines}{$mname}->validos;
    }
    else {
      $list = $self->get_machine_attrib( $mname )->get( 'validos' );
    }

    #print "New OS: $value " . length( $value ) . "\n";
    
    #foreach ( @$list ) {
    #  print "$_ ";
    #  print length( $_ ) . "\n";;
    #}
    
    my $match;
    map { $match = 1 if $_ =~ /$value/ } @$list;
	# special case for DontCare
	$match = 1 if ($value eq "DontCare");

    return $self->{eh}->( "Selected OS is not a valid os for $mname" )
      unless $match;

    $self->{transact}{attrib}{$mname}{$attrib} = $value;
  }
  elsif ( $attrib eq 'splat_add' ) {
    $self->{transact}{splat_add}{$value->{part}} = { mac => $value->{mac}, int => $value->{interface}, name => $mname };
  }
  elsif ( $attrib eq 'splat_attach' ) {

    return $self->{eh}->( "The splat port you entered doesn't exist...Check the room number..." )
      unless $self->{splatdb}->check_port( $value );

    $self->{transact}{splat_attach}{$mname} = $value;
  }
  elsif ( $attrib eq 'splat_detach' ) {
    $self->{transact}{splat_detach}{$mname} = $value;
  }
  elsif ( $attrib eq 'jacknum' || $attrib eq 'idf' ) {
    #want to skip the attrib list for this one
    return $self->{eh}->( "You didn't enter the $attrib" )
      if $value eq "Jack" || $value eq "IDF";

    
    $mach->$attrib( $value );
  }
  elsif ( $attrib eq 'dns' ) {
    $self->{transact}{dns}{$mname} = $value;
  }
  else {
    $self->{transact}{attrib}{$mname}{$attrib} = $value;
  }

  #if the machine is registered, update the data
  $mach->$attrib( $value );
  
  return undef;
}
 
#
#  get_machine_attrib
#  --get the attrib based on machine name
#

sub get_machine_attrib {

  my ( $self, $mname ) = @_;

  #see if the machine is already registered
  return $self->{machines}{$mname}->get_attrib if $self->{machines}{$mname};
    
  my $ref1 = $self->{invdb}->query_machine_attrib( $mname );
  my $ref2 = $self->{splatdb}->query_machine_attrib( $mname );

  #add the splat info into the mix
  while ( my ($key, $value) = each( %$ref2 ) ) {
    next if $key eq 'hostname';

    if ( $key eq 'part' ) {
      $ref1->{ethpart} = $value;
      next;
    }

    $ref1->{$key} = $value;
  }
  
  return new Mchange::Attribute( $ref1 );
}

#
#  set_rt
#  --setup the rt info 
#

sub set_rt {

  my ( $self, $rt ) = @_;
  
  $rt = 'None' unless $rt || $rt =~ /{\d}+/;
  $self->{transact}{rt} = $rt;

}

#
#  machine_query
#  --see if a machine exists in the database
#

sub machine_exists {

  my ( $self, $machine ) = @_;

  return 1 if $self->{machines}{$machine};
  return $self->{invdb}->query_machine( $machine );
}

#
#  accounts
#  -- trasnfer prime user?
#

sub trans_prime {

  my ( $self, $old_machine, $new_machine ) = @_;

  return -1 if (!$old_machine || !$new_machine);
  return $self->{transact}{transfer} = 1;
}

#
#  search_machine
#  --find a machine that is like the string given
#

sub search_machine {

  my ( $self, $string ) = @_;

  return $self->{invdb}->find_machine( $string );
  
}

#
#  transact_query
#  --see if there is a transaction with the part
#

sub transact_query {

  my ( $self, $invnum ) = @_;

  return $self->{transact}{parts}{$invnum};
  
}

#
#  set_holdover
#  --set a machine to be the holdover information
#

sub set_holdover {

  my ( $self, $machine ) = @_;

  $self->{holdover} = $machine;

}

#
#  get_holdover
#  --return the holdover info
#

sub get_holdover {

  my ( $self ) = @_;

  return $self->{holdover};
  
}

#
#  do_script
#  --write the script out 
#

sub do_script {

  my ( $self, $run, $machine_order  ) = @_;

  my $script = new Mchange::Script( data => $self->{transact} );

  if ( $run ) { 
    $script->write_and_run;
  }
  else {
    $script->write;
  }  
}

#
#  is_valid_mname
#  Returns true if a given string is a valid CSL hostname for a computer.
#  Class (static) function.
#
sub is_valid_mname {
    my ( $self_or_class, $mname ) = @_;

    # first, just check it as a domain name (including .sp)
    my $v = Data::Validate::Domain->new(
					domain_allow_single_label => 1,
					domain_private_tld => {
					    'sp'   =>      1,
					}
					);
    my $result = $v->is_domain($mname);
    if ( defined($result) ) {
	return 1;
    } 

    # ok, it failed. It might be "just" a hostname
    $result = $v->is_hostname($mname);
    if ( defined($result) ) {
	return 1;
    } 

    # fail
    return 0;
}

#

1;

