<?php

namespace Modules\FontControl\Services;

use App\Helper\Files;
use App\Models\Table;
use App\Models\Branch;
use App\Models\FileStorage;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Color\Color;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\Label\LabelAlignment;
use Endroid\QrCode\Label\Margin\Margin;
use Endroid\QrCode\RoundBlockSizeMode;
use Endroid\QrCode\Writer\PngWriter;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File as FileFacade;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Symfony\Component\HttpFoundation\File\File;

/**
 * Regenerates table QR codes using locally downloaded fonts and configurable styles.
 */
class TableQrRegenerator
{
    // Cache key for settings hash - regenerate only when settings change
    private const CACHE_KEY = 'fontcontrol.qr_settings_hash';
    private const CACHE_TTL_MINUTES = 1440; // 24 hours
    private const THROTTLE_KEY = 'fontcontrol.qr_regenerated.throttle';
    private const LOCK_FILE = 'fontcontrol_qr_regeneration.lock';

    public static function runOnce(): void
    {
        // Compute hash of current QR settings
        $currentHash = self::computeSettingsHash();
        $cachedHash = self::safeCacheGet(self::CACHE_KEY);

        // Skip regeneration if settings haven't changed
        if ($cachedHash === $currentHash) {
            return;
        }

        // Acquire exclusive lock to prevent race condition
        $lockFile = storage_path('framework/' . self::LOCK_FILE);
        $lockHandle = @fopen($lockFile, 'c');
        if (!$lockHandle) {
            return; // Can't get lock file, skip
        }

        // Non-blocking lock - if another process has it, just return
        if (!flock($lockHandle, LOCK_EX | LOCK_NB)) {
            fclose($lockHandle);
            return; // Another process is already regenerating
        }

        try {
            // Double-check hash after acquiring lock (another process may have finished)
            $cachedHashAfterLock = self::safeCacheGet(self::CACHE_KEY);
            if ($cachedHashAfterLock === $currentHash) {
                return; // Already regenerated by another process
            }

            // Write cache FIRST to prevent other processes from starting
            self::safeCachePut(self::CACHE_KEY, $currentHash, self::CACHE_TTL_MINUTES);

            // Now regenerate
            self::regenerateAll();
            self::regenerateBranches();
        } finally {
            flock($lockHandle, LOCK_UN);
            fclose($lockHandle);
        }
    }

    public static function forceRun(): void
    {
        // Acquire exclusive lock
        $lockFile = storage_path('framework/' . self::LOCK_FILE);
        $lockHandle = @fopen($lockFile, 'c');
        if (!$lockHandle) {
            return;
        }

        // Wait for lock (blocking) since this is an explicit force run
        if (!flock($lockHandle, LOCK_EX)) {
            fclose($lockHandle);
            return;
        }

        try {
            Cache::forget(self::CACHE_KEY);
            $currentHash = self::computeSettingsHash();
            self::safeCachePut(self::CACHE_KEY, $currentHash, self::CACHE_TTL_MINUTES);
            
            self::regenerateAll();
            self::regenerateBranches();
        } finally {
            flock($lockHandle, LOCK_UN);
            fclose($lockHandle);
        }
    }

    /**
     * Force regeneration but throttle to at most once every $minutes.
     */
    public static function runThrottled(int $minutes = 1): void
    {
        $key = self::THROTTLE_KEY;
        if (self::safeCacheHas($key)) {
            return;
        }
        self::forceRun();
        self::safeCachePut($key, true, $minutes);
    }

    private static function regenerateAll(): void
    {
        $tables = Table::query()->get();

        foreach ($tables as $table) {
            try {
                self::buildQrForTable($table);
            } catch (\Throwable $e) {
                // Log but keep going so other tables regenerate
                \Log::warning('Table QR regenerate failed', [
                    'table_id' => $table->id ?? null,
                    'error' => $e->getMessage(),
                ]);
                continue;
            }
        }
    }

