<?php
/**
 * Costudio_Image_Generator
 *
 * Generates images via AI (Gemini Imagen or OpenAI DALL-E), uploads them to
 * the WordPress Media Library of the target site via REST API, and returns
 * the attachment ID and URL.
 *
 * Image provider + API key are read from ContentOS Core Settings.
 *
 * @package Contentosapp_Studio
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages go to PHP error log, never to HTML output.

class Costudio_Image_Generator {

    const IMAGEN_ENDPOINT_BASE = 'https://generativelanguage.googleapis.com/v1beta/models/';
    const IMAGEN_DEFAULT_MODEL = 'gemini-3.1-flash-image';
    const DALLE_ENDPOINT       = 'https://api.openai.com/v1/images/generations';
    const LOCAL_SITE_KEY       = '__local__';

    /** User-Agent de browser para requisições remotas (WAF bypass). */
    const REMOTE_BROWSER_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';

    private string $provider;
    private string $api_key;
    private string $model;
    private string $site_url;
    private string $auth_header;
    private string $site_key;
    private bool   $local_mode = false;
    private int    $production_id = 0;

    /** Per-call HTTP timeout override (seconds). Null = provider default (150s). */
    private ?int $http_timeout_budget = null;

    /**
     * @param string $site_key  Key within the contentos_sites option array.
     */
    public function __construct( string $site_key ) {
        $cts    = costudio_settings();
        $sites_repo = costudio_sites();
        $sites      = $sites_repo ? $sites_repo->get_all() : [];
        $this->site_key = $site_key;

        // Provider. Production mode is the routing source of truth:
        // ContentOS Auto uses managed-service quota; BYOK never falls back to ContentOS.
		$ai_mode        = $cts ? (string) $cts->get( 'ai_mode', 'contentos' ) : 'contentos';
		$saved_provider = $cts ? (string) $cts->get( 'ai_image_provider', '' ) : '';
		if ( 'contentos' === $ai_mode ) {
			$this->provider = 'contentos';
        } else {
            $this->provider = ( 'contentos' === $saved_provider ) ? '' : $saved_provider;
            if ( empty( $this->provider ) ) {
                $this->provider = 'none';
            }
        }

        // Model (for Gemini Imagen — user-selected or default).
        $saved_model  = $cts ? (string) $cts->get( 'ai_image_model', '' ) : '';
        $this->model  = ! empty( $saved_model ) ? $saved_model : self::IMAGEN_DEFAULT_MODEL;

        // API key (image API key, or empty to disable images).
        $this->api_key = $cts ? (string) $cts->get( 'ai_image_api_key', '' ) : '';

        // Site credentials from the centralized Core repository.
        $site_cfg = $sites[ $site_key ] ?? [];
        if ( empty( $site_cfg ) || empty( $site_cfg['active'] ) ) {
            throw new Exception( __( 'Site is inactive (or not configured).', 'contentosapp-studio' ) );
        }
		if ( $site_key === self::LOCAL_SITE_KEY ) {
            $this->local_mode  = true;
            $this->site_url    = rtrim( home_url(), '/' );
            $this->auth_header = '';
            return;
        }

        $this->site_url    = costudio_safe_http_url( rtrim( (string) ( $site_cfg['url'] ?? '' ), '/' ) );
        if ( '' === $this->site_url ) {
            throw new Exception( __( 'Invalid or unsafe site URL.', 'contentosapp-studio' ) );
        }
        $user              = $site_cfg['user']     ?? '';
        $raw_pw            = Costudio_Sites_Crypto::decrypt( $site_cfg['password'] ?? '' );
        $this->auth_header = 'Basic ' . base64_encode( "{$user}:{$raw_pw}" );
    }

    // ── Public interface ─────────────────────────────────────────────────────

    public function set_production_id( int $production_id ): void {
        $this->production_id = max( 0, $production_id );
    }

    /**
     * Cap the provider HTTP request timeout to a caller-supplied budget
     * (seconds), instead of the 150s default. Used by chunked publishing so a
     * slow provider call reliably finishes — or fails — inside the durable
     * worker's lease, instead of being killed mid-flight with no checkpoint.
     */
    public function set_http_timeout_budget( int $seconds ): void {
        $this->http_timeout_budget = max( 10, $seconds );
    }

    private function generation_timeout(): int {
        return $this->http_timeout_budget ?? 150;
    }

    private function production_tracking_uid(): string {
        if ( $this->production_id > 0 && class_exists( 'Costudio_Production' ) ) {
            return Costudio_Production::tracking_uid_for( $this->production_id );
        }

        return '';
    }

    public function is_enabled(): bool {
        if ( $this->provider === 'contentos' ) {
            return $this->local_mode || ! empty( $this->site_url );
        }
        if ( $this->local_mode ) {
            return $this->provider !== 'none' && ! empty( $this->api_key );
        }
        return $this->provider !== 'none'
            && ! empty( $this->api_key )
            && ! empty( $this->site_url );
    }

    /**
     * Generate an image and upload it to the target WP site.
     *
     * @param  string $prompt    Image prompt (English recommended).
     * @param  string $alt       Alt text for the media item.
     * @param  string $caption   Caption text.
     * @param  string $filename  Target filename, e.g. "imagem1.webp".
     * @return array { attachment_id: int, url: string }
     * @throws Exception
     */
    public function generate_and_upload( string $prompt, string $alt, string $caption, string $filename ): array {
        $this->log_debug(
            'image.generate_and_upload.start',
            [
                'filename'       => $filename,
                'prompt_length'  => strlen( $prompt ),
                'alt_length'     => strlen( $alt ),
                'caption_length' => strlen( $caption ),
                'enabled'        => $this->is_enabled(),
            ]
        );

        if ( ! $this->is_enabled() ) {
            throw new Exception( __( 'Image generation is not configured. Configure it in Contentosapp Studio -> Settings -> Images.', 'contentosapp-studio' ) );
        }

        try {
            $image_data = $this->generate_image_bytes( $prompt );

            // Convert to WebP if the filename expects it and GD is available
            if ( str_ends_with( strtolower( $filename ), '.webp' ) && function_exists( 'imagecreatefromstring' ) && function_exists( 'imagewebp' ) ) {
                $img = @imagecreatefromstring( $image_data );
                if ( $img !== false ) {
                    ob_start();
                    imagewebp( $img, null, 80 ); // 80% quality is a great balance for WebP
                    $webp_data = ob_get_clean();
                    imagedestroy( $img );
                    if ( ! empty( $webp_data ) ) {
                        $image_data = $webp_data;
                        $this->log_debug( 'image.convert_webp.success', [ 'filename' => $filename, 'new_bytes' => strlen( $image_data ) ] );
                    }
                }
            }

            $result = $this->upload_binary( $image_data, $filename, $alt, $caption );

            $this->log_info(
                'image.generate_and_upload.success',
                [
                    'filename'      => $filename,
                    'attachment_id' => (int) ( $result['attachment_id'] ?? 0 ),
                    'url'           => (string) ( $result['url'] ?? '' ),
                    'bytes'         => strlen( $image_data ),
                ]
            );

            return $result;
        } catch ( Exception $e ) {
            $this->log_error(
                'image.generate_and_upload.failed',
                [
                    'filename' => $filename,
                    'error'    => $e->getMessage(),
                ]
            );
            throw $e;
        }
    }

    public function generate_image_bytes( string $prompt ): string {
        $this->log_debug(
            'image.generate.start',
            [
                'provider'      => $this->provider,
                'model'         => $this->model,
                'prompt_length' => strlen( $prompt ),
            ]
        );

        if ( $this->provider === 'contentos' ) {
            $image_data = $this->generate_contentos( $prompt );
        } elseif ( $this->provider === 'gemini_imagen' ) {
            $image_data = $this->generate_imagen( $prompt );
        } elseif ( $this->provider === 'openai_dalle' ) {
            $image_data = $this->generate_dalle( $prompt );
        } else {
            throw new Exception( "Provedor de imagem desconhecido: {$this->provider}" );
        }

        $this->log_info(
            'image.generate.success',
            [
                'provider' => $this->provider,
                'model'    => $this->model,
                'bytes'    => strlen( $image_data ),
            ]
        );

        return $image_data;
    }

    public function upload_binary( string $image_data, string $filename, string $alt, string $caption, string $transport = 'auto' ): array {
        $filename = $this->sanitize_upload_filename( $filename );

        $this->log_debug(
            'image.upload.start',
            [
                'filename'       => $filename,
                'bytes'          => strlen( $image_data ),
                'mime_guess'     => $this->detect_mime_for_filename( $filename ),
                'local_mode'     => $this->local_mode,
                'site_url'       => $this->site_url,
                'caption_length' => strlen( $caption ),
                'alt_length'     => strlen( $alt ),
                'transport'      => $transport,
            ]
        );

        return $this->local_mode
            ? $this->upload_to_wp_local( $image_data, $filename, $alt, $caption )
            : $this->upload_to_wp( $image_data, $filename, $alt, $caption, $transport );
    }

    public function get_debug_context(): array {
        return [
            'site_key'        => $this->site_key,
            'site_url'        => $this->site_url,
            'local_mode'      => $this->local_mode,
            'provider'        => $this->provider,
            'model'           => $this->model,
            'has_api_key'     => ! empty( $this->api_key ),
            'has_auth_header' => ! empty( $this->auth_header ),
            'production_id'   => $this->production_id,
        ];
    }

    // ── ContentOS Auto ───────────────────────────────────────────────────────

    private function generate_contentos( string $prompt ): string {
        // ── Atalho local: se ContentOS API roda no mesmo servidor, ──────────
        // chama CAP_Image::generate() diretamente (sem HTTP, sem Cloudflare).
        if ( defined( 'CAP_API_SERVER' ) ) {
            return $this->generate_contentos_local( $prompt );
        }

        // ── Auto-Retry Loop para resiliência ───────────────────────────────
        $max_attempts = 3;
        $last_error   = '';

        for ( $attempt = 1; $attempt <= $max_attempts; $attempt++ ) {
            try {
                return $this->generate_contentos_remote( $prompt );
            } catch ( Costudio_Image_Provider_Unavailable_Exception $e ) {
                // Account-level billing failures are deterministic. Retrying the
                // same request cannot recover and only multiplies failed calls.
                if ( class_exists( 'Costudio_Telemetry' ) ) {
                    Costudio_Telemetry::queue( 'image.failed', [
                        'reason'  => $e->get_reason(),
                        'model'   => $this->model,
                        'attempt' => $attempt,
                    ] );
                }
                throw $e;
            } catch ( Exception $e ) {
                $last_error = $e->getMessage();
                
                // Se for erro de demanda (503) ou limite (429) ou bloqueio WAF (Cloudflare/LiteSpeed), espera e tenta de novo.
                $is_retriable = ( strpos( $last_error, '503' ) !== false || strpos( $last_error, '429' ) !== false || strpos( $last_error, 'Cloudflare/LiteSpeed' ) !== false );
                
                if ( $is_retriable && $attempt < $max_attempts ) {
                    // Espera exponencial: 2s, 4s...
                    sleep( $attempt * 2 );
                    continue;
                }

                // Log telemetry before failing.
                if ( class_exists( 'Costudio_Telemetry' ) ) {
                    Costudio_Telemetry::queue( 'image.failed', [
                        'error'   => $last_error,
                        'model'   => $this->model,
                        'attempt' => $attempt,
                    ] );
                }

                // Se não for erro de retry ou esgotou tentativas, explode.
                throw $e;
            }
        }

        throw new Exception( $last_error );
    }

    /**
     * Gera imagem localmente roteando via CAP_Routes::handle_generate_image().
     *
     * Bypassa Cloudflare/LiteSpeed (sem HTTP) mas mantém o check de plano do
     * route handler (Guideline 6 — o serviço, não o plugin, decide o gating).
     */
    private function generate_contentos_local( string $prompt ): string {
        if ( ! class_exists( 'CAP_Routes' ) && defined( 'CAP_DIR' ) ) {
            $routes_file = CAP_DIR . 'includes/class-cap-routes.php';
            if ( file_exists( $routes_file ) ) {
                require_once $routes_file;
            }
        }

        if ( ! class_exists( 'CAP_Routes' ) || ! class_exists( 'WP_REST_Request' ) ) {
            throw new Exception( __( 'Local ContentOS Image API is unavailable. CAP_Routes class not found.', 'contentosapp-studio' ) );
        }

        $license_key = (string) get_option( 'costudio_license_key', '' );
        $fingerprint = (string) get_option( 'costudio_site_fingerprint', '' );

        $body = [
            'prompt'        => $prompt,
            'production_id' => $this->production_id,
            'site_url'      => get_site_url(),
        ];
        if ( class_exists( 'Costudio_Telemetry' ) && method_exists( 'Costudio_Telemetry', 'current_site_hash' ) ) {
            $body['site_hash'] = Costudio_Telemetry::current_site_hash();
        }
        $production_uid = $this->production_tracking_uid();
        if ( '' !== $production_uid ) {
            $body['production_uid'] = $production_uid;
        }

        $request = new WP_REST_Request( 'POST', '/contentosapp-api/v1/ai/generate-image' );
        $request->set_header( 'Content-Type', 'application/json' );
        $request->set_header( 'X-FOS-Site-ID', $fingerprint );

        if ( '' !== $license_key ) {
            $request->set_header( 'Authorization', 'Bearer ' . $license_key );

            if ( class_exists( 'Costudio_Standalone_Crypto' ) ) {
                $secret = class_exists( 'Costudio_Standalone_License' )
                    ? Costudio_Standalone_License::get_signing_secret( $license_key )
                    : $license_key;
                $request->set_header( 'X-FOS-Signature', Costudio_Standalone_Crypto::sign_payload( wp_json_encode( $body ), $secret ) );
            }
        }

        $request->set_body( wp_json_encode( $body ) );

        $response = CAP_Routes::handle_generate_image( $request );
        $code     = (int) $response->get_status();
        $data     = (array) $response->get_data();

        // Plan gating: service returns HTTP 200 with generated:false for Starter plans.
        if ( isset( $data['generated'] ) && $data['generated'] === false ) {
            throw new Costudio_Image_Plan_Limited_Exception( (string) ( $data['reason'] ?? 'plan_limit' ) );
        }

        $this->throw_if_managed_provider_unavailable( $code, $data );

        if ( $code !== 200 ) {
            $msg = $data['error']['message'] ?? $data['message'] ?? "HTTP {$code}";
            $this->log_error( 'image.generate.contentos_local.failed', [ 'code' => $code, 'error' => $msg ] );
            throw new Exception( __( 'ContentOS Image generation error:', 'contentosapp-studio' ) . ' ' . $msg );
        }

        $b64 = $data['data'] ?? '';
        if ( empty( $b64 ) ) {
            throw new Exception( __( 'ContentOS Image API returned an empty image (local mode).', 'contentosapp-studio' ) );
        }

        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
        return base64_decode( $b64 );
    }

    /**
     * Gera imagem via chamada HTTP remota ao contentosapp.com.
     * Usado quando Contentosapp Studio roda em servidor diferente da API.
     */
    private function generate_contentos_remote( string $prompt ): string {
        $license_key = get_option( 'costudio_license_key', '' );
        
        $fingerprint = get_option( 'costudio_site_fingerprint' );
        if ( ! $fingerprint ) {
            $fingerprint = md5( get_site_url() . wp_generate_password( 20, false ) );
            update_option( 'costudio_site_fingerprint', $fingerprint, false );
        }

        $body = [
            'prompt'        => $prompt,
            'production_id' => $this->production_id,
            'site_url'      => get_site_url(),
        ];
        if ( class_exists( 'Costudio_Telemetry' ) && method_exists( 'Costudio_Telemetry', 'current_site_hash' ) ) {
            $body['site_hash'] = Costudio_Telemetry::current_site_hash();
        }
        $production_uid = $this->production_tracking_uid();
        if ( '' !== $production_uid ) {
            $body['production_uid'] = $production_uid;
        }

        // Cabeçalhos para contornar WAF (Cloudflare/LiteSpeed).
        $base_headers = [
            'Content-Type'     => 'application/json',
            'Accept'           => 'application/json, */*;q=0.8',
            'X-FOS-Site-ID'    => $fingerprint,
            'X-Requested-With' => 'XMLHttpRequest',
            'Origin'           => COSTUDIO_API_URL,
            'Referer'          => COSTUDIO_API_URL . '/wp-admin/',
        ];

        if ( ! empty( $license_key ) ) {
            $base_headers['Authorization'] = 'Bearer ' . $license_key;

            // Assinatura HMAC para garantir integridade e autenticidade.
            if ( class_exists( 'Costudio_Standalone_Crypto' ) ) {
                $secret = class_exists( 'Costudio_Standalone_License' )
                    ? Costudio_Standalone_License::get_signing_secret( $license_key )
                    : $license_key;
                $base_headers['X-FOS-Signature'] = Costudio_Standalone_Crypto::sign_payload( wp_json_encode( $body ), $secret );
            }
        }

        $timeout = (int) apply_filters( 'costudio_contentos_api_timeout', 180 );
        $ua      = self::REMOTE_BROWSER_UA;

        // 1. Tentar via admin-ajax (mais resiliente a firewalls).
        $endpoint_ajax = costudio_api_ajax_url( 'cap_api_generate_image' );
        $response = wp_remote_post( $endpoint_ajax, [
            'timeout'    => max( 30, $timeout ),
            'headers'    => $base_headers,
            'body'       => wp_json_encode( $body ),
            'user-agent' => $ua,
        ] );

        // 2. Se falhar ou não for JSON, tentar via REST API.
        $raw = is_wp_error( $response ) ? '' : wp_remote_retrieve_body( $response );
        $this->log_debug(
            'image.generate.contentos_remote.ajax_response',
            [
                'endpoint' => $endpoint_ajax,
                'wp_error' => is_wp_error( $response ),
                'code'     => is_wp_error( $response ) ? null : (int) wp_remote_retrieve_response_code( $response ),
                'snippet'  => is_string( $raw ) ? substr( $raw, 0, 180 ) : '',
            ]
        );
        if ( is_wp_error( $response ) || empty( $raw ) || $raw[0] !== '{' ) {
            $endpoint_rest = COSTUDIO_API_URL . '/wp-json/contentosapp-api/v1/ai/generate-image';
            $response = wp_remote_post( $endpoint_rest, [
                'timeout'    => max( 30, $timeout ),
                'headers'    => $base_headers,
                'body'       => wp_json_encode( $body ),
                'user-agent' => $ua,
            ] );
            $raw = is_wp_error( $response ) ? '' : wp_remote_retrieve_body( $response );
            $this->log_debug(
                'image.generate.contentos_remote.rest_response',
                [
                    'endpoint' => $endpoint_rest,
                    'wp_error' => is_wp_error( $response ),
                    'code'     => is_wp_error( $response ) ? null : (int) wp_remote_retrieve_response_code( $response ),
                    'snippet'  => is_string( $raw ) ? substr( $raw, 0, 180 ) : '',
                ]
            );
        }

        if ( is_wp_error( $response ) ) {
            throw new Exception( __( 'ContentOS Image API error:', 'contentosapp-studio' ) . ' ' . $response->get_error_message() );
        }

        $code = (int) wp_remote_retrieve_response_code( $response );
        $raw  = isset( $raw ) && is_string( $raw ) ? $raw : wp_remote_retrieve_body( $response );
        $data = json_decode( $raw, true );

        $this->throw_if_managed_provider_unavailable(
            $code,
            is_array( $data ) ? $data : [],
            $raw
        );

        if ( $code !== 200 || ! is_array( $data ) ) {
            // Tratar bloqueio transparente de firewall (WAF / LiteSpeed Bot Verification / Cloudflare)
            if ( stripos( $raw, '<html' ) !== false && ( stripos( $raw, 'grecaptcha' ) !== false || stripos( $raw, 'cloudflare' ) !== false || stripos( $raw, 'bot verification' ) !== false || stripos( $raw, '<head' ) !== false ) ) {
                throw new Exception( __( 'Blocked by Cloudflare/LiteSpeed or firewall on contentosapp.com. The API returned an HTML page instead of JSON.', 'contentosapp-studio' ) );
            }

            $msg = $data['error']['message'] ?? $data['message'] ?? substr( $raw, 0, 200 );
            if ( $code === 429 ) {
                throw new Exception( __( 'ContentOS Auto managed-service quota or rate limit reached. BYOK image generation remains available with your own provider key.', 'contentosapp-studio' ) );
            }
            if ( $code === 401 || $code === 403 ) {
                throw new Exception( __( 'ContentOS Auto service authentication failed. Check the managed-service license in the License menu, or switch image generation to BYOK with your own provider key.', 'contentosapp-studio' ) );
            }
            throw new Exception( "ContentOS Image API Error ({$code}): {$msg}" );
        }

        // Plan gating: service returns HTTP 200 with generated:false for Starter plans.
        // Keep the [IMG:] placeholder in the article instead of failing the production.
        if ( isset( $data['generated'] ) && $data['generated'] === false ) {
            throw new Costudio_Image_Plan_Limited_Exception( (string) ( $data['reason'] ?? 'plan_limit' ) );
        }

        $b64 = $data['data'] ?? $data['output'] ?? $data['b64'] ?? '';

        if ( empty( $b64 ) ) {
            $dump = is_array($data) ? wp_json_encode($data) : substr($raw, 0, 200);
            throw new Exception( __( 'ContentOS Image API returned an empty image.', 'contentosapp-studio' ) . ' Raw response: ' . $dump );
        }

        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
        return base64_decode( $b64 );
    }

    /**
     * Convert both the structured API response (1.2.63+) and the legacy
     * 500-wrapped Gemini message into one deterministic exception.
     */
    private function throw_if_managed_provider_unavailable( int $http_code, array $data, string $raw = '' ): void {
        $error_code = strtolower( (string) ( $data['error']['code'] ?? '' ) );
        $reason     = strtolower( (string) ( $data['error']['reason'] ?? '' ) );
        $message    = (string) ( $data['error']['message'] ?? $data['message'] ?? $raw );
        $haystack   = strtolower( $error_code . ' ' . $reason . ' ' . $message );

        $structured = 'managed_image_provider_billing' === $error_code
            || 'provider_billing' === $reason;
        $legacy = strpos( $haystack, 'prepayment credits are depleted' ) !== false
            || strpos( $haystack, 'billing#prepay' ) !== false
            || strpos( $haystack, 'manage your project and billing' ) !== false;

        if ( ! $structured && ! $legacy ) {
            return;
        }

        $this->log_error(
            'image.generate.managed_provider_unavailable',
            [
                'http_code' => $http_code,
                'reason'    => 'provider_billing',
                'api_code'  => $error_code,
                'provider'  => $this->provider,
                'model'     => $this->model,
            ]
        );

        throw new Costudio_Image_Provider_Unavailable_Exception(
            self::managed_provider_unavailable_message(),
            'provider_billing'
        );
    }

    public static function managed_provider_unavailable_message(): string {
        return __(
            'The ContentOS Auto image service is temporarily unavailable. Your article will still be published normally, without the missing AI images. Please try again later or use a BYOK image provider in Settings → AI.',
            'contentosapp-studio'
        );
    }

    // ── Gemini Imagen ────────────────────────────────────────────────────────

    /**
     * Detect whether the selected model uses the legacy Imagen :predict endpoint
     * (imagegeneration@*, imagen-3.*) or the newer Gemini :generateContent endpoint
     * (gemini-2.*, flash-image-*, nano-banana / Nano Banana, etc.).
     */
    private function is_legacy_imagen_model(): bool {
        $m = strtolower( $this->model );
        return (
            strpos( $m, 'imagegeneration' ) !== false ||
            ( strpos( $m, 'imagen-3' ) !== false && strpos( $m, 'gemini' ) === false ) ||
            ( strpos( $m, 'imagen-4' ) !== false && strpos( $m, 'gemini' ) === false )
        );
    }

    private function generate_imagen( string $prompt ): string {
        $is_legacy = $this->is_legacy_imagen_model();
        $this->log_debug(
            'image.generate.imagen.routing',
            [
                'model'      => $this->model,
                'is_legacy'  => $is_legacy,
                'endpoint'   => $is_legacy ? ':predict' : ':generateContent',
            ]
        );
        return $is_legacy
            ? $this->generate_imagen_legacy( $prompt )
            : $this->generate_imagen_gemini( $prompt );
    }

    /**
     * Imagen 3 / legacy Vertex AI-style endpoint.
     * POST models/{model}:predict  →  predictions[0].bytesBase64Encoded
     */
    private function generate_imagen_legacy( string $prompt ): string {
        $body = [
            'instances'  => [ [ 'prompt' => $prompt ] ],
            'parameters' => [
                'sampleCount'       => 1,
                'aspectRatio'       => '16:9',
                'safetyFilterLevel' => 'BLOCK_SOME',
                'personGeneration'  => 'DONT_ALLOW',
            ],
        ];

        $endpoint = self::IMAGEN_ENDPOINT_BASE . $this->model . ':predict';
        $response = $this->post_with_retry( $endpoint . '?key=' . $this->api_key, [
            'timeout' => $this->generation_timeout(),
            'headers' => [ 'Content-Type' => 'application/json' ],
            'body'    => wp_json_encode( $body ),
        ] );

        if ( is_wp_error( $response ) ) {
            throw new Exception( __( 'Imagen API error:', 'contentosapp-studio' ) . ' ' . $response->get_error_message() );
        }

        $code = (int) wp_remote_retrieve_response_code( $response );
        $data = json_decode( wp_remote_retrieve_body( $response ), true );

        if ( $code !== 200 ) {
            $msg = $data['error']['message'] ?? wp_remote_retrieve_body( $response );
            throw new Exception( "Imagen API HTTP {$code}: {$msg}" );
        }

        $b64 = $data['predictions'][0]['bytesBase64Encoded'] ?? '';
        if ( empty( $b64 ) ) {
            throw new Exception( __( 'Imagen API returned an empty image.', 'contentosapp-studio' ) );
        }

        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
        return base64_decode( $b64 );
    }

    /**
     * Gemini 2.x image generation (flash-image-preview, Nano Banana, etc.).
     * POST models/{model}:generateContent  →  candidates[0].content.parts[].inlineData.data
     */
    private function generate_imagen_gemini( string $prompt ): string {
        $body = [
            'contents'         => [
                [
                    'parts' => [ [ 'text' => $prompt ] ],
                ],
            ],
            'generationConfig' => [
                'responseModalities' => [ 'IMAGE', 'TEXT' ],
            ],
        ];

        $endpoint = self::IMAGEN_ENDPOINT_BASE . $this->model . ':generateContent';
        $response = $this->post_with_retry( $endpoint . '?key=' . $this->api_key, [
            'timeout' => $this->generation_timeout(),
            'headers' => [ 'Content-Type' => 'application/json' ],
            'body'    => wp_json_encode( $body ),
        ] );

        if ( is_wp_error( $response ) ) {
            throw new Exception( __( 'Gemini Image API error:', 'contentosapp-studio' ) . ' ' . $response->get_error_message() );
        }

        $code = (int) wp_remote_retrieve_response_code( $response );
        $data = json_decode( wp_remote_retrieve_body( $response ), true );

        if ( $code !== 200 ) {
            $msg = $data['error']['message'] ?? wp_remote_retrieve_body( $response );
            throw new Exception( "Gemini Image API HTTP {$code}: {$msg}" );
        }

        // Find the first inlineData part across all candidates/parts.
        $b64 = '';
        foreach ( (array) ( $data['candidates'] ?? [] ) as $candidate ) {
            foreach ( (array) ( $candidate['content']['parts'] ?? [] ) as $part ) {
                if ( ! empty( $part['inlineData']['data'] ) ) {
                    $b64 = $part['inlineData']['data'];
                    break 2;
                }
            }
        }

        if ( empty( $b64 ) ) {
            throw new Exception( __( 'Gemini Image API returned an empty image.', 'contentosapp-studio' ) );
        }

        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
        return base64_decode( $b64 );
    }

    // ── OpenAI DALL-E ─────────────────────────────────────────────────────────

    private function generate_dalle( string $prompt ): string {
        $body = [
            'model'   => 'gpt-image-1',
            'prompt'  => $prompt,
            'n'       => 1,
            'size'    => '1792x1024',
            'quality' => 'standard',
        ];

        $response = $this->post_with_retry( self::DALLE_ENDPOINT, [
            'timeout' => $this->generation_timeout(),
            'headers' => [
                'Authorization' => 'Bearer ' . $this->api_key,
                'Content-Type'  => 'application/json',
            ],
            'body' => wp_json_encode( $body ),
        ] );

        if ( is_wp_error( $response ) ) {
            throw new Exception( __( 'DALL-E API error:', 'contentosapp-studio' ) . ' ' . $response->get_error_message() );
        }

        $code = (int) wp_remote_retrieve_response_code( $response );
        $data = json_decode( wp_remote_retrieve_body( $response ), true );

        if ( $code !== 200 ) {
            $msg = $data['error']['message'] ?? wp_remote_retrieve_body( $response );
            throw new Exception( "DALL-E API HTTP {$code}: {$msg}" );
        }

        $temp_url = costudio_safe_http_url( (string) ( $data['data'][0]['url'] ?? '' ) );
        if ( empty( $temp_url ) ) {
            throw new Exception( __( 'DALL-E returned an empty or unsafe URL.', 'contentosapp-studio' ) );
        }

        $img_response = wp_safe_remote_get( $temp_url, costudio_safe_remote_args( [ 'timeout' => 30 ] ) );
        if ( is_wp_error( $img_response ) ) {
            throw new Exception( __( 'Failed to download DALL-E image:', 'contentosapp-studio' ) . ' ' . $img_response->get_error_message() );
        }

        $img_code = (int) wp_remote_retrieve_response_code( $img_response );
        if ( $img_code < 200 || $img_code >= 300 ) {
            throw new Exception( "DALL-E image download HTTP {$img_code}" );
        }

        $content_type = strtolower( (string) wp_remote_retrieve_header( $img_response, 'content-type' ) );
        if ( '' !== $content_type && strpos( $content_type, 'image/' ) !== 0 && strpos( $content_type, 'application/octet-stream' ) !== 0 ) {
            throw new Exception( __( 'DALL-E image download returned a non-image response.', 'contentosapp-studio' ) );
        }

        $body = (string) wp_remote_retrieve_body( $img_response );
        if ( '' === $body ) {
            throw new Exception( __( 'DALL-E image download returned an empty body.', 'contentosapp-studio' ) );
        }

        return $body;
    }

    // ── Upload to WP Media Library ────────────────────────────────────────────

    private function upload_to_wp( string $image_data, string $filename, string $alt, string $caption, string $transport = 'auto' ): array {
        if ( empty( $this->site_url ) ) {
            throw new Exception( __( 'Site URL is not configured.', 'contentosapp-studio' ) );
        }

        $mime = $this->detect_mime_for_filename( $filename );
        $attempts = $transport === 'auto'
            ? [ 'raw', 'multipart' ]
            : [ $transport ];

        $last_exception = null;

        foreach ( $attempts as $attempt_transport ) {
            try {
                if ( $attempt_transport === 'multipart' ) {
                    return $this->upload_to_wp_remote_multipart( $image_data, $filename, $alt, $caption, $mime );
                }

                return $this->upload_to_wp_remote_raw( $image_data, $filename, $alt, $caption, $mime );
            } catch ( Exception $e ) {
                $last_exception = $e;

                if ( $attempt_transport !== 'raw' || ! $this->should_retry_media_upload_with_multipart( $e->getMessage() ) ) {
                    throw $e;
                }

                $this->log_info(
                    'image.upload.remote.fallback_to_multipart',
                    [
                        'filename' => $filename,
                        'reason'   => $e->getMessage(),
                    ]
                );
            }
        }

        throw $last_exception instanceof Exception
            ? $last_exception
            : new Exception( __( 'Remote upload failed.', 'contentosapp-studio' ) );
    }

    // ── Local (native WP) media upload ────────────────────────────────────────

    private function upload_to_wp_local( string $image_data, string $filename, string $alt, string $caption ): array {
        require_once ABSPATH . 'wp-admin/includes/media.php';
        require_once ABSPATH . 'wp-admin/includes/file.php';
        require_once ABSPATH . 'wp-admin/includes/image.php';

        $mime = $this->detect_mime_for_filename( $filename );

        $upload = wp_upload_bits( $filename, null, $image_data );
        if ( ! empty( $upload['error'] ) ) {
            $this->log_error(
                'image.upload.local.failed',
                [
                    'filename' => $filename,
                    'error'    => $upload['error'],
                ]
            );
            throw new Exception( 'Local upload error: ' . $upload['error'] );
        }

        $attachment_id = wp_insert_attachment(
            [
                'post_mime_type' => $mime,
                'post_title'     => sanitize_file_name( pathinfo( $filename, PATHINFO_FILENAME ) ),
                'post_content'   => '',
                'post_status'    => 'inherit',
                'post_excerpt'   => sanitize_text_field( $caption ),
            ],
            $upload['file']
        );

        if ( is_wp_error( $attachment_id ) ) {
            throw new Exception( $attachment_id->get_error_message() );
        }

        wp_update_attachment_metadata(
            $attachment_id,
            wp_generate_attachment_metadata( $attachment_id, $upload['file'] )
        );

        update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $alt ) );

        $this->log_info(
            'image.upload.local.success',
            [
                'filename'      => $filename,
                'attachment_id' => $attachment_id,
                'url'           => $upload['url'],
            ]
        );

        return [
            'attachment_id' => $attachment_id,
            'url'           => $upload['url'],
        ];
    }

    /**
     * POST helper with one automatic retry on transport-level failures
     * (cURL timeout, DNS, connection reset). HTTP errors from the server
     * (4xx/5xx) are NOT retried — they're returned to the caller as-is.
     *
     * Imagen 4 / Gemini 2.x image generation can occasionally take 60-90s,
     * so a single retry with short backoff prevents spurious cURL error 28s.
     *
     * @param  string $url
     * @param  array  $args  wp_remote_post args (timeout already set by caller).
     * @return array|WP_Error
     */
    private function post_with_retry( string $url, array $args ) {
        $response = wp_safe_remote_post( $url, costudio_safe_remote_args( $args ) );

        if ( ! is_wp_error( $response ) ) {
            return $response;
        }

        // Short backoff before single retry. Pipeline already runs in background,
        // so blocking 2s is acceptable — the alternative is failing the image entirely.
        sleep( 2 );

        $this->log_info(
            'image.api.retry',
            [
                'url'   => preg_replace( '/key=[^&]+/', 'key=***', $url ),
                'error' => $response->get_error_message(),
            ]
        );

        return wp_safe_remote_post( $url, costudio_safe_remote_args( $args ) );
    }

    private function detect_mime_for_filename( string $filename ): string {
        $ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
        $mime_map = [
            'jpg'  => 'image/jpeg',
            'jpeg' => 'image/jpeg',
            'gif'  => 'image/gif',
            'webp' => 'image/webp',
            'png'  => 'image/png',
        ];

        return $mime_map[ $ext ] ?? 'image/png';
    }

    private function sanitize_upload_filename( string $filename ): string {
        $filename = str_replace( array( "\r", "\n", '\\', '/' ), '-', $filename );
        $filename = function_exists( 'sanitize_file_name' )
            ? sanitize_file_name( $filename )
            : preg_replace( '/[^a-z0-9._-]+/i', '-', $filename );
        $filename = trim( (string) $filename, '.-_' );

        if ( '' === $filename ) {
            return 'costudio-image.webp';
        }

        $ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
        if ( ! in_array( $ext, array( 'jpg', 'jpeg', 'gif', 'webp', 'png' ), true ) ) {
            $filename .= '.webp';
        }

        return $filename;
    }

    private function upload_to_wp_remote_raw( string $image_data, string $filename, string $alt, string $caption, string $mime ): array {
        $url = costudio_safe_http_url( "{$this->site_url}/wp-json/wp/v2/media" );
        if ( '' === $url ) {
            throw new Exception( __( 'Invalid or unsafe site URL.', 'contentosapp-studio' ) );
        }

        $upload = wp_safe_remote_post(
            $url,
            costudio_safe_remote_args( [
                'timeout'    => 60,
                'headers'    => $this->build_remote_media_headers( 'raw', $filename, $mime ),
                'user-agent' => self::REMOTE_BROWSER_UA,
                'body'       => $image_data,
            ] )
        );

        return $this->finalize_remote_media_upload( $upload, $filename, $alt, $caption, 'raw' );
    }

    private function upload_to_wp_remote_multipart( string $image_data, string $filename, string $alt, string $caption, string $mime ): array {
        $boundary = '----Contentosapp StudioBoundary' . wp_generate_password( 16, false, false );
        $body     = $this->build_multipart_body( $boundary, $filename, $mime, $image_data );

        $url = costudio_safe_http_url( "{$this->site_url}/wp-json/wp/v2/media" );
        if ( '' === $url ) {
            throw new Exception( __( 'Invalid or unsafe site URL.', 'contentosapp-studio' ) );
        }

        $upload = wp_safe_remote_post(
            $url,
            costudio_safe_remote_args( [
                'timeout'    => 60,
                'headers'    => $this->build_remote_media_headers( 'multipart', $filename, $mime, $boundary ),
                'user-agent' => self::REMOTE_BROWSER_UA,
                'body'       => $body,
            ] )
        );

        return $this->finalize_remote_media_upload( $upload, $filename, $alt, $caption, 'multipart' );
    }

    private function finalize_remote_media_upload( $upload, string $filename, string $alt, string $caption, string $transport ): array {
        if ( is_wp_error( $upload ) ) {
            $this->log_error(
                'image.upload.remote.wp_error',
                [
                    'filename'  => $filename,
                    'transport' => $transport,
                    'error'     => $upload->get_error_message(),
                ]
            );
            throw new Exception( __( 'Image upload failed:', 'contentosapp-studio' ) . ' ' . $upload->get_error_message() );
        }

        $upload_code = (int) wp_remote_retrieve_response_code( $upload );
        $upload_raw  = (string) wp_remote_retrieve_body( $upload );
        $upload_body = json_decode( $upload_raw, true );

        $this->log_debug(
            'image.upload.remote.response',
            [
                'filename'  => $filename,
                'transport' => $transport,
                'code'      => $upload_code,
                'headers'   => $this->normalize_response_headers( $upload ),
                'snippet'   => substr( $upload_raw, 0, 240 ),
            ]
        );

        if ( ! in_array( $upload_code, [ 200, 201 ], true ) ) {
            $msg = $upload_body['message'] ?? $upload_raw;
            throw new Exception( "WP Media upload HTTP {$upload_code}: {$msg}" );
        }

        $attachment_id  = (int) ( $upload_body['id'] ?? 0 );
        $attachment_url = $upload_body['source_url'] ?? '';

        if ( ! $attachment_id ) {
            throw new Exception( __( 'Upload completed, but attachment_id was not returned.', 'contentosapp-studio' ) );
        }

        $this->update_remote_media_meta( $attachment_id, $alt, $caption );

        $this->log_info(
            'image.upload.remote.success',
            [
                'filename'      => $filename,
                'transport'     => $transport,
                'attachment_id' => $attachment_id,
                'url'           => $attachment_url,
            ]
        );

        return [
            'attachment_id' => $attachment_id,
            'url'           => $attachment_url,
        ];
    }

    private function update_remote_media_meta( int $attachment_id, string $alt, string $caption ): void {
        $url = costudio_safe_http_url( "{$this->site_url}/wp-json/wp/v2/media/{$attachment_id}" );
        if ( '' === $url ) {
            $this->log_error(
                'image.upload.remote.meta_update.unsafe_url',
                [ 'attachment_id' => $attachment_id ]
            );
            return;
        }

        $response = wp_safe_remote_request(
            $url,
            costudio_safe_remote_args( [
                'method'  => 'POST',
                'timeout' => 15,
                'headers' => [
                    'Authorization' => $this->auth_header,
                    'Content-Type'  => 'application/json',
                    'Accept'        => 'application/json',
                ],
                'user-agent' => self::REMOTE_BROWSER_UA,
                'body'       => wp_json_encode(
                    [
                        'alt_text' => $alt,
                        'caption'  => $caption,
                    ]
                ),
            ] )
        );

        if ( is_wp_error( $response ) ) {
            $this->log_error(
                'image.upload.remote.meta_update.wp_error',
                [
                    'attachment_id' => $attachment_id,
                    'error'         => $response->get_error_message(),
                ]
            );
            return;
        }

        $this->log_debug(
            'image.upload.remote.meta_update.response',
            [
                'attachment_id' => $attachment_id,
                'code'          => (int) wp_remote_retrieve_response_code( $response ),
                'snippet'       => substr( (string) wp_remote_retrieve_body( $response ), 0, 180 ),
            ]
        );
    }

    private function build_remote_media_headers( string $transport, string $filename, string $mime, string $boundary = '' ): array {
        $headers = [
            'Authorization'   => $this->auth_header,
            'Accept'          => 'application/json, */*;q=0.8',
            'Accept-Language' => 'en-US,en;q=0.9,pt-BR;q=0.8,pt;q=0.7',
            'Origin'          => $this->site_url,
            'Referer'         => $this->site_url . '/wp-admin/',
            'X-Requested-With' => 'XMLHttpRequest',
        ];

        if ( $transport === 'multipart' ) {
            $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
        } else {
            $headers['Content-Disposition'] = 'attachment; filename="' . $filename . '"';
            $headers['Content-Type']        = $mime;
        }

        return $headers;
    }

    private function build_multipart_body( string $boundary, string $filename, string $mime, string $image_data ): string {
        $eol  = "\r\n";
        $body = '';
        $body .= '--' . $boundary . $eol;
        $body .= 'Content-Disposition: form-data; name="file"; filename="' . $filename . '"' . $eol;
        $body .= 'Content-Type: ' . $mime . $eol . $eol;
        $body .= $image_data . $eol;
        $body .= '--' . $boundary . '--' . $eol;

        return $body;
    }

    private function should_retry_media_upload_with_multipart( string $message ): bool {
        $needles = [
            'WP Media upload HTTP 406',
            'Mod_Security',
            'Not Acceptable',
            'HTTP 403',
        ];

        foreach ( $needles as $needle ) {
            if ( stripos( $message, $needle ) !== false ) {
                return true;
            }
        }

        return false;
    }

    private function normalize_response_headers( $response ): array {
        $headers = wp_remote_retrieve_headers( $response );

        if ( is_array( $headers ) ) {
            return $headers;
        }

        if ( is_object( $headers ) && method_exists( $headers, 'getAll' ) ) {
            $all = $headers->getAll();
            return is_array( $all ) ? $all : [];
        }

        if ( $headers instanceof Traversable ) {
            return iterator_to_array( $headers );
        }

        return [];
    }

    private function log_debug( string $message, array $context = [] ): void {
        $this->log( 'debug', $message, $context );
    }

    private function log_info( string $message, array $context = [] ): void {
        $this->log( 'info', $message, $context );
    }

    private function log_error( string $message, array $context = [] ): void {
        $this->log( 'error', $message, $context );
    }

    private function log( string $level, string $message, array $context = [] ): void {
        if ( $level === 'debug' && ! costudio_debug_enabled() ) {
            return;
        }

		$payload = array_merge( $this->get_debug_context(), $context );
		$encoded = costudio_redact_log_value( $payload );
		if ( costudio_debug_enabled() ) {
			costudio_log_debug( 'image.' . $message, $payload );
		}

		if ( ! class_exists( 'Costudio_Logger' ) ) {
			return;
        }

        $safe_payload = json_decode( $encoded, true );
        if ( ! is_array( $safe_payload ) ) {
            $safe_payload = [ 'context' => $encoded ];
        }

        if ( $level === 'error' ) {
            Costudio_Logger::error( $message, $safe_payload, $this->production_id ?: null );
        } elseif ( $level === 'info' ) {
            Costudio_Logger::info( $message, $safe_payload, $this->production_id ?: null );
        } else {
            Costudio_Logger::debug( $message, $safe_payload, $this->production_id ?: null );
        }
    }
}
