<?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 $defaultTimingData = [
        DOWNLOAD_MODE_PDF => ["samples" => []],
        DOWNLOAD_MODE_ZIP => ["samples" => []],
        DOWNLOAD_MODE_EXCEL => ["samples" => []],
        DOWNLOAD_MODE_TEXT => ["samples" => []],
    ];

    /**
     * @param string $jobId Unique job identifier shared between JS and PHP
     * @throws \UserFormException
     */
    public function __construct($jobId) {
        $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
     * @throws \Exception
     */
    public function updateProgressJson($status, $currentFiles) {
        $updateProgressData = file_get_contents($this->progressDataPath);
        if ($updateProgressData === false) {
            throw new \DownloadException("Progress data file does not exist");
        }
        $updateProgressData = json_decode($updateProgressData, 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 . '.tmp';
            file_put_contents($tmp, json_encode(self::$defaultTimingData));
            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::$defaultTimingData;
        }

        // 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]['samples']) > 4) {
                    array_shift($timingData[DOWNLOAD_MODE_PDF]['samples']);
                }
                $samples = $timingData[DOWNLOAD_MODE_PDF]['samples'];
                $samples[] = $value;
                $timingData[DOWNLOAD_MODE_PDF]['samples'] = $samples;
                break;
            case DOWNLOAD_MODE_ZIP:
                if (count($timingData[DOWNLOAD_MODE_ZIP]['samples']) > 4) {
                    array_shift($timingData[DOWNLOAD_MODE_ZIP]['samples']);
                }
                $samples = $timingData[DOWNLOAD_MODE_ZIP]['samples'];
                $samples[] = $value;
                $timingData[DOWNLOAD_MODE_ZIP]['samples'] = $samples;
                break;
            case DOWNLOAD_MODE_EXCEL:
                if (count($timingData[DOWNLOAD_MODE_EXCEL]['samples']) > 4) {
                    array_shift($timingData[DOWNLOAD_MODE_EXCEL]['samples']);
                }
                $samples = $timingData[DOWNLOAD_MODE_EXCEL]['samples'];
                $samples[] = $value;
                $timingData[DOWNLOAD_MODE_EXCEL]['samples'] = $samples;
                break;
            case DOWNLOAD_MODE_TEXT:
                if (count($timingData[DOWNLOAD_MODE_TEXT]['samples']) > 4) {
                    array_shift($timingData[DOWNLOAD_MODE_TEXT]['samples']);
                }
                $samples = $timingData[DOWNLOAD_MODE_TEXT]['samples'];
                $samples[] = $value;
                $timingData[DOWNLOAD_MODE_TEXT]['samples'] = $samples;
                break;
        }
        // Atomic write: write to temp file first, then rename to avoid empty file on crash
        $tmp = $this->timingDataPath . '.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);
        $timingPath = $timingDataDir . 'timing.json';

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

        // Normalize alias modes to their canonical key used in the timing file (they can handle with the same code)
        switch ($downloadMode) {
            case DOWNLOAD_MODE_QFQPDF:
                $downloadMode = DOWNLOAD_MODE_PDF;
                break;
        }
        $timingFile = file_get_contents($timingPath);
        $timingFile = json_decode($timingFile, true);

        $samples = $timingFile[$downloadMode]['samples'];
        $result = 0;
        $sumWeights = 0;
        $count = count($samples);
        // No samples yet — use hardcoded defaults as initial estimate (in seconds)
        if ($count === 0) {
            switch ($downloadMode) {
                case DOWNLOAD_MODE_PDF:
                    $result = 3;
                    break;
                case DOWNLOAD_MODE_ZIP:
                    $result = 5;
                    break;
                case DOWNLOAD_MODE_EXCEL:
                    $result = 7;
                    break;
                case DOWNLOAD_MODE_TEXT:
                    $result = 1;
                    break;
                default:
                    $result = 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];
    }
}