    /**
     * Regenerate branch QR codes so they pick up the same styling (colors/logo)
     * but without any label text.
     */
    private static function regenerateBranches(): void
    {
        $branches = Branch::query()->with('restaurant')->get();

        foreach ($branches as $branch) {
            try {
                // Ensure old assets/records are removed so the fresh QR replaces them
                $filename = $branch->getQrCodeFileName();
                FileStorage::where('filename', $filename)->delete();
                try {
                    Storage::disk(config('filesystems.default', 'public'))->delete('qrcodes/' . $filename);
                } catch (\Throwable $e) {
                    // ignore remote delete issues
                }
                @unlink(public_path(Files::UPLOAD_FOLDER . '/qrcodes/' . $filename));

                // Regenerate token to mirror core behavior (branch QR uses a fresh unique_hash each time)
                // Keep existing token; only generate if missing to remain consistent with core links
                if (empty($branch->unique_hash)) {
                    $branch->generateUniqueHash();
                    $branch->saveQuietly();
                    $branch->refresh();
                }

                // Build branch QR with current styling but no label text
                $binary = self::buildBranchQr($branch);

                $filePath = public_path(Files::UPLOAD_FOLDER . '/qrcodes/' . $filename);
                Files::createDirectoryIfNotExist('qrcodes');
                @unlink($filePath);
                file_put_contents($filePath, $binary);

                Files::fileStore(
                    new File($filePath),
                    'qrcodes',
                    $filename,
                    uploaded: false,
                    restaurantId: $branch->restaurant_id
                );

                if (config('filesystems.default') !== 'local') {
                    try {
                        Storage::disk(config('filesystems.default'))->delete('qrcodes/' . $filename);
                    } catch (\Throwable $e) {
                        // ignore
                    }
                    $contents = FileFacade::get($filePath);
                    Storage::disk(config('filesystems.default'))->put('qrcodes/' . $filename, $contents);
                }
            } catch (\Throwable $e) {
                \Log::warning('Branch QR regenerate failed', [
                    'branch_id' => $branch->id ?? null,
                    'error' => $e->getMessage(),
                ]);
                continue;
            }
        }
    }

    private static function buildQrForTable(Table $table): void
    {
        $url = self::tableUrl($table);
        $restaurantId = method_exists($table, 'getRestaurantId')
            ? $table->getRestaurantId()
            : ($table->restaurant_id ?? ($table->branch->restaurant_id ?? null));

        $config = QrStyleManager::get($restaurantId);
        $advanced = QrStyleManager::isAdvanced($config);

        $fileName = $table->getQrCodeFileName(); // keep same filename to avoid URL changes
        $filePath = public_path(Files::UPLOAD_FOLDER . '/qrcodes/' . $fileName);

        $result = $advanced
            ? self::buildAdvanced($url, $table, $config, $restaurantId)
            : self::buildBasic($url, $table, $config, $restaurantId);

        Files::createDirectoryIfNotExist('qrcodes');

        // ⚠️ Delete old file_storage entry BEFORE creating new one to prevent DB bloat
        FileStorage::where('filename', $fileName)->delete();

        // Remove old file copies so overwrite is guaranteed
        if (file_exists($filePath)) {
            @unlink($filePath);
        }
        $result->saveToFile($filePath);


        Files::fileStore(
            new File($filePath),
            'qrcodes',
            $fileName,
            uploaded: false,
            restaurantId: $table->getRestaurantId()
        );

        if (config('filesystems.default') !== 'local') {
            // remove remote copy before re-uploading
            try {
                Storage::disk(config('filesystems.default'))->delete('qrcodes/' . $fileName);
            } catch (\Throwable $e) {
                // ignore
            }
            $contents = FileFacade::get($filePath);
            Storage::disk(config('filesystems.default'))->put('qrcodes/' . $fileName, $contents);
            // keep local copy to avoid missing assets if remote is slow
        }

        // bump updated_at so URL cache buster changes
        $table->touch();
    }

    /**
     * Cache helpers that won't crash on permission errors.
     */
    private static function safeCacheHas(string $key): bool
    {
        try {
            return Cache::has($key);
        } catch (\Throwable $e) {
            return false;
        }
    }

    private static function safeCachePut(string $key, mixed $value, int $ttlMinutes): void
    {
        try {
            Cache::put($key, $value, now()->addMinutes($ttlMinutes));
        } catch (\Throwable $e) {
            // ignore cache write issues (e.g., filesystem permissions)
        }
    }

    private static function safeCacheGet(string $key): mixed
    {
        try {
            return Cache::get($key);
        } catch (\Throwable $e) {
            return null;
        }
    }

    /**
     * Compute a hash of QR settings to detect changes.
     * Regeneration only happens when this hash changes.
     */
    private static function computeSettingsHash(): string
    {
        try {
            $settings = \DB::table('font_control_qr_settings')->get()->toArray();
            return md5(json_encode($settings) . 'v1');
        } catch (\Throwable $e) {
            // If table doesn't exist, return empty hash to trigger regeneration
            return '';
        }
    }

