<?php
namespace EasyChat\Controller;
 

use EasyChat\Model\Message;
use EasyChat\Model\Manager;
use EasyChat\Model\Setting;
use EasyChat\Library\I18n;
use EasyChat\Library\AttachmentPolicy;
use EasyChat\Library\RequestContext;
use EasyChat\Library\WidgetDomainPolicy;
use EasyChat\Template;
use EasyChat\View\AccessibleColor;
use Exception;

Abstract Class Base {

	protected $registry;
	protected $template;
	protected $layouts;  
	
	public $vars = array();

	 
	function __construct($registry) {
		$this->registry = $registry;
		 
		$this->template = new Template($this->layouts, get_class($this), $this);

		$db = $registry->get('db');
		$public_controllers = ['widget', 'api', 'messenger', 'webpush', 'widget_push', 'lead_planner'];
		$is_lightweight_admin_route = $this->isLightweightAdminRoute();
		$session_available = (!defined('EASYCHAT_SESSION_AVAILABLE') || EASYCHAT_SESSION_AVAILABLE)
			&& class_exists('\\EasyChat\\Library\\SessionBootstrap')
			&& \EasyChat\Library\SessionBootstrap::isAvailable();
		$session = \EasyChat\Library\RequestContext::session();

		 
		 
		if (!empty($session['cms_handoff_pending_claim'])
			&& !$this->isController(array_merge($public_controllers, ['auth']))
		) {
			header('Cache-Control: no-store, no-cache, must-revalidate');
			header('Location: index.php?route=auth');
			exit;
		}
		if (!$session_available
			&& !$this->isController(array_merge($public_controllers, ['auth', 'install', 'provisioning']))
		) {
			header('Cache-Control: no-store, no-cache, must-revalidate');
			header('Location: index.php?route=auth');
			exit;
		}

		$chat_widget_color = '#6366f1';
		$chat_on_accent_mode = AccessibleColor::MODE_AUTO;
		if ($db && !$is_lightweight_admin_route) {
			$model_setting = new Setting($this->registry);
			$chat_widget_color = $this->normalizeAdminThemeColor($model_setting->get('chat_widget_color', '#6366f1'));
			$chat_on_accent_mode = AccessibleColor::normalizeMode($model_setting->get('chat_on_accent_mode', AccessibleColor::MODE_AUTO));
		}
		$this->template->vars('chat_widget_color', $chat_widget_color);
		$this->template->vars('chat_on_accent_mode', $chat_on_accent_mode);
		$this->template->vars('chat_on_accent_color', AccessibleColor::foregroundFor($chat_widget_color, $chat_on_accent_mode));

		 
		if ($session_available && $db && isset(\EasyChat\Library\RequestContext::session()['manager_id']) && !$this->isController($public_controllers)) {
			$manager_id = (int)\EasyChat\Library\RequestContext::session()['manager_id'];
			$prefix = easychat_db_prefix();
			$query = $db->query("SELECT `password`, `status`, `role` FROM `{$prefix}managers` WHERE `id` = " . $manager_id . " LIMIT 1");

			$is_valid = false;
			if ($query && $query->row) {
				$manager_db = $query->row;
				$session_hash = (string)(\EasyChat\Library\RequestContext::session()['manager_pass_hash'] ?? '');
				$session_role = (string)(\EasyChat\Library\RequestContext::session()['manager_role'] ?? '');
				$db_hash = md5($manager_db['password']);
				$db_role = (string)($manager_db['role'] ?? '');

				 
				 
				 
				if ($session_hash !== ''
					&& $session_role !== ''
					&& hash_equals($db_hash, $session_hash)
					&& hash_equals($db_role, $session_role)
					&& (int)$manager_db['status'] === 1
				) {
					$is_valid = true;
				}
			}

			if (!$is_valid) {
				\EasyChat\Library\SessionBootstrap::destroy();
				if (!$this->isController(['auth'])) {
					header('Location: index.php?route=auth');
					exit;
				}
			}
		}

		if ($is_lightweight_admin_route
			&& $session_available
			&& isset(\EasyChat\Library\RequestContext::session()['manager_id'])
			&& session_status() === PHP_SESSION_ACTIVE
		) {
			 
			 
			 
			session_write_close();
		}

		if (!$db) {
			 
			$this->template->vars('chat_ai_status', '0');
			$this->template->vars('chat_ai_tools_available', '0');
			$this->template->vars('chat_admin_help_status', '0');
		}

		if ($session_available
			&& $db
			&& isset(RequestContext::session()['manager_id'])
			&& empty($session['cms_handoff_pending_claim'])
			&& !$this->isController($public_controllers)
			&& !$is_lightweight_admin_route
		) {
			$this->template->vars('easychat_csrf_token', $this->ensureCsrfToken());
		} else {
			$this->template->vars('easychat_csrf_token', '');
		}

		if ($this->shouldRedirectToFirstLaunchOnboarding($db, $public_controllers, $is_lightweight_admin_route)) {
			$this->redirectToRoute('onboarding');
			exit;
		}

		$admin_locale = 'ru';
		if ($session_available && $db && !$this->isController($public_controllers) && !$is_lightweight_admin_route) {
			$model_setting = new Setting($this->registry);
			$admin_locale = I18n::normalizeLocale(
				\EasyChat\Library\RequestContext::session()['easychat_locale']
				?? $model_setting->get('chat_admin_locale', '')
				?: I18n::resolveFromAcceptLanguage(\EasyChat\Library\RequestContext::server()['HTTP_ACCEPT_LANGUAGE'] ?? '')
			);
			\EasyChat\Library\RequestContext::session()['easychat_locale'] = $admin_locale;
		}
		$this->template->vars('easychat_locale', $admin_locale);
		$this->template->vars('easychat_supported_locales', I18n::supportedLocales());

		 
		 
		 

		 
		 
		if ($db && !$this->isController($public_controllers) && !$is_lightweight_admin_route) {
			$_ai = $db->query("SELECT `value` FROM `" . easychat_db_prefix() . "settings` WHERE `key` = 'chat_ai_status' LIMIT 1");
			$this->template->vars('chat_ai_status', ($_ai && $_ai->row) ? $_ai->row['value'] : '0');
			$_ai_key = $db->query("SELECT `value` FROM `" . easychat_db_prefix() . "settings` WHERE `key` = 'chat_ai_key' LIMIT 1");
			$ai_key = ($_ai_key && $_ai_key->row) ? trim((string)$_ai_key->row['value']) : '';
			$this->template->vars('chat_ai_tools_available', $ai_key !== '' ? '1' : '0');
			$admin_help_env = strtolower(\EasyChat\Library\Support\OfficialSupportRuntimeConfig::value('EASYCHAT_ADMIN_HELP_ENABLED'));
			$admin_help_enabled = $admin_help_env !== ''
				? in_array($admin_help_env, ['1', 'true', 'yes', 'on'], true)
				: (string)$model_setting->get('chat_admin_help_status', '0') === '1';
			$this->template->vars('chat_admin_help_status', $admin_help_enabled ? '1' : '0');
		}

		 
		$total_unread = 0;
		$current_qr_payload = '';

		if ($session_available && $db && isset(\EasyChat\Library\RequestContext::session()['manager_id']) && !$this->isController($public_controllers) && !$is_lightweight_admin_route) {
			$manager_id = (int)\EasyChat\Library\RequestContext::session()['manager_id'];

			 
			$model_message = new Message($this->registry);
			$total_unread = $model_message->getUnreadCountForManager($manager_id);

			 
			$model_manager = new Manager($this->registry);
			$manager_info = $model_manager->getManagerById($manager_id);

			if ($manager_info) {
				$model_setting = new Setting($this->registry);
				$app_master_token = $model_setting->get('chat_app_master_token');

				if (empty($app_master_token)) {
					$app_master_token = bin2hex(random_bytes(16));
					$model_setting->set('chat_app_master_token', $app_master_token);
				}

				$url = HTTP_SERVER;
				$key = md5($manager_info['salt'] . $manager_info['password']);
				$sig = hash_hmac('sha256', rtrim($url, '/') . '|' . $app_master_token, $key);
				$current_qr_payload = base64_encode(json_encode([
					'url'   => $url,
					'token' => $app_master_token,
					'sig'   => $sig
				]));
			}
		}

		$this->template->vars('total_unread', $total_unread);
		$this->template->vars('current_qr_payload', $current_qr_payload);
	}

	protected function isController(array $names) {
		$class = strtolower(get_class($this));
		if (strpos($class, 'easychat\\controller\\') === 0) {
			$class = substr($class, strlen('easychat\\controller\\'));
		}
		if (strpos($class, 'controller_') === 0) {
			$class = substr($class, strlen('controller_'));
		}
		$class = str_replace('_', '', $class);
		foreach ($names as $name) {
			if ($class === str_replace('_', '', strtolower((string)$name))) {
				return true;
			}
		}
		return false;
	}

	protected function handleWidgetCorsPreflight(?Setting $model_setting = null, $methods = 'POST, OPTIONS', $headers = 'Content-Type') {
		if (RequestContext::method('GET') !== 'OPTIONS') {
			return false;
		}

		if (!$this->sendWidgetCorsHeaders($model_setting, $methods, $headers)) {
			http_response_code(403);
			header('Content-Type: application/json; charset=utf-8');
			echo json_encode(['success' => 0, 'error' => 'unauthorized_widget_domain'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
			return true;
		}

		http_response_code(204);
		return true;
	}

	protected function sendWidgetCorsHeaders(?Setting $model_setting = null, $methods = 'POST, OPTIONS', $headers = 'Content-Type') {
		$server = RequestContext::server();
		$origin = trim((string)($server['HTTP_ORIGIN'] ?? ''));
		$origin_host = WidgetDomainPolicy::normalizeHost($origin);
		$referer_host = WidgetDomainPolicy::normalizeHost((string)($server['HTTP_REFERER'] ?? ''));

		if ($origin_host === '' && $referer_host === '') {
			return true;
		}

		$policy = WidgetDomainPolicy::fromSettings($model_setting ?: new Setting($this->registry));
		if ($origin_host !== '') {
			if (!$policy->isHostAllowed($origin_host)) {
				return false;
			}

			$cors_origin = $this->normalizedCorsOrigin($origin);
			if ($cors_origin === '') {
				return false;
			}

			header('Access-Control-Allow-Origin: ' . $cors_origin);
			header('Access-Control-Allow-Methods: ' . $methods);
			header('Access-Control-Allow-Headers: ' . $headers);
			header('Access-Control-Max-Age: 600');
			header('Vary: Origin', false);
		} elseif (!$policy->isHostAllowed($referer_host)) {
			return false;
		}

		header('X-Content-Type-Options: nosniff');
		return true;
	}

	private function normalizedCorsOrigin($origin) {
		$parts = parse_url((string)$origin);
		if (!is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) {
			return '';
		}

		$scheme = strtolower((string)$parts['scheme']);
		if ($scheme !== 'http' && $scheme !== 'https') {
			return '';
		}

		$host = WidgetDomainPolicy::normalizeHost((string)$parts['host']);
		if ($host === '') {
			return '';
		}

		$normalized = $scheme . '://' . $host;
		if (isset($parts['port'])) {
			$port = (int)$parts['port'];
			if ($port > 0 && $port <= 65535) {
				$normalized .= ':' . $port;
			}
		}

		return $normalized;
	}

	protected function isCurrentRoute(array $routes) {
		$current = defined('EASYCHAT_CURRENT_ROUTE_NORMALIZED') ? (string)EASYCHAT_CURRENT_ROUTE_NORMALIZED : '';
		$current = str_replace('\\', '/', strtolower(trim($current, '/')));
		foreach ($routes as $route) {
			$expected = str_replace('\\', '/', strtolower(trim((string)$route, '/')));
			if ($current === $expected) {
				return true;
			}
		}
		return false;
	}

	protected function isLightweightAdminRoute() {
		return $this->isCurrentRoute([
			'index/image',
			'index/contactavatar',
			'index/commerceimage',
			'index/getchats',
			'index/openchat',
			'index/getmessages',
			'index/acknowledgechatview',
			'index/customerpanel',
			'index/getunreadcount',
			'index/presence',
			'index/improveoutbounddraft',
			'notification_health/status',
		]);
	}

	protected function shouldRedirectToFirstLaunchOnboarding($db, array $public_controllers, $is_lightweight_admin_route) {
		if (!$db || $is_lightweight_admin_route || $this->isController(array_merge($public_controllers, ['auth', 'install', 'onboarding', 'provisioning']))) {
			return false;
		}

		if (empty(RequestContext::session()['manager_id']) || (string)(RequestContext::session()['manager_role'] ?? '') !== 'admin') {
			return false;
		}

		$model_setting = new Setting($this->registry);
		return (string)$model_setting->get('chat_first_launch_consent_required', '0') === '1'
			&& (string)$model_setting->get('chat_first_launch_consent_completed', '0') !== '1';
	}

	protected function ensureCsrfToken() {
		$token = isset(RequestContext::session()['easychat_csrf_token'])
			? (string)RequestContext::session()['easychat_csrf_token']
			: '';

		if (!preg_match('/^[a-f0-9]{64}$/i', $token)) {
			$token = bin2hex(random_bytes(32));
			RequestContext::session()['easychat_csrf_token'] = $token;
		}

		return $token;
	}

	protected function routeUrl($route) {
		$base = defined('EASYCHAT_ROUTE_BASE_URL') ? (string)EASYCHAT_ROUTE_BASE_URL : 'index.php?route=';
		return $base . ltrim((string)$route, '/');
	}

	protected function redirectToRoute($route) {
		header('Location: ' . $this->routeUrl($route));
	}

	protected function verifyCsrfToken($field = 'csrf_token') {
		$expected = isset(RequestContext::session()['easychat_csrf_token'])
			? (string)RequestContext::session()['easychat_csrf_token']
			: '';
		$actual = isset(RequestContext::post()[$field])
			? (string)RequestContext::post()[$field]
			: '';

		if ($actual === '') {
			$server = RequestContext::server();
			$actual = isset($server['HTTP_X_EASYCHAT_CSRF']) ? (string)$server['HTTP_X_EASYCHAT_CSRF'] : '';
		}

		return $expected !== '' && $actual !== '' && hash_equals($expected, $actual);
	}

	protected function requireCsrfTokenJson($field = 'csrf_token') {
		if (RequestContext::method() !== 'POST') {
			$this->sendJsonSecurityError('method_not_allowed', 405);
		}

		if (!$this->verifyCsrfToken($field)) {
			$this->sendJsonSecurityError('csrf_validation_failed', 403);
		}
	}

	protected function sendJsonSecurityError($code, $status = 403) {
		http_response_code((int)$status);
		echo json_encode([
			'success' => false,
			'error' => (string)$code,
		], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
		exit;
	}

	protected function sendPrivateImmutableBinaryCacheHeaders($etag, $last_modified = 0, $max_age = 2592000) {
		if (headers_sent()) {
			return;
		}

		$max_age = max(60, (int)$max_age);
		if (function_exists('header_remove')) {
			 
			 
			 
			header_remove('Pragma');
			header_remove('Expires');
			header_remove('Cache-Control');
		}

		header('X-Content-Type-Options: nosniff');
		header('Cache-Control: private, max-age=' . $max_age . ', immutable');
		header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $max_age) . ' GMT');
		if ((int)$last_modified > 0) {
			header('Last-Modified: ' . gmdate('D, d M Y H:i:s', (int)$last_modified) . ' GMT');
		}
		if ((string)$etag !== '') {
			header('ETag: ' . (string)$etag);
		}
	}

	protected function releaseSessionLockForReadOnlyBinaryResponse() {
		if (session_status() === PHP_SESSION_ACTIVE) {
			 
			 
			 
			session_write_close();
		}
	}

	protected function releaseSessionLockForReadOnlyJsonResponse() {
		if (session_status() === PHP_SESSION_ACTIVE) {
			 
			 
			 
			session_write_close();
		}
	}

	protected function sendLocalBinaryFile($file_path, $mime, $download_name = '', $force_attachment = false, array $extra_headers = []) {
		if (!is_file($file_path)) {
			header("HTTP/1.1 404 Not Found");
			exit;
		}

		foreach ($extra_headers as $header) {
			header((string)$header);
		}

		$size = (int)filesize($file_path);
		$mtime = (int)filemtime($file_path);
		$version = sha1(basename((string)$file_path) . '|' . $size . '|' . $mtime);
		$etag = '"' . $version . '"';
		if (function_exists('header_remove')) {
			 
			 
			 
			header_remove('Pragma');
			header_remove('Expires');
			header_remove('Cache-Control');
		}
		$request_version = trim((string)(RequestContext::get()['v'] ?? ''));
		$has_current_version = $request_version !== '' && hash_equals($version, $request_version);
		header($has_current_version
			? 'Cache-Control: private, max-age=2592000, immutable'
			: 'Cache-Control: private, max-age=0, must-revalidate');
		if ($has_current_version) {
			header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 2592000) . ' GMT');
		}
		header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
		header('ETag: ' . $etag);

		$if_none_match = (string)(RequestContext::server()['HTTP_IF_NONE_MATCH'] ?? '');
		$if_modified_since = (string)(RequestContext::server()['HTTP_IF_MODIFIED_SINCE'] ?? '');
		if ((RequestContext::server()['HTTP_RANGE'] ?? '') === '') {
			$etag_matches = $if_none_match !== '' && hash_equals($etag, trim($if_none_match));
			$modified_since = $if_modified_since !== '' ? strtotime($if_modified_since) : false;
			$time_matches = $modified_since !== false && $modified_since >= $mtime;
			if ($etag_matches || ($if_none_match === '' && $time_matches)) {
				header('HTTP/1.1 304 Not Modified');
				exit;
			}
		}

		$start = 0;
		$end = $size > 0 ? $size - 1 : 0;
		$status = 200;
		$range = (string)(RequestContext::server()['HTTP_RANGE'] ?? '');
		$handle = fopen($file_path, 'rb');
		if ($handle === false) {
			header('HTTP/1.1 500 Internal Server Error');
			exit;
		}

		if ($range !== '' && preg_match('/^bytes=(\d*)-(\d*)$/', $range, $matches)) {
			$range_start = $matches[1];
			$range_end = $matches[2];
			if ($range_start === '' && $range_end !== '') {
				$suffix = max(0, (int)$range_end);
				$start = max(0, $size - $suffix);
			} elseif ($range_start !== '') {
				$start = (int)$range_start;
				if ($range_end !== '') {
					$end = min($end, (int)$range_end);
				}
			}

			if ($size <= 0 || $start > $end || $start >= $size) {
				header('HTTP/1.1 416 Range Not Satisfiable');
				header('Accept-Ranges: bytes');
				header('Content-Range: bytes */' . max(0, $size));
				fclose($handle);
				exit;
			}
			$status = 206;
		}

		$length = $size > 0 ? ($end - $start + 1) : 0;
		if ($status === 206) {
			header('HTTP/1.1 206 Partial Content');
			header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size);
		}

		header('Accept-Ranges: bytes');
		header('X-Content-Type-Options: nosniff');
		header('Content-Type: ' . (string)$mime);
		header('Content-Length: ' . $length);
		if ($force_attachment) {
			$download_name = $this->safeAttachmentDownloadName(
				$download_name !== '' ? (string)$download_name : basename((string)$file_path)
			);
			$ascii_name = preg_replace('/[^A-Za-z0-9._-]+/', '_', $download_name);
			$ascii_name = is_string($ascii_name) && $ascii_name !== '' ? $ascii_name : 'document';
			header('Content-Disposition: attachment; filename="' . str_replace('"', '', $ascii_name) . '"; filename*=UTF-8\'\'' . rawurlencode($download_name));
			header('Content-Security-Policy: sandbox');
			header('X-Download-Options: noopen');
		}

		if (RequestContext::method() === 'HEAD') {
			fclose($handle);
			exit;
		}

		if ($start > 0) {
			fseek($handle, $start);
		}
		$remaining = $length;
		while ($remaining > 0 && !feof($handle)) {
			$chunk = fread($handle, min(8192, $remaining));
			if ($chunk === false || $chunk === '') {
				break;
			}
			echo $chunk;
			$remaining -= strlen($chunk);
			if (function_exists('flush')) {
				flush();
			}
		}
		fclose($handle);
		exit;
	}

	protected function localAttachmentResponse($file_path, $stored_name, $requested_name = '') {
		$stored_name = basename((string)$stored_name);
		$extension = strtolower(pathinfo($stored_name, PATHINFO_EXTENSION));
		$detected_mime = AttachmentPolicy::detectMime((string)$file_path, $extension);
		if (AttachmentPolicy::isImageExtension($extension, true)
			&& AttachmentPolicy::isImageMime($detected_mime, true)) {
			return [
				'mime' => $detected_mime,
				'download_name' => $stored_name,
				'force_attachment' => false,
			];
		}

		if (AttachmentPolicy::isAudioExtension($extension)) {
			$size = is_file($file_path) ? (int)filesize($file_path) : 0;
			$max_mb = max(1, (int)ceil(max(1, $size) / 1024 / 1024));
			$validation = AttachmentPolicy::validateAudioFile((string)$file_path, $stored_name, $size, $max_mb);
			if (!empty($validation['success'])) {
				return [
					'mime' => (string)$validation['mime'],
					'download_name' => $stored_name,
					'force_attachment' => false,
				];
			}
		}

		if (AttachmentPolicy::isDocumentExtension($extension)) {
			$size = is_file($file_path) ? (int)filesize($file_path) : 0;
			$max_mb = max(1, (int)ceil(max(1, $size) / 1024 / 1024));
			$validation = AttachmentPolicy::validateDocumentFile((string)$file_path, $stored_name, $size, $max_mb);
			if (!empty($validation['success'])) {
				$download_name = $this->safeAttachmentDownloadName((string)$requested_name);
				$expected_extension = (string)$validation['extension'];
				if ($download_name === '') {
					$download_name = $stored_name;
				}
				if (strtolower(pathinfo($download_name, PATHINFO_EXTENSION)) !== $expected_extension) {
					$base = pathinfo($download_name, PATHINFO_FILENAME);
					$download_name = ($base !== '' ? $base : 'document') . '.' . $expected_extension;
				}
				return [
					'mime' => (string)$validation['mime'],
					'download_name' => $download_name,
					'force_attachment' => true,
				];
			}
		}

		return [
			'mime' => 'application/octet-stream',
			'download_name' => $stored_name,
			'force_attachment' => true,
		];
	}

	private function safeAttachmentDownloadName($value) {
		$value = basename(trim((string)$value));
		$value = preg_replace('/[\x00-\x1F\x7F]+/', '_', $value);
		if (!is_string($value)) {
			return '';
		}
		return function_exists('mb_substr') ? mb_substr($value, 0, 240, 'UTF-8') : substr($value, 0, 240);
	}

	protected function normalizeAdminThemeColor($value) {
		$value = trim((string)$value);
		return preg_match('/^#[0-9a-f]{6}$/i', $value) ? $value : '#6366f1';
	}

	abstract function index();
}
