<?php
/**
 * User: zzalap
 * Date: 14.05.2025
 * Time: 18:23 PM
 *
 */
namespace IMATHUZH\Qfq\Core;

use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Report\Link;
use IMATHUZH\Qfq\Core\Store\Config;
use IMATHUZH\Qfq\Core\Store\Session;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Typo3\T3Handler;

/**
 * DeveloperPanel
 *
 * This class acts as the backend handler for the Developer Panel interface.
 * It allows the frontend to retrieve system-related data such as:
 * - Store content
 * - SQL performance metrics
 * - Log contents
 * - Generated SIP links
 * - Typo3 / PHP / DB info
 *
 * It supports a JSON-based API interface via DeveloperPanel::process().
 *
 * @package qfq
 */
class DeveloperPanel {
    /**
     * @var Store|null Reference to the Store instance.
     */
    private ?Store $store;

    /**
     * @var Database Database connection instance.
     */
    private Database $db;

    /**
     * @var Sip SIP handler instance.
     */
    private Sip $sip;

    /**
     * @var Config QFQ configuration handler.
     */
    private Config $config;

    /**
     * @var Link
     */
    private Link $link;

    /**
     * @var string|mixed Requested developer panel action key.
     */
    private mixed $action;

    /**
     * @var array Mapping from store key to readable name for use in API.
     */
    private array $storeNameMap = [
        STORE_CLIENT => STORE_NAME_CLIENT,
        STORE_RECORD => STORE_NAME_RECORD,
        STORE_SIP => STORE_NAME_SIP,
        STORE_USER => STORE_NAME_USER,
        STORE_TYPO3 => STORE_NAME_TYPO3,
        STORE_SYSTEM => STORE_NAME_SYSTEM
    ];

    /**
     * DeveloperPanel constructor.
     *
     * Initializes required service instances and resolves the requested action via SIP.
     *
     * @param bool $phpUnit Whether the panel is being initialized in unit test context.
     * @throws \CodeException If it fails to get Session instance
     * @throws \Exception If no sip is found in request
     */
    public function __construct($phpUnit = false) {
        $sipParam = $_REQUEST[CLIENT_SIP] ?? null;
        if (!$sipParam) {
            // Exit early if so sip is given
            throw new \Exception('Missing required SIP parameter');
        }
        $sipParam = Sanitize::sanitize($sipParam, SANITIZE_ALLOW_ALNUMX);

        // Loads session
        // This does not need to be saved in a variable and only needs to be called to allow other Classes / function to have access to current qfq session
        Session::getInstance($phpUnit);

        // Set the changes flag back to false so that when a page reload happens changes from dev panel are reset.
        if (Session::get(DEV_PANEL_SESSION_FLAG_CHANGES)) {
            Session::set(DEV_PANEL_SESSION_FLAG_CHANGES, false);
        }

        // Set the DEV_PANEL_SESSION_FLAG_BUILD to 'yes' so other pages load the dev panel
        Session::set(DEV_PANEL_SESSION_FLAG_BUILD, DEV_PANEL_SESSION_FLAG_BUILD_YES);

        $this->store = Store::getInstance('', $phpUnit);
        $this->db = new Database();
        $this->config = new Config();
        $this->sip = new Sip();
        $this->link = new Link($this->sip);

        // Get the action from the sip
        $sip = $this->sip->getVarsFromSip($sipParam);
        $this->action = $sip[API_DEV_PANEL_ACTION] ?? '';
    }

