###############################################################################
## Copyright 2005-2016 OCSInventory-NG/OCSInventory-Server contributors.
## See the Contributors file for more details about them.
## 
## This file is part of OCSInventory-NG/OCSInventory-ocsreports.
##
## OCSInventory-NG/OCSInventory-Server is free software: you can redistribute
## it and/or modify it under the terms of the GNU General Public License as
## published by the Free Software Foundation, either version 2 of the License,
## or (at your option) any later version.
##
## OCSInventory-NG/OCSInventory-Server is distributed in the hope that it
## will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
## of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with OCSInventory-NG/OCSInventory-ocsreports. if not, write to the
## Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
## MA 02110-1301, USA.
################################################################################
package Apache::Ocsinventory::Server::Duplicate;

use strict;

BEGIN{
  if($ENV{'OCS_MODPERL_VERSION'} == 1){
    require Apache::Ocsinventory::Server::Modperl1;
    Apache::Ocsinventory::Server::Modperl1->import();
  }elsif($ENV{'OCS_MODPERL_VERSION'} == 2){
    require Apache::Ocsinventory::Server::Modperl2;
    Apache::Ocsinventory::Server::Modperl2->import();
  }
}

require Exporter;

our @ISA = qw /Exporter/;

our @EXPORT = qw / _duplicate_main /;

use Apache::Ocsinventory::Server::Constants;
use Apache::Ocsinventory::Server::System qw /:server _modules_get_duplicate_handlers/;
use Apache::Ocsinventory::Map;

# Subroutine called at the end of database inventory insertions
sub _duplicate_main{
  my %exist;
  my $red;
  
  my $result = $Apache::Ocsinventory::CURRENT_CONTEXT{'XML_ENTRY'};
  my $dbh = $Apache::Ocsinventory::CURRENT_CONTEXT{'DBI_HANDLE'};
  my $DeviceID = $Apache::Ocsinventory::CURRENT_CONTEXT{'DATABASE_ID'};

  # workaround agent old_deviceid bug
  if(!$result->{CONTENT}->{OLD_DEVICEID} and $result->{CONTENT}->{DOWNLOAD}->{HISTORY}->{OLD_DEVICEID})
  {
    $result->{CONTENT}->{OLD_DEVICEID} = $result->{CONTENT}->{DOWNLOAD}->{HISTORY}->{OLD_DEVICEID};
  }

  # If the duplicate is specified
  if($result->{CONTENT}->{OLD_DEVICEID} and $result->{CONTENT}->{OLD_DEVICEID} ne $Apache::Ocsinventory::CURRENT_CONTEXT{'DEVICEID'}){
    &_log(326,'duplicate',$result->{CONTENT}->{OLD_DEVICEID}) if $ENV{'OCS_OPT_LOGLEVEL'};
    # Looking for database id of old deviceid
    my $request = $dbh->prepare('SELECT ID FROM hardware WHERE DEVICEID=?');
    $request->execute($result->{CONTENT}->{OLD_DEVICEID});
    if(my $row = $request->fetchrow_hashref){
      if(&_duplicate_replace($row->{'ID'})){
        # If there is an invalid old deviceid
        &_log(513,'duplicate','old deviceid') if $ENV{'OCS_OPT_LOGLEVEL'};
        $dbh->rollback;
      }else{
        $dbh->commit;
        $red = 1;
      }
    }
  }
  
  # Handle duplicates if $ENV{'OCS_OPT_AUTO_DUPLICATE_LVL'} is set
  if($ENV{'OCS_OPT_AUTO_DUPLICATE_LVL'}){
    # Trying to find some duplicate evidences
    &_duplicate_detect(\%exist);
    # For each result, we are trying to know if it is a true duplicate (according to AUTO_DUPLICATE_LVL
    for(sort keys(%exist)){
      if(&_duplicate_evaluate(\%exist, $_)){
        if(&_duplicate_replace($_)){
          &_log(517,'duplicate','replacing_error') if $ENV{'OCS_OPT_LOGLEVEL'};
          $dbh->rollback;
        }else{
          $dbh->commit;
          $red = 1;
        }
      }
    }
  }
  return $red;
}

sub _already_in_array {
        my $lookfor = shift;
        my $ref   = shift;
        foreach (@$ref){
          return 1 if($lookfor eq $_);
        }
        return 0;
}

