#!/usr/bin/perl
# regserver - a registration server for the CMDBQuick
#
# Written by Digant C Kasundra <digant@stanford.edu>
# Copyright 2009 Board of Trustees, Leland Stanford Jr. University
#
# See LICENSE for licensing terms.

#############################################################################
# Modules and declarations
#############################################################################

use strict;

use AppConfig;
use Log::Log4perl qw(get_logger);
use POSIX;
use QCMDB::Database;
use QCMDB::Config;
use XML::Simple;
use Data::Dumper;


my $CFGFILE = '/etc/qcmdb.conf';

# globals
my $log;
my $reshash;
my $authedMDRID;
my $qdb = QCMDB::Database->new(QCMDB::Config->new($CFGFILE))
    or die "Could not connect to the DB.";
my $dbh = $qdb->{dbh};
##############################################################################
# Internal helper methods
##############################################################################

# Gets or creates and returns the id of a item type by name
sub _get_type {
    my ($tname) = @_;
    my $tid;

    $log->debug("_get_type: Looking for type $tname....");

    # Find the node by name
    eval {
        my $sql = "select id from cmdb_typenames where name = ?";
        $tid = $dbh->selectrow_array ($sql, undef, $tname);
        $dbh->commit;
    };
    if ($@) {
        $log->error("_get_type: Select failed when looking for type $tname: $@");
        $dbh->rollback;
        return;
    }

    if (defined $tid) {
        $log->debug("_get_type: Found type $tname with id $tid"); 
        return $tid;
    }     

    # not found so must create
    $log->debug( "_get_type: Type not found, must create type $tname");

    eval {
        my $sql = "insert into cmdb_typenames(name) " .
            "values('$tname') ";
        $dbh->do($sql);
        $tid = $dbh->last_insert_id(undef, undef, qw(cmdb_typenames id))
            or $log->error("_get_type: Couldn't find id of new prop name.");
    };
    if ($@) {
        $log->error("_get_type: Insert failed when adding for type $tname: $@");
        $dbh->rollback;
        return;
    }

    $log->debug("_get_type: Created type $tname with id $tid"); 
    return $tid;
}

# Gets or creates and returns the id of a property by name
sub _get_propid {
    my ($pname) = @_;
    my $pid;

    $log->debug("_get_propid: Looking for property $pname");
    # Find the prop by name
    eval {
        my $sql = "select id from cmdb_propnames where name = ?";
        $pid = $dbh->selectrow_array ($sql, undef, $pname);
        $dbh->commit;
    };
    if ($@) {
        $log->error("_get_propid: Failed to select propname $pname: $@");
        $dbh->rollback;
        return;
    }
    if (defined $pid) {
        $log->debug("_get_propid: Found property $pname with id $pid");
        return $pid;
    }     

    # not found so must create
    $log->debug("_get_propid: Property not found, must create prop $pname");

    eval {
        my $sql = "insert into cmdb_propnames(name) " .
            "values('$pname') ";
        $dbh->do($sql);
        $pid = $dbh->last_insert_id(undef, undef, qw(cmdb_propnames id))
            or $log-error("_get_propid: Couldn't find id of new prop name.");
    };
    if ($@) {
        $log->error("_get_propid: Failed to insert propname $pname: $@");
        $dbh->rollback;
        return;
    }

    $log->debug("_get_propid: Created property $pname with id $pid");
    return $pid;
}

# Get an existing item if found, or return error
sub _get_item {
    my ($mdrId, $localId) = @_;
    my ($iid, $tid);

    $log->debug("_get_item: localId: $localId");

    # Find the item 
    eval {
        my $sql = "select id from cmdb_items where localId = ? and mdrId = ?";
        $iid = $dbh->selectrow_array ($sql, undef, $localId, $mdrId);
        $dbh->commit;
    };
    if ($@) {
        $log->error("_get_item: select failed for item $localId: $@");
        $dbh->rollback;
        return;
    }
    if (!defined $iid) {
        $log->debug("_get_item: Did not find item $localId");
        return;
    }
    eval {
        my $sql = sprintf("update cmdb_items set timestamp=CURRENT_TIMESTAMP " 
            . "where id=%d", $iid);
        $dbh->do ($sql);
        $dbh->commit;
    };
    if ($@) {
        $log->error("_get_item: Could not update timestamp for $localId: $@");
        $dbh->rollback;
        return;
    }

    $log->debug("_get_item: Found item with id $iid ($localId)"); 
    return $iid;
}