    /**
     * Dispatches the requested action and returns the corresponding result.
     *
     * @return array Response payload
     * @throws \UserFormException
     * @throws \CodeException
     * @throws \UserReportException
     * @throws \DbException
     * @throws \Exception If no known action is given
     */
    public function process(): array {
        // Match action
        return match ($this->action) {
            API_DEV_PANEL_ACTION_GET_STORE => $this->getStoreData(),
            API_DEV_PANEL_ACTION_GET_INFO => $this->getInfoData(),
            API_DEV_PANEL_ACTION_GET_PERFORMANCE => $this->getPerformanceData(),
            API_DEV_PANEL_ACTION_GENERATE_LINK => [JS_ARRAY_KEY_LINK => $this->generateSipLink()],
            API_DEV_PANEL_ACTION_POLL_LOGS => $this->pollLogs(),
            API_DEV_PANEL_ACTION_CLEAR_DIRTY => [API_MESSAGE => $this->truncateDirty()],
            API_DEV_PANEL_ACTION_APPLY_CHANGES => [API_MESSAGE => $this->applyStoreChanges()],
            default => throw new \Exception("Unknown action: $this->action"),
        };
    }

    /**
     * Clears the Dirty table in the database.
     *
     * @return string
     * @throws \DbException If truncation fails.
     */
    private function truncateDirty(): string {
        try {
            $this->db->sql("TRUNCATE Dirty", ROW_EXPECT_0);
        } catch ( \Exception $e) {
            throw new \DbException("Failed to Clear Dirty Table: " . $e);
        }
        return 'Success';
    }

    /**
     * Returns store data from session with readonly flags.
     *
     * @return array Nested structure with readOnly flags and key-value data.
     */
    private function getStoreData(): array {
        $stores = $this->store->getStoreStateFromSession();
        // Configure stores witch should be shown
        // With API_STORE_READONLY_FLAG_KEY set if the store is readonly. This only affects frontend handling
        $data = [
            STORE_NAME_RECORD => [API_STORE_READONLY_FLAG_KEY => true, API_STORE_DATA_KEY => []],
            STORE_NAME_CLIENT => [API_STORE_READONLY_FLAG_KEY => false, API_STORE_DATA_KEY => []],
            STORE_NAME_SIP => [API_STORE_READONLY_FLAG_KEY => false, API_STORE_DATA_KEY => []],
            STORE_NAME_USER => [API_STORE_READONLY_FLAG_KEY => false, API_STORE_DATA_KEY => []],
            STORE_NAME_SYSTEM => [API_STORE_READONLY_FLAG_KEY => false, API_STORE_DATA_KEY => []],
            STORE_NAME_TYPO3 => [API_STORE_READONLY_FLAG_KEY => false, API_STORE_DATA_KEY => []],
        ];

        // Get Store data
        foreach ($this->storeNameMap AS $key => $name) {
            if (isset($stores[$key]) && array_key_exists($name, $data)) {
                $data[$name][API_STORE_DATA_KEY] = $stores[$key];
            }
        }

        return $data;
    }

    /**
     * Returns basic system information like PHP, DB, and TYPO3 version.
     *
     * @return array
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function getInfoData(): array {
        $data = [];

        // Typo Version
        $data[API_INFO_KEY_TYPO3] = T3Handler::getTypo3Version() ?? 'Unknown';

        // QFQ Extension Version
        $data[API_INFO_KEY_QFQ] = $this->config->getExtVersion()[EXT_KEY] ?? 'Unknown';

        // PHP version of host system
        $phpVersionLines = [];
        if (function_exists('exec')) {
            exec('php --version', $phpVersionLines);
            $data[API_INFO_KEY_PHP] = $phpVersionLines[0] ?? 'Unknown';
        } else {
            $data[API_INFO_KEY_PHP] = 'exec() disabled';
        }

        // Webserver Version
        $serverVersion = $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown';
        $data[API_INFO_KEY_WEBSERVER] = Sanitize::sanitize($serverVersion, SANITIZE_ALLOW_ALNUMX);

        // DB Server + Version
        $data[API_INFO_KEY_DB] = $this->db->sql("SELECT @@VERSION", ROW_EXPECT_1)["@@VERSION"] ?? 'Unknown';


        return $data;
    }
    /**
     * Returns SQL performance metrics stored in the session.
     *
     * @return array Query execution times (key => ms).
     */
    private function getPerformanceData(): array {
        $sqlData = Session::get(PERFORMANCE_SESSION_KEY);
        $sqlData = $sqlData ?? [];

        // UnsetArray to have a clean start when a new Page is loaded
        Session::unsetItem(PERFORMANCE_SESSION_KEY);
        return $sqlData;
    }