sub _duplicate_evaluate{
  my $exist = shift;
  my $key = shift;
  
  # Check duplicate , according to AUTO_DUPLICATE_LVL
  $exist->{$key}->{'MASK'} = 0;
  $exist->{$key}->{'MASK'}|=DUP_HOSTNAME_FL if $exist->{$key}->{'HOSTNAME'};
  $exist->{$key}->{'MASK'}|=DUP_SERIAL_FL if $exist->{$key}->{'SSN'};
  $exist->{$key}->{'MASK'}|=DUP_MACADDR_FL if $exist->{$key}->{'MACADDRESS'};
  $exist->{$key}->{'MASK'}|=DUP_SMODEL_FL if $exist->{$key}->{'SMODEL'};
  $exist->{$key}->{'MASK'}|=DUP_UUID_FL if $exist->{$key}->{'UUID'};
  $exist->{$key}->{'MASK'}|=DUP_ASSETTAG_FL if $exist->{$key}->{'ASSETTAG'};
  
  if((($ENV{'OCS_OPT_AUTO_DUPLICATE_LVL'} & $exist->{$key}->{'MASK'})) == $ENV{'OCS_OPT_AUTO_DUPLICATE_LVL'}){
    return(1);
  }else{
    return(0);
  }
}

sub _duplicate_detect{

  my $exist = shift;
  
  my $result = $Apache::Ocsinventory::CURRENT_CONTEXT{'XML_ENTRY'};
  my $dbh = $Apache::Ocsinventory::CURRENT_CONTEXT{'DBI_HANDLE'};
  my $DeviceID = $Apache::Ocsinventory::CURRENT_CONTEXT{'DATABASE_ID'};
    
  my $request;
  my $row;
  
  my(@bad_serial, @bad_mac);
  
  # Retrieve generic mac addresses
  $request = $dbh->prepare('SELECT MACADDRESS FROM blacklist_macaddresses');
  $request->execute();
  push @bad_mac, $row->{MACADDRESS} while($row = $request->fetchrow_hashref());
  
  # Retrieve generic serials
  $request = $dbh->prepare('SELECT SERIAL FROM blacklist_serials');
  $request->execute();
  push @bad_serial, $row->{SERIAL} while($row = $request->fetchrow_hashref());
  
  # Do we already have the hostname
  $request = $dbh->prepare('SELECT ID, NAME FROM hardware WHERE NAME=? AND ID<>? ORDER BY ID');
  $request->execute($result->{CONTENT}->{HARDWARE}->{NAME}, $DeviceID);
  while($row = $request->fetchrow_hashref()){
    if(!($row->{'NAME'} eq '')){
      $exist->{$row->{'ID'}}->{'HOSTNAME'}=1;
    }
  }
  
  # Do we already have the assettag ?
  $request = $dbh->prepare('SELECT HARDWARE_ID, ASSETTAG FROM bios WHERE ASSETTAG=? AND HARDWARE_ID<>? ORDER BY HARDWARE_ID');
  $request->execute($result->{CONTENT}->{BIOS}->{ASSETTAG}, $DeviceID);
  while($row = $request->fetchrow_hashref()){
    if(!($row->{'ASSETTAG'} eq '')){
      $exist->{$row->{'ID'}}->{'ASSETTAG'}=1;
    }
  }
  # Do we already have the uuid ?
  $request = $dbh->prepare('SELECT ID, UUID FROM hardware WHERE UUID=? AND ID<>? ORDER BY ID');
  $request->execute($result->{CONTENT}->{HARDWARE}->{UUID}, $DeviceID);
  while($row = $request->fetchrow_hashref()){
    if(!($row->{'UUID'} eq '')){
      $exist->{$row->{'ID'}}->{'UUID'}=1;
    }
  }

  # ...and one MAC of this machine
  for(@{$result->{CONTENT}->{NETWORKS}}){
    $request = $dbh->prepare('SELECT HARDWARE_ID,DESCRIPTION,MACADDR FROM networks WHERE MACADDR=? AND HARDWARE_ID<>?');
    $request->execute($_->{MACADDR}, $DeviceID);
    while($row = $request->fetchrow_hashref()){
      if(!&_already_in_array($row->{'MACADDR'}, \@bad_mac)){
        $exist->{$row->{'HARDWARE_ID'}}->{'MACADDRESS'}++;
      }
    }
  }
  # ...or its serial
  if($result->{CONTENT}->{BIOS}->{SSN}){
    $request = $dbh->prepare('SELECT HARDWARE_ID, SSN FROM bios WHERE SSN=? AND HARDWARE_ID<>?');
    $request->execute($result->{CONTENT}->{BIOS}->{SSN}, $DeviceID);
    while($row = $request->fetchrow_hashref()){
      if(!&_already_in_array($row->{'SSN'}, \@bad_serial)){
        $exist->{$row->{'HARDWARE_ID'}}->{'SSN'}=1;
      }
    }
  }
  
  # ...or its serial model
  if($result->{CONTENT}->{BIOS}->{SMODEL}){
    $request = $dbh->prepare('SELECT HARDWARE_ID, SMODEL FROM bios WHERE SMODEL=? AND HARDWARE_ID<>?');
    $request->execute($result->{CONTENT}->{BIOS}->{SMODEL}, $DeviceID);
    while($row = $request->fetchrow_hashref()){
      $exist->{$row->{'HARDWARE_ID'}}->{'SMODEL'}=1;
    }
  }
  
  $request->finish;
}


