<?php

namespace XcVm\Core\Diagnostics;

use XcVm\Core\Database\Database;
use XcVm\Core\Http\ApiClient;

/**
 * Diagnostics Service
 *
 * downloadPanelLogs, submitPanelLogs.
 *
 * Panel-log methods accept a $db parameter; other methods are stateless.
 *
 * @package XC_VM_Core_Diagnostics
 * @author  Divarion_D <https://github.com/Divarion-D>
 * @copyright 2025-2026 Vateron Media
 * @link    https://github.com/Vateron-Media/XC_VM
 * @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
 */

class DiagnosticsService {

	/**
	 * Parse SSL certificate info from nginx config or a specific file
	 *
	 * @param string|null $certificate  Path to certificate file (auto-detects from nginx if null)
	 * @return array ['serial', 'expiration', 'subject', 'path']
	 */
	public static function getCertificateInfo($certificate = null) {
		$result = ['serial' => null, 'expiration' => null, 'subject' => null, 'path' => null];

		if (!$certificate) {
			$config = explode("\n", file_get_contents(BIN_PATH . 'nginx/conf/ssl.conf'));
			foreach ($config as $line) {
				$rTrimmed = trim($line);
				if (strncasecmp($rTrimmed, 'ssl_certificate ', 16) === 0) {
					$certificate = trim(explode(';', substr($rTrimmed, 16), 2)[0]);
					break;
				}
			}
		}

		if (!$certificate || !file_exists($certificate)) {
			return null;
		}

		$result['path'] = pathinfo($certificate)['dirname'];
		$output = [];
		exec('openssl x509 -serial -enddate -subject -noout -in ' . escapeshellarg($certificate), $output, $returnVar);
		if ($returnVar !== 0) {
			return null;
		}
		foreach ($output as $line) {
			if (stripos($line, 'serial=') !== false) {
				$result['serial'] = trim(explode('serial=', $line)[1]);
			} elseif (stripos($line, 'subject=') !== false) {
				$result['subject'] = trim(explode('subject=', $line)[1]);
			} elseif (stripos($line, 'notAfter=') !== false) {
				$result['expiration'] = strtotime(trim(explode('notAfter=', $line)[1]));
			}
		}

		return $result;
	}

	/**
	 * Check if stream codecs are compatible with the player
	 *
	 * @param array|string $data       FFProbe output (array or JSON string)
	 * @param bool         $allowHEVC  Whether HEVC/H265 + AC3 are allowed
	 * @return bool
	 */
	public static function checkCompatibility($data, $allowHEVC = false) {
		if (!is_array($data)) {
			$data = json_decode($data, true);
		}

		if (!is_array($data) || !isset($data['codecs']) || !is_array($data['codecs'])) {
			return false;
		}

		$audioCodec = $data['codecs']['audio']['codec_name'] ?? null;
		$videoCodec = $data['codecs']['video']['codec_name'] ?? null;

		$audioCodecs = ['aac', 'libfdk_aac', 'opus', 'vorbis', 'pcm_s16le', 'mp2', 'mp3', 'flac'];
		$videoCodecs = ['h264', 'vp8', 'vp9', 'ogg', 'av1'];

		if ($allowHEVC) {
			$videoCodecs[] = 'hevc';
			$videoCodecs[] = 'h265';
			$audioCodecs[] = 'ac3';
		}

		if (!$videoCodec) {
			return false;
		}

		if (!in_array(strtolower($videoCodec), $videoCodecs, true)) {
			return false;
		}

		if ($audioCodec && !in_array(strtolower($audioCodec), $audioCodecs, true)) {
			return false;
		}

		return true;
	}