    /**
     * Generates a SIP link based on user input.
     *
     * @return string Rendered link or SIP
     * @throws Exception\RedirectResponse
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     * @throws \Exception
     */
    private function generateSipLink(): string {
        // Get request body
        $rawBody = file_get_contents('php://input');
        $requestBody = json_decode($rawBody, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \Exception('Invalid JSON in request body: ' . json_last_error_msg());
        }

        if (!isset($requestBody[JS_ARRAY_KEY_LINK])) {
            throw new \Exception('Missing "link" parameter in request body');
        }
        $link = $requestBody[JS_ARRAY_KEY_LINK];
        // Get Url and Path Variables
        $linkArray = explode('?',$link, 2);

        switch (count($linkArray)) {
            case 2:
                // Url and Parameters are given
                $result = $this->link->renderLink('s|r:7|u:' . $linkArray[0] . '|U:' . $linkArray[1]);
                break;
            case 1:
                if ($linkArray[0] === '') {
                    // Body was given but was empty
                    $result = 'No input found';
                    break;
                }
                // Only Parameters are given
                $result = $this->link->renderLink('s|r:8|U:' . $linkArray[0]);
                break;
            default:
                $result = 'No input found';
        }

        return $result;
    }

    /**
     * Reads and returns the last 50 lines from each log file.
     *
     * @return array Associative array with log keys and file content
     * @throws \UserFormException
     */
    private function pollLogs(): array {

        $logs[API_LOGS_SQL_KEY] = $this->readFileTail(Path::absoluteSqlLogFile());
        $logs[API_LOGS_MAIL_KEY] = $this->readFileTail(Path::absoluteMailLogFile());
        $logs[API_LOGS_QFQ_KEY] = $this->readFileTail(Path::absoluteQfqLogFile());

        return $logs;
    }

    /**
     * Reads the last 50 lines of a given file using a backwards read strategy.
     *
     * @param string $file Absolute file path.
     * @return string Tail content or error message.
     */
    private function readFileTail($file): string {
        $lines = 50;
        if (!file_exists($file)) {
            return 'File does not Exists';
        }
        if (!is_readable($file)) {
            return 'File Exists but cant be Read';
        }
        $fp = fopen($file, 'rb');
        if (!$fp) {
            return 'File Exists but cant be Opened';
        }

        // go to end
        fseek($fp, 0, SEEK_END);
        $pos = ftell($fp);
        $data = '';
        $lineCount = 0;

        // read blocks from end until we have enough lines
        while ($pos > 0 && $lineCount <= $lines) {
            // how far we jump back
            $seek = min(4096, $pos);
            $pos -= $seek;
            fseek($fp, $pos);
            $chunk = fread($fp, $seek);
            $data = $chunk . $data;
            $lineCount = substr_count($data, "\n");
        }
        fclose($fp);

        // split and grab only the last $lines entries and rejoin all lines before returning
        $allLines = explode("\n", trim($data));
        return join("\n", array_slice($allLines, -$lines));
    }

    /**
     * Applies editable store data from the frontend to the session.
     *
     * @return string 'Success'
     * @throws \Exception If JSON is invalid or improperly structured.
     */
    private function applyStoreChanges(): string {
        $rawBody = file_get_contents('php://input');
        $requestBody = json_decode($rawBody, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \Exception('Invalid JSON in request body: ' . json_last_error_msg());
        }

        foreach ($this->storeNameMap as $key => $name) {
            if (isset($requestBody[$name]) && !$requestBody[$name][API_STORE_READONLY_FLAG_KEY]) {
                Store::saveInSession($key, $requestBody[$name][API_STORE_DATA_KEY]);
            }
        }
        Session::set(DEV_PANEL_SESSION_FLAG_CHANGES, true);

        return 'Success';
    }
}
