<?php

namespace IMATHUZH\Qfq\Core\Helper;

/**
 * Manages per-job progress tracking and timing estimation for modal downloads.
 * Progress state is persisted as JSON files on disk, keyed by jobId.
 */
class DownloadProgress {
    private string $jobId;
    private string $progressDataPath;
    private string $timingDataPath;
    private string $progressDataDir;

    // Default timing structure for all download modes — populated with measured samples over time
    private static array $emptyTimingDataArray = [
        DOWNLOAD_MODE_PDF => [DOWNLOAD_MODAL_TIMING_SAMPLES => []],
        DOWNLOAD_MODE_ZIP => [DOWNLOAD_MODAL_TIMING_SAMPLES => []],
        DOWNLOAD_MODE_EXCEL => [DOWNLOAD_MODAL_TIMING_SAMPLES => []],
        DOWNLOAD_MODE_TEXT => [DOWNLOAD_MODAL_TIMING_SAMPLES => []],
    ];

    private static array $defaultTimingDataArray = [
        DOWNLOAD_MODE_PDF => [DOWNLOAD_MODAL_TIMING_SAMPLES => [3]],
        DOWNLOAD_MODE_ZIP => [DOWNLOAD_MODAL_TIMING_SAMPLES => [5]],
        DOWNLOAD_MODE_EXCEL => [DOWNLOAD_MODAL_TIMING_SAMPLES => [7]],
        DOWNLOAD_MODE_TEXT => [DOWNLOAD_MODAL_TIMING_SAMPLES => [1]],
    ];

    /**
     * @param string $jobId Unique job identifier shared between JS and PHP
     * @throws \DownloadException
     * @throws \UserFormException
     */
    public function __construct($jobId) {
        //The ‘isValidFileName()’ helper class has been misused to prevent users from leaving a directory.
        if (!HelperFile::isValidFileName((string)$jobId)) {
            throw new \DownloadException(json_encode([
                ERROR_MESSAGE_TO_USER => 'Download failed',
                ERROR_MESSAGE_TO_DEVELOPER => "Invalid jobId '$jobId'. Name may only consist of alphanumeric characters and _ . -"]),
                ERROR_INVALID_OR_MISSING_PARAMETER);
        }
        $this->jobId = $jobId;
        $this->progressDataDir = Path::absoluteApp(DOWNLOAD_MODAL_PROGRESS_FILE_PATH);
        $this->progressDataPath = $this->progressDataDir . $jobId . '.json';
        $this->timingDataPath = $this->progressDataDir . 'timing.json';
    }

    /**
     * Creates the progress JSON file for a new download job.
     * Called once at the start of file generation before any processing begins.
     *
     * @param int $totalFiles Total number of files to process
     * @param string $downloadMode The download mode (pdf, zip, excel, etc.)
     * @return void
     */
    public function writeProgressJson($totalFiles, $downloadMode) {
        $progressData = [
            DOWNLOAD_MODAL_GET_JOBID => $this->jobId,
            DOWNLOAD_MODAL_GET_STATUS => DOWNLOAD_MODAL_STATUS_PREPARING,
            DOWNLOAD_MODAL_STATUS_CURRENT_FILES => 0,
            DOWNLOAD_MODAL_STATUS_TOTAL_FILES => $totalFiles,
            DOWNLOAD_MODAL_GET_MODE => $downloadMode,
        ];

        if (!is_dir($this->progressDataDir)) {
            mkdir($this->progressDataDir, 0755, true);
        }

        // Delete stale progress files older than 24 hours (skipping timing.json)
        $this->deleteStaleProgressFiles();

        file_put_contents($this->progressDataPath, json_encode($progressData));
    }

    /**
     * Deletes progress JSON files older than 24 hours from the progress data directory.
     * The shared timing.json file is excluded from deletion.
     *
     * @return void
     */
    private function deleteStaleProgressFiles() {
        $cutoff = time() - 86400;
        foreach (glob($this->progressDataDir . 'qfq*.json') as $file) {
            if (filemtime($file) < $cutoff) {
                unlink($file);
            }
        }
    }

