<?php

namespace IMATHUZH\Qfq\Core\Report;

use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Evaluate;
use IMATHUZH\Qfq\Core\Exception\Thrower;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Typo3\T3Handler;
use Symfony\Component\VarDumper\Cloner\Data;

class ReportAsFile {
    /**
     * Finds the keyword file=<pathFileName> and returns pathFileName of report.
     * If the given path starts with an underscore and a system report with the given path exists, then the path of the system file is returned instead.
     * Returns null if the keyword is not present.
     *
     * @param string $bodyText
     * @return string|null pathFileName of report relative to CWD or null
     * @throws \UserFormException
     */
    public static function parseFileKeyword(string $bodyText) // : ?string
    {
        if (preg_match('/^\s*' . TOKEN_REPORT_FILE . '\s*=\s*([^\s]*)/m', $bodyText, $matches)) {
            $providedPathFileName = $matches[1];

            // Check for sip variable 'file' if exists
            $db = new Database();
            $store = Store::getInstance();
            $eval = new Evaluate($store, $db);

            // strip curly braces and get variable name
            $providedPathFileName = str_replace('{{', '', $providedPathFileName);
            $providedPathFileName = str_replace('}}', '', $providedPathFileName);
            $providedPathFileName = trim($providedPathFileName, "'");
            $key = explode(':', $providedPathFileName, 2)[0] ?? '';

            if ($key === 'file') {
                // set variable for enabled switch button in formEditor
                $store::setVar(EXTRA_ENABLE_SWITCH, 'yes', STORE_EXTRA);
                $providedPathFileName = $eval->substitute($providedPathFileName);
            } else {
                $store::unsetVar(EXTRA_ENABLE_SWITCH, STORE_EXTRA);
            }

            // Set page alias or slug variable for searchRefactor.qfqr based on actual used T3 version
            if (T3Handler::useSlugsInsteadOfPageAlias()) {
                $store::setVar(EXTRA_COLUMN_NAME_AlIAS_SLUG, 'slug', STORE_EXTRA);
            } else {
                $store::setVar(EXTRA_COLUMN_NAME_AlIAS_SLUG, 'alias', STORE_EXTRA);
            }

            if ($providedPathFileName === '') {
                return null;
            }

            if (isset($providedPathFileName[0]) && $providedPathFileName[0] === '_') {

                $pathFileNameSystem = Path::absoluteExt(Path::EXT_TO_REPORT_SYSTEM, substr($providedPathFileName, 1) . REPORT_FILE_EXTENSION);
                if (HelperFile::isReadableException($pathFileNameSystem)) {
                    return $pathFileNameSystem;
                }
            }
            return Path::join(self::reportPath(), $providedPathFileName);
        } else {
            return null;
        }
    }

    /**
     * Read report file from given pathFileName. Throw exception on failure.
     *
     * @param string $reportPathFileNameFull
     * @return string|null
     * @throws \UserReportException
     */
    public static function read_report_file(string $reportPathFileNameFull): string {
        $fileContents = file_get_contents($reportPathFileNameFull);
        if ($fileContents === false) {
            throw new \UserReportException(json_encode([
                ERROR_MESSAGE_TO_USER => "File read error.",
                ERROR_MESSAGE_TO_DEVELOPER => "Report file not found or no permission to read file: '$reportPathFileNameFull'"]),
                ERROR_FORM_NOT_FOUND);
        }
        return $fileContents;
    }

