<?php

namespace App\Services\FaceSwap;

use App\Contracts\FaceSwapDriverInterface;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class ReplicateDriver implements FaceSwapDriverInterface
{
    protected string $baseUrl;
    protected string $apiToken;
    protected string $modelVersion;
    protected int $timeout;
    protected int $pollInterval;
    protected int $maxPollAttempts;
    protected int $maxInputBytes;

    public function __construct()
    {
        $this->baseUrl = rtrim(config('faceswap.drivers.replicate.base_url'), '/');
        $this->apiToken = (string) config('faceswap.drivers.replicate.api_token');
        $this->modelVersion = (string) config('faceswap.drivers.replicate.model_version');
        $this->timeout = (int) config('faceswap.drivers.replicate.timeout', 60);
        $this->pollInterval = (int) config('faceswap.drivers.replicate.poll_interval', 3);
        $this->maxPollAttempts = (int) config('faceswap.drivers.replicate.max_poll_attempts', 60);
        // Replicate accepts data URLs ≤256kb. Keep a safety margin.
        $this->maxInputBytes = (int) config('faceswap.drivers.replicate.max_input_bytes', 240 * 1024);
    }

    public function process(string $faceImageData, string $productImageData): string
    {
        if ($this->apiToken === '') {
            throw new \Exception('Replicate API token is not configured');
        }
        if ($this->modelVersion === '') {
            throw new \Exception('Replicate model version is not configured');
        }

        // Normalize both inputs to JPEG data URLs. This mirrors the verified-
        // working TestReplicate command (tryon:test-replicate) byte-for-byte
        // so we never send PNG/WebP data URIs — the cdingram/face-swap model
        // is only documented to accept JPEG inputs.
        $swapUri = 'data:image/jpeg;base64,' . base64_encode($this->shrinkToLimit($faceImageData));
        $inputUri = 'data:image/jpeg;base64,' . base64_encode($this->shrinkToLimit($productImageData));

        $predictionId = $this->createPrediction($swapUri, $inputUri);
        Log::info('Replicate prediction created', ['id' => $predictionId]);

        $outputUrl = $this->pollUntilComplete($predictionId);

        return $this->downloadResult($outputUrl);
    }

    protected function createPrediction(string $swapUri, string $inputUri): string
    {
        $response = Http::timeout($this->timeout)
            ->withHeaders([
                'Authorization' => 'Bearer ' . $this->apiToken,
                'Content-Type' => 'application/json',
            ])
            ->post($this->baseUrl . '/v1/predictions', [
                'version' => $this->modelVersion,
                'input' => [
                    'swap_image' => $swapUri,
                    'input_image' => $inputUri,
                ],
            ]);

        if (!$response->successful()) {
            throw new \Exception('Replicate create prediction failed: HTTP ' . $response->status());
        }

        $data = $response->json();
        $id = $data['id'] ?? null;
        if (!$id) {
            throw new \Exception('Replicate create prediction returned no id');
        }

        return $id;
    }

    protected function pollUntilComplete(string $predictionId): string
    {
        for ($i = 0; $i < $this->maxPollAttempts; $i++) {
            sleep($this->pollInterval);

            $response = Http::timeout(30)
                ->withHeaders(['Authorization' => 'Bearer ' . $this->apiToken])
                ->get($this->baseUrl . '/v1/predictions/' . $predictionId);

            if (!$response->successful()) {
                Log::warning('Replicate poll error', [
                    'id' => $predictionId,
                    'attempt' => $i + 1,
                    'status' => $response->status(),
                ]);
                continue;
            }

            $data = $response->json();
            $status = $data['status'] ?? null;

            if ($status === 'succeeded') {
                $output = $data['output'] ?? null;
                if (is_array($output)) {
                    $output = $output[0] ?? null;
                }
                if (!$output) {
                    throw new \Exception('Replicate prediction succeeded but no output');
                }
                return (string) $output;
            }

            if ($status === 'failed' || $status === 'canceled') {
                throw new \Exception('Replicate prediction ' . $status);
            }

            Log::debug('Replicate polling', [
                'id' => $predictionId,
                'attempt' => $i + 1,
                'status' => $status,
            ]);
        }

        throw new \Exception('Replicate prediction timed out after ' . ($this->maxPollAttempts * $this->pollInterval) . ' seconds');
    }

    protected function downloadResult(string $url): string
    {
        // Pass the API token even on output download — some Replicate output
        // hosts require it depending on model/account configuration.
        $response = Http::timeout(60)
            ->withHeaders(['Authorization' => 'Bearer ' . $this->apiToken])
            ->get($url);
        if (!$response->successful()) {
            throw new \Exception('Failed to download Replicate result: HTTP ' . $response->status());
        }
        return $response->body();
    }

    /**
     * Re-encode the image as JPEG and ensure it fits under the Replicate data
     * URL cap (~256kb). Always returns JPEG bytes — even when the input is
     * already small — so the caller can safely emit `data:image/jpeg;base64,…`
     * without mime sniffing.
     *
     * Progressively reduces JPEG quality, then dimension, until under the cap.
     */
    protected function shrinkToLimit(string $imageData): string
    {
        if (!function_exists('imagecreatefromstring')) {
            throw new \Exception('GD extension required to encode images for Replicate');
        }

        try {
            $image = \Intervention\Image\Facades\Image::make($imageData);
        } catch (\Intervention\Image\Exception\NotReadableException $_) {
            throw new \Exception('Invalid image format for Replicate input');
        }

        $maxDim = 1024;
        $quality = 85;

        for ($attempt = 0; $attempt < 6; $attempt++) {
            $copy = clone $image;
            if ($copy->width() > $maxDim || $copy->height() > $maxDim) {
                $copy->resize($maxDim, $maxDim, function ($c) {
                    $c->aspectRatio();
                    $c->upsize();
                });
            }
            $encoded = (string) $copy->encode('jpg', $quality);
            if (strlen($encoded) <= $this->maxInputBytes) {
                return $encoded;
            }

            if ($quality > 50) {
                $quality -= 10;
            } else {
                $maxDim = (int) ($maxDim * 0.8);
            }
        }

        throw new \Exception('Could not shrink image under Replicate data URL limit (~256kb)');
    }
}