# Gets or creates and returns the id of an item by localid
sub _get_or_make_item {
    my ($mdrId, $localId, $type) = @_;
    my ($iid, $tid);

    $log->debug("_get_or_make_item: started for $localId");
    $iid = _get_item($mdrId, $localId);
    if (defined $iid) {
        $log->debug("_get_or_make_item: Found item $localId");
        return $iid;
    }

    # not found so must create
    $log->debug("_get_or_make_item: Item not found, must create item $localId");

    # Get the type id by name
    $tid = _get_type($type);
    if (!$tid) { 
        $log->error("_get_or_make_item: Could not generate or find type id."); 
        return;
    }
    eval {
        my $sql = "insert into cmdb_items(type,mdrId,localId) " .
            "values($tid, '$mdrId', '$localId')";
        $dbh->do($sql);
        $iid = $dbh->last_insert_id(undef, undef, qw(cmdb_items id))
            or $log->error("_get_or_make_item: Couldn't find id of new item.");
    };
    if ($@) {
        $dbh->rollback;
        return;
    }
    
    $log->debug("_get_or_make_item: created item $iid ($localId)");
    return $iid;
}

# Gets or creates and returns the id of an relationship by localid
sub _get_or_make_rel {
    my ($mdrId, $localId, $type, $source, $target) = @_;
    my ($rid, $tid);

    $log->debug("_get_or_make_rel: $localId");

    # Find the node by name
    eval {
        my $sql = "select id from cmdb_rels where localId = ? and mdrId = ?";
        $rid = $dbh->selectrow_array ($sql, undef, $localId, $mdrId);
        $dbh->commit;
    };
    if ($@) {
        $log->error("_get_or_make_rel: Could not select for rel $localId");
        $dbh->rollback;
        return;
    }
    if (defined $rid) {
        $log->debug("_get_or_make_rel: Found relationship $localId");
        eval {
            my $sql = sprintf("update cmdb_rels set " .
                "timestamp=CURRENT_TIMESTAMP where id=%d", $rid);
            $dbh->do ($sql);
            $dbh->commit;
        };
        if ($@) {
            $log->error("_get_or_make_rel: Could not update rel $localId: $@");
            $dbh->rollback;
            return;
        }

        $log->debug("_get_or_make_rel: Found rel $rid ($localId)");
        return $rid;
    }

    # not found so must create
    $log->debug("_get_or_make_rel: Relationship not found, must create rel $localId");

    # Get the type id by name
    $tid = _get_type($type);
    if (!$tid) { 
        $log->error("_get_or_make_rel: Could not generate or find type id."); 
        return;
    }
    # Get the ID of the source
    my $srcid = _get_item($mdrId, $source);
    if (!$srcid) {
        $log->error("_get_or_make_rel: Could not find source $source");
        return;
    }
    # Get the ID of the target
    my $tarid = _get_item($mdrId, $target);
    if (!$tarid) {
        $log->error("_get_or_make_rel: Could not find target $target");
        return;
    }
    eval {
        my $sql = "insert into cmdb_rels(type,mdrId,localId,source,target) " .
            "values($tid, '$mdrId', '$localId', $srcid, $tarid)";
        $dbh->do($sql);
        $rid = $dbh->last_insert_id(undef, undef, qw(cmdb_items id))
            or $log->error("_get_or_make_rel: Couldn't find id of new rel.");
    };
    if ($@) {
        $log->error("_get_or_make_rel: couldn't insert new rel $localId: $@");
        $dbh->rollback;
        return;
    }

    $log->debug("_get_or_make_rel: Created new rel $rid ($localId)");
    return $rid;
}