	/**
	 * Download panel logs from database, format them and clear the logs table
	 *
	 * @param \XcVm\Core\Database\DatabaseHandler $db  Database handler (must have ->query(), ->get_rows())
	 * @return array ['errors' => [...], 'version' => string]
	 * @throws \Exception
	 */
	public static function downloadPanelLogs($db): array {
		ini_set('default_socket_timeout', 60);
		$errors = [];

		try {
			$query = "SELECT `type`, `log_message`, `log_extra`, `line`, `date` 
                  FROM `panel_logs` 
                  WHERE `type` <> 'epg' 
                  ORDER BY `date` DESC 
                  LIMIT 1000";

			$result = $db->query($query);
			if (!$result) {
				throw new \Exception('Failed to execute database query');
			}

			$allErrors = $db->get_rows() ?: [];

			foreach ($allErrors as $error) {
				$errorData = [
					'type'    => isset($error['type']) ? htmlspecialchars($error['type'], ENT_QUOTES, 'UTF-8') : 'unknown',
					'message' => isset($error['log_message']) ? htmlspecialchars($error['log_message'], ENT_QUOTES, 'UTF-8') : '',
					'file'    => isset($error['log_extra']) ? htmlspecialchars($error['log_extra'], ENT_QUOTES, 'UTF-8') : '',
					'line'    => isset($error['line']) ? (int)$error['line'] : 0,
					'date'    => isset($error['date']) ? (int)$error['date'] : 0,
				];

				try {
					if ($errorData['date'] > 0) {
						$dt = new \DateTime('@' . $errorData['date']);
						$dt->setTimezone(new \DateTimeZone('UTC'));
						$errorData['human_date'] = $dt->format('Y-m-d H:i:s');
					} else {
						$errorData['human_date'] = 'invalid_timestamp';
					}
				} catch (\Exception $e) {
					$errorData['human_date'] = 'conversion_error';
				}

				$errors[] = $errorData;
			}

			if (!empty($errors)) {
				$truncateResult = $db->query('TRUNCATE `panel_logs`;');
				if (!$truncateResult) {
					throw new \Exception('Failed to truncate panel logs table');
				}
			}
		} catch (\Exception $e) {
			throw new \Exception('Failed to process panel logs');
		}

		return [
			'errors'  => $errors,
			'version' => defined('XC_VM_VERSION') ? XC_VM_VERSION : 'unknown',
		];
	}

	/**
	 * Submit panel logs to the central API server
	 *
	 * @param \XcVm\Core\Database\DatabaseHandler $db  Database handler
	 * @return string|false  API response or false on failure
	 */
	public static function submitPanelLogs($db) {
		ini_set('default_socket_timeout', 60);

		// Select only logs not yet marked as sent
		$db->query("SELECT `id`, `type`, `log_message`, `log_extra`, `line`, `date` FROM `panel_logs` WHERE `type` <> 'epg' AND IFNULL(`sent`,0)=0 GROUP BY CONCAT(`type`, `log_message`, `log_extra`) ORDER BY `date` DESC LIMIT 1000;");

		$rows = $db->get_rows() ?: [];

		$rAPI = base64_decode(implode('', [
			'aHR0cHM6L',
			'y93d3cueG',
			'N2bS50ZWN',
			'oL2FwaS92',
			'MS9yZXBvcnQ='
		]));

		$errorsForApi = [];
		$ids = [];

		foreach ($rows as $row) {
			$ts = isset($row['date']) ? (int)$row['date'] : 0;
			$errorsForApi[] = [
				'type'        => $row['type'] ?? '',
				'log_message' => $row['log_message'] ?? '',
				'log_extra'   => $row['log_extra'] ?? '',
				'line'        => isset($row['line']) ? (string)$row['line'] : '',
				'date'        => $ts > 0 ? gmdate('Y-m-d H:i:s', $ts) : '',
			];
			if (isset($row['id'])) {
				$ids[] = (int)$row['id'];
			}
		}

		$payload = json_encode([
			'errors'  => $errorsForApi,
			'version' => defined('XC_VM_VERSION') ? XC_VM_VERSION : 'unknown',
		], JSON_UNESCAPED_UNICODE);

		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $rAPI);
		curl_setopt($ch, CURLOPT_POST, true);
		curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
		curl_setopt($ch, CURLOPT_TIMEOUT, 60);
		curl_setopt($ch, CURLOPT_HTTPHEADER, [
			'Content-Type: application/json',
			'Content-Length: ' . strlen($payload),
		]);

		$response = curl_exec($ch);

		curl_close($ch);

		if ($response !== false) {
			$responseData = json_decode($response, true);
			if (isset($responseData['status']) && $responseData['status'] === 'success' && !empty($ids)) {
				// mark sent logs instead of truncating the whole table
				$idList = implode(',', array_map('intval', $ids));
				$db->query("UPDATE `panel_logs` SET `sent` = 1 WHERE `id` IN ($idList);");
			}
		}

