<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;

/**
 * Step-by-step sanity check for the configured Replicate API token.
 *
 * Use:
 *   docker compose exec laravel php artisan tryon:test-replicate
 *
 * Steps:
 *   1. Verify token via GET /v1/account (no cost).
 *   2. Run replicate/hello-world prediction (CPU model, near-zero cost).
 *   3. (Optional) Run the configured face-swap model with a tiny synthetic image
 *      — only when --faceswap is passed, since it consumes real credit.
 */
class TestReplicate extends Command
{
    protected $signature = 'tryon:test-replicate
                            {--faceswap : Also run the configured face-swap model (consumes credit)}
                            {--skip-hello : Skip step 2 (hello-world) — useful when rate-limited (6/min) on small orgs}
                            {--face= : Path to face image (only used with --faceswap)}
                            {--target= : Path to target image (only used with --faceswap)}
                            {--output= : Where to save the result image (default: result.jpg next to --face)}';

    protected $description = 'Sanity-check the Replicate API token & prediction pipeline.';

    protected string $baseUrl;
    protected string $token;

    public function handle(): int
    {
        $this->baseUrl = rtrim((string) config('faceswap.drivers.replicate.base_url', 'https://api.replicate.com'), '/');
        $this->token = (string) config('faceswap.drivers.replicate.api_token');

        if ($this->token === '') {
            $this->error('REPLICATE_API_TOKEN is empty. Set it in docker/docker.env and run config:clear.');
            return self::FAILURE;
        }

        $this->line('Token length: ' . strlen($this->token) . ' (prefix: ' . substr($this->token, 0, 6) . '…)');

        if (!$this->step1Account()) return self::FAILURE;
        if ($this->option('skip-hello')) {
            $this->newLine();
            $this->comment('Step 2/3 — skipped (--skip-hello)');
        } else {
            if (!$this->step2HelloWorld()) return self::FAILURE;
        }

        if ($this->option('faceswap')) {
            if (!$this->step3FaceSwap()) return self::FAILURE;
        } else {
            $this->newLine();
            $this->comment('Skipping face-swap test (re-run with --faceswap to include it; consumes credit).');
        }

        $this->newLine();
        $this->info('All checks passed.');
        return self::SUCCESS;
    }

    /** Step 1 — token / account verification. No credit cost. */
    protected function step1Account(): bool
    {
        $this->newLine();
        $this->info('Step 1/3 — GET /v1/account');

        $response = Http::timeout(15)
            ->withHeaders(['Authorization' => 'Bearer ' . $this->token])
            ->get($this->baseUrl . '/v1/account');

        if (!$response->successful()) {
            $this->error('  HTTP ' . $response->status() . ' ' . $response->body());
            return false;
        }

        $data = $response->json();
        $this->line('  username : ' . ($data['username'] ?? '?'));
        $this->line('  type     : ' . ($data['type'] ?? '?'));
        $this->line('  github   : ' . ($data['github_url'] ?? '-'));
        return true;
    }

    /** Step 2 — run replicate/hello-world. Costs ~$0 (CPU echo model). */
    protected function step2HelloWorld(): bool
    {
        $this->newLine();
        $this->info('Step 2/3 — replicate/hello-world prediction (~$0)');

        // Pinned canonical version, see https://replicate.com/replicate/hello-world
        $version = '5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa';

        $output = $this->runPrediction($version, ['text' => 'moro-tryon-sanity-check'], 30);
        if ($output === null) return false;

        $text = is_array($output) ? implode('', $output) : $output;
        $this->line('  output: ' . $text);
        return true;
    }

    /** Step 3 — optional face-swap test. Consumes credit. */
    protected function step3FaceSwap(): bool
    {
        $this->newLine();
        $this->info('Step 3/3 — face-swap prediction (consumes credit)');

        $version = (string) config('faceswap.drivers.replicate.model_version');
        if ($version === '') {
            $this->error('  REPLICATE_MODEL_VERSION not configured.');
            return false;
        }

        $facePath   = (string) $this->option('face');
        $targetPath = (string) $this->option('target');

        if ($facePath !== '' && $targetPath !== '') {
            // Real face-swap with user-provided images.
            if (!is_file($facePath))   { $this->error('  --face not found: ' . $facePath); return false; }
            if (!is_file($targetPath)) { $this->error('  --target not found: ' . $targetPath); return false; }

            $this->line('  face   : ' . $facePath . ' (' . filesize($facePath) . ' bytes)');
            $this->line('  target : ' . $targetPath . ' (' . filesize($targetPath) . ' bytes)');

            $faceUri   = $this->encodeImageForReplicate(file_get_contents($facePath));
            $targetUri = $this->encodeImageForReplicate(file_get_contents($targetPath));

            $this->line('  encoded face   : ' . strlen($faceUri) . ' chars');
            $this->line('  encoded target : ' . strlen($targetUri) . ' chars');
        } else {
            // Placeholder — proves connectivity but model will likely fail face detection.
            $tiny = $this->minimalJpeg();
            $faceUri = $targetUri = 'data:image/jpeg;base64,' . base64_encode($tiny);
            $this->warn('  Using 1x1 placeholder (pass --face=PATH --target=PATH for a real swap).');
        }

        $this->line('  version: ' . substr($version, 0, 16) . '…');

        $output = $this->runPrediction($version, [
            'swap_image'  => $faceUri,
            'input_image' => $targetUri,
        ], 120);

        if ($output === null) return false;
        $resultUrl = is_array($output) ? ($output[0] ?? null) : $output;
        if (!$resultUrl) { $this->error('  no output URL'); return false; }
        $this->line('  output URL: ' . $resultUrl);

        // Save result image
        $outputPath = (string) $this->option('output');
        if ($outputPath === '' && $facePath !== '') {
            $outputPath = dirname($facePath) . '/result.jpg';
        }
        if ($outputPath !== '') {
            $this->line('  downloading…');
            $resp = Http::timeout(60)
                ->withHeaders(['Authorization' => 'Bearer ' . $this->token])
                ->get($resultUrl);
            if (!$resp->successful()) {
                $this->error('  download failed: HTTP ' . $resp->status());
                return false;
            }
            file_put_contents($outputPath, $resp->body());
            $this->info('  ✓ saved ' . strlen($resp->body()) . ' bytes → ' . $outputPath);
        }
        return true;
    }

