<?php
/**
 * Implements the notification system of Titan.
 *
 * @author Camilo Carromeu <camilo@carromeu.com>
 * @category class
 * @package core
 * @subpackage alert
 * @copyright 2005-2017 Titan Framework
 * @license http://www.titanframework.com/license/ BSD License (3 Clause)
 * @see Instance
 * @link http://www.titanframework.com/docs/tutorials/alerts/
 */
class Alert
{
	static private $alert = FALSE;

	private $templates = array ();

	private $tags = array ();

	static private $active = NULL;

	private final function __construct ()
	{
		$array = Instance::singleton ()->getAlert ();

		if (!array_key_exists ('xml-path', $array))
			throw new Exception ('Not located [xml-path] attribute on &lt;alert&gt;&lt;/alert&gt; tag in file [configure/titan.xml]!');

		$file = $array ['xml-path'];

		$cacheFile = Instance::singleton ()->getCachePath () .'parsed/'. fileName ($file) .'_'. md5_file ($file) .'.php';

		if (file_exists ($cacheFile))
			$array = include $cacheFile;
		else
		{
			$xml = new Xml ($file);

			$array = $xml->getArray ();

			$array = $array ['alert-mapping'][0];

			xmlCache ($cacheFile, $array);
		}

		if (array_key_exists ('alert', $array))
			foreach ($array ['alert'] as $trash => $alert)
			{
				if (!array_key_exists ('id', $alert))
					continue;

				$this->templates [$alert ['id']] = $alert;
			}
	}

	static public function singleton ()
	{
		if (self::$alert !== FALSE)
			return self::$alert;

		$class = __CLASS__;

		self::$alert = new $class ();

		return self::$alert;
	}

	public function getAlerts ($userId)
	{
		$sql = "SELECT a.*, au._read, at._name AS author, av._name AS user,
				CASE WHEN a._until IS NULL THEN to_char (now(), 'MM-DD-YYYY') ELSE to_char (a._until, 'MM-DD-YYYY') END AS f_until,
				extract (epoch from a._until) as u_until,
				to_char (a._create, 'HH24-MI-SS-MM-DD-YYYY') AS f_create
				FROM _alert_user au
				INNER JOIN _alert a ON a._id = au._alert
				LEFT JOIN _user at ON at._id = a._user
				INNER JOIN _user av ON av._id = au._user
				WHERE au._user = :user AND au._delete = B'0' AND (a._until IS NULL OR a._until > CURRENT_TIMESTAMP)
				ORDER BY a._update DESC";

		$sth = Database::singleton ()->prepare ($sql);

		$sth->execute (array (':user' => $userId));

		$array = array ();

		$dTags = array ('[SYSTEM]' => Instance::singleton ()->getName (),
						'[URL]' => Instance::singleton ()->getUrl ());

		while ($obj = $sth->fetch (PDO::FETCH_OBJ))
		{
			if (!array_key_exists ($obj->_template, $this->templates))
				continue;

			$tags = $dTags;

			$tags ['[AUTHOR]'] = is_null ($obj->author) ? Instance::singleton ()->getName () : $obj->author;
			$tags ['[USER]'] = $obj->user;
			$tags ['[DAYS_MISSING]'] = (int) $obj->u_until <= 0 ? 0 : floor (($obj->u_until - time ()) / (60 * 60 * 24));

			$u = explode ('-', $obj->f_until);
			$tags ['[UNTIL]'] = strftime ('%x', mktime (0, 0, 0, (int) $u [0], (int) $u [1], (int) $u [2]));

			$d = explode ('-', $obj->f_create);
			$tags ['[DATE]'] = strftime ('%c', mktime ((int) $d [0], (int) $d [1], (int) $d [2], (int) $d [3], (int) $d [4], (int) $d [5]));

			$uTags = unserialize ($obj->_parameters);

			if (is_array ($uTags))
				$tags = array_merge ($tags, $uTags);

			$array [$obj->_id]['_MESSAGE_'] = str_replace (array_keys ($tags), $tags, $this->getFromTemplate ($obj->_template, 'message'));
			$array [$obj->_id]['_GO_'] = str_replace (array_keys ($tags), $tags, $this->getFromTemplate ($obj->_template, 'go'));
			$array [$obj->_id]['_ICON_'] = $this->getFromTemplate ($obj->_template, 'icon');
			$array [$obj->_id]['_READ_'] = (int) $obj->_read ? 'true' : 'false';
		}

		return $array;
	}

