<?php

namespace App\Assets;

use App\Models\Asset;
use App\Services\Meta\Meta;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;

/**
 * Small copies of a creative, made once and kept.
 *
 * The panel shows creatives at 56 pixels and the generator writes 8MB JPEGs, so
 * previewing two ads pulled 16MB down the wire. Resizing once and serving the
 * copy is the difference between a panel that appears and one that hangs.
 *
 * Falls back to the original whenever anything is unusual: no GD, an
 * unsupported format, an image already smaller than the target. A slow preview
 * is better than a broken one.
 *
 * Always written to a private disk, never the one holding the source. Ids are
 * sequential, so a thumbnail on the web-served public disk would be reachable
 * at a guessable URL by anyone, which is the thing the preview route exists to
 * prevent.
 */
class Thumbnails
{
    /** Wide enough for a retina thumbnail, small enough to be instant. */
    private const WIDTH = 320;

    /** The disk thumbnails are written to. Never web served. */
    private const DISK = 'local';

    /**
     * A path on the thumbnail disk, or null to serve the original.
     *
     * @return array{0: string, 1: string}|null [disk, path]
     */
    public function for(Asset $asset): ?array
    {
        // A video has no frame we can produce here, so this returned null and
        // the preview route served the video's own bytes. The panel renders a
        // creative in an <img>, so an ad with a video showed an empty box and
        // pulled the whole file down to do it.
        //
        // Meta already made a still when it transcoded the upload, which is the
        // same frame the ad itself uses, so it is the honest thumbnail rather
        // than a frame of our choosing.
        if ($asset->kind === 'video') {
            return $this->fromPlatform($asset);
        }

        if ($asset->kind !== 'image' || ! function_exists('imagecreatetruecolor')) {
            return null;
        }

        $source = Storage::disk($asset->disk ?: 'local');
        $disk = Storage::disk(self::DISK);
        $target = 'thumbnails/'.$asset->id.'-'.self::WIDTH.'.jpg';

        if ($disk->exists($target)) {
            return [self::DISK, $target];
        }

        if (! $source->exists($asset->path)) {
            return null;
        }

        try {
            $image = @imagecreatefromstring($source->get($asset->path));

            if ($image === false) {
                return null;
            }

            $width = imagesx($image);
            $height = imagesy($image);

            // Already small: keep serving the original rather than writing a
            // second copy of the same thing.
            // No imagedestroy(): deprecated in PHP 8.5, which this repo runs,
            // and a no-op since 8.0 because GD images are objects now.
            if ($width <= self::WIDTH) {
                return null;
            }

            $scaled = imagescale($image, self::WIDTH, (int) round($height * self::WIDTH / $width));

            if ($scaled === false) {
                return null;
            }

            ob_start();

            try {
                imagejpeg($scaled, null, 82);
                $bytes = (string) ob_get_contents();
            } finally {
                // Closed on the way out whatever happens. A throw between
                // ob_start and ob_get_clean would otherwise leave the buffer
                // open and the encoded image would end up inside the response.
                ob_end_clean();
            }

            $disk->put($target, $bytes);

            return [self::DISK, $target];
        } catch (\Throwable) {
            // A creative that will not resize still has to be viewable.
            return null;
        }
    }

    /**
     * The still the platform made when it transcoded an uploaded video.
     *
     * Fetched once and kept, like every other thumbnail here, so the panel is
     * not making a Graph call and an image download per row on every render.
     *
     * Null until the video has actually been uploaded to an ad account. Before
     * that there is no platform still to ask for, and the caller falls back to
     * serving the original, which is what it did for every video until now.
     *
     * @return array{0: string, 1: string}|null [disk, path]
     */
    private function fromPlatform(Asset $asset): ?array
    {
        $disk = Storage::disk(self::DISK);
        $target = 'thumbnails/'.$asset->id.'-video.jpg';

        if ($disk->exists($target)) {
            return [self::DISK, $target];
        }

        $ref = $asset->platformRefs()
            ->where('platform', 'meta')
            ->whereNotNull('external_id')
            ->first();

        if ($ref === null) {
            return null;
        }

        try {
            $uri = app(Meta::class)->videoThumbnail((string) $ref->external_id);

            if (blank($uri)) {
                return null;
            }

            // Short timeout on purpose: this runs while somebody is waiting for
            // the panel, and a missing thumbnail is a worse reason to hang than
            // it is to show a blank.
            $response = Http::timeout(5)->get($uri);

            if ($response->failed() || blank($response->body())) {
                return null;
            }

            $disk->put($target, $response->body());

            return [self::DISK, $target];
        } catch (\Throwable) {
            // Same rule as the resize path: a creative we cannot preview must
            // not take the panel down with it.
            return null;
        }
    }
}
