<?php

namespace App\Services;

use App\UserFacePhoto;
use App\VirtualTryonResult;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Str;
use Intervention\Image\Facades\Image;
use RuntimeException;

class VirtualTryonImageService
{
    const MAX_FACE_PHOTOS = 3;
    const SIGNED_URL_TTL_MINUTES = 30;

    /**
     * Return the disk used for private try-on assets (face photos + results).
     *
     * Production / staging: must use `do_spaces_private` (visibility: private).
     * Local / testing: falls back to `tryon_local` (local private disk under storage/).
     *
     * Never falls back silently to the public disk: doing so would leak biometric PII
     * via /storage/... if DO_SPACES keys are misconfigured in a deployed environment.
     */
    public function diskName(): string
    {
        if (config('filesystems.disks.do_spaces_private.key')) {
            return 'do_spaces_private';
        }

        if (app()->environment(['local', 'testing'])) {
            return 'tryon_local';
        }

        throw new RuntimeException(
            'Private storage for virtual try-on is not configured. Set DO_SPACES_PRIVATE_* (or DO_SPACES_*) in .env.'
        );
    }

    /**
     * Indicates whether the active disk is cloud (S3/DO Spaces) and therefore
     * supports temporary signed URLs at the storage layer.
     */
    protected function isCloudDisk(string $disk): bool
    {
        return config("filesystems.disks.{$disk}.driver") === 's3';
    }

    /**
     * Upload a face photo and create DB record.
     */
    public function uploadFacePhoto(UploadedFile $file, int $userId): UserFacePhoto
    {
        $existingCount = UserFacePhoto::where('user_id', $userId)->count();
        if ($existingCount >= self::MAX_FACE_PHOTOS) {
            throw new \Exception('保存できる顔写真は最大' . self::MAX_FACE_PHOTOS . '枚です');
        }

        $disk = $this->diskName();

        // Image bomb protection: check dimensions before loading
        $imageInfo = getimagesize($file->getRealPath());
        if (!$imageInfo || $imageInfo[0] > 4096 || $imageInfo[1] > 4096) {
            throw new \Exception('画像サイズが大きすぎます（最大4096x4096px）');
        }

        // Normalize orientation, resize, and re-encode (strips EXIF / embedded payload).
        // Face photos are stored on a portrait canvas so camera and library uploads
        // display consistently across devices.
        $image = Image::make($file)->orientate();
        $image->resize(512, 682, function ($constraint) {
            $constraint->aspectRatio();
            $constraint->upsize();
        });
        $image->resizeCanvas(512, 682, 'center', false, '#ffffff');
        $imageData = (string) $image->encode('jpg', 90);

        $filename = Str::uuid() . '.jpg';
        $storagePath = 'face-photos/' . $userId . '/' . $filename;

        Storage::disk($disk)->put($storagePath, $imageData, 'private');

        $isDefault = $existingCount === 0;

        // `file_path` column is kept for backwards compatibility with the existing
        // schema (NOT NULL). We store the same private storage path — the public
        // URL previously written here leaked biometric PII via CDN and is no
        // longer exposed to clients (signed proxy URLs are generated on demand).
        return UserFacePhoto::create([
            'user_id' => $userId,
            'file_path' => $storagePath,
            'storage_path' => $storagePath,
            'is_default' => $isDefault,
        ]);
    }

    /**
     * Delete a face photo from storage and DB.
     */
    public function deleteFacePhoto(UserFacePhoto $photo): void
    {
        if ($photo->storage_path) {
            Storage::disk($this->diskName())->delete($photo->storage_path);
        }
        $photo->delete();
    }

    /**
     * Generate a short-lived signed proxy URL that streams the face photo image
     * through the Laravel application (enforces ownership + audit logs).
     *
     * We deliberately do NOT return a DO Spaces presigned URL or a public CDN URL,
     * so that biometric PII never leaves a server-controlled path.
     */
    public function facePhotoSignedUrl(UserFacePhoto $photo): string
    {
        return URL::temporarySignedRoute(
            'tryon.face-photos.image',
            now()->addMinutes(self::SIGNED_URL_TTL_MINUTES),
            ['id' => $photo->id]
        );
    }

    /**
     * Signed proxy URL for a try-on result image.
     */
    public function resultImageSignedUrl(VirtualTryonResult $result): ?string
    {
        if (!$result->result_image) {
            return null;
        }

        return URL::temporarySignedRoute(
            'tryon.results.image',
            now()->addMinutes(self::SIGNED_URL_TTL_MINUTES),
            ['code' => $result->code]
        );
    }