	public function getAlertMessage ($id, $user, $useUntil = TRUE)
	{
		$sql = "SELECT a.*, au._read, at._name AS author, av._name AS user,
				CASE WHEN a._until IS NULL THEN to_char (now(), 'MM-DD-YYYY') ELSE to_char (a._until, 'MM-DD-YYYY') END AS f_until,
				extract (epoch from a._until) as u_until,
				to_char (a._create, 'HH24-MI-SS-MM-DD-YYYY') AS f_create
				FROM _alert_user au
				INNER JOIN _alert a ON a._id = au._alert
				LEFT JOIN _user at ON at._id = a._user
				INNER JOIN _user av ON av._id = au._user
				WHERE au._user = :user AND a._id = :id";

		if ($useUntil)
			$sql .= " AND (a._until IS NULL OR a._until > CURRENT_TIMESTAMP)";

		$sth = Database::singleton ()->prepare ($sql);

		$sth->bindParam (':id', $id, PDO::PARAM_INT);
		$sth->bindParam (':user', $user, PDO::PARAM_INT);

		$sth->execute ();

		$array = array ();

		$dTags = array ('[SYSTEM]' => Instance::singleton ()->getName (),
						'[URL]' => Instance::singleton ()->getUrl ());

		$obj = $sth->fetch (PDO::FETCH_OBJ);

		if (!is_object ($obj) || !array_key_exists ($obj->_template, $this->templates))
			return '';

		$tags = $dTags;

		$tags ['[AUTHOR]'] = is_null ($obj->author) ? Instance::singleton ()->getName () : $obj->author;
		$tags ['[USER]'] = $obj->user;
		$tags ['[DAYS_MISSING]'] = (int) $obj->u_until <= 0 ? 0 : floor (($obj->u_until - time ()) / (60 * 60 * 24));

		$u = explode ('-', $obj->f_until);
		$tags ['[UNTIL]'] = strftime ('%x', mktime (0, 0, 0, (int) $u [0], (int) $u [1], (int) $u [2]));

		$d = explode ('-', $obj->f_create);
		$tags ['[DATE]'] = strftime ('%c', mktime ((int) $d [0], (int) $d [1], (int) $d [2], (int) $d [3], (int) $d [4], (int) $d [5]));

		$uTags = unserialize ($obj->_parameters);

		if (is_array ($uTags))
			$tags = array_merge ($tags, $uTags);

		return str_replace (array_keys ($tags), $tags, $this->getFromTemplate ($obj->_template, 'message'));
	}

	private function getFromTemplate ($template, $attribute)
	{
		if (!array_key_exists ($template, $this->templates))
			return 'N/A';

		if (!array_key_exists ($attribute, $this->templates [$template]))
			return 'N/A';

		return $this->templates [$template][$attribute];
	}

