<?php

/**
 * Vvveb
 *
 * Copyright (C) 2022  Ziadin Givan
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

namespace Vvveb\Controller;

use function Vvveb\__;
use function Vvveb\arrayInsertArrayAfter;
use function Vvveb\filter;
use Vvveb\Sql\TaxonomySQL;
use Vvveb\System\Cache;
use Vvveb\System\CacheManager;
use Vvveb\System\Core\FrontController;
use Vvveb\System\Core\Request;
use Vvveb\System\Core\View;
use Vvveb\System\Event;
use Vvveb\System\Extensions\Plugins;
use Vvveb\System\Functions\Str;
use Vvveb\System\Images;
use Vvveb\System\Locale;
use Vvveb\System\PageCache;
use Vvveb\System\Session;
use Vvveb\System\Sites;
use Vvveb\System\Traits\Permission;
use Vvveb\System\User\Admin;

#[\AllowDynamicProperties]
class Base {
	use Permission;

	/** @var mixed Dynamic view instance */
	public mixed $view;

	/** @var mixed Dynamic request instance */
	public mixed $request;

	/** @var mixed Dynamic response instance */
	public mixed $response;

	/** @var mixed Dynamic session instance */
	public mixed $session;

	/** @var array<string, mixed> Global template variables */
	protected array $global = [];

	/** @var array<int, array<string, mixed>> Taxonomy records for current site */
	protected array $taxonomies = [];

	/**
	 * Set the current site and store its details in the session.
	 *
	 * @param int|false $site_id Site ID to set, or false for default
	 *
	 * @return int The site_id that was set
	 */
	protected function setSite(int|false $site_id = false): int {
		//if no id set default
		if ($site_id) {
			$site  = Sites::getSiteById($site_id);
		}

		if (! $site_id || ! $site) {
			$site = Sites::getDefault();
		}

		$this->session->set('site', $site);
		$this->session->set('site_id', $site['site_id']);
		$this->session->set('site_url', $site['url']);
		$this->session->set('host', $site['host']);
		$this->session->set('path', $site['path'] ?? '');
		$this->session->set('state', $site['state'] ?? 'live');

		return $site['site_id'];
	}

	/**
	 * Get all taxonomies for the current global context.
	 *
	 * @return array<int, array<string, mixed>> Array of taxonomy records
	 */
	protected function getTaxonomies(): array {
		$categories  = new TaxonomySQL();
		$taxonomies  = $categories->getAll($this->global);

		return $taxonomies['taxonomy'] ?? [];
	}

	/**
	 * Build the custom post types menu structure.
	 *
	 * @return array<string, array<string, mixed>> Associative array of post type menu items
	 */
	protected function customPost(): array {
		//custom posts -- add to menu
		$default_custom_posts =
		[
			'post' => [
				'type'    => 'post',
				'plural'  => 'posts',
				'icon'    => 'icon-document-text-outline',
				'comments'=> true,
			],
			'page' => [
				'type'    => 'page',
				'plural'  => 'pages',
				'icon'    => 'icon-document-outline',
				'comments'=> false,
			],
		];

		$custom_posts_types        = \Vvveb\getSetting('post', 'types', []);
		$custom_posts_types += $default_custom_posts;
		list($custom_posts_types) = Event::trigger('Vvveb\postTypes', __FUNCTION__, $custom_posts_types);

		$custom_post_menu = \Vvveb\config('custom-post-menu', []);
		$posts_menu       = [];

		foreach ($custom_posts_types as $type => $settings) {
			if ($type === 'page') {
				continue;
			}
			$posts_menu[$type] = $custom_post_menu;

			$posts_menu[$type]['name']                   =
			$posts_menu[$type]['items']['posts']['name'] =
			__(ucfirst($settings['plural']));

			$posts_menu[$type]['icon']     = $settings['icon'] ?? '';
			$posts_menu[$type]['icon-img'] = (isset($settings['icon-img']) && $settings['icon-img']) ? Images::image($settings['icon-img'], $type) : '';
			$posts_menu[$type]['url'] .= "&type=$type";

			if (isset($settings['comments']) && ! $settings['comments']) {
				unset($posts_menu[$type]['items']['comments']);
			}

			foreach ($posts_menu[$type]['items'] as $item => &$values) {
				if (isset($values['url'])) {
					$values['url'] .= "&type=$type";
				}
			}

			$admin_path         = \Vvveb\adminPath();
			//add taxonomies for post type
			foreach ($this->taxonomies as $taxonomy) {
				if ($taxonomy['post_type'] !== $type) {
					continue;
				}
				$key    = $taxonomy['post_type'] . '_' . $taxonomy['type'] . $taxonomy['taxonomy_id'];
				$module = "content/{$taxonomy['type']}";
				$icon   = $taxonomy['type'] === 'tags' ? 'la la-tags' : 'la la-boxes';
				$tax    = [$key => [
					'name' => __($taxonomy['name']),
					//'subtitle' => __('(Flat)'),
					'url'    => "{$admin_path}index.php?module=$module&type={$taxonomy['post_type']}&taxonomy_id={$taxonomy['taxonomy_id']}",
					'module' => $module,
					'action' => 'index',
					'icon'   => $icon,
				]];

				$posts_menu[$type]['items'] = arrayInsertArrayAfter('taxonomy-heading', $posts_menu[$type]['items'], $tax);
			}
		}

		return $posts_menu;
	}

	/**
	 * Build the custom product types menu structure.
	 *
	 * @return array<string, array<string, mixed>> Associative array of product type menu items
	 */
	protected function customProduct(): array {
		//custom products -- add to menu
		$default_custom_products =
		[
			'product' => [
				'type'   => 'product',
				'plural' => 'products',
				'icon'   => 'icon-cube-outline',
			],
		];

		$custom_products_types       = \Vvveb\getSetting('product', 'types', []);
		$custom_products_types += $default_custom_products;
		list($custom_products_types) = Event::trigger('Vvveb\postTypes', __FUNCTION__, $custom_products_types);

		$custom_product_menu = \Vvveb\config('custom-product-menu', []);
		$products_menu       = [];

		foreach ($custom_products_types as $type => $settings) {
			if ($type === 'page') {
				continue;
			}
			$products_menu[$type] = $custom_product_menu;

			$products_menu[$type]['name']                      =
			$products_menu[$type]['items']['products']['name'] =
			__(ucfirst($settings['plural']));

			$products_menu[$type]['icon']     = $settings['icon'] ?? '';
			$products_menu[$type]['icon-img'] = (isset($settings['icon-img']) && $settings['icon-img']) ? Images::image($settings['icon-img'], $type) : '';
			$products_menu[$type]['url'] .= "&type=$type";

			foreach ($products_menu[$type]['items'] as $item => &$values) {
				if (isset($values['url'])) {
					$values['url'] .= "&type=$type";
				}
			}

			$admin_path         = \Vvveb\adminPath();
			//add taxonomies for post type
			foreach ($this->taxonomies as $taxonomy) {
				if ($taxonomy['post_type'] !== $type) {
					continue;
				}
				$key = $taxonomy['post_type'] . '_' . $taxonomy['type'] . $taxonomy['taxonomy_id'];

				$module = "content/{$taxonomy['type']}";
				$icon   = $taxonomy['type'] === 'tags' ? 'la la-tags' : 'la la-boxes';
				$tax    = [$key => [
					'name' => __($taxonomy['name']),
					//'subtitle' => __('(Flat)'),
					'url'    => "{$admin_path}index.php?module=$module&type={$taxonomy['post_type']}&taxonomy_id={$taxonomy['taxonomy_id']}",
					'module' => $module,
					'action' => 'index',
					'icon'   => $icon,
				]];

				$products_menu[$type]['items'] = arrayInsertArrayAfter('taxonomy-heading', $products_menu[$type]['items'], $tax);
			}
		}

		return $products_menu;
	}

	/**
	 * Set the current language and locale for the session.
	 *
	 * @param string|false $defaultLanguage   Default language code
	 * @param string|false $defaultLanguageId Default language ID
	 * @param string|false $defaultLocale     Default locale string
	 * @param string|false $defaultCode       Default locale code (e.g. en_US)
	 * @param array<string, array<string, mixed>>|null $languages Available languages array
	 */
	protected function language(string|false $defaultLanguage = false, string|false $defaultLanguageId = false, string|false $defaultLocale = false, string|false $defaultCode = false, ?array $languages = null): void {
		$languages = $languages ?? Locale::availableLanguages();

		$default_language    = $this->session->get('default_language');
		$default_language_id = $this->session->get('default_language_id');
		$default_locale      = $this->session->get('default_locale');
		$default_code        = $this->session->get('default_code');
		$default_rtl         = false;
		$site_language       = false;

		if (($lang = ($this->request->post['language'] ?? false)) && ! is_array($lang)) {
			$language  = filter('/[A-Za-z_-]+/', $lang, 10);

			if (isset($languages[$language])) {
				$this->session->set('language', $language);
				$this->session->set('language_id', $languages[$language]['language_id']);
				$this->session->set('locale', $languages[$language]['locale']);
				$this->session->set('code', $languages[$language]['code']);
				$this->session->set('rtl', $languages[$language]['rtl'] ?? false);
				$default_language = false; //recheck default language
				Locale::clearLanguageCache($language);
			}
		}

		if (! $default_language) {
			foreach ($languages as $code => $lang) {
				//set site language
				if ($defaultLanguageId && ($defaultLanguageId === $lang['language_id'])) {
					$site_language    = $code;
					$site_language_id = $lang['language_id'];
					$site_locale      = $lang['locale'];
					$site_code        = $lang['code'];
				}

				//set global default language
				if ($lang['default']) {
					$default_language    = $code;
					$default_language_id = $lang['language_id'];
					$default_locale      = $lang['locale'];
					$default_code        = $lang['code'];
					$default_rtl         = $lang['rtl'] ?? false;

					break;
				}
			}

			if ($site_language) {
				$default_language    = $site_language;
				$default_language_id = $site_language_id;
				$default_locale      = $site_locale;
				$default_code        = $site_code;
			}

			//no valid default site or global language? set english as default
			if (! $default_language) {
				$default_language    = 'en';
				$default_language_id = 1;
				$default_locale      = 'en-us';
				$default_code       = 'en_US';
				$default_rtl        = false;
			}

			$this->session->set('default_language', $default_language);
			$this->session->set('default_language_id', $default_language_id);
			$this->session->set('default_locale', $default_locale);
			$this->session->set('default_code', $default_code);
		}

		$sessLang    = $this->session->get('language');
		$language    = $sessLang ?? $default_language;
		$language_id = $this->session->get('language_id') ?? $default_language_id;
		$locale      = $this->session->get('locale') ?? $default_locale;
		$code        = $this->session->get('code') ?? $default_code;
		$rtl         = $this->session->get('rtl') ?? false;

		//if no default language configured then set first language as current language
		if (! isset($languages[$language])) {
			$language = null;
			$default_language    = key($languages);
			$lang                = $languages[$default_language] ?? [];

			if ($lang) {
				$default_language_id = $lang['language_id'] ?? $defaultLanguageId;
				$default_locale      = $lang['locale'] ?? $defaultLocale;
				$default_code        = $lang['code'] ?? $defaultCode;
				$default_rtl         = $lang['rtl'] ?? false;
			}
		}

		//if no language configured then set default language as current language
		if (! $language) {
			$language    = $default_language;
			$language_id = $default_language_id;
			$locale      = $default_locale;
			$code        = $default_code;
			$rtl         = $default_rtl;

			$this->session->set('language', $language);
			$this->session->set('language_id', $language_id);
			$this->session->set('locale', $locale);
			$this->session->set('code', $code);
			$this->session->set('rtl', $rtl);
		}

		$this->global['language']            = $language;
		$this->global['locale']              = $locale;
		//$this->global['code']                = $code;
		$this->global['language_id']         = $language_id;
		$this->global['rtl']                 = $rtl;
		$this->global['default_language']    = $default_language;
		$this->global['default_locale']      = $default_locale;
		$this->global['default_language_id'] = $default_language_id;

		if (! $sessLang) {
			$this->session->set('language', $language);
			$this->session->set('language_id', $language_id);
			$this->session->set('locale', $locale);
			$this->session->set('code', $code);
			$this->session->set('rtl', $rtl);
		}

		Locale::setLanguage($code);

		if (! defined('CLI')) {
			$view                = $this->view;
			$view->languagesList = $languages;
		}
	}

	/**
	 * Set the current currency for the session.
	 *
	 * @param string|false $defaultCurrency   Default currency code
	 * @param string|false $defaultCurrencyId Default currency ID
	 */
	protected function currency(string|false $defaultCurrency = false, string|false $defaultCurrencyId = false): void {
		if (($currency = ($this->request->post['currency'] ?? false)) && ! is_array($currency)) {
			$currency   = filter('/[A-Za-z_-]+/', $currency, 50);
			$currencies = Locale::availableCurrencies();

			if (isset($currencies[$currency])) {
				$this->session->set('currency_id', $currencies[$currency]['currency_id']);
				$this->session->set('currency', $currency);

				\Vvveb\System\Cart\Cart::getInstance($this->global)->updateCart();
			}
		}

		$currency    = $this->session->get('currency');
		$currency_id = $this->session->get('currency_id');

		if (! $currency || ! $currency_id) {
			$currencies = Locale::availableCurrencies();

			if ($currencies) {
				foreach ($currencies as $code => $c) {
					if (($defaultCurrency && $defaultCurrency === $c['code']) ||
					   ($defaultCurrencyId && $defaultCurrencyId === $c['currency_id'])) {
						$currency    = $c['code'];
						$currency_id = $c['currency_id'];

						break;
					}
				}

				//if no site currency configured set first available currency
				if (! $currency) {
					$c           = reset($currencies);
					$currency    = $c['code'];
					$currency_id = $c['currency_id'];
				}
			}

			$this->session->set('currency', $currency);
			$this->session->set('currency_id', $currency_id);
		}

		$this->global['currency']            = $currency;
		$this->global['currency_id']         = $currency_id;
	}

	/**
	 * Initialize the controller: session, CSRF, language, currency, permissions, menu, and view.
	 */
	public function init(): ?string {
		$view = View::getInstance();
		$view->removeVattrs(false);

		if (isset($this->request->get['errors']) && $this->request->get['errors']) {
			$view->errors['get'] = htmlspecialchars($this->request->get['errors']);
		}

		if (isset($this->request->get['success']) && $this->request->get['success']) {
			$view->success['get'] = htmlspecialchars($this->request->get['success']);
		}

		//prevent admin loading in iframe
		$this->response->addHeader('X-Frame-Options', 'SAMEORIGIN');
		$this->response->addHeader('X-Content-Type-Options', 'nosniff');

		if (! $this->session->get('csrf')) {
			$this->session->set('csrf', Str::random());
		}

		$this->language();
		$this->currency();

		$admin = Admin::current();

		if (! $admin) {
			$this->requireLogin();
		}

		//$className = get_class($this);
		//$errorController = (strncmp($className, 'Vvveb\Controller\Error', 22) === 0);

		if (/*!$errorController && */! $this->checkCsrf()) {
			return null;
		}

		if (($site_id = ($this->request->post['site'] ?? false)) && is_numeric($site_id)) {
			$this->setSite($site_id);
		}

		$site_id = $this->session->get('site_id');

		if (! $site_id) {
			$site_id = $this->setSite(SITE_ID);
		}

		$adminPath = \Vvveb\adminPath();

		//change site status (live, under maintenance etc)
		if ($state = ($this->request->post['state'] ?? false)) {
			if (Admin::hasPermission('settings/site/save')) {
				if (Sites::setSiteDataById($site_id, 'state', $state)) {
					$this->session->set('state', $state);
					CacheManager::clearObjectCache('site');
					CacheManager::clearPageCache();
				}
			} else {
				$message               = __('Your role does not have permission to access this action!');
				$this->view->errors[]  = $message;
				$this->view->adminPath = $adminPath;
			}
		}

		$page        = $this->request->get['page'] ?? 1 ?: 1;
		$limit       = $this->request->get['limit'] ?? 10 ?: 10;

		$this->global['site_id']  = $site_id;
		$this->global['host']     = $this->session->get('host');
		$this->global['path']     = $this->session->get('path');
		$this->global['site_url'] = $this->session->get('site_url');
		$this->global['admin_id'] = $admin['admin_id'];
		$this->global['state']    = $state;
		$this->global['page']     = $page;
		$this->global['start']    = ($page - 1) * $limit;
		$this->global['limit']    = $limit;

		//Check permissions
		//if (!$errorController) {
		$this->permission();
		$this->setPermissions();
		//}

		//load plugins for active site if safe mode is not selected
		if (! isset($admin['safemode']) || ! $admin['safemode']) {
			Plugins::loadPlugins($site_id);
		}

		if ($errors = $this->session->get('errors')) {
			if (is_array($errors)) {
				$view->errors = ($view->errors ?? []) + $errors;
			} else {
				$view->errors['session'] = $errors;
			}
			$this->session->delete('errors');
		}

		if ($success = $this->session->get('success')) {
			if (is_array($success)) {
				$view->success = ($view->success ?? []) + $success;
			} else {
				$view->success['session'] = $success;
			}
			$this->session->delete('success');
		}

		//don't initialize menu items for CLI
		if (defined('CLI')) {
			return null;
		}

		$cacheDriver = Cache::getInstance();
		$cacheKey    = $this->global['admin_id'] . '.' . $this->global['site_id'] . '.' . $this->global['language_id'];

		$menu = $cacheDriver->get('admin-menu', $cacheKey);

		if (! $menu) {
			$menu             = \Vvveb\config('admin-menu', []);

			//custom posts -- add to menu
			$this->taxonomies = $this->getTaxonomies();
			$posts_menu       = $this->customPost();
			$menu             = arrayInsertArrayAfter('edit', $menu, $posts_menu);

			//products - add to menu
			$products_menu = $this->customProduct();
			$menu          = arrayInsertArrayAfter('sales', $menu, $products_menu);

			list($menu)       = Event::trigger(__CLASS__, __FUNCTION__ . '-menu', $menu);

			$urls = [];
			$this->getPermissionsFromUrl($menu, $urls);
			$permissions = Admin::hasPermission($urls);
			//$urls        = array_map(fn ($value) => $value ? ($permissions[$value] ?? false) : false, $urls);
			$urls        = array_map(function ($value) use ($permissions) { return $value ? ($permissions[$value] ?? false) : false; }, $urls);
			$this->setPermissionsFromUrl($menu, $urls);

			$cacheDriver->set('admin-menu', $cacheKey, $menu);
		}

		$view->menu         = $menu;
		$view->global       = $this->global;

		//send to view for button visibility check
		$this->view->actionPermissions = $this->actionPermissions ?? [];
		$this->view->modulePermissions = $this->modulePermissions ?? [];

		$view->adminPath  = $adminPath;
		$view->publicPath = PUBLIC_PATH;
		$view->mediaPath  = PUBLIC_PATH . 'media';

		Event::trigger(__CLASS__, __FUNCTION__);
		
		return null;
	}

	/**
	 * Redirect to a URL with optional parameters.
	 *
	 * @param string $url       The URL to redirect to
	 * @param array<string, mixed> $parameters Query parameters to append
	 * @param bool   $stop      Whether to stop execution after redirect
	 *
	 * @return void
	 */
	protected function redirect(array|string $url = '/', array $parameters = [], bool $stop = true): void {
		$redirect = \Vvveb\url($url, $parameters);

		if ($redirect) {
			$url = $redirect;
		}

		header("Location: $url");

		if ($stop) {
			$this->session->close();
			FrontController::closeConnections();
			PageCache::getInstance()->cleanUp();

			die();
		}
	}

	/**
	 * Call this method if the action requires login, if the user is not logged in, a login form will be shown.
	 *
	 * @return never
	 */
	protected function requireLogin(): never {
		//return \Vvveb\System\Core\FrontController::redirect('user/login');
		//$view = view :: getInstance();
		$admin_path            = \Vvveb\adminPath();
		$this->view->redir     = $_SERVER['REQUEST_URI'] ?? '';
		$this->view->adminPath = $admin_path;
		$this->view->action    = "{$admin_path}index.php?module=user/login";
		$this->view->template('user' . DS . 'login.html');

		$this->view->render();

		die();
	}

	/**
	 * Call this method on form posts to protect against csrf attacks.
	 *
	 * @return bool True if CSRF is valid, false otherwise (will call notFound)
	 */
	protected function checkCsrf(): bool {
		if (! defined('CLI') &&
			(($method = $this->request->getMethod()) == 'post') &&
			(! ($csrf = $this->session->get('csrf')) || ! ($postCsrf = ($this->request->post['csrf'] ?? false)) || ($csrf != $postCsrf))) {
			$this->notFound('Invalid csrf!', 403);
			return false;
		}

		return true;
	}

	/**
	 * Shows a "Not found", "Internal server error" or "Permission denied" page.
	 *
	 * @param string|false $message    Error message to display
	 * @param int          $statusCode HTTP status code (404, 500, 403, etc.)
	 * @param mixed        $service    Service or controller to handle the error
	 *
	 * @return never
	 */
	protected function notFound(string|array|false $message = false, int $statusCode = 404, mixed $service = false): never {
		FrontController::notFound($service, $message, $statusCode);
	}

	/**
	 * Generates the documentation link for current page.
	 *
	 * @param string|null $module Module name, or null to use request value
	 * @param string|null $action Action name, or null to use request value
	 *
	 * @return string The documentation URL
	 */
	protected function getDocUrlForPage(?string $module = null, ?string $action = null): string {
		$module = $module ?? $this->request->get['module'] ?? '';
		$action = $action ?? $this->request->get['origaction'] ?? '';
		$type   = $this->request->get['type'] ?? '';
		$action = $action ? '/' . $action : '';
		$url    = 'https://www.vvveb.com/docs/';

		if ($type === 'post' || $type === 'page'/* || $type == 'product'*/) {
			$type   = $type ? '/' . $type : '';
		} else {
			$type = '';
		}

		$documentationList             = include DIR_SYSTEM . 'data/documentation-map.php';

		if (isset($documentationList[$module . $action . $type])) {
			$url .= $documentationList[$module . $action . $type];
		} else {
			$url .= str_replace('/', '-', $module . $action . $type);
		}

		return $url;
	}

	/**
	 * Redirect to the documentation page for the current admin page.
	 */
	public function goToHelp(): void {
		$url = $this->getDocUrlForPage();

		header("Location: $url");
	}
}
