<?php

namespace Vn\Web;

use Vn\Db;
use Vn\Lib\Locale;
use Vn\Lib\UserException;

const MIN   = 60;
const HOUR  = 60 * MIN;
const DAY   = 24 * HOUR;
const WEEK  = 7 * DAY;

/**
 * Thrown when user credentials could not be fetched.
 */
class SessionExpiredException extends UserException {}

/**
 * Thrown when user credentials are invalid.
 */
class BadLoginException extends UserException {}

/**
 * Thrown when client version is outdated.
 */
class OutdatedVersionException extends UserException {}

/**
 * Main class for web applications.
 */
abstract class Service
{
	protected $app;
	protected $db;
	protected $userDb = NULL;

	function __construct ($app)
	{
		$this->app = $app;
	}

	function init ()
	{
		$this->db = $this->app->getSysConn ();
	}

	/**
	 * Starts the user session.
	 */
	function startSession ()
	{
		$db = $this->app->getSysConn ();

		ini_set ('session.cookie_secure', $this->isHttps ());
		ini_set ('session.hash_function', 'sha256');

		session_set_save_handler (new DbSessionHandler ($db));
		session_start ();

		// Setting the locale

		if (isset ($_SERVER['HTTP_ACCEPT_LANGUAGE']))
		if (!isset ($_SESSION['httpLanguage'])
		|| $_SESSION['httpLanguage'] != $_SERVER['HTTP_ACCEPT_LANGUAGE'])
		{
			$_SESSION['httpLanguage'] = $_SERVER['HTTP_ACCEPT_LANGUAGE'];			
			$regexp = '/([a-z]{1,4})(?:-[a-z]{1,4})?\s*(?:;\s*q\s*=\s*(?:1|0\.[0-9]+))?,?/i';

			preg_match_all ($regexp, $_SERVER['HTTP_ACCEPT_LANGUAGE'], $languages);

			foreach ($languages[1] as $lang)
			if (TRUE || stream_resolve_include_path ("locale/$lang"))
			{
				$_SESSION['lang'] = $lang;
				break;
			}
		}

		if (!isset ($_SESSION['lang']))
			$_SESSION['lang'] = NULL;

		Locale::set ($_SESSION['lang']);
		Locale::addPath ('vn/web');

		// Registering the visit

		if (isset ($_COOKIE['PHPSESSID'])
		|| isset ($_SESSION['access'])
		|| isset ($_SESSION['skipVisit']))
			return;

		$agent = $_SERVER['HTTP_USER_AGENT'];
		$browser = get_browser ($agent, TRUE);
		
		if (!empty ($browser['crawler']))
		{
			$_SESSION['skipVisit'] = TRUE;
			return;
		}

		if (isset ($_SERVER['REMOTE_ADDR']))
			$ip = ip2long ($_SERVER['REMOTE_ADDR']);

		$row = $db->getRow (
			'CALL visitRegister (#, #, #, #, #, #, #, #, #)',
			[
				 nullIf ($_COOKIE, 'vnVisit')
				,nullIf ($browser, 'platform')
				,nullIf ($browser, 'browser') 
				,nullIf ($browser, 'version')
				,nullIf ($browser, 'javascript')
				,nullIf ($browser, 'cookies')
				,isset ($agent) ? $agent : NULL
				,isset ($ip) && $ip ? $ip : NULL
				,nullIf ($_SERVER, 'HTTP_REFERER')
			]
		);

		if (isset ($row['access']))
		{
			setcookie ('vnVisit', $row['visit'], time () + 31536000); // 1 Year
			$_SESSION['access'] = $row['access'];
		}
		else
			$_SESSION['skipVisit'] = TRUE;
	}
	
