<?php

namespace App\Domain\Shared\Branding;

use App\Domain\Shared\Exceptions\DomainException;
use Illuminate\Support\Facades\Storage;

/**
 * Produces the square PNG icon set a PWA manifest and a native shell need,
 * from whatever logo the tenant uploaded.
 *
 * Written against GD directly rather than pulling in intervention/image: GD is
 * the one image extension the Dockerfile installs (imagick, webp and avif are
 * not there), the whole job is "decode, letterbox onto a square, resize, write
 * PNG", and a new Composer dependency is a permanent supply-chain and
 * build-time cost for about forty lines of work.
 *
 * Filenames are content-hashed — `icon-{size}-{sha}.png` — because the nginx
 * rule serves assets with `expires 1y; immutable`. A stable filename would pin
 * a rebranded icon on every installed device for a year.
 */
class AppIconGenerator
{
    /** The sizes a manifest, iOS and Android between them actually ask for. */
    public const SIZES = [48, 72, 96, 128, 144, 152, 180, 192, 256, 384, 512];

    private const MAX_SOURCE_PIXELS = 40_000_000; // ~6300x6300, decoded ≈ 160MB

    /**
     * @return array<int, string> size → storage path on the public disk
     */
    public function generate(int $academyId, string $sourcePath, string $backgroundHex = '#ffffff'): array
    {
        if (! extension_loaded('gd')) {
            throw new DomainException('امتداد الصور GD غير مُفعّل على الخادم');
        }

        $disk = Storage::disk('public');

        if (! $disk->exists($sourcePath)) {
            throw new DomainException('ملف الشعار غير موجود');
        }

        $bytes = $disk->get($sourcePath);

        // Dimensions are read from the header BEFORE decoding: a small file can
        // declare enormous dimensions and exhaust memory on imagecreatefrom*.
        $info = @getimagesizefromstring($bytes);

        if ($info === false) {
            throw new DomainException('تعذّرت قراءة ملف الشعار — يجب أن يكون PNG أو JPG');
        }

        [$width, $height, $type] = $info;

        if (! in_array($type, [IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_WEBP], true)) {
            throw new DomainException('صيغة الشعار غير مدعومة — استخدم PNG أو JPG');
        }

        if ($width * $height > self::MAX_SOURCE_PIXELS) {
            throw new DomainException('أبعاد الشعار كبيرة جداً');
        }

        $source = @imagecreatefromstring($bytes);

        if ($source === false) {
            throw new DomainException('تعذّر فك ترميز ملف الشعار');
        }

        [$bgR, $bgG, $bgB] = ColorRamp::hexToRgb($backgroundHex);

        $written = [];

        try {
            foreach (self::SIZES as $size) {
                $canvas = imagecreatetruecolor($size, $size);
                imagealphablending($canvas, false);
                imagesavealpha($canvas, true);

                // Solid ground, not transparency: iOS composites a transparent
                // icon onto black and the logo disappears.
                $bg = imagecolorallocate($canvas, $bgR, $bgG, $bgB);
                imagefilledrectangle($canvas, 0, 0, $size, $size, $bg);
                imagealphablending($canvas, true);

                // Letterbox with a 12% margin so the logo is never cropped and
                // never touches the rounded-corner mask.
                $box = (int) round($size * 0.76);
                $scale = min($box / $width, $box / $height);
                $w = max(1, (int) round($width * $scale));
                $h = max(1, (int) round($height * $scale));

                imagecopyresampled(
                    $canvas, $source,
                    (int) (($size - $w) / 2), (int) (($size - $h) / 2),
                    0, 0,
                    $w, $h, $width, $height
                );

                ob_start();
                imagepng($canvas, null, 9);
                $png = (string) ob_get_clean();
                imagedestroy($canvas);

                $hash = substr(hash('sha256', $png), 0, 12);
                $path = "branding/{$academyId}/icons/icon-{$size}-{$hash}.png";

                $disk->put($path, $png);
                $written[$size] = $path;
            }
        } finally {
            imagedestroy($source);
        }

        $this->pruneStaleIcons($academyId, $written);

        return $written;
    }

    /** Content-hashed names accumulate; keep only the set just written. */
    private function pruneStaleIcons(int $academyId, array $keep): void
    {
        $disk = Storage::disk('public');
        $dir = "branding/{$academyId}/icons";

        if (! $disk->exists($dir)) {
            return;
        }

        $keepPaths = array_flip($keep);

        foreach ($disk->files($dir) as $file) {
            if (! isset($keepPaths[$file])) {
                $disk->delete($file);
            }
        }
    }
}
