#!/usr/bin/perl

use strict;

use Config::Simple;
use DBI;
use File::Basename;
use POSIX;

use SVN::Core;
use SVN::Repos;
use SVN::Fs;

use Net::LDAP;
use Net::LDAP qw(LDAP_SUCCESS);
use URI;
use Mail::Sendmail;

my $CFG_FILE = '@CFGDIR@/codepot.ini';
my $REPOFS = $ARGV[0];
my $REPOBASE = basename($REPOFS);
my $REV = $ARGV[1];

my $QC = '';

sub get_config
{
	my $cfg = new Config::Simple();

	if (!$cfg->read ($CFG_FILE))
	{
		return undef;
	}

	my $config = {
		login_model => $cfg->param ('login_model'),
		
		ldap_server_uri => $cfg->param ('ldap_server_uri'),
		ldap_server_protocol_version => $cfg->param ('ldap_server_protocol_version'),
		ldap_auth_mode => $cfg->param ('ldap_auth_mode'),
		ldap_userid_format => $cfg->param ('ldap_userid_format'),
		ldap_password_format => $cfg->param ('ldap_password_format'),
		ldap_admin_binddn => $cfg->param ('ldap_admin_binddn'),
		ldap_admin_password => $cfg->param ('ldap_admin_password'),
		ldap_userid_search_base => $cfg->param ('ldap_userid_search_base'),
		ldap_userid_search_filter => $cfg->param ('ldap_userid_search_filter'),
		ldap_mail_attribute_name => $cfg->param ('ldap_mail_attribute_name'),

		database_hostname => $cfg->param ("database_hostname"),
		database_port => $cfg->param ("database_port"),
		database_username => $cfg->param ("database_username"),
		database_password => $cfg->param ("database_password"),
		database_name => $cfg->param ("database_name"),
		database_driver => $cfg->param ("database_driver"),
		database_prefix => $cfg->param ("database_prefix"),
		database_store_gmt => $cfg->param ("database_store_gmt"),

		email_sender => $cfg->param ("email_sender"),
		commit_notification => $cfg->param ("commit_notification"),
		commit_notification_url => $cfg->param ("commit_notification_url")
	};

	return $config;
}

sub open_database
{
	my ($cfg) = @_;

	my $dbtype = $cfg->{database_driver};
	my $dbname = $cfg->{database_name};
	my $dbhost = $cfg->{database_hostname};
	my $dbport = $cfg->{database_port};

	if ($dbtype eq 'postgre') { $dbtype = 'Pg'; }
	elsif ($dbtype eq 'oci8') { $dbtype = 'Oracle'; }
	elsif ($dbtype eq 'mysqli') { $dbtype = 'mysql'; }

	my $dbstr;
	my $dbuser;
	my $dbpass;
	if ($dbtype eq 'Oracle')
	{
		$QC = '"';
		$dbstr = "DBI:$dbtype:";
		$dbuser = $cfg->{database_username} . '/' . $cfg->{database_password} . '@' . $dbhost;
		$dbpass = '';
	}
	else
	{
		$dbstr = "DBI:$dbtype:database=$dbname;";
		if (length($dbhost) > 0) { $dbstr .= "host=$dbhost;"; }
		if (length($dbport) > 0) { $dbstr .= "port=$dbport;"; }

		$dbuser = $cfg->{database_username};
		$dbpass = $cfg->{database_password};
	}

	my $dbh = DBI->connect(
		$dbstr, $dbuser, $dbpass,
		{ RaiseError => 0, PrintError => 0, AutoCommit => 0 }
	);

	return $dbh;
}

sub close_database
{
	my ($dbh) = @_;
	$dbh->disconnect ();
}