    /**
     * Create new file named after the tt_content header or the UID if not defined.
     * - Invalid characters are stripped from filename.
     * - If file exists, throw exception.
     * - The path of the file is derived from the typo3 page structure (use page alias if exists, else page name).
     * - The path is created if not exists.
     *
     * @param int $uid
     * @param Database $databaseT3
     * @return string pathFileName of report relative to CWD
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function create_file_from_ttContent(int $uid, Database $databaseT3): string {
        // Select tt_content
        $dbT3 = Store::getInstance()->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
        $sql = "SELECT `bodytext`, `header`, `pid` FROM `$dbT3`.`tt_content` WHERE `uid` = ?";
        $ttContent = $databaseT3->sql($sql, ROW_EXPECT_1,
            [$uid], "Typo3 content element not found. Uid: $uid");

        // read page path of tt-content element (use page alias if exists)
        $reportPath = '';
        $pid = $ttContent[COLUMN_PID];
        while (intval($pid) !== 0) {
            $sql = "SELECT `pid`, `alias`, `title` FROM `$dbT3`.`pages` WHERE `uid` = ?";
            $page = $databaseT3->sql($sql, ROW_EXPECT_1,
                [$pid], "Typo3 page not found. Uid: $pid");
            $supDir = $page[COLUMN_ALIAS] !== '' ? $page['alias'] : $page['title'];
            $reportPath = HelperFile::joinPathFilename(Sanitize::safeFilename($supDir), $reportPath);
            $pid = $page[COLUMN_PID];
            if (strlen($reportPath) > 4096) {
                // Throw exception in case of infinite loop.
                throw new \UserReportException(json_encode([
                    ERROR_MESSAGE_TO_USER => "Exporting report to file failed.",
                    ERROR_MESSAGE_TO_DEVELOPER => "Report path too long: '$reportPath'"]),
                    ERROR_FORM_NOT_FOUND);
            }
        }
        $reportPathFull = HelperFile::joinPathFilename(self::reportPath(), $reportPath);

        // create path in filesystem if not exists
        HelperFile::createPathRecursive($reportPathFull);

        // add filename
        $fileName = Sanitize::safeFilename($ttContent[COLUMN_HEADER]);
        if (empty($fileName)) {
            $fileName = strval($uid);
        }
        $reportPathFileNameFull = HelperFile::joinPathFilename($reportPathFull, $fileName) . REPORT_FILE_EXTENSION;

        // if file exists, throw exception
        if (file_exists($reportPathFileNameFull)) {
            throw new \UserReportException(json_encode([
                ERROR_MESSAGE_TO_USER => "Exporting report to file failed.",
                ERROR_MESSAGE_TO_DEVELOPER => "File already exists: '$reportPathFileNameFull'"]),
                ERROR_FORM_NOT_FOUND);
        }

        // create file with content bodytext
        HelperFile::file_put_contents($reportPathFileNameFull, $ttContent[COLUMN_BODYTEXT]);

        // replace tt-content bodytext with file=<path>
        $newBodytext = TOKEN_REPORT_FILE . "=" . HelperFile::joinPathFilename($reportPath, $fileName) . REPORT_FILE_EXTENSION;
        self::write_tt_content($uid, $databaseT3, $newBodytext);

        return $reportPathFileNameFull;
    }

    /**
     * Replace bodytext of given tt-content element with the given string.
     *
     * @param int $uid
     * @param string $newBodytext
     * @param Database $databaseT3
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function write_tt_content(int $uid, Database $databaseT3, ?string $newBodytext = null, string $newHeader = null, string $newSubheader = null) {
        $dbT3 = Store::getInstance()->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
        if ($newBodytext === null) {
            $sql = "UPDATE `$dbT3`.`tt_content` SET `header` = ?, `subheader` = ?, `tstamp` = UNIX_TIMESTAMP(NOW()) WHERE `uid` = ?";
            $databaseT3->sql($sql, ROW_REGULAR, [$newHeader, $newSubheader, $uid]);
        } else if ($newHeader === null || $newSubheader === null) {
            $sql = "UPDATE `$dbT3`.`tt_content` SET `bodytext` = ?, `tstamp` = UNIX_TIMESTAMP(NOW()) WHERE `uid` = ?";
            $databaseT3->sql($sql, ROW_REGULAR, [$newBodytext, $uid]);
        } else {
            $sql = "UPDATE `$dbT3`.`tt_content` SET `bodytext` = ?, `header` = ?, `subheader` = ?,  `tstamp` = UNIX_TIMESTAMP(NOW()) WHERE `uid` = ?";
            $databaseT3->sql($sql, ROW_REGULAR, [$newBodytext, $newHeader, $newSubheader, $uid]);
        }

        // Clear cache because it is used to restore page-specific cache
        $table = T3Handler::typo3VersionGreaterEqual10() ? 'cache_pages' : 'cf_cache_pages';
        $databaseT3->sql("TRUNCATE `$dbT3`.`$table`");
    }

    /**
     * Overwrites report file given by the 'file=<path>' keyword of the tt_content element with the given uid.
     *
     * @param int $uid
     * @param string $newContent
     * @param Database $databaseT3
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function write_file_uid(int $uid, string $newContent, Database $databaseT3, string $header = '', string $subheader = '') // : void
    {
        // Get the report file path from tt_content
        $dbT3 = Store::getInstance()->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
        $sql = "SELECT `bodytext` FROM `$dbT3`.`tt_content` WHERE `uid` = ?";
        $ttContent = $databaseT3->sql($sql, ROW_EXPECT_1,
            [$uid], "Typo3 content element not found. Uid: $uid");

        $absoluteReportFilePath = self::parseFileKeyword($ttContent[COLUMN_BODYTEXT]);
        if ($absoluteReportFilePath === null) {
            throw new \UserReportException(json_encode([
                ERROR_MESSAGE_TO_USER => "No report file defined.",
                ERROR_MESSAGE_TO_DEVELOPER => "The keyword '" . TOKEN_REPORT_FILE . "' is not present in the typo3 content element with id $uid"]),
                ERROR_FORM_NOT_FOUND);
        }

        // Independently of report file, save header and subheader changes from tt_content
        ReportAsFile::write_tt_content($uid, $databaseT3, null, $header, $subheader);
        // Update report file
        self::backupReportFile($absoluteReportFilePath);
        HelperFile::file_put_contents($absoluteReportFilePath, $newContent);
    }

    /**
     * Create copy of given report file in _backup directory. If given file does not exist, do nothing.
     * New file name: <reportFileName>.YYYMMDDhhmmss.json
     *
     * @param string $absoluteReportFilePath
     * @throws \UserFormException
     */
    private static function backupReportFile(string $absoluteReportFilePath) {
        if (file_exists($absoluteReportFilePath)) {
            if (!is_readable($absoluteReportFilePath)) {
                Thrower::userFormException('Error while trying to backup report file.', "Report file is not readable: $absoluteReportFilePath");
            }

            // copy file
            $absoluteBackupFilePath = self::newBackupPathFileName($absoluteReportFilePath);
            $success = copy($absoluteReportFilePath, $absoluteBackupFilePath);
            if ($success === false) {
                Thrower::userFormException('Error while trying to backup report file.', "Can't copy file $absoluteReportFilePath to $absoluteBackupFilePath");
            }
        }
    }