	/**
	 * Tries to retrieve user credentials from many sources such as POST,
	 * SESSION or COOKIES. If $_POST['remember'] is defined the user credentials 
	 * are saved on the client brownser for future logins, cookies names are
	 * 'vn_user' for the user name and 'vn_pass' for user password, the
	 * password is encoded using base64_encode() function and should be decoded
	 * using base64_decode().
	 *
	 * return Db\Conn The database connection
	 */
	function login ()
	{
		$db = $this->db;
		$anonymousUser = FALSE;
		
		if (isset ($_POST['user']) && isset ($_POST['password']))
		{
			$user = strtolower ($_POST['user']);
			
			try {
				$db->query ('CALL account.userLogin (#, #)',
					[$user, $_POST['password']]);
			}
			catch (Db\Exception $e)
			{
				if ($e->getMessage () == 'INVALID_CREDENTIALS')
				{
					sleep (3);
					throw new BadLoginException ();
				}
				else
					throw $e;
			}
		}
		else
		{
			if (isset ($_POST['token']) || isset ($_GET['token']))
			{
				if (isset ($_POST['token']))
					$token = $_POST['token'];
				if (isset ($_GET['token']))
					$token = $_GET['token'];

				$key = $db->getValue ('SELECT jwtKey FROM config');

				try {
					$jwtPayload = Jwt::decode ($token, $key);
				}
				catch (\Exception $e)
				{
					throw new BadLoginException ($e->getMessage ());
				}

				$expiration = $jwtPayload['exp'];

				if (empty ($expiration) || $expiration <= time())
					throw new SessionExpiredException ();

				$user = $jwtPayload['sub'];
					
				if (!empty ($jwtPayload['recover']))
					$db->query (
						'UPDATE account.user SET recoverPass = TRUE
							WHERE name = #',
						[$user]
					);
			}
			else
			{
				$user = $db->getValue ('SELECT guest_user FROM config');
				$anonymousUser = TRUE;
			}

			$db->query ('CALL account.userLoginWithName (#)', [$user]);
		}

		$userChanged = !$anonymousUser
			&& (empty ($_SESSION['user']) || $_SESSION['user'] != $user);

		$_SESSION['user'] = $user;

		// Registering the user access

		if (isset ($_SESSION['access']) && $userChanged)
			$db->query (
				'CALL visitUserNew (#, #)',
				[$_SESSION['access'], session_id ()]
			);
	}

	/**
	 * Logouts the current user. Cleans the last saved used credentials.
	 */
	function logout ()
	{
		unset ($_SESSION['user']);
	}
	
	/**
	 * Creates or returns a database connection where the authenticated user
	 * is the current logged user.
	 *
	 * @return {Db\Conn} The database connection
	 */
	function getUserDb ($user)
	{
		if ($this->userDb)
			return $this->userDb;
			
		$password = $this->db->getValue (
			'SELECT password FROM account.user WHERE name = #', [$user]);
		return $this->userDb = $this->app->createConnection ($user, $password);
	}
	
	/**
	 * Generates a JWT authentication token for the specified $user.
	 *
	 * @param {string} $user The user name
	 * @param {boolean} $remember Wether to create long live token
	 * @param {boolean} $recover Wether to enable recovery mode on login
	 * @return {string} The JWT generated token
	 */
	function createToken ($user, $remember = FALSE, $recover = FALSE)
	{
		if ($remember)
			$tokenLife = WEEK;
		else
			$tokenLife = 30 * MIN;

		$payload = [
			'sub' => $user,
			'exp' => time () + $tokenLife
		];
		
		if ($recover)
			$payload['recover'] = 'TRUE';
		
		$key = $this->db->getValue ('SELECT jwtKey FROM config');
		return Jwt::encode ($payload, $key);
	}
	
	/**
	 * Obtains the application version number. It is extracted and
	 * cached from package.json file.
	 *
	 * @return string The version number
	 */
	function getVersion ()
	{
		$appName = $this->app->getName ();
		$version = apc_fetch("$appName.version", $success);

		if (!$success)
		{
			if (file_exists ('package.json'))
			{
				$package = json_decode (file_get_contents ('package.json'));
				$version = $package->version;
			}
			else
				$version = '0.0.0';

			apc_store ("$appName.version", $version);
		}

		return $version;
	}

	/**
	 * Checks the client version.
	 */
	function checkVersion ()
	{
		if (!empty ($_COOKIE['vnVersion']))
			$clientVersion = $_COOKIE['vnVersion'];

		if (isset ($clientVersion)
		&& $clientVersion < $this->getVersion ())
			throw new OutdatedVersionException ();
	}

	/**
	 * Checks if the HTTP connection is secure.
	 *
	 * @return boolean Return %TRUE if its secure, %FALSE otherwise
	 */
	function isHttps ()
	{
		return isset ($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on';
	}

	/**
	 * Returns the current URI without the GET part.
	 *
	 * @return string The current URI
	 */
	function getUri ()
	{
		return "{$_SERVER['SERVER_NAME']}{$_SERVER['REQUEST_URI']}";
	}
	
	/**
	 * Returns the current URL without the GET part.
	 *
	 * @return string The current URL
	 */
	function getUrl ()
	{
		$proto = $this->isHttps () ? 'https' : 'http';
		return "$proto://{$this->getUri()}";
	}
}

