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

use LWP;
use LWP::UserAgent;
use HTML::TreeBuilder;
use Getopt::Long;

my $DEBUG;

sub usage()
{	print STDERR <<EOF;
$0: Collects and prints system and warranty information from 
Dell website for specified systems by service tag.

Usage:
$0 [ --machine_readable ] [ -file file.txt ] [ system1 [ system2 ... ]]

Systems can be given as arguments or in a file (with --file option, one
system per line).  You must specify the service tag; you can optionally
precede with a system name followed by a colon, e.g. either 1ABC0742 or
mysystem:1ABC0742 is acceptable.

In machine_readable mode, output is 
servicetag|hostname|model|shipdate|enddate|daysleft
otherwise try to be more user readable.

EOF
}

my $VERSION='0.2.0';

#Changelog:
#	0.2.0: Adding versioning, -debug flag, -Version flag
#		LWP::UserAgent now config'ed to support cookies and redirects
#		of post requests (handle slight change in Dell's URL and web
#		site).
#	unversioned: original version
sub debug(@)
#Print message if in debug mode
{	if ( $DEBUG)
	{	print STDERR @_, "\n";
	}
}

my $ua;

sub _get_dell_warranty_info_page($)
#Given service tag, get the dell warranty info page (as raw html)
#Returns string containing the html, or undef if error.
#Dies on errors
{	my $svctag = shift;
	$svctag = lc $svctag;

	unless ( $ua && ref($ua) )
	{	$ua = new LWP::UserAgent;
		#Allow POST requests to follow redirects
		push @{ $ua->requests_redirectable }, 'POST';
	}
	$ua->timeout(30);
	$ua->cookie_jar({});
	my $url="http://support.dell.com/support/topics/global.aspx/support/my_systems_info/details";
	#my $url="http://support.dell.com/support/topics/global.aspx/support/my_systems_info/details?c=us&l=en&s=hied";
	my %args=( 	c=>'us',
			l=>'en',
			s=>'gen',
			tab=>1,
			ServiceTag=>$svctag,
		);

	my $resp = $ua->post($url,\%args);
	if ( $resp->is_success )
	{	debug("Successfully got page $url");
		return $resp->content;
	} else
	{	my $err=$resp->status_line;
		die "Error requesting Dell warranty info page: $err\n";
	}
}

#Override in case want name of hask key different than label in page
#Usage: label => key
my %sysinfo_key_override= ();

sub _read_sysinfo_table($)
#Reads and parses the system information HTML table (HTML::Element for table)
#Returns hash ref with keys:
#	service_tag
#	system_type
#	ship_date
{	my $table = shift;

	my $sysinfo = {};

	my @rows = $table->look_down('_tag','tr');
	my ( $row, @text, $label, $value);
	foreach $row (@rows)
	{	@text = $row->look_down('_tag','~text');
		$label = $text[0]->attr('text');
		$label = lc $label;
		$label =~ s/[\s_]+/_/g;
		$label =~ s/\W//g;
		$label = $sysinfo_key_override{$label} 
			if $sysinfo_key_override{$label};
		$value = $text[1]->attr('text');
		$value =~ s/\xA0/ /g; #Convert &nbsp; to regular spaces
		$value=~s/\s+/ /g;
		$value=~s/^\s*//; $value=~s/\s*$//;

		$sysinfo->{$label} = $value;
	}
	return $sysinfo;
}

#Override in case want name of hash key different than label in page
#Usage: label => key
my %winfo_key_override= ();