	private function register ($template, $assign, $users, $tags = NULL, $until = NULL, $author = NULL, $overwrite = TRUE, $mail = NULL)
	{
		if ((!is_integer ($users) && !is_array ($users)) || (!is_null ($until) && !is_integer ($until)) || (!is_null ($until) && is_integer ($until) && $until > 0 && $until < time ()))
			return FALSE;

		if (!array_key_exists ($template, $this->templates))
			return FALSE;

		if (!is_null ($until) && !$until)
			$until = NULL;

		if (!is_array ($tags))
			$tags = array ();

		if (!is_array ($users))
			$users = array ($users);

		$users = array_filter ($users);

		if (!sizeof ($users))
			return FALSE;

		if ((!is_array ($mail) && !is_integer ($mail)) || (!is_array ($mail) && is_integer ($mail) && !$mail))
			$mail = array ();

		if (is_integer ($mail) && $mail > time ())
			$mail = array ($mail);

		if (is_null ($author))
			$author = User::singleton ()->getId ();

		$db = Database::singleton ();

		$sql = "SELECT _id FROM _alert WHERE _template = :template AND _assign = :assign";

		$sth = $db->prepare ($sql);

		$sth->execute (array (':template' => $template, ':assign' => $assign));

		$obj = $sth->fetch (PDO::FETCH_OBJ);

		try
		{
			if (is_object ($obj))
			{
				if (!$overwrite)
					return FALSE;

				$id = $obj->_id;

				$sql = "UPDATE _alert SET _until = timestamptz 'epoch' + :until * interval '1 second', _parameters = :parameters, _user = :user, _update = NOW() WHERE _id = :id";

				$db->prepare ($sql)->execute (array (':until' => $until, ':parameters' => serialize ($tags), ':user' => $author, ':id' => $id));

				$sql = "UPDATE _alert_user SET _read = B'0', _delete = B'0' WHERE _alert = :id";

				$sth = $db->prepare ($sql);

				$sth->execute (array (':id' => $id));
			}
			else
			{
				$id = Database::nextId ('_alert');

				$sql = "INSERT INTO _alert (_id, _template, _assign, _user, _until, _parameters) VALUES (:id, :template, :assign, :user, timestamptz 'epoch' + :until * interval '1 second', :parameters)";

				$db->prepare ($sql)->execute (array (':template' => $template, ':assign' => $assign, ':until' => $until, ':parameters' => serialize ($tags), ':user' => $author, ':id' => $id));
			}
		}
		catch (PDOException $e)
		{
			toLog ($e->getMessage ());

			return FALSE;
		}

		$sql = "INSERT INTO _alert_user (_alert, _user) VALUES (:id, :user)";

		$sth = $db->prepare ($sql);

		foreach ($users as $trash => $user)
		{
			try
			{
				$sth->execute (array (':id' => $id, ':user' => $user));
			}
			catch (PDOException $e)
			{
				continue;
			}
		}

		try
		{
			$db->beginTransaction ();

			$db->exec ("DELETE FROM _alert_mail WHERE _alert = '". $id ."'");

			$sql = "INSERT INTO _alert_mail (_alert, _trigger) VALUES (:id, timestamptz 'epoch' + :trigger * interval '1 second')";

			$sth = $db->prepare ($sql);

			$now = time ();

			$today = mktime (0, 0, 0, date ('m'), date ('d') + 1, date ('Y'));

			foreach ($mail as $trash => $trigger)
				if ($trigger > $today)
					$sth->execute (array (':id' => $id, ':trigger' => $trigger));

			$sth->execute (array (':id' => $id, ':trigger' => $now));

			$db->commit ();
		}
		catch (PDOException $e)
		{
			toLog ($e->getMessage ());

			$db->rollBack ();
		}

		$this->sendMail ($id);

		return TRUE;
	}