    /**
     * Encode an image for Replicate data URL input (≤256kb cap).
     * Resize + re-compress JPEG until under the limit.
     */
    protected function encodeImageForReplicate(string $imageData, int $maxBytes = 240 * 1024): string
    {
        if (strlen($imageData) <= $maxBytes) {
            return 'data:image/jpeg;base64,' . base64_encode($imageData);
        }

        $image = imagecreatefromstring($imageData);
        if ($image === false) {
            throw new \RuntimeException('Cannot decode image (corrupt or unsupported format)');
        }

        $width  = imagesx($image);
        $height = imagesy($image);
        $maxDim = 1024;
        $quality = 85;

        for ($attempt = 0; $attempt < 6; $attempt++) {
            $w = $width; $h = $height;
            if ($w > $maxDim || $h > $maxDim) {
                $ratio = min($maxDim / $w, $maxDim / $h);
                $w = (int) ($w * $ratio);
                $h = (int) ($h * $ratio);
            }
            $resized = imagecreatetruecolor($w, $h);
            imagecopyresampled($resized, $image, 0, 0, 0, 0, $w, $h, $width, $height);
            ob_start();
            imagejpeg($resized, null, $quality);
            $bytes = ob_get_clean();
            imagedestroy($resized);

            if (strlen($bytes) <= $maxBytes) {
                imagedestroy($image);
                return 'data:image/jpeg;base64,' . base64_encode($bytes);
            }

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

        imagedestroy($image);
        throw new \RuntimeException('Could not shrink image under ' . $maxBytes . ' bytes');
    }

    /**
     * Create a prediction, poll until terminal state, return the output (or null on failure).
     */
    protected function runPrediction(string $version, array $input, int $maxSeconds): mixed
    {
        $createResp = Http::timeout(30)
            ->withHeaders([
                'Authorization' => 'Bearer ' . $this->token,
                'Content-Type'  => 'application/json',
            ])
            ->post($this->baseUrl . '/v1/predictions', [
                'version' => $version,
                'input'   => $input,
            ]);

        if (!$createResp->successful()) {
            $this->error('  create failed: HTTP ' . $createResp->status() . ' ' . $createResp->body());
            if ($createResp->status() === 402) {
                $this->warn('  → Token is VALID but account has NO CREDIT. Top up at https://replicate.com/account/billing');
            } elseif ($createResp->status() === 401) {
                $this->warn('  → Token rejected. Check it is correct and not revoked.');
            }
            return null;
        }

        $id = $createResp->json('id');
        if (!$id) {
            $this->error('  create returned no id: ' . $createResp->body());
            return null;
        }
        $this->line('  prediction id: ' . $id);

        $deadline = time() + $maxSeconds;
        $attempt = 0;
        while (time() < $deadline) {
            sleep(2);
            $attempt++;

            $statusResp = Http::timeout(15)
                ->withHeaders(['Authorization' => 'Bearer ' . $this->token])
                ->get($this->baseUrl . '/v1/predictions/' . $id);

            if (!$statusResp->successful()) {
                $this->warn('  poll #' . $attempt . ' HTTP ' . $statusResp->status());
                continue;
            }

            $status = $statusResp->json('status');
            $this->line('  poll #' . $attempt . ' status=' . $status);

            if ($status === 'succeeded') {
                return $statusResp->json('output');
            }
            if ($status === 'failed' || $status === 'canceled') {
                $err = $statusResp->json('error') ?? '(no error message)';
                $this->error('  prediction ' . $status . ': ' . (is_string($err) ? $err : json_encode($err)));
                return null;
            }
        }

        $this->error('  timed out after ' . $maxSeconds . 's');
        return null;
    }

    /**
     * Smallest valid JPEG: 1x1 grey pixel. Returned as raw bytes.
     */
    protected function minimalJpeg(): string
    {
        $img = imagecreatetruecolor(1, 1);
        imagefilledrectangle($img, 0, 0, 1, 1, imagecolorallocate($img, 200, 200, 200));
        ob_start();
        imagejpeg($img, null, 85);
        $bytes = ob_get_clean();
        imagedestroy($img);
        return (string) $bytes;
    }
}