    /**
     * Updates the progress JSON file with the current processing state.
     * Called repeatedly during file generation to track progress.
     *
     * @param string $status Current status (e.g. preparing, processing, complete)
     * @param int $currentFiles Number of files processed so far
     * @return false|int
     */
    public function updateProgressJson($status, $currentFiles) {
        // Progress tracking is cosmetic: if the progress file is gone (cleaned up or removed
        // externally), skip the update instead of aborting the whole download. The file_exists
        // check also avoids a file_get_contents() warning on a missing file.
        if (!file_exists($this->progressDataPath)) {
            return false;
        }
        $updateProgressData = json_decode(file_get_contents($this->progressDataPath), true);
        $updateProgressData[DOWNLOAD_MODAL_GET_STATUS] = $status;
        $updateProgressData[DOWNLOAD_MODAL_STATUS_CURRENT_FILES] = $currentFiles;

        return file_put_contents($this->progressDataPath, json_encode($updateProgressData));
    }

    /**
     * @return false|mixed
     */
    public function downloadProgressStatus() {
        if (!file_exists($this->progressDataPath)) {
            return false;
        }
        return json_decode(file_get_contents($this->progressDataPath), true);
    }

    /**
     * @return string
     */
    public function getJobId(): string {
        return $this->jobId;
    }

    /**
     * Deletes the progress JSON file for this job.
     * Returns diagnostic info about whether the file was found and successfully deleted.
     *
     * @return array{fileFound: bool, fileDeleted: bool}
     */
    public function deleteProgressFile() {
        $data['fileFound'] = file_exists($this->progressDataPath);
        $data['fileDeleted'] = $data['fileFound'] ? unlink($this->progressDataPath) : false;

        return $data;
    }

    /**
     * @param $value
     * @param $downloadMode
     * @return void
     */
    public function writeTimingJson($value, $downloadMode) {
        if (!is_dir($this->progressDataDir)) {
            mkdir($this->progressDataDir, 0755, true);
        }

        if (!file_exists($this->timingDataPath)) {
            $tmp = $this->timingDataPath . '.' . uniqid('', true) . '.tmp';
            file_put_contents($tmp, json_encode(self::$emptyTimingDataArray));
            rename($tmp, $this->timingDataPath);
        }
        $timingData = file_get_contents($this->timingDataPath);
        $timingData = json_decode($timingData, true);

        // Fallback: file exists but is empty or contains invalid JSON
        if (empty($timingData)) {
            $timingData = self::$emptyTimingDataArray;
        }

        // Write the timings to the progress file depending on the download mode.
        switch ($downloadMode) {
            case DOWNLOAD_MODE_QFQPDF:
            case DOWNLOAD_MODE_PDF:
                if (count($timingData[DOWNLOAD_MODE_PDF][DOWNLOAD_MODAL_TIMING_SAMPLES]) > 4) {
                    array_shift($timingData[DOWNLOAD_MODE_PDF][DOWNLOAD_MODAL_TIMING_SAMPLES]);
                }
                $samples = $timingData[DOWNLOAD_MODE_PDF][DOWNLOAD_MODAL_TIMING_SAMPLES];
                $samples[] = $value;
                $timingData[DOWNLOAD_MODE_PDF][DOWNLOAD_MODAL_TIMING_SAMPLES] = $samples;
                break;
            case DOWNLOAD_MODE_ZIP:
                if (count($timingData[DOWNLOAD_MODE_ZIP][DOWNLOAD_MODAL_TIMING_SAMPLES]) > 4) {
                    array_shift($timingData[DOWNLOAD_MODE_ZIP][DOWNLOAD_MODAL_TIMING_SAMPLES]);
                }
                $samples = $timingData[DOWNLOAD_MODE_ZIP][DOWNLOAD_MODAL_TIMING_SAMPLES];
                $samples[] = $value;
                $timingData[DOWNLOAD_MODE_ZIP][DOWNLOAD_MODAL_TIMING_SAMPLES] = $samples;
                break;
            case DOWNLOAD_MODE_EXCEL:
                if (count($timingData[DOWNLOAD_MODE_EXCEL][DOWNLOAD_MODAL_TIMING_SAMPLES]) > 4) {
                    array_shift($timingData[DOWNLOAD_MODE_EXCEL][DOWNLOAD_MODAL_TIMING_SAMPLES]);
                }
                $samples = $timingData[DOWNLOAD_MODE_EXCEL][DOWNLOAD_MODAL_TIMING_SAMPLES];
                $samples[] = $value;
                $timingData[DOWNLOAD_MODE_EXCEL][DOWNLOAD_MODAL_TIMING_SAMPLES] = $samples;
                break;
            case DOWNLOAD_MODE_TEXT:
                if (count($timingData[DOWNLOAD_MODE_TEXT][DOWNLOAD_MODAL_TIMING_SAMPLES]) > 4) {
                    array_shift($timingData[DOWNLOAD_MODE_TEXT][DOWNLOAD_MODAL_TIMING_SAMPLES]);
                }
                $samples = $timingData[DOWNLOAD_MODE_TEXT][DOWNLOAD_MODAL_TIMING_SAMPLES];
                $samples[] = $value;
                $timingData[DOWNLOAD_MODE_TEXT][DOWNLOAD_MODAL_TIMING_SAMPLES] = $samples;
                break;
        }
        // Atomic write: write to temp file first, then rename to avoid empty file on crash
        $tmp = $this->timingDataPath . '.' . uniqid('', true) . '.tmp';
        file_put_contents($tmp, json_encode($timingData));
        rename($tmp, $this->timingDataPath);
    }