	public function sendMail ($id = NULL)
	{
		if (!is_null ($id) && (!is_numeric ($id) || !(int) $id))
			return FALSE;

		$today = mktime (0, 0, 0, date ('m'), date ('d') + 1, date ('Y'));

		$db = Database::singleton ();

		$sql = "SELECT a.*, at._name AS author, at._email AS a_mail, av._name AS user, av._email AS u_mail, av._id AS u_id, av._login AS u_login,
				to_char (a._until, 'DD/MM/YYYY') AS f_until,
				extract (epoch FROM a._until) AS u_until,
				to_char (a._create, 'DD/MM/YYYY HH24:MI:SS') AS f_create
				FROM _alert_user au
				INNER JOIN _alert a ON a._id = au._alert
				LEFT JOIN _user at ON at._id = a._user
				INNER JOIN _user av ON av._id = au._user
				WHERE (a._until IS NULL OR a._until > CURRENT_TIMESTAMP) AND ". (is_null ($id) ? "" : "au._alert = :id AND ") ."
				EXISTS (SELECT 1 FROM _alert_mail WHERE _alert = a._id AND _send = B'0' AND timestamptz 'epoch' + :today * interval '1 second' > _trigger)
				AND av._active = B'1' AND av._deleted = B'0'";

		$sth = $db->prepare ($sql);

		if (is_null ($id))
			$sth->execute (array (':today' => $today));
		else
			$sth->execute (array (':id' => $id, ':today' => $today));

		$dTags = array ('[SYSTEM]' => Instance::singleton ()->getName (),
						'[URL]' => Instance::singleton ()->getUrl ());

		$flag = FALSE;

		while ($obj = $sth->fetch (PDO::FETCH_OBJ))
		{
			if (!array_key_exists ($obj->_template, $this->templates) ||
				!array_key_exists ('subject', $this->templates [$obj->_template]) ||
				!array_key_exists (0, $this->templates [$obj->_template]) ||
				trim ($this->templates [$obj->_template]['subject']) == '' ||
				trim ($this->templates [$obj->_template][0]) == '')
				continue;

			try
			{
				$query = $db->query ("SELECT _alert FROM _user WHERE _id = '". $obj->u_id ."'");

				$enabled = $query->fetchColumn ();

				if (!is_null ($enabled) && !(int) $enabled)
					continue;
			}
			catch (PDOException $e)
			{}

			$auth = is_null ($obj->author) ? Instance::singleton ()->getName () : $obj->author;
			$mail = is_null ($obj->a_mail) ? Instance::singleton ()->getEmail () : $obj->a_mail;

			$tags = $dTags;

			$tags ['[AUTHOR]'] = $auth;
			$tags ['[USER]'] = $obj->user;
			$tags ['[DAYS_MISSING]'] = (int) $obj->u_until <= 0 ? 0 : floor (($obj->u_until - time ()) / (60 * 60 * 24));
			$tags ['[UNTIL]'] = $obj->f_until;
			$tags ['[DATE]'] = $obj->f_create;

			$hash = Security::singleton ()->getHash ();

			if (Instance::singleton ()->getFriendlyUrl ('disable-alerts') == '')
				$tags ['[DISABLE]'] = Instance::singleton ()->getUrl () .'titan.php?target=disableAlerts&login='. urlencode ($obj->u_login) .'&hash='. shortlyHash (md5 ($hash . $obj->user . $hash . $obj->u_id . $hash . $obj->u_mail . $hash));
			else
				$tags ['[DISABLE]'] = Instance::singleton ()->getUrl () . Instance::singleton ()->getFriendlyUrl ('disable-alerts') .'/'. urlencode ($obj->u_login) .'/'. shortlyHash (md5 ($hash . $obj->user . $hash . $obj->u_id . $hash . $obj->u_mail . $hash));

			$uTags = unserialize ($obj->_parameters);

			if (is_array ($uTags))
				$tags = array_merge ($tags, $uTags);

			if (!@mail ($obj->u_mail,
						'=?utf-8?B?'. base64_encode (str_replace (array_keys ($tags), $tags, $this->getFromTemplate ($obj->_template, 'subject'))) .'?=',
						str_replace (array_keys ($tags), $tags, $this->getFromTemplate ($obj->_template, 0)),
						"From: ". $auth ." <". Instance::singleton ()->getEmail () .">\r\nReply-To: ". $mail ."\r\nX-Mailer: PHP/". phpversion () ."\r\nContent-Type: text/plain; charset=utf-8"))
			{
				toLog ('Impossible to send alert mail! [To: '. $obj->u_mail .'] [Subject: '. str_replace (array_keys ($tags), $tags, $this->getFromTemplate ($obj->_template, 'subject')) .']');

				continue;
			}

			$flag = TRUE;
		}

		if ($flag)
		{
			try
			{
				$sql = "UPDATE _alert_mail SET _send = B'1' WHERE _send = B'0' AND timestamptz 'epoch' + :today * interval '1 second' > _trigger". (is_null ($id) ? "" : " AND _alert = :id");

				$sth = $db->prepare ($sql);

				if (is_null ($id))
					$sth->execute (array (':today' => $today));
				else
					$sth->execute (array (':id' => $id, ':today' => $today));
			}
			catch (PDOException $e)
			{
				toLog ($e->getMessage ());

				return FALSE;
			}
		}

		return $flag;
	}