    private static function buildBasic(string $url, Table $table, array $config, ?int $restaurantId)
    {
        $label = QrStyleManager::labelForTable($table, $config);
        $font = QrStyleManager::labelFont($restaurantId, $config);
        $fg = QrStyleManager::foregroundColor($config);
        $bg = QrStyleManager::backgroundColor($config);
        $labelColor = QrStyleManager::labelColor($config);

        $builder = Builder::create()
            ->writer(new PngWriter())
            ->writerOptions([])
            ->data($url)
            ->encoding(new Encoding('UTF-8'))
            ->errorCorrectionLevel(ErrorCorrectionLevel::High)
            ->size((int) ($config['qr_size'] ?? 300))
            ->margin((int) ($config['qr_margin'] ?? 10))
            ->foregroundColor(new Color($fg['r'], $fg['g'], $fg['b']))
            ->backgroundColor(new Color($bg['r'], $bg['g'], $bg['b']))
            ->roundBlockSizeMode(($config['qr_round_block_size'] ?? true) ? RoundBlockSizeMode::Margin : RoundBlockSizeMode::None)
            ->validateResult(false)
            ->labelText($label)
            ->labelFont($font)
            ->labelTextColor(new Color($labelColor['r'], $labelColor['g'], $labelColor['b']))
            ->labelAlignment(LabelAlignment::Center)
            ->labelMargin(new Margin(12, 12, 12, 12));

        return $builder->build();
    }

    private static function buildAdvanced(string $url, Table $table, array $config, ?int $restaurantId)
    {
        $label = QrStyleManager::labelForTable($table, $config);
        $font = QrStyleManager::labelFont($restaurantId, $config);
        $fg = QrStyleManager::foregroundColor($config);
        $bg = QrStyleManager::backgroundColor($config);
        $labelColor = QrStyleManager::labelColor($config);
        $ecc = QrStyleManager::ecc($config);
        $size = (int) ($config['qr_size'] ?? 300);
        $margin = (int) ($config['qr_margin'] ?? 10);

        // Build with Endroid and then optionally overlay logo via GD
        $builder = Builder::create()
            ->writer(new PngWriter())
            ->writerOptions([])
            ->data($url)
            ->encoding(new Encoding('UTF-8'))
            ->errorCorrectionLevel(self::mapEcc($ecc))
            ->size($size)
            ->margin($margin)
            ->foregroundColor(new Color($fg['r'], $fg['g'], $fg['b']))
            ->backgroundColor(new Color($bg['r'], $bg['g'], $bg['b']))
            ->roundBlockSizeMode(($config['qr_round_block_size'] ?? true) ? RoundBlockSizeMode::Margin : RoundBlockSizeMode::None)
            ->validateResult(false)
            ->labelText($label)
            ->labelFont($font)
            ->labelTextColor(new Color($labelColor['r'], $labelColor['g'], $labelColor['b']))
            ->labelAlignment(LabelAlignment::Center)
            ->labelMargin(new Margin(12, 12, 12, 12));

        $result = $builder->build();
        $binary = $result->getString();
        $logoPath = QrStyleManager::logoPath($config);
        $logoSize = QrStyleManager::logoSize($config, $size);

        if ($logoPath && extension_loaded('gd')) {
            $binary = self::mergeLogo($binary, $logoPath, $logoSize);
        } elseif ($logoPath && ! extension_loaded('gd')) {
            \Log::warning('QR logo skipped because GD is missing', ['logo' => $logoPath]);
        }

        return new class($binary) {
            public function __construct(private string $data) {}
            public function saveToFile(string $path): void
            {
                file_put_contents($path, $this->data);
            }
            public function getString(): string
            {
                return $this->data;
            }
        };
    }

    private static function mapEcc(string $level): ErrorCorrectionLevel
    {
        return match (strtoupper($level)) {
            'NONE', 'N' => ErrorCorrectionLevel::Low,
            'L' => ErrorCorrectionLevel::Low,
            'M' => ErrorCorrectionLevel::Medium,
            'Q' => ErrorCorrectionLevel::Quartile,
            default => ErrorCorrectionLevel::High,
        };
    }