sub _read_warranty_table($)
#Given the HTML::Element representing the table containing the warranty
#info, returns a hash ref consisting of
#	end_date => date last warranty expires
#	days_left => # of days until last warranty expires
#	detail => list ref consising of hash refs with keys
#		description => description of support level
#		start_date => date starts
#		end_date => date expires
#		days_left => # days until expires
{	my $table = shift;
	return unless $table && ref($table);

	my @rows = $table->look_down('_tag','tr');
	return unless @rows;

	my $header_row = shift @rows;
	#Get text content
	my @fields = $header_row->look_down('_tag','~text');
	@fields = map { $_->attr('text') } @fields;
	#Normalize and do any substitutions
	foreach (@fields)
	{	$_ = lc $_;
		s/[\s_]+/_/g;
		s/\W//g;
		$_ = $winfo_key_override{$_} 
			if $winfo_key_override{$_};
	}

	my $details = [];
	my $end_date;
	my $days_left = -1;

	my ( $row, @data, $fld, $val, $i);
	foreach $row (@rows)
	{	#Get and normalize the data fields
		@data = $row->look_down('_tag','~text');
		@data = map { $_->attr('text') } @data;
		foreach (@data)
		{ 	s/\xA0/ /g; #Convert &nbsp; to regular spaces
			s/\s+/ /g;
			s/^\s*//; s/\s*$//;
		}

		my $rec = {};
		foreach $i ( 0 .. scalar(@data)-1 )
		{	$rec->{$fields[$i]} = $data[$i];
		}
		push @$details, $rec;
		if ( $rec->{days_left} > $days_left )
		{	$days_left = $rec->{days_left};
			$end_date = $rec->{end_date};
		}
	}

	return {	days_left=>$days_left,
			end_date=>$end_date,
			details=>$details,
		};
}

sub _parse_dell_warranty_html($)
#Given a text string containing the raw html of the Dell warranty info page,
#we parse it and return a hash ref containing info about the requested system
{	my $html = shift;

	my $tree = new HTML::TreeBuilder;
	$tree->parse_content($html);
	$tree->elementify;
	$tree->delete_ignorable_whitespace;
	$tree->objectify_text;

	#Find the ~text pseudo-tag containing the line 'System Summary'
	my $syssum_textnode = $tree->look_down( 'text', 'System Summary');
	unless ( $syssum_textnode && ref($syssum_textnode) )
	{	warn "Cannot find System Summary information\n";
		return;
	}
	#Find the table tag containing it
	my $syssum_table = $syssum_textnode->look_up('_tag', 'table');

	#Find the tr tag containing this table
	my $syssum_tr = $syssum_table->look_up('_tag','tr');

	#Find sibling table tags to right of syssum_table
	my @sib_tables = $syssum_table->right;
	@sib_tables = grep { $_->tag eq 'table' } @sib_tables;

	#First sibling table to right is sysinfo table
	my $sysinfo_table = shift @sib_tables;
	die "No sysinfo table found." unless $sysinfo_table;
	my $sysinfo = _read_sysinfo_table($sysinfo_table);

	#Find sibling tr's of syssum_tr
	my @sib_tr = $syssum_tr->right;
	@sib_tr = grep { $_->tag eq 'tr' } @sib_tr;

	#Find first sibling tr with a table containing another table
	my ($sib);
	my $warranty_table;
	foreach $sib (@sib_tr)
	{	$warranty_table = $sib->look_down('_tag','table');
		if ( $warranty_table )
		{	#Want a table cotaining another table
			my @temp = $warranty_table->look_down(
				'_tag','table');
			shift @temp; #Skip the root table
			$warranty_table = shift @temp;
		}
		last if $warranty_table;
	}

	my $winfo;
	if ( $warranty_table )
	{	$winfo = _read_warranty_table($warranty_table);
	} else
	{	warn "No warranty table found.\n";
		$winfo = {};
	}

	$tree->delete;
	my %hash = ( %$sysinfo, %$winfo );
	return \%hash;
}

sub get_dell_warranty_info($)
#Given a service tag number, return hash ref containing system and warranty
#information
{	my $svctag = shift;
	my $html = _get_dell_warranty_info_page($svctag);
	my $info = _parse_dell_warranty_html($html);
	return $info;
}