sub find_issue_reference_in_commit_message
{
	my ($dbh, $prefix, $projectid, $revision, $commit_message) = @_;

	# find [[#IXXXX]]
	my @issue_ids = ($commit_message =~ /\[\[#I(\d+)\]\]/g);
	# find #XXXX
	my @issue_ids2 = ($commit_message =~ /(^|[^#])#(\d+)(\D|$)/g);

	# find unique issue ids in the findings.
	my %tmp;
	@tmp{@issue_ids} = 1;
	for (my $i = 0; $i < scalar(@issue_ids2); $i += 3)
	{
		my $id = @issue_ids2[$i + 1];
		@tmp{$id} = 1;
	}
	my @unique_issue_ids = keys(%tmp);

	$dbh->begin_work ();

	my $query = $dbh->prepare ("DELETE FROM ${QC}${prefix}issue_coderev${QC} WHERE ${QC}codeproid${QC}=? AND ${QC}coderev${QC}=?");
	if (!$query || !$query->execute ($projectid, $revision))
	{
		my $errstr = $dbh->errstr();
		if ($query) { $query->finish (); }
		$dbh->rollback ();
		return (-1, $errstr);
	}
	$query->finish ();

	for my $issue_id(@unique_issue_ids)
	{
		my $query = $dbh->prepare ("INSERT INTO ${QC}${prefix}issue_coderev${QC} (${QC}projectid${QC},${QC}issueid${QC},${QC}codeproid${QC},${QC}coderev${QC}) VALUES (?,?,?,?)");
		if ($query)
		{
			# ignore errors
			$query->execute ($projectid, $issue_id, $projectid, $revision);
			$query->finish ();
		}
	}

	$dbh->commit ();
	return (0, undef);
}

sub write_commit_log
{
	my ($dbh, $prefix, $projectid, $revision, $userid, $store_gmt) = @_;

#+------+---------+-----------+---------------------------+---------------------+---------------+-----------------+
#| id   | type    | projectid | message                   | createdon           | action        | userid          |
#+------+---------+-----------+---------------------------+---------------------+---------------+-----------------+
#|  895 | code    | codepot   | svn,codepot,72            | 2011-10-10 14:26:43 | commit        | hyunghwan.chung | 

	my $message = "svn,$projectid,$revision";
	my $timestamp;

	if (($store_gmt =~ /^\d+?$/ && int($store_gmt) != 0) || (lc($store_gmt) eq 'yes'))
	{
		$timestamp = POSIX::strftime ('%Y-%m-%d %H:%M:%S', gmtime());
	}
	else
	{
		$timestamp = POSIX::strftime ('%Y-%m-%d %H:%M:%S', localtime());
	}

	# the PHP side is executing ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS.FF.
	# do i have to do it here or  use the database time (CURRENT_TIMESTAMP) instead?
	# make sure that you have the same time between the app server and the data server.
	# to minize side-effect of using the time of data server.
	#my $createdon = POSIX::strftime ('%Y-%m-%d %H:%M:%S', localtime());

	$dbh->begin_work ();

	#my $query = $dbh->prepare ("INSERT INTO ${QC}${prefix}log${QC} (${QC}type${QC},${QC}projectid${QC},${QC}message${QC},${QC}createdon${QC},${QC}action${QC},${QC}userid${QC}) VALUES (?,?,?,?,?,?)");
	#if (!$query || !$query->execute ('code', $projectid, $message, $createdon, 'commit', $userid))

	my $query = $dbh->prepare ("INSERT INTO ${QC}${prefix}log${QC} (${QC}type${QC},${QC}projectid${QC},${QC}message${QC},${QC}createdon${QC},${QC}action${QC},${QC}userid${QC}) VALUES (?,?,?,?,?,?)");
	if (!$query || !$query->execute ('code', $projectid, $message, $timestamp, 'commit', $userid))
	{
		my $errstr = $dbh->errstr();
		if ($query) { $query->finish (); }
		$dbh->rollback ();
		return (-1, $errstr);
	}

	$query->finish ();
	$dbh->commit ();
	return (0, undef);
}

sub get_author
{
	my $pool = SVN::Pool->new(undef); 
	my $svn = eval { SVN::Repos::open ($REPOFS, $pool) };
	if (!defined($svn))
	{
		print (STDERR "Cannot open svn - $REPOFS\n");
		return undef;
	}
	
	my $fs = $svn->fs ();
	if (!defined($fs))
	{
		print (STDERR "Cannot open fs - $REPOFS\n");
		return undef;
	}

	my $author = $fs->revision_prop ($REV, 'svn:author');
	return $author;
}

sub get_commit_message
{
	my $pool = SVN::Pool->new(undef);
	my $svn = eval { SVN::Repos::open ($REPOFS, $pool) };
	if (!defined($svn))
	{
		print (STDERR "Cannot open svn - $REPOFS\n");
		return undef;
	}

	my $fs = $svn->fs ();
	if (!defined($fs))
	{
		print (STDERR "Cannot open fs - $REPOFS\n");
		return undef;
	}

	my $logmsg = $fs->revision_prop ($REV, 'svn:log');
	return $logmsg;
}


sub format_string 
{
	my ($fmt, $userid, $password) = @_;

	my $out = $fmt;
	$out =~ s/\$\{userid\}/$userid/g;
	$out =~ s/\$\{password\}/$password/g;

	return $out;
}

sub get_author_email_ldap
{
	my ($cfg, $userid) = @_;

	my $uri = URI->new ($cfg->{ldap_server_uri});
	my $ldap = Net::LDAP->new (
		$uri->host, 
		scheme => $uri->scheme,
		port => $uri->port,
		version => $cfg->{ldap_server_protocol_version}
	);
	if (!defined($ldap))
	{
		print (STDERR 'Cannot create LDAP');
		return (-1, undef);
	}

	my $f_rootdn = format_string ($cfg->{ldap_admin_binddn}, $userid, '');
	my $f_rootpw = format_string ($cfg->{ldap_admin_password}, $userid, '');

	my $res = $ldap->bind ($f_rootdn, password => $f_rootpw);
	if ($res->code != LDAP_SUCCESS) 
	{
		print (STDERR "Cannot bind LDAP as $f_rootdn - " . $res->error());
		$ldap->unbind();
		return (-1, undef); 
	}


	my $f_basedn = '';
	my $f_filter = '';

	if ($cfg->{ldap_auth_mode} == 2)
	{
		$f_basedn = format_string ($cfg->{ldap_userid_search_base}, $userid, '');
		$f_filter = format_string ($cfg->{ldap_userid_search_filter}, $userid, '');

		$res = $ldap->search (base => $f_basedn, scope => 'sub', filter => $f_filter);
		if ($res->code != LDAP_SUCCESS)
		{
			$ldap->unbind();
			return (-1, undef);
		} 

		my $entry = $res->entry(0); # get the first entry only
		if (!defined($entry))
		{
			$ldap->unbind();
			return (0, undef);
		}

		$f_basedn = $entry->dn ();
	}
	else
	{
		$f_basedn = format_string ($cfg->{ldap_userid_format}, $userid, '');
	}

	$f_filter = '(' . $cfg->{ldap_mail_attribute_name} . '=*)';

	$res = $ldap->search (base => $f_basedn, scope => 'sub', filter => $f_filter);
	if ($res->code != LDAP_SUCCESS) 
	{
		$ldap->unbind();
		return (0, undef);
	}

	my $entry = $res->entry(0); # get the first entry only
	if (!defined($entry))
	{
		$ldap->unbind();
		return (0, undef);
	}

	my $xret = 0;
	my $email = '';
	my @attrs = $entry->attributes ();
	foreach my $attr (@attrs)
	{
		if ($attr eq  $cfg->{ldap_mail_attribute_name})
		{
			$email = $entry->get_value ($attr);
			$xret = 1;
			last;
		 }
	}

	$ldap->unbind();
	return ($xret, $email);
}

sub get_author_email_db
{
	my ($cfg, $dbh, $prefix, $userid) = @_;

	my $query = $dbh->prepare ("SELECT ${QC}email${QC} FROM ${QC}${prefix}user_account${QC} WHERE ${QC}userid${QC}=?");
	if (!$query || !$query->execute ($userid))
	{
		return (-1, $dbh->errstr());
	}

	if (my @row = $query->fetchrow_array())
	{
		return (1, @row[0]);
	}

	return (0, undef);
}

sub email_message_to_project_members
{
	my ($cfg, $dbh, $prefix, $projectid, $subject, $message) = @_;

	my $query = $dbh->prepare ("SELECT ${QC}userid${QC} FROM ${QC}${prefix}project_membership${QC} WHERE ${QC}projectid${QC}=?");
	if (!$query || !$query->execute ($projectid))
	{
		if ($query) { $query->finish (); }
		return (-1, $dbh->errstr());
	}

	my @members;
	while (my @row = $query->fetchrow_array())
	{
		push (@members, $row[0]);
	}
	$query->finish ();

	my $recipients = '';
	foreach my $member (@members)
	{
		my $xret;
		my $email;

		if ($cfg->{login_model} eq 'LdapLoginModel')
		{
			($xret, $email) = get_author_email_ldap ($cfg, $member)
		}
		elsif ($cfg->{login_model} eq 'DbLoginModel')
		{
			($xret, $email) = get_author_email_db ($cfg, $dbh, $prefix, $member);
		}
		else
		{
			$xret = -2;
			$email = '';
		}

		if ($xret >= 1 && defined($email) && length($email) > 0)
		{
			if (length($recipients) > 0) { $recipients .= ', '; }
			$recipients .= $email;
		}
	}

	if (length($recipients) <= 0) { return (0, undef); }

	my %mail = (
		To => $recipients,
		Subject => $subject,
		Message => $message
	);

	if (length($cfg->{email_sender}) > 0)
	{
		$mail{From} .= $cfg->{email_sender};
	}

	Mail::Sendmail::sendmail (%mail);
	return (1, undef);
}

sub format_commit_url 
{
	my ($fmt, $projectid, $author, $rev) = @_;

	my $out = $fmt;
	$out =~ s/\$\{PROJECTID\}/$projectid/g;
	$out =~ s/\$\{AUTHOR\}/$author/g;
	$out =~ s/\$\{REV\}/$rev/g;

	return $out;
}
#------------------------------------------------------------
# MAIN
#------------------------------------------------------------

my $AUTHOR = get_author();
if (!defined($AUTHOR))
{
	print (STDERR "Cannot get author for $REPOBASE $REV\n");
	exit (1);
}
chomp ($AUTHOR);

my $cfg = get_config ();
if (!defined($cfg))
{
	print (STDERR "Cannot load codepot configuration file\n");
	exit (1);
}

my $dbh = open_database ($cfg);
if (!defined($dbh))
{
	printf (STDERR "Cannot open database - %s\n", $DBI::errstr);
	exit (1);
}

my $raw_commit_message = get_commit_message();

find_issue_reference_in_commit_message ($dbh, $cfg->{database_prefix}, $REPOBASE, $REV, $raw_commit_message);
write_commit_log ($dbh, $cfg->{database_prefix}, $REPOBASE, $REV, $AUTHOR, $cfg->{database_store_gmt});

if (lc($cfg->{commit_notification}) eq 'yes')
{
	my $commit_subject = "Commit r$REV by $AUTHOR in $REPOBASE";
	my $commit_message = '';

	if ($cfg->{commit_notification_url} eq '')
	{
		$commit_message = $commit_subject;
	}
	else
	{
		$commit_message = format_commit_url($cfg->{commit_notification_url}, $REPOBASE, $AUTHOR, $REV);
	}

	if (defined($raw_commit_message))
	{
		$commit_message = $commit_message . "\n" . $raw_commit_message;
	}


	email_message_to_project_members ($cfg, $dbh, $cfg->{database_prefix}, $REPOBASE, $commit_subject, $commit_message);
}

close_database ($dbh);

exit (0);
