<?php

namespace App\Assets;

use App\Models\Asset;
use App\Models\Creative;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

/**
 * The one place an Asset comes into being.
 *
 * There were two ways to end up with something publishable and only one of them
 * made an Asset: a chat upload did, and everything the creative generator
 * produced did not, so a generated creative could never reach an ad. Both go
 * through here now, which is also the only way the checksum, the mime and the
 * disk get filled in consistently.
 *
 * Deliberately not merging Asset and Creative. An Asset is a file we can put on
 * a platform. A Creative is the record of how it was made: the brief, prompt,
 * avatar, voice, script and what it was a variation of. Keeping both means the
 * provenance survives, and the publish path stays unaware of how a file
 * happened to be produced.
 */
class AssetLibrary
{
    /** A file the user attached to the chat. */
    public function fromUpload(UploadedFile $file, ?Model $owner): Asset
    {
        $checksum = hash_file('sha256', $file->getRealPath());

        return $this->existing($checksum, $owner) ?? Asset::create([
            'owner_type' => $owner?->getMorphClass(),
            'owner_id' => $owner?->getKey(),
            'kind' => str_starts_with((string) $file->getMimeType(), 'video/') ? 'video' : 'image',
            'source' => 'upload',
            'disk' => config('filesystems.default'),
            'path' => $file->store('assets'),
            'original_name' => $file->getClientOriginalName(),
            'mime' => $file->getMimeType(),
            'bytes' => $file->getSize(),
            'checksum' => $checksum,
        ]);
    }

    /**
     * The publishable half of a finished creative.
     *
     * Returns null while the creative is still rendering, or if the file it
     * claims to have written is not there. Both happen: a video render can fail
     * after the row is updated, and a half-written file would otherwise become
     * an Asset that fails much later, at the upload to Meta.
     */
    public function fromCreative(Creative $creative): ?Asset
    {
        // Only reuse the linked asset while it still describes the same file.
        // Regenerating clears asset_path and writes a new one without clearing
        // asset_id, so returning early here would leave the campaign showing
        // the creative it replaced.
        if ($creative->asset_id && $creative->asset?->path === $creative->asset_path) {
            return $creative->asset;
        }

        if ($creative->status !== 'completed' || blank($creative->asset_path)) {
            return null;
        }

        $disk = $this->diskFor($creative);

        if (! Storage::disk($disk)->exists($creative->asset_path)) {
            return null;
        }

        $checksum = hash('sha256', Storage::disk($disk)->get($creative->asset_path));
        $owner = $creative->user;

        $asset = $this->existing($checksum, $owner) ?? Asset::create([
            'owner_type' => $owner?->getMorphClass(),
            'owner_id' => $owner?->getKey(),
            'kind' => $creative->type === 'video' ? 'video' : 'image',
            'source' => 'generated',
            'disk' => $disk,
            'path' => $creative->asset_path,
            // Meta is sent this as the filename, so give it something that says
            // where the file came from rather than a bare ulid.
            'original_name' => $this->filenameFor($creative),
            'mime' => Storage::disk($disk)->mimeType($creative->asset_path) ?: null,
            'bytes' => Storage::disk($disk)->size($creative->asset_path),
            'checksum' => $checksum,
        ]);

        $creative->forceFill(['asset_id' => $asset->id])->saveQuietly();

        return $asset;
    }

    /**
     * The same file attached twice is one asset.
     *
     * Scoped to the owner so a checksum collision across users cannot hand one
     * user's file to another.
     */
    private function existing(string $checksum, ?Model $owner): ?Asset
    {
        return Asset::query()
            ->where('checksum', $checksum)
            ->where('owner_type', $owner?->getMorphClass())
            ->where('owner_id', $owner?->getKey())
            ->first();
    }

    /** Creative files are written to the public disk by both generators. */
    private function diskFor(Creative $creative): string
    {
        return config('creative.disk', 'public');
    }

    private function filenameFor(Creative $creative): string
    {
        $extension = pathinfo((string) $creative->asset_path, PATHINFO_EXTENSION) ?: 'bin';

        return 'creative-'.$creative->id.'.'.$extension;
    }
}