# Updates or adds the property and value to a given item
sub _upd_itm_prop {
    my ($iid, $prop, $value) = @_;

    $log->debug("_upd_itm_prop: update $iid: $prop = $value");
    # Get the property ID by name
    my $pid = _get_propid($prop);
    if (!$pid) {
        $log->error("_upd_itm_prop: could not get prop id for $prop");
        return;
    }
    $log->debug("_upd_itm_prop: Got prop id $pid for $prop.");

    eval {
        my $sql = "insert into cmdb_itemprops(iid,pid,value) " .
            "values($iid, $pid, '$value') " .
            "on duplicate key update value='$value'";
        $dbh->do($sql);
        $dbh->commit;
    };
    if ($@) {
        $log->error("_upd_itm_prop: could not insert/upd $prop: $@");
        $dbh->rollback;
        return;
    }

    return 1;
}

# Updates or adds the property and value to a given relationship
sub _upd_rel_prop {
    my ($rid, $prop, $value) = @_;

    $log->debug("_upd_rel_prop: update $rid: $prop = $value");

    # Get the property ID by name
    my $pid = _get_propid($prop);
    if (!$pid) {
        $log->error("_upd_rel_prop: could not get id for prop $prop");
        return;
    }
    $log->debug("_upd_rel_prop: Got prop id $pid for $prop.");

    eval {
        my $sql = "insert into cmdb_relprops(rid,pid,value) " .
            "values($rid, $pid, '$value') " .
            "on duplicate key update value='$value'";
        $dbh->do($sql);
        $dbh->commit;
    };
    if ($@) {
        $log->error("_upd_rel_prop: could not insert/upd $prop: $@");
        $dbh->rollback;
        return;
    }

    return 1;
}

# Dismantle and register an item
sub _reg_item {
    my ($item) = @_;
    my $type;
    my @props;
    my $record;

    # Break apart into bits we care about
    my $mdrId = $item->{instanceId}->{mdrId};
    my $localId = $item->{instanceId}->{localId};
    $log->debug("_reg_item: $localId");


    # Figure out the type.  Records usually have the CI type and
    # recordMetadata as elements, so we just assume anything we find that
    # isn't recordMetadata is the type.  Note this will break with multiple
    # record support. 
    foreach my $key (keys %{$item->{record}}) {
        if ($key ne "recordMetadata") {
            $type = $key;
            $record = $item->{record}->{$key};
            $log->debug("_reg_item: type is $type");
        }
    }

    # Create or get the item id
    my $iid = _get_or_make_item ($mdrId, $localId, $type);
    if (!$iid) {
        $log->error( "Unable to create or find item $localId");
        _add_reg_response($localId, undef, 0, 
            "An error occured while registering");
        return;
    }

    $log->debug("_reg_item: created/found item $iid ($localId)");

    # Add the given property's and values to the item
    foreach my $key (keys %$record) {
        if (!_upd_itm_prop($iid, $key, $record->{$key})) {
            $log->error("_reg_item: Unable to add property $key to $localId");
            return;
        }
        $log->debug("_reg_item: added prop $key=$record->{$key} to $localId");
    }

    $log->debug("_reg_item: end of function");
    _add_reg_response($localId, $iid, 1, undef);
    return 1;
}