		return $response;
	}

	/**
	 * Fetch the list of running process PIDs from a server via the system API.
	 *
	 * @param int $rServerID Server id to query.
	 * @return array Process info keyed/listed as returned by the server.
	 */
	public static function getPIDs($rServerID) {
		$rReturn = array();
		$rProcesses = json_decode(ApiClient::systemRequest($rServerID, array('action' => 'get_pids')), true);
		if (!is_array($rProcesses)) {
			return $rReturn;
		}
		array_shift($rProcesses);

		foreach ($rProcesses as $rProcess) {
			$rSplit = explode(' ', preg_replace('!\\s+!', ' ', trim($rProcess)));

			if ($rSplit[0] == 'xc_vm') {
				$rUsage = array(0, 0, 0);
				$rTimer = explode('-', $rSplit[9]);

				if (1 < count($rTimer)) {
					$rDays = intval($rTimer[0]);
					$rTime = $rTimer[1];
				} else {
					$rDays = 0;
					$rTime = $rTimer[0];
				}

				$rTime = explode(':', $rTime);

				if (count($rTime) == 3) {
					$rSeconds = intval($rTime[0]) * 3600 + intval($rTime[1]) * 60 + intval($rTime[2]);
				} else {
					if (count($rTime) == 2) {
						$rSeconds = intval($rTime[0]) * 60 + intval($rTime[1]);
					} else {
						$rSeconds = intval($rTime[2]);
					}
				}

				$rUsage[0] = $rSeconds + $rDays * 86400;
				$rTimer = explode('-', $rSplit[8]);

				if (1 < count($rTimer)) {
					$rDays = intval($rTimer[0]);
					$rTime = $rTimer[1];
				} else {
					$rDays = 0;
					$rTime = $rTimer[0];
				}

				$rTime = explode(':', $rTime);

				if (count($rTime) == 3) {
					$rSeconds = intval($rTime[0]) * 3600 + intval($rTime[1]) * 60 + intval($rTime[2]);
				} else {
					if (count($rTime) == 2) {
						$rSeconds = intval($rTime[0]) * 60 + intval($rTime[1]);
					} else {
						$rSeconds = intval($rTime[2]);
					}
				}

				$rUsage[1] = $rSeconds + $rDays * 86400;
				if ($rUsage[0] != 0) {
					$rUsage[2] = $rUsage[1] / $rUsage[0] * 100;
				} else {
					$rUsage[2] = 0;
				}

				$rReturn[] = array('user' => $rSplit[0], 'pid' => $rSplit[1], 'cpu' => $rSplit[2], 'mem' => $rSplit[3], 'vsz' => $rSplit[4], 'rss' => $rSplit[5], 'tty' => $rSplit[6], 'stat' => $rSplit[7], 'time' => $rUsage[1], 'etime' => $rUsage[0], 'load_average' => $rUsage[2], 'command' => implode(' ', array_splice($rSplit, 10, count($rSplit) - 10)));
			}
		}

		return $rReturn;
	}

	/**
	 * List NVENC (GPU) encoding processes for a server.
	 *
	 * @param int $rServerID Server id to inspect.
	 * @return array NVENC process details.
	 */
	public static function getNVENCProcesses($rServerID) {
		global $db;
		$rProcesses = array();
		$rServer = \XcVm\Domain\Server\ServerRepository::getById($rServerID);
		$rGPUInfo = json_decode($rServer['gpu_info'], true);

		if (!is_array($rGPUInfo)) {
		} else {
			foreach ($rGPUInfo['gpus'] as $rGPU) {
				foreach ($rGPU['processes'] as $rProcess) {
					$rArray = array('pid' => $rProcess['pid'], 'memory' => $rProcess['memory'], 'stream_id' => null);
					$db->query('SELECT `stream_id` FROM `streams_servers` WHERE `pid` = ? AND `server_id` = ?;', $rProcess['pid'], $rServerID);

					if (0 >= $db->num_rows()) {
					} else {
						$rArray['stream_id'] = $db->get_row()['stream_id'];
					}

					$rProcesses[] = $rArray;
				}
			}
		}

		return $rProcesses;
	}
}
