<?php

namespace Vn\Web;

include __DIR__.'/uid.php';

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 user credentials are invalid.
 */
class ForbiddenException extends UserException {}

/**
 * Thrown when user credentials are invalid.
 */
class UserDisabledException 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'])
		|| !isset($_SERVER['HTTP_USER_AGENT']))
			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 visit_register(#, #, #, #, #, #, #, #, #)',
			[
				 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'])) {
			$domain = explode(':', $_SERVER['HTTP_HOST'])[0];
			setcookie('vnVisit', $row['visit'], [
				'expires' => time() + 31536000, // 1 Year
				'samesite' => 'Lax',
				'domain' => $domain
			]);
			$_SESSION['access'] = $row['access'];
		} else
			$_SESSION['skipVisit'] = TRUE;
	}
	
	/**
	 * Authenticates the user with it's credentials or token.
	 *
	 * return Db\Conn The database connection
	 */
	function login() {
		$db = $this->db;
		$anonymousUser = TRUE;
		$token = NULL;

		if (!empty($_SERVER['HTTP_AUTHORIZATION']))
			$token = $_SERVER['HTTP_AUTHORIZATION'];
		if (!empty($_GET['access_token']))
			$token = $_GET['access_token'];

		if (isset($token)) {
			$userId = $db->getValue(
				'SELECT userId FROM salix.AccessToken
					WHERE id = #
						AND NOW() <= TIMESTAMPADD(SECOND, ttl, created)',
				[$token]
			);

			if (!$userId)
				throw new SessionExpiredException();

			$anonymousUser = FALSE;
			$user = $db->getValue(
				'SELECT `name` FROM account.user WHERE id = #',
				[$userId]
			);
		} else
			$user = $db->getValue('SELECT guestUser FROM config');

		if (!$anonymousUser) {
			$isActive = $db->getValue(
				'SELECT active FROM account.user WHERE `name` = #',
				[$user]
			);

			if (!$isActive)
				throw new UserDisabledException();
		}
		$db->query('CALL account.myUser_loginWithName(#)', [$user]);

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

		$_SESSION['user'] = $user;

		// Registering the user access
		if (isset($_SESSION['access']) && $userChanged)
			$db->query(
				'CALL visitUser_new(#, #)',
				[$_SESSION['access'], session_id()]
			);
	}

	/**
	 * Logouts the current user. Cleans the last saved used credentials.
	 */
	function logout() {
		if (!empty($_SERVER['HTTP_AUTHORIZATION']))
			$db->query(
				'DELETE FROM salix.AccessToken WHERE id = #',
				[$_SERVER['HTTP_AUTHORIZATION']]
			);

		unset($_SESSION['user']);
	}

	/**
	 * Creates or returns a database connection where the authenticated user
	 * is the role of the current logged user.
	 *
	 * @return {Db\Conn} The database connection
	 */
	function getUserDb($user) {
		if ($this->userDb)
			return $this->userDb;

		$row = $this->db->getObject(
			'SELECT r.name, rc.mysqlPassword, rc.rolePrefix, uc.loginKey
				FROM account.user u
					JOIN account.role r ON r.id = u.role
					JOIN account.roleConfig rc ON TRUE
					JOIN account.userConfig uc ON TRUE
				WHERE u.name = #',
			[$user]
		);

		$userName = "{$row->rolePrefix}{$row->name}";
		$password = base64_decode($row->mysqlPassword);
		$userDb = $this->app->createConnection($userName, $password, TRUE);

		$userDb->query('CALL account.myUser_loginWithKey(#, #)', [$user, $row->loginKey]);
		return $userDb;
	}
	
	/**
	 * Generates a authentication token for the specified $user.
	 *
	 * @param {string} $user The user name
	 * @param {boolean} $remember Wether to create long live token
	 * @return {string} The generated token
	 */
	function createToken($user, $remember = FALSE) {
		if ($remember)
			$tokenLife = 2 * WEEK;
		else
			$tokenLife = 30 * MIN;

		$token = uid(DEFAULT_TOKEN_LEN);
		$userId = $this->db->getValue(
			'SELECT id FROM account.user WHERE `name` = #',
			[$user]
		);

		$this->db->query(
			'INSERT INTO salix.AccessToken
				SET id = #,
					ttl = #,
					created = NOW(),
					userId = #',
			[$token, $tokenLife, $userId]
		);

		return $token;
	}

	/**
	 * Generates a JWT authentication token for the specified $user.
	 *
	 * @param {string} $user The user name
	 * @param {boolean} $remember Wether to create long live token
	 * @return {string} The JWT generated token
	 */
	function createJwtToken($user, $remember = FALSE) {
		if ($remember)
			$tokenLife = WEEK;
		else
			$tokenLife = 30 * MIN;

		$payload = [
			'sub' => $user,
			'exp' => time() + $tokenLife
		];
		
		$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 = apcu_fetch("$appName.version", $success);

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

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

		return $version;
	}

	/**
	 * Checks the client version.
	 */
	function checkVersion() {
		if (empty($_COOKIE['vnVersion'])) return;
		
		$client = new Version($_COOKIE['vnVersion']);
		$last = $this->getVersion();

		if ($client->isLowerThan($last))
			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()}";
	}
}