	private function unregister ($template, $assign)
	{
		try
		{
			$sth = Database::singleton ()->prepare ("DELETE FROM _alert WHERE _template = :template AND _assign = :assign");

			$sth->bindParam (':template', $template, PDO::PARAM_STR, 64);
			$sth->bindParam (':assign', $assign, PDO::PARAM_STR, 64);

			$sth->execute ();
		}
		catch (PDOException $e)
		{
			return FALSE;
		}

		return TRUE;
	}

	public function read ($id, $user)
	{
		try
		{
			$sth = Database::singleton ()->prepare ("UPDATE _alert_user SET _read = B'1' WHERE _alert = :id AND _user = :user");

			$sth->bindParam (':id', $id, PDO::PARAM_INT);
			$sth->bindParam (':user', $user, PDO::PARAM_INT);

			$sth->execute ();
		}
		catch (PDOException $e)
		{
			toLog ($e->getMessage ());

			return FALSE;
		}

		return TRUE;
	}

	public function delete ($id, $user)
	{
		try
		{
			$sth = Database::singleton ()->prepare ("UPDATE _alert_user SET _delete = B'1' WHERE _alert = :id AND _user = :user");

			$sth->bindParam (':id', $id, PDO::PARAM_INT);
			$sth->bindParam (':user', $user, PDO::PARAM_INT);

			$sth->execute ();
		}
		catch (PDOException $e)
		{
			toLog ($e->getMessage ());

			return FALSE;
		}

		return TRUE;
	}

	public static function add ($template, $assign, $users, $tags = NULL, $until = NULL, $author = NULL, $overwrite = TRUE, $mail = NULL)
	{
		if (!self::isActive ())
			return FALSE;

		return Alert::singleton ()->register ($template, $assign, $users, $tags, $until, $author, $overwrite, $mail);
	}

	public static function remove ($template, $assign)
	{
		if (!self::isActive ())
			return FALSE;

		return Alert::singleton ()->unregister ($template, $assign);
	}

	public static function garbageCollector ()
	{
		$db = Database::singleton ();

		$sql = "SELECT a._id
				FROM _alert a
				WHERE
					(NOT EXISTS (SELECT 1 FROM _alert_mail WHERE _alert = a._id AND _send = B'0') AND NOT EXISTS (SELECT 1 FROM _alert_user WHERE _alert = a._id AND _delete = B'0')) OR
					(a._until IS NOT NULL AND a._until < date_trunc ('day', now() - interval '1 day'))";

		$sth = $db->prepare ($sql);

		$sth->execute ();

		$garbage = $sth->fetchAll (PDO::FETCH_COLUMN, 0);

		try
		{
			if (sizeof ($garbage))
			{
				$sql = "DELETE FROM _alert WHERE _id IN (". implode (", ", $garbage) .")";

				$db->exec ($sql);
			}
		}
		catch (Exception $e)
		{
			toLog ($e->getMessage ());
		}
	}

	public static function isActive ()
	{
		if (is_null (self::$active))
			self::$active = Database::tableExists ('_alert');

		 return self::$active;
	}

	public static function sendMobileNotification ()
	{
		if (!self::isActive () || !MobileDevice::isActive () || !Api::isActive ())
			return FALSE;

		$db = Database::singleton ();

		$mark = $db->prepare ("UPDATE _alert_user SET _mobile = B'1' WHERE _alert = :id AND _user = :user");

		$sql = "SELECT _user AS user, MIN(_alert) AS id FROM _alert_user WHERE _mobile = B'0' AND _read = B'0' AND _delete = B'0' GROUP BY _user";

		$sth = $db->prepare ($sql);

		$sth->execute ();

		while ($obj = $sth->fetch (PDO::FETCH_OBJ))
		{
			$message = self::singleton ()->getAlertMessage ($obj->id, $obj->user);

			if (trim ($message) == '')
				continue;

			while ($app = Api::singleton ()->getApp ())
			{
				if (!$app->sendAlerts ())
					continue;

				$data = array ('id' => $obj->id, 'message' => $message);

				$app->sendNotification ($obj->user, $data);

				$mark->execute (array (':id' => $obj->id, ':user' => $obj->user));
			}
		}
	}
}