<?php

declare(strict_types=1);

namespace App\Services;

use RuntimeException;
use Throwable;

class QrCodeService
{
    private string $libraryFile;

    public function __construct()
    {
        $this->libraryFile = QRCODE_PATH . '/qrlib.php';

        if (!is_file($this->libraryFile)) {
            throw new RuntimeException(
                'PHP QR Code library was not found at: '
                . $this->libraryFile
            );
        }

        /*
         * Load the legacy library while suppressing only
         * PHP 8.5 deprecation notices.
         */
        $this->runWithoutDeprecationOutput(
            function (): void {
                require_once $this->libraryFile;
            }
        );

        if (!class_exists('QRcode')) {
            throw new RuntimeException(
                'The QRcode class could not be loaded.'
            );
        }
    }

    /**
     * Generate a PNG QR code.
     *
     * Returns a path relative to public/uploads.
     */
    public function generate(
        string $content,
        string $fileName,
        string $errorCorrection = 'H',
        int $size = 8,
        int $margin = 2
    ): string {
        if (trim($content) === '') {
            throw new RuntimeException(
                'QR-code content cannot be empty.'
            );
        }

        $directory = PUBLIC_PATH . '/uploads/qr';

        $this->ensureDirectoryExists($directory);

        $safeFileName = $this->sanitiseFileName(
            $fileName
        );

        $absolutePath = $directory
            . '/'
            . $safeFileName
            . '.png';

        $errorCorrectionLevel =
            $this->resolveErrorCorrection(
                $errorCorrection
            );

        try {
            /*
             * PHP QR Code calls imagedestroy(), which produces
             * deprecation output in PHP 8.5.
             *
             * We suppress only deprecation messages during this call.
             * Other warnings and errors remain visible/logged.
             */
            $this->runWithoutDeprecationOutput(
                function () use (
                    $content,
                    $absolutePath,
                    $errorCorrectionLevel,
                    $size,
                    $margin
                ): void {
                    \QRcode::png(
                        $content,
                        $absolutePath,
                        $errorCorrectionLevel,
                        max(1, min($size, 20)),
                        max(0, min($margin, 10))
                    );
                }
            );
        } catch (Throwable $exception) {
            throw new RuntimeException(
                'QR-code generation failed: '
                . $exception->getMessage(),
                0,
                $exception
            );
        }

        clearstatcache(true, $absolutePath);

        if (
            !is_file($absolutePath) ||
            filesize($absolutePath) === 0
        ) {
            throw new RuntimeException(
                'The QR-code image was not created.'
            );
        }

        return 'qr/' . basename($absolutePath);
    }

    /**
     * Generate a QR code for conference check-in.
     */
    public function generateDelegateQr(
        string $qrToken,
        string $registrationNumber
    ): string {
        if (trim($qrToken) === '') {
            throw new RuntimeException(
                'The delegate QR token is required.'
            );
        }

        $checkInUrl = base_url(
            '/check-in/verify/'
            . rawurlencode($qrToken)
        );

        return $this->generate(
            $checkInUrl,
            $registrationNumber,
            'H',
            8,
            2
        );
    }

    /**
     * Delete a generated QR code.
     */
    public function delete(
        ?string $relativePath
    ): bool {
        if (!$relativePath) {
            return false;
        }

        $relativePath = ltrim(
            str_replace(
                [
                    '../',
                    '..\\',
                ],
                '',
                $relativePath
            ),
            '/'
        );

        $absolutePath = PUBLIC_PATH
            . '/uploads/'
            . $relativePath;

        if (!is_file($absolutePath)) {
            return false;
        }

        return unlink($absolutePath);
    }

    /**
     * Return the absolute server path.
     */
    public function absolutePath(
        string $relativePath
    ): string {
        return PUBLIC_PATH
            . '/uploads/'
            . ltrim($relativePath, '/');
    }

    /**
     * Return the public browser URL.
     */
    public function publicUrl(
        string $relativePath
    ): string {
        return upload_url($relativePath);
    }

    /**
     * Run legacy library code without allowing PHP deprecation
     * messages to be written into the HTTP response.
     */
    private function runWithoutDeprecationOutput(
        callable $callback
    ): mixed {
        $previousErrorReporting = error_reporting();

        $initialBufferLevel = ob_get_level();

        /*
         * Start a temporary output buffer so that any unexpected
         * legacy output cannot break redirects or response headers.
         */
        ob_start();

        /*
         * Intercept only deprecation notices.
         * Return false for all other errors so normal PHP handling
         * remains in place.
         */
        $previousErrorHandler = set_error_handler(
            static function (
                int $severity,
                string $message,
                string $file,
                int $line
            ): bool {
                if (
                    $severity === E_DEPRECATED ||
                    $severity === E_USER_DEPRECATED
                ) {
                    error_log(
                        sprintf(
                            'Suppressed legacy QR deprecation: %s in %s:%d',
                            $message,
                            $file,
                            $line
                        )
                    );

                    return true;
                }

                return false;
            }
        );

        error_reporting(
            $previousErrorReporting
            & ~E_DEPRECATED
            & ~E_USER_DEPRECATED
        );

        try {
            return $callback();
        } finally {
            error_reporting(
                $previousErrorReporting
            );

            restore_error_handler();

            /*
             * Discard only the temporary buffers created here.
             */
            while (ob_get_level() > $initialBufferLevel) {
                ob_end_clean();
            }
        }
    }

    private function ensureDirectoryExists(
        string $directory
    ): void {
        if (is_dir($directory)) {
            return;
        }

        if (
            !mkdir(
                $directory,
                0775,
                true
            ) &&
            !is_dir($directory)
        ) {
            throw new RuntimeException(
                'Unable to create QR upload directory: '
                . $directory
            );
        }
    }

    private function sanitiseFileName(
        string $fileName
    ): string {
        $fileName = pathinfo(
            $fileName,
            PATHINFO_FILENAME
        );

        $safeFileName = preg_replace(
            '/[^A-Za-z0-9\-_]/',
            '-',
            $fileName
        );

        $safeFileName = preg_replace(
            '/-+/',
            '-',
            (string) $safeFileName
        );

        $safeFileName = trim(
            (string) $safeFileName,
            '-_'
        );

        if ($safeFileName === '') {
            $safeFileName = 'qr-'
                . bin2hex(
                    random_bytes(12)
                );
        }

        return $safeFileName;
    }

    private function resolveErrorCorrection(
        string $level
    ): int {
        return match (strtoupper($level)) {
            'L' => QR_ECLEVEL_L,
            'M' => QR_ECLEVEL_M,
            'Q' => QR_ECLEVEL_Q,
            default => QR_ECLEVEL_H,
        };
    }
}