<?php

declare(strict_types=1);

namespace App\Services\Images;

use App\Models\Artwork;
use App\Repositories\Uploads\ArtworkFileRepository;
use App\Services\Cdn\ArtworkCdnPurgeService;
use App\Services\ThumbnailService;
use App\Services\Uploads\UploadDerivativesService;
use App\Services\Uploads\UploadStorageService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use RuntimeException;

final class ArtworkSquareThumbnailBackfillService
{
    public function __construct(
        private readonly UploadDerivativesService $derivatives,
        private readonly UploadStorageService $storage,
        private readonly ArtworkFileRepository $artworkFiles,
        private readonly ArtworkCdnPurgeService $cdnPurge,
    ) {
    }

    /**
     * @return array<string, mixed>
     */
    public function ensureSquareThumbnail(Artwork $artwork, bool $force = false, bool $dryRun = false): array
    {
        $hash = strtolower((string) ($artwork->hash ?? ''));
        if ($hash === '') {
            throw new RuntimeException('Artwork hash is required to generate a square thumbnail.');
        }

        $existing = DB::table('artwork_files')
            ->where('artwork_id', $artwork->id)
            ->where('variant', 'sq')
            ->first(['path']);

        if ($existing !== null && ! $force) {
            return [
                'status' => 'skipped',
                'reason' => 'already_exists',
                'artwork_id' => $artwork->id,
                'path' => (string) ($existing->path ?? ''),
            ];
        }

        $resolved = $this->resolveBestSource($artwork);
        if ($dryRun) {
            return [
                'status' => 'dry_run',
                'artwork_id' => $artwork->id,
                'source_variant' => $resolved['variant'],
                'source_path' => $resolved['source_path'],
                'object_path' => $this->storage->objectPathForVariant('sq', $hash, $hash . '.webp'),
            ];
        }

        try {
            $asset = $this->derivatives->generateSquareDerivative($resolved['source_path'], $hash, [
                'context' => ['artwork' => $artwork],
            ]);

            $this->artworkFiles->upsert($artwork->id, 'sq', $asset['path'], $asset['mime'], $asset['size']);

            $this->cdnPurge->purgeArtworkObjectPaths([$asset['path']], [
                'artwork_id' => $artwork->id,
                'reason' => 'square_thumbnail_regenerated',
            ]);

            if (! is_string($artwork->thumb_ext) || trim($artwork->thumb_ext) === '') {
                $artwork->forceFill(['thumb_ext' => 'webp'])->saveQuietly();
            }

            return [
                'status' => 'generated',
                'artwork_id' => $artwork->id,
                'path' => $asset['path'],
                'source_variant' => $resolved['variant'],
                'crop_mode' => $asset['result']?->cropMode,
            ];
        } finally {
            if (($resolved['cleanup'] ?? false) === true) {
                File::delete($resolved['source_path']);
            }
        }
    }

    /**
     * @return array{variant: string, source_path: string, cleanup: bool}
     */
    private function resolveBestSource(Artwork $artwork): array
    {
        $hash = strtolower((string) ($artwork->hash ?? ''));
        $files = DB::table('artwork_files')
            ->where('artwork_id', $artwork->id)
            ->pluck('path', 'variant')
            ->all();

        $variants = ['orig_image', 'orig', 'xl', 'lg', 'md', 'sm', 'xs'];

        foreach ($variants as $variant) {
            $path = $files[$variant] ?? null;
            if (! is_string($path) || trim($path) === '') {
                continue;
            }

            if ($variant === 'orig_image' || $variant === 'orig') {
                $filename = basename($path);
                $localPath = $this->storage->localOriginalPath($hash, $filename);
                if (is_file($localPath)) {
                    return [
                        'variant' => $variant,
                        'source_path' => $localPath,
                        'cleanup' => false,
                    ];
                }
            }

            $temporary = $this->downloadToTempFile($path, pathinfo($path, PATHINFO_EXTENSION) ?: 'webp');
            if ($temporary !== null) {
                return [
                    'variant' => $variant,
                    'source_path' => $temporary,
                    'cleanup' => true,
                ];
            }
        }

        $directSource = $this->resolveArtworkFilePathSource($artwork);
        if ($directSource !== null) {
            return $directSource;
        }

        $canonicalDerivativeSource = $this->resolveCanonicalDerivativeSource($artwork);
        if ($canonicalDerivativeSource !== null) {
            return $canonicalDerivativeSource;
        }

        throw new RuntimeException(sprintf('No usable source image was found for artwork %d.', (int) $artwork->id));
    }

    /**
     * @return array{variant: string, source_path: string, cleanup: bool}|null
     */
    private function resolveArtworkFilePathSource(Artwork $artwork): ?array
    {
        $relativePath = trim((string) ($artwork->file_path ?? ''), '/');
        if ($relativePath === '') {
            return null;
        }

        foreach ($this->localFilePathCandidates($relativePath) as $candidate) {
            if (is_file($candidate)) {
                return [
                    'variant' => 'file_path',
                    'source_path' => $candidate,
                    'cleanup' => false,
                ];
            }
        }

        $downloaded = $this->downloadUrlToTempFile($this->cdnUrlForPath($relativePath), pathinfo($relativePath, PATHINFO_EXTENSION));

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

        return [
            'variant' => 'file_path',
            'source_path' => $downloaded,
            'cleanup' => true,
        ];
    }