sub _duplicate_replace{
  my $device = shift;
  
  #Locks the device
  if( &_lock($device) ){
    &_log( 516, 'duplicate', 'device locked');
    return 1;
  }
  my $DeviceID = $Apache::Ocsinventory::CURRENT_CONTEXT{'DATABASE_ID'};
  my $dbh = $Apache::Ocsinventory::CURRENT_CONTEXT{'DBI_HANDLE'};
  my $result = $Apache::Ocsinventory::CURRENT_CONTEXT{'XML_ENTRY'};
          
  # We keep the old quality and fidelity
  my $request=$dbh->prepare('SELECT QUALITY,FIDELITY,CHECKSUM,USERID FROM hardware WHERE ID=?');
  $request->execute($device);
  
  # If it does not exist
  unless($request->rows){
    &_unlock($device);
    return(1);
  }
  my $row = $request->fetchrow_hashref;
  my $quality = $row->{'QUALITY'}?$row->{'QUALITY'}:0;
  my $fidelity = $row->{'FIDELITY'};
  my $checksum = $row->{'CHECKSUM'};
  my $userid = $row->{'USERID'};
  $request->finish;
  
  # Current userid or previous one ? 
  if( $result->{CONTENT}->{HARDWARE}->{USERID}!~/system|localsystem/i ){
     $userid = $result->{CONTENT}->{HARDWARE}->{USERID};
  }
  # TODO: catch the queries return code
  # Keeping few informations from hardware 
  $dbh->do(" UPDATE hardware SET QUALITY=".$dbh->quote($quality).",
             FIDELITY=".$dbh->quote($fidelity).",
             CHECKSUM=(".(defined($checksum)?$checksum:CHECKSUM_MAX_VALUE)."|".(defined($result->{CONTENT}->{HARDWARE}->{CHECKSUM})?$result->{CONTENT}->{HARDWARE}->{CHECKSUM}:CHECKSUM_MAX_VALUE)."),
             USERID=".$dbh->quote($userid)." 
             WHERE ID=".$DeviceID
  ) ;
  $dbh->do("DELETE FROM hardware WHERE ID=?", {}, $device) ;

  # We keep the informations of the following tables: devices, accountinfo, itmgmt_comments
  $dbh->do('DELETE FROM accountinfo WHERE HARDWARE_ID=?', {}, $DeviceID) ;
  $dbh->do('UPDATE accountinfo SET HARDWARE_ID=? WHERE HARDWARE_ID=?', {}, $DeviceID, $device) ;
  $dbh->do('DELETE FROM devices WHERE HARDWARE_ID=?', {}, $DeviceID) ;
  $dbh->do('UPDATE devices SET HARDWARE_ID=? WHERE HARDWARE_ID=?', {}, $DeviceID, $device) ;
  $dbh->do('UPDATE itmgmt_comments SET HARDWARE_ID=? WHERE HARDWARE_ID=?', {}, $DeviceID, $device) ;
  # We keep the static inclusions/exclusions (STATIC=1|2)
  $dbh->do('UPDATE groups_cache SET HARDWARE_ID=? WHERE HARDWARE_ID=? AND (STATIC=1 OR STATIC=2)', {}, $DeviceID, $device) ;
  # The computer may not correspond to the previous dynamic groups, as its inventory could potentially change
  $dbh->do('DELETE FROM groups_cache WHERE HARDWARE_ID=?', {}, $device) ;
  
  # Drop old computer from "auto" tables in Map
  # TODO: is it possible to manage the not auto section => not auto for import but auto for deletion ?
  for (keys(%DATA_MAP)){
    next if !$DATA_MAP{$_}->{delOnReplace} || !$DATA_MAP{$_}->{auto} || $DATA_MAP{$_}->{capacities};
    unless($dbh->do("DELETE FROM $_ WHERE HARDWARE_ID=?", {}, $device)){
      &_unlock($device);
      return(1);
    }
  }

  # Trace duplicate if needed
  if($ENV{'OCS_OPT_TRACE_DELETED'}){
    unless(  $dbh->do('INSERT INTO deleted_equiv(DATE,DELETED,EQUIVALENT) VALUES(NULL,?,?)', {} , $device,$DeviceID)){
      &_unlock($device);
      return(1);
    }
  }

  # To enable option managing duplicates
  for(&_modules_get_duplicate_handlers()){
    last if $_==0;
    # Returning 1 will abort replacement
    unless(&$_(\%Apache::Ocsinventory::CURRENT_CONTEXT, $device)){
      &_unlock($device);
      return(1);
    }
  }

  &_log(300,'duplicate',"$device => $DeviceID") if $ENV{'OCS_OPT_LOGLEVEL'};

  #Remove lock
  &_unlock($device);
  0;

}
1;
