<?php

namespace App\Services;

use App\Models\UserProfile;
use Carbon\Carbon;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Drivers\Gd\Driver as GdDriver;
use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver;
use Intervention\Image\Encoders\PngEncoder;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\ImageManager;
use RuntimeException;

class AvatarService
{
    private const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp'];

    private const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp'];

    private const ALLOWED_POSITIONS = [
        'top-left',
        'top',
        'top-right',
        'left',
        'center',
        'right',
        'bottom-left',
        'bottom',
        'bottom-right',
    ];

    protected $sizes = [
        'xs' => 32,
        'sm' => 64,
        'md' => 128,
        'lg' => 256,
        'xl' => 512,
    ];

    protected $quality = 85;

    private ?ImageManager $manager = null;

    public function __construct()
    {
        $configuredSizes = array_values(array_filter((array) config('avatars.sizes', [32, 64, 128, 256, 512]), static fn ($size) => (int) $size > 0));
        if ($configuredSizes !== []) {
            $this->sizes = array_fill_keys(array_map('strval', $configuredSizes), null);
            $this->sizes = array_combine(array_keys($this->sizes), $configuredSizes);
        }

        $this->quality = (int) config('avatars.quality', 85);

        try {
            $this->manager = extension_loaded('gd')
                ? new ImageManager(new GdDriver())
                : new ImageManager(new ImagickDriver());
        } catch (\Throwable $e) {
            logger()->warning('Avatar image manager configuration failed: ' . $e->getMessage());
            $this->manager = null;
        }
    }

    public function storeFromUploadedFile(int $userId, UploadedFile $file, string $position = 'center'): string
    {
        $this->assertImageManagerAvailable();
        $this->assertStorageIsAllowed();
        $binary = $this->assertSecureImageUpload($file);

        return $this->storeFromBinary($userId, $binary, $position);
    }

    public function storeFromLegacyFile(int $userId, string $path): ?string
    {
        $this->assertImageManagerAvailable();
        $this->assertStorageIsAllowed();

        if (!file_exists($path) || !is_readable($path)) {
            return null;
        }

        $binary = file_get_contents($path);
        if ($binary === false || $binary === '') {
            return null;
        }

        return $this->storeFromBinary($userId, $binary);
    }

    public function removeAvatar(int $userId): void
    {
        $diskName = (string) config('avatars.disk', 's3');
        $disk = Storage::disk($diskName);
        $existingHash = UserProfile::query()
            ->where('user_id', $userId)
            ->value('avatar_hash');

        if (is_string($existingHash) && trim($existingHash) !== '') {
            $disk->deleteDirectory($this->avatarDirectory(trim($existingHash)));
        }

        $disk->deleteDirectory("avatars/{$userId}");

        UserProfile::query()->updateOrCreate(
            ['user_id' => $userId],
            [
                'avatar_hash' => null,
                'avatar_mime' => null,
                'avatar_updated_at' => Carbon::now(),
            ]
        );
    }

    private function storeFromBinary(int $userId, string $binary, string $position = 'center'): string
    {
        $image = $this->readImageFromBinary($binary);
        $image = $this->normalizeImage($image);
        $cropPosition = $this->normalizePosition($position);
        $normalizedSource = (string) $image->encode(new PngEncoder());

        if ($normalizedSource === '') {
            throw new RuntimeException('Avatar processing failed to prepare the source image.');
        }

        $diskName = (string) config('avatars.disk', 's3');
        $disk = Storage::disk($diskName);

        $existingHash = UserProfile::query()
            ->where('user_id', $userId)
            ->value('avatar_hash');

        $hashSeed = '';
        $encodedVariants = [];
        foreach ($this->sizes as $size) {
            $variant = $this->manager->read($normalizedSource)->cover($size, $size, $cropPosition);
            $encoded = (string) $variant->encode(new WebpEncoder($this->quality));
            $encodedVariants[(int) $size] = $encoded;

            if ($size === 128) {
                $hashSeed = $encoded;
            }
        }

        if ($hashSeed === '') {
            throw new RuntimeException('Avatar processing failed to generate a hash seed.');
        }

        $hash = hash('sha256', $hashSeed);
        $basePath = $this->avatarDirectory($hash);

        foreach ($encodedVariants as $size => $encoded) {
            $disk->put("{$basePath}/{$size}.webp", $encoded, [
                'visibility' => 'public',
                'CacheControl' => 'public, max-age=31536000, immutable',
                'ContentType' => 'image/webp',
            ]);
        }

        if (is_string($existingHash) && trim($existingHash) !== '' && trim($existingHash) !== $hash) {
            $disk->deleteDirectory($this->avatarDirectory(trim($existingHash)));
        }

        $disk->deleteDirectory("avatars/{$userId}");
        $this->updateProfileMetadata($userId, $hash);

        return $hash;
    }

    private function avatarDirectory(string $hash): string
    {
        $p1 = substr($hash, 0, 2);
        $p2 = substr($hash, 2, 2);

        return sprintf('avatars/%s/%s/%s', $p1, $p2, $hash);
    }

    private function normalizePosition(string $position): string
    {
        $normalized = strtolower(trim($position));

        if (in_array($normalized, self::ALLOWED_POSITIONS, true)) {
            return $normalized;
        }

        return 'center';
    }