    /**
     * @return array{variant: string, source_path: string, cleanup: bool}|null
     */
    private function resolveCanonicalDerivativeSource(Artwork $artwork): ?array
    {
        $hash = strtolower((string) ($artwork->hash ?? ''));
        $thumbExt = strtolower(ltrim((string) ($artwork->thumb_ext ?? ''), '.'));

        if ($hash === '' || $thumbExt === '') {
            return null;
        }

        foreach (['xl', 'lg', 'md', 'sm', 'xs'] as $variant) {
            $url = ThumbnailService::fromHash($hash, $thumbExt, $variant);
            if (! is_string($url) || $url === '') {
                continue;
            }

            $downloaded = $this->downloadUrlToTempFile($url, $thumbExt);
            if ($downloaded === null) {
                continue;
            }

            return [
                'variant' => $variant,
                'source_path' => $downloaded,
                'cleanup' => true,
            ];
        }

        return null;
    }

    /**
     * @return array<int, string>
     */
    private function localFilePathCandidates(string $relativePath): array
    {
        $normalizedPath = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relativePath);

        return array_values(array_unique([
            $normalizedPath,
            base_path($normalizedPath),
            public_path($normalizedPath),
            storage_path('app/public' . DIRECTORY_SEPARATOR . $normalizedPath),
            storage_path('app/private' . DIRECTORY_SEPARATOR . $normalizedPath),
        ]));
    }

    private function cdnUrlForPath(string $relativePath): string
    {
        return rtrim((string) config('cdn.files_url', 'https://cdn.skinbase.org'), '/') . '/' . ltrim($relativePath, '/');
    }

    private function downloadUrlToTempFile(string $url, string $extension = ''): ?string
    {
        $context = stream_context_create([
            'http' => [
                'method' => 'GET',
                'timeout' => 30,
                'ignore_errors' => true,
                'header' => implode("\r\n", [
                    'User-Agent: Skinbase Nova square-thumb backfill',
                    'Accept: image/*,*/*;q=0.8',
                    'Accept-Encoding: identity',
                    'Connection: close',
                ]) . "\r\n",
            ],
            'ssl' => [
                'verify_peer' => true,
                'verify_peer_name' => true,
            ],
        ]);

        $contents = @file_get_contents($url, false, $context);
        $headers = $http_response_header ?? [];

        if (! is_string($contents) || $contents === '' || ! $this->isSuccessfulHttpResponse($url, $headers)) {
            return null;
        }

        if (! is_string($contents) || $contents === '') {
            return null;
        }

        $resolvedExtension = trim($extension) !== ''
            ? trim($extension)
            : $this->extensionFromContentType($this->contentTypeFromHeaders($headers));

        return $this->writeTemporaryFile($contents, $resolvedExtension);
    }

    /**
     * @param array<int, string> $headers
     */
    private function isSuccessfulHttpResponse(string $url, array $headers): bool
    {
        if ($headers === [] && parse_url($url, PHP_URL_SCHEME) === 'file') {
            return true;
        }

        $statusLine = $headers[0] ?? '';
        if (! is_string($statusLine) || ! preg_match('/\s(\d{3})\s/', $statusLine, $matches)) {
            return false;
        }

        $statusCode = (int) ($matches[1] ?? 0);

        return $statusCode >= 200 && $statusCode < 300;
    }

    /**
     * @param array<int, string> $headers
     */
    private function contentTypeFromHeaders(array $headers): string
    {
        foreach ($headers as $header) {
            if (! is_string($header) || stripos($header, 'Content-Type:') !== 0) {
                continue;
            }

            return trim(substr($header, strlen('Content-Type:')));
        }

        return '';
    }

    private function writeTemporaryFile(string $contents, string $extension = ''): string
    {
        $temp = tempnam(sys_get_temp_dir(), 'sq-thumb-');
        if ($temp === false) {
            throw new RuntimeException('Unable to allocate a temporary file for square thumbnail generation.');
        }

        $normalizedExtension = trim((string) $extension);
        $path = $normalizedExtension !== '' ? $temp . '.' . $normalizedExtension : $temp;

        if ($normalizedExtension !== '') {
            rename($temp, $path);
        }

        File::put($path, $contents);

        return $path;
    }

    private function extensionFromContentType(string $contentType): string
    {
        $normalized = strtolower(trim(strtok($contentType, ';') ?: ''));

        return match ($normalized) {
            'image/jpeg', 'image/jpg' => 'jpg',
            'image/png' => 'png',
            'image/webp' => 'webp',
            'image/gif' => 'gif',
            default => '',
        };
    }

    private function downloadToTempFile(string $objectPath, string $extension): ?string
    {
        $contents = $this->storage->readObject($objectPath);
        if (! is_string($contents) || $contents === '') {
            return null;
        }

        return $this->writeTemporaryFile($contents, $extension);
    }
}