# Dismantle and register a relationship
sub _reg_rel {
    my ($rel) = @_;
    my ($type, $name);
    my @props;
    my $record;

    # Break apart into bits we care about
    my $mdrId = $rel->{instanceId}->{mdrId};
    my $localId = $rel->{instanceId}->{localId};
    my $source = $rel->{source}->{localId};
    my $target = $rel->{target}->{localId};
    $log->debug("_reg_rel: $localId");

    # Figure out the type.  Records usually have the CI type and
    # recordMetadata as elements, so we just assume anything we find that
    # isn't recordMetadata is the type.  Note this will break with multiple
    # record support. 
    foreach my $key (keys %{$rel->{record}}) {
        if ($key ne "recordMetadata") {
            $type = $key;
            $record = $rel->{record}->{$key};
            $name = $rel->{record}->{$key}->{'Name'};
            $log->debug("_reg_rel: type is $type; name is $name");
        }
    }

    # Create or get the item id
    my $rid = _get_or_make_rel ($mdrId, $localId, $type, $source, $target);
    if (!$rid) {
        $log->error( "_reg_rel: Unable to create or find rel $localId");
        _add_reg_response($localId, undef, 0, 
            "An error occured while registering");
        return;
    }

    $log->debug("_reg_rel: created/found rel $rid ($localId)");

    # Add the given property's and values to the item
    foreach my $key (keys %$record) {
        if (!_upd_rel_prop($rid, $key, $record->{$key})) {
            $log->error("_reg_rel: Unable to add property $key to $localId");
            return;
        }
        $log->debug("_reg_item: added prop $key=$record->{$key} to $localId");
    }

    $log->debug("_reg_rel: end of function");
    _add_reg_response($localId, $rid, 1, undef);
    return 1;
}

# adds a hash element to our response hash given the following parameters:
#  the localId, the primary key in DB (if applicable), 0/1 to indicate if it
#  was accepted, and reason if it was declined
sub _add_reg_response {
    my ($id, $aid, $accepted, $reason) = @_;
    my $res;
    
    $res->{instanceId}->{mdrId}=$authedMDRID;
    $res->{instanceId}->{localId}=$id;
    if ($accepted) {
        $res->{accepted}->{alternateInstanceId} = $aid;
    } else {
        $res->{declined}->{reason} = $reason;
    }

    push (@{$reshash->{registerInstanceResponse}}, $res);
}

sub _write_response {
    my ($restype) = @_;
    return XMLout($reshash, 
            RootName => "registerResponse",
            KeyAttr => { 
                accepted => "alternateInstanceId", 
                declined => "reason" },
            NoAttr => 1,
        );
}

sub _authenticate_mdr {
    return 1;
}
###########################################################################
# MAIN
###########################################################################

my $CONF = AppConfig->new({CREATE => 1}, 'logconfig=s');
if (!$CONF->file($CFGFILE)) {
    die "Could not get logconfig file location."
}

# Setup logging
Log::Log4perl->init($CONF->logconfig());
$log = Log::Log4perl->get_logger("RS");

if ($ARGV[1] eq undef) {
    print "Usage: regservice <register> <regXml>\n";
    exit;
}
# Get the input
my @input = @ARGV;
# Get rid of the first parameter,  which is useful only to remctl
shift @input;

# Convert the input
my $xml = join(' ', @input);
my $reghash;

eval {
    $reghash = XMLin($xml);
};
if ($@) {
    $log->error("Problem parsing XML: $@");
    $reshash->{registerInstanceResponse}[0]->{instanceId} = "GLOBAL";
    $reshash->{registerInstanceResponse}[0]->{declined}->{reason}
        = "Could not Parse XML";
    print _write_response();
    exit;
}

# Authenticate the MDR
$authedMDRID = $reghash->{mdrId};
if (!_authenticate_mdr()) {
    $log->error("Problem authenticating MDR.");
    $reshash->{registerInstanceResponse}[0]->{instanceId} = "GLOBAL";
    $reshash->{registerInstanceResponse}[0]->{declined}->{reason}
        = "Unauthorized MDR";
    print _write_response();
    exit;
}

# Process the input
my $items = $reghash->{itemList}->{item};
my $rels = $reghash->{relationshipList}->{relationship};

foreach my $item (@$items) {
    if (_reg_item($item)) {
        $log->info("Registered item $item->{instanceId}->{localId}");
    } else {
        $log->error("Reg failed: item $item->{instanceId}->{localId}"); 
    }
}

foreach my $rel (@$rels) {
    if (_reg_rel($rel)) {
        $log->info("Registered rel $rel->{instanceId}->{localId}");
    } else {
        $log->error("Reg failed: rel $rel->{instanceId}->{localId}"); 
    }
}

print _write_response();
exit $?;