sub dump_info_machine($$;$)
#Given a service tag, the warranty_info hash, and an optional hostname,
#dump the output
{	my $svctag = uc shift;
	my $info = shift;
	my $hname = shift;

	unless ( $svctag )
	{	#No svctag, just print header
		print <<EOF;
#ServiceTag|Hostname|SystemType|ShipDate|EndDate|DaysLeft
EOF
		return;
	}

	my $htext = $svctag;
	$htext = "$svctag ($hname)" if $hname;
	my ($systype, $shipdate, $enddate, $daysleft);

	if ( $info && ref($info) eq 'HASH' )
	{	#Looks valid
		#Check svctags match
		my $svctag2 = uc $info->{service_tag};
		if ( $svctag ne $svctag2 )
		{	warn "$htext: Service tag mismatch, got '$svctag2'\n";
			#Don;t use hname, use hash based svctag 
			$svctag = $svctag2;
			$hname="TAG-MISMATCH";
		}
		$systype = $info->{system_type} || '';
		$shipdate = $info->{ship_date} || '';
		$enddate = $info->{end_date} || 'unknown';
		$daysleft = $info->{days_left};
		$daysleft = '' unless defined $daysleft;
	} else
	{	warn "$htext: No information available from Dell web page\n";
		$systype='unknown';
		$shipdate='unknown';
		$enddate='unknown';
		$daysleft='';
	}
	print <<EOF;
$svctag|$hname|$systype|$shipdate|$enddate|$daysleft
EOF
}

sub dump_info_human($$;$)
#Given a service tag, the warranty_info hash, and an optional hostname,
#dump the output
{	my $svctag = uc shift;
	my $info = shift;
	my $hname = shift;

	return unless $svctag; #No headers in human mode

	my $htext = "ServiceTag: $svctag";
	$htext = "$hname: $svctag" if $hname;

	unless ( $info && ref($info) eq 'HASH' )
	{	print "$htext: No information about this system.\n";
		return;
	}

	#Check svctags match
	my $svctag2 = uc $info->{service_tag};
	if ( $svctag ne $svctag2 )
	{	print "$htext: Service tag mismatch, got '$svctag2'\n";
		return;
	}

	my $systype = $info->{system_type} || 'unknown';
	my $shipdate = $info->{ship_date} || 'unknown';
	my $enddate = $info->{end_date} || 'unknown';
	my $daysleft = $info->{days_left};
	$daysleft = '' unless defined $daysleft;
	print <<EOF;
$htext
	System Type: $systype
	Shipped on: $shipdate
	Warranty Expires: $enddate
	Days left on Warranty: $daysleft
EOF

}

sub dump_info($$$;$)
#Dumps information, in human or machine readable mode
{	my $mach_mode = shift;
	my $svctag = uc shift;
	my $info = shift;
	my $hname = shift;
	
	if ( $mach_mode )
	{	dump_info_machine($svctag, $info, $hname);
	} else
	{	dump_info_human($svctag, $info, $hname);
	}
}

#Main
my $help;
my $mach_mode=0;
my $file;
my $version;

my $res = GetOptions(
	'help!' => \$help,
	'debug!' => \$DEBUG,
	'Version|VERSION!' => \$version,

	'machine_readable!' => \$mach_mode,
	'file=s' => \$file,
	);

unless ( $res )
{	usage;
	die "Error parsing options.\n";
}

if ( $help )
{	usage;
	exit 0;
}

if ( $version )
{	print STDERR "$0: Version $VERSION\n";
	exit 0;
}

debug("Entering debug mode.");

my @systems = @ARGV;

unless ( scalar(@systems) || $file )
{	usage;
	die "You must specify systems/service tags or a file.\n";
}

if ( $file )
{	unless ( -r $file )
	{	usage;
		die "File '$file' is not readable.\n";
	}
	open(FILE,"<$file") or die "Unable to open file $file for reading: $!";
	my @lines = <FILE>;
	close(FILE);
	chomp(@lines);
	@lines = grep !/^\s*#/, @lines; #Skip comments
	push @systems, @lines;
}

#Print header
dump_info($mach_mode,undef,undef,undef);

my ($system, $hname, $tag);
foreach $system (@systems)
{	
	$system=~s/^\s*//; $system=~s/\s*$//;
	if ( $system=~/:/ )
	{	($hname,$tag) = split /\s*:\s*/, $system;
	} elsif ( $system =~ /\s/ )
	{	($hname,$tag) = split /\s+/, $system;
	} else
	{	$hname=undef;
		$tag = $system;
	}	

	my $info = get_dell_warranty_info($tag);
	dump_info($mach_mode,$tag,$info,$hname);
}