    private static function mergeLogo(string $pngBinary, string $logoPath, ?int $logoWidth): string
    {
        try {
            $qr = @imagecreatefromstring($pngBinary);
            $logoData = @file_get_contents($logoPath);
            if ($logoData === false) {
                \Log::warning('QR logo not found', ['path' => $logoPath]);
                return $pngBinary;
            }
            $logo = @imagecreatefromstring($logoData);
            if (! $qr || ! $logo) {
                \Log::warning('QR logo parse failed', ['path' => $logoPath]);
                return $pngBinary;
            }

            $qrW = imagesx($qr);
            $qrH = imagesy($qr);
            $lw = imagesx($logo);
            $lh = imagesy($logo);

            // target width
            $targetW = $logoWidth ?: (int) ($qrW * 0.2);
            $ratio = $lw > 0 ? ($targetW / $lw) : 1;
            $targetH = max(1, (int) ($lh * $ratio));

            $resized = imagecreatetruecolor($targetW, $targetH);
            imagesavealpha($resized, true);
            $trans = imagecolorallocatealpha($resized, 0, 0, 0, 127);
            imagefill($resized, 0, 0, $trans);
            imagecopyresampled($resized, $logo, 0, 0, 0, 0, $targetW, $targetH, $lw, $lh);

            $dstX = (int) (($qrW - $targetW) / 2);
            $dstY = (int) (($qrH - $targetH) / 2);
            imagecopy($qr, $resized, $dstX, $dstY, 0, 0, $targetW, $targetH);

            ob_start();
            imagepng($qr);
            $out = ob_get_clean();

            imagedestroy($qr);
            imagedestroy($logo);
            imagedestroy($resized);

            return $out ?: $pngBinary;
        } catch (\Throwable $e) {
            return $pngBinary;
        }
    }

    private static function tableUrl(Table $table): string
    {
        // Prefer named route if present
        if (function_exists('route')) {
            try {
                return route('table_order', [$table->hash]);
            } catch (\Throwable $e) {
                // fall through to URL fallback
            }
        }

        $base = URL::to('/');
        return rtrim($base, '/') . '/table-order/' . ($table->hash ?? '');
    }

    /**
     * Build a branch-level QR using current styling with no label text.
     */
    private static function buildBranchQr(Branch $branch): string
    {
        $config = QrStyleManager::get($branch->restaurant_id);
        $advanced = QrStyleManager::isAdvanced($config);

        // Prefer a table hash in the path so ShopController resolves table/branch correctly
        $tableHash = \App\Models\Table::where('branch_id', $branch->id)->value('hash');

        // Branch URL with fresh token.
        // Match core logic: use restaurant id in the path, branch unique_hash in query.
        $pathParam = $branch->restaurant_id;
        $query = '?branch=' . $branch->unique_hash . '&hash=' . ($branch->restaurant->hash ?? '') . '&from_qr=1';

        try {
            $url = route('table_order', [$pathParam]) . $query;
        } catch (\Throwable $e) {
            $base = rtrim(URL::to('/'), '/');
            $url = $base . '/table-order/' . $pathParam . $query;
        }

        $fg = QrStyleManager::foregroundColor($config);
        $bg = QrStyleManager::backgroundColor($config);
        $labelColor = QrStyleManager::labelColor($config);
        $ecc = QrStyleManager::ecc($config);
        $size = (int) ($config['qr_size'] ?? 300);
        $margin = (int) ($config['qr_margin'] ?? 10);
        $round = (bool) ($config['qr_round_block_size'] ?? true);

        $builder = Builder::create()
            ->writer(new PngWriter())
            ->writerOptions([])
            ->data($url)
            ->encoding(new Encoding('UTF-8'))
            ->errorCorrectionLevel(self::mapEcc($ecc))
            ->size($size)
            ->margin($margin)
            ->foregroundColor(new Color($fg['r'], $fg['g'], $fg['b']))
            ->backgroundColor(new Color($bg['r'], $bg['g'], $bg['b']))
            ->roundBlockSizeMode($round ? RoundBlockSizeMode::Margin : RoundBlockSizeMode::None)
            ->validateResult(false);

        // No label for branch QR
        $logoPath = QrStyleManager::logoPath($config);
        $logoSize = QrStyleManager::logoSize($config, $size);

        $result = $builder->build();
        $binary = $result->getString();

        if ($advanced && $logoPath && extension_loaded('gd')) {
            $binary = self::mergeLogo($binary, $logoPath, $logoSize);
        }

        return $binary;
    }
}