    /**
     * @param $downloadMode
     * @return array
     * @throws \UserFormException
     */
    public static function readTimingJson($downloadMode) {
        $timingDataDir = Path::absoluteApp(DOWNLOAD_MODAL_PROGRESS_FILE_PATH);
        $timingPathFile = $timingDataDir . 'timing.json';

        // Normalize alias modes to their canonical key used in the timing file (they can handle with the same code)
        if ($downloadMode === DOWNLOAD_MODE_QFQPDF) {
            $downloadMode = DOWNLOAD_MODE_PDF;
        }

        // Initialize with default structure if timing file is missing or contains invalid JSON
        $timingData = file_exists($timingPathFile) ? json_decode(file_get_contents($timingPathFile), true) : null;
        if (empty($timingData)) {
            if (!is_dir($timingDataDir)) {
                mkdir($timingDataDir, 0755, true);
            }
            $timingData = self::$emptyTimingDataArray;
            $tmp = $timingPathFile . '.' . uniqid('', true) . '.tmp';
            file_put_contents($tmp, json_encode($timingData));
            rename($tmp, $timingPathFile);
        }

        $samples = $timingData[$downloadMode][DOWNLOAD_MODAL_TIMING_SAMPLES] ?? [
        ];
        $result = 0;
        $sumWeights = 0;
        $count = count($samples);
        // No samples yet — use hardcoded defaults as initial estimate (in seconds)
        if ($count === 0) {
            $result = self::$defaultTimingDataArray[$downloadMode][DOWNLOAD_MODAL_TIMING_SAMPLES][0] ?? 3;
        } else {
            // Calculate weighted average — newer samples (higher index) get higher weights
            $weights = range(1, $count);
            foreach ($samples as $i => $sample) {
                $result += $sample * $weights[$i];
                $sumWeights += $weights[$i];
            }
            $result = $result / $sumWeights;
        }

        return [DOWNLOAD_MODAL_GET_TIMING => $result, DOWNLOAD_MODAL_GET_MODE => $downloadMode];
    }
}