    /**
     * Return the path to a (non-existing) report backup file with name:
     * <reportFileName>.YYYMMDDhhmmss.<tag>.qfqr
     *
     * @param string $absoluteReportFilePath
     * @return string
     * @throws \UserFormException
     */
    private static function newBackupPathFileName(string $absoluteReportFilePath): string {
        // create backup path if not exists
        $absoluteBackupPath = Path::join(dirname($absoluteReportFilePath), Path::REPORT_FILE_TO_BACKUP);
        if (!is_dir($absoluteBackupPath)) {
            $success = mkdir($absoluteBackupPath, 0777, true);
            if ($success === false) {
                Thrower::userFormException('Error while trying to backup report file.', "Can't create backup path: $absoluteBackupPath");
            }
        }

        $absoluteBackupFilePath = Path::join($absoluteBackupPath, basename($absoluteReportFilePath, REPORT_FILE_EXTENSION) . REPORT_FILE_EXTENSION . '.' . date('Y-m-d_H-i-s'));

        // add index to filename if backup file with current timestamp already exists
        $index = 1;
        while (file_exists($absoluteBackupFilePath)) {
            $absoluteBackupFilePath = Path::join($absoluteBackupPath, basename($absoluteReportFilePath, REPORT_FILE_EXTENSION) . REPORT_FILE_EXTENSION . '.' . date('Y-m-d_H-i-s') . ".$index");
            $index++;
            if ($index > 20) {
                Thrower::userFormException('Error while trying to backup report file.', 'Infinite loop.');
            }
        }

        return $absoluteBackupFilePath;
    }

    /**
     * Return the path of the report directory relative to CWD.
     * Create path if it doesn't exist.
     *
     * @return string
     * @throws \UserFormException
     */
    private static function reportPath(): string {
        $absoluteReportPath = Path::absoluteProject(Path::projectToReport());
        HelperFile::createPathRecursive($absoluteReportPath);
        return $absoluteReportPath;
    }
}