    /**
     * Stream the raw face photo bytes for the proxy endpoint.
     */
    public function streamFacePhoto(UserFacePhoto $photo)
    {
        $disk = $this->diskName();

        if (!$photo->storage_path || !Storage::disk($disk)->exists($photo->storage_path)) {
            throw new \Exception('画像が見つかりません');
        }

        $data = Storage::disk($disk)->get($photo->storage_path);

        return response($data, 200, [
            'Content-Type' => 'image/jpeg',
            'Cache-Control' => 'private, max-age=0, no-store',
        ]);
    }

    /**
     * Stream the raw result image bytes for the proxy endpoint (inline view).
     */
    public function streamResultImage(VirtualTryonResult $result)
    {
        $disk = $this->diskName();

        if (!$result->result_image || !Storage::disk($disk)->exists($result->result_image)) {
            throw new \Exception('試着結果画像が見つかりません');
        }

        $data = Storage::disk($disk)->get($result->result_image);

        return response($data, 200, [
            'Content-Type' => 'image/jpeg',
            'Cache-Control' => 'private, max-age=0, no-store',
        ]);
    }

    /**
     * Download result image with KIREI watermark.
     *
     * Fails closed: if the PNG asset is missing we fall back to a GD text
     * watermark rather than letting a non-watermarked image leave the server.
     */
    public function downloadWithWatermark(VirtualTryonResult $result)
    {
        if (!$result->result_image) {
            throw new \Exception('試着結果画像が見つかりません');
        }

        $imageData = Storage::disk($this->diskName())->get($result->result_image);
        if (!$imageData) {
            throw new \Exception('画像の読み込みに失敗しました');
        }

        $image = Image::make($imageData);

        $this->applyWatermark($image);

        $filename = 'kirei_tryon_' . $result->code . '.jpg';
        $encodedImage = (string) $image->encode('jpg', 90);

        return response($encodedImage, 200, [
            'Content-Type' => 'image/jpeg',
            'Content-Disposition' => 'attachment; filename="' . $filename . '"',
            'Content-Length' => strlen($encodedImage),
        ]);
    }

    /**
     * Apply KIREI watermark: logo + 「着麗 - KIREI -」 text, side-by-side at bottom-right.
     * Falls back to text-only if logo asset missing.
     */
    protected function applyWatermark($image): void
    {
        $imgWidth = $image->width();
        $imgHeight = $image->height();

        $watermarkHeight = max(36, intval($imgWidth * 0.06));
        $fontSize = intval($watermarkHeight * 0.75);
        $text = '着麗 - KIREI -';

        $logoPath = public_path('images/logo_white.png');
        $fontPath = storage_path('fonts/ipaexg.ttf');

        // 右余白はかなり詰める、下余白は若干広めに
        $rightMargin = max(4, intval($imgWidth * 0.003));
        $bottomMargin = max(12, intval($imgWidth * 0.015));
        // ロゴとテキストの隙間は控えめに
        $gap = max(4, intval($watermarkHeight * 0.08));

        // テキストの実寸を imagettfbbox で取得
        $textWidth = 0;
        if (file_exists($fontPath) && function_exists('imagettfbbox')) {
            $bbox = imagettfbbox($fontSize, 0, $fontPath, $text);
            if (is_array($bbox)) {
                $textWidth = abs($bbox[4] - $bbox[0]);
            }
        }
        if ($textWidth <= 0) {
            $textWidth = intval(mb_strlen($text) * $fontSize * 0.85);
        }

        $logo = null;
        $logoWidth = 0;
        if (file_exists($logoPath)) {
            $logo = Image::make($logoPath);
            // 周囲の透明余白を削って実コンテンツ幅にフィットさせる
            $logo->trim('transparent');
            $logo->resize(null, $watermarkHeight, function ($constraint) {
                $constraint->aspectRatio();
                $constraint->upsize();
            });
            $logo->opacity(85);
            $logoWidth = $logo->width();
        }

        $rightX = $imgWidth - $rightMargin;
        $bottomY = $imgHeight - $bottomMargin;
        $totalWidth = $logoWidth + ($logo ? $gap : 0) + $textWidth;

        $logoX = $logo ? ($rightX - $totalWidth) : null;
        if ($logo) {
            $logoY = $bottomY - $watermarkHeight;
            $image->insert($logo, 'top-left', $logoX, $logoY);
        }

        // テキストはロゴ右端 + gap の位置から左揃えで描画（gap が確実に gap だけ空くように）
        $textX = $logo ? ($logoX + $logoWidth + $gap) : ($rightX - $textWidth);
        $textY = $bottomY - intval($watermarkHeight / 2);

        $image->text($text, $textX, $textY, function ($font) use ($fontSize, $fontPath) {
            if (file_exists($fontPath)) {
                $font->file($fontPath);
            }
            $font->size($fontSize);
            $font->color([255, 255, 255, 0.9]);
            $font->align('left');
            $font->valign('middle');
        });
    }
}