    private function normalizeImage($image)
    {
        try {
            $core = $image->getCore();
            $isImagickCore = is_object($core) && strtolower(get_class($core)) === 'imagick';
            if ($isImagickCore) {
                try {
                    $core->stripImage();
                } catch (\Throwable $_) {
                }

                try {
                    $colorSpaceRgb = defined('\\Imagick::COLORSPACE_RGB') ? constant('\\Imagick::COLORSPACE_RGB') : null;
                    $colorSpaceSRgb = defined('\\Imagick::COLORSPACE_SRGB') ? constant('\\Imagick::COLORSPACE_SRGB') : null;
                    if (is_int($colorSpaceRgb)) {
                        $core->setImageColorspace($colorSpaceRgb);
                    } elseif (is_int($colorSpaceSRgb)) {
                        $core->setImageColorspace($colorSpaceSRgb);
                    }
                } catch (\Throwable $_) {
                }

                try {
                    $alphaRemove = defined('\\Imagick::ALPHACHANNEL_REMOVE') ? constant('\\Imagick::ALPHACHANNEL_REMOVE') : null;
                    if (is_int($alphaRemove)) {
                        $core->setImageAlphaChannel($alphaRemove);
                    }
                } catch (\Throwable $_) {
                }

                try {
                    $core->setBackgroundColor('white');
                    $layerFlatten = defined('\\Imagick::LAYERMETHOD_FLATTEN') ? constant('\\Imagick::LAYERMETHOD_FLATTEN') : null;
                    $flattened = is_int($layerFlatten) ? $core->mergeImageLayers($layerFlatten) : null;
                    if (is_object($flattened) && strtolower(get_class($flattened)) === 'imagick') {
                        $core->clear();
                        $core->destroy();
                        $image = $this->manager->read((string) $flattened->getImageBlob());
                    }
                } catch (\Throwable $_) {
                }

                return $image;
            }

            $isGdCore = is_resource($core) || (is_object($core) && strtolower(get_class($core)) === 'gdimage');
            if ($isGdCore) {
                $width = imagesx($core);
                $height = imagesy($core);
                if ($width > 0 && $height > 0) {
                    $flattened = imagecreatetruecolor($width, $height);
                    if ($flattened !== false) {
                        $white = imagecolorallocate($flattened, 255, 255, 255);
                        imagefilledrectangle($flattened, 0, 0, $width, $height, $white);
                        imagecopy($flattened, $core, 0, 0, 0, 0, $width, $height);

                        ob_start();
                        imagepng($flattened);
                        $pngBinary = (string) ob_get_clean();
                        imagedestroy($flattened);

                        if ($pngBinary !== '') {
                            return $this->manager->read($pngBinary);
                        }
                    }
                }
            }
        } catch (\Throwable $_) {
        }

        return $image;
    }

    private function readImageFromBinary(string $binary)
    {
        try {
            return $this->manager->read($binary);
        } catch (\Throwable $e) {
            throw new RuntimeException('Failed to decode uploaded image.');
        }
    }

    private function updateProfileMetadata(int $userId, string $hash): void
    {
        UserProfile::query()->updateOrCreate(
            ['user_id' => $userId],
            [
                'avatar_hash' => $hash,
                'avatar_mime' => 'image/webp',
                'avatar_updated_at' => Carbon::now(),
            ]
        );
    }

    private function assertImageManagerAvailable(): void
    {
        if ($this->manager !== null) {
            return;
        }

        throw new RuntimeException('Avatar image processing is not available on this environment.');
    }

    private function assertStorageIsAllowed(): void
    {
        if (!app()->environment('production')) {
            return;
        }

        $diskName = (string) config('avatars.disk', 's3');
        if (in_array($diskName, ['local', 'public'], true)) {
            throw new RuntimeException('Production avatar storage must use object storage, not local/public disks.');
        }
    }

    private function assertSecureImageUpload(UploadedFile $file): string
    {
        if (! $file->isValid()) {
            throw new RuntimeException('Avatar upload is not valid.');
        }

        $extension = strtolower((string) $file->getClientOriginalExtension());
        if (!in_array($extension, self::ALLOWED_EXTENSIONS, true)) {
            throw new RuntimeException('Unsupported avatar file extension.');
        }

        $detectedMime = (string) $file->getMimeType();
        if (!in_array($detectedMime, self::ALLOWED_MIME_TYPES, true)) {
            throw new RuntimeException('Unsupported avatar MIME type.');
        }

        $uploadPath = (string) ($file->getRealPath() ?: $file->getPathname());
        if ($uploadPath === '' || !is_readable($uploadPath)) {
            throw new RuntimeException('Unable to resolve uploaded avatar path.');
        }

        $binary = file_get_contents($uploadPath);
        if ($binary === false || $binary === '') {
            throw new RuntimeException('Unable to read uploaded avatar data.');
        }

        $finfo = new \finfo(FILEINFO_MIME_TYPE);
        $finfoMime = (string) $finfo->buffer($binary);
        if (!in_array($finfoMime, self::ALLOWED_MIME_TYPES, true)) {
            throw new RuntimeException('Avatar content did not match allowed image MIME types.');
        }

        $dimensions = @getimagesizefromstring($binary);
        if (!is_array($dimensions) || ($dimensions[0] ?? 0) < 1 || ($dimensions[1] ?? 0) < 1) {
            throw new RuntimeException('Uploaded avatar is not a valid image.');
        }

        return $binary;
    }
}
