<?php
/**
 * DBA Image Generator - Generates featured images via OpenAI DALL-E
 *
 * ═══════════════════════════════════════════════════════════════════
 *  IMAGE SPEC:
 *    Subject:     Entrepreneur (woman or man, randomly chosen by AI)
 *                 in portrait pose (upper body / walking / candid)
 *    Orientation: Landscape (950 x 534 px — 16:9 crop)
 *    File weight:  ≤ 90 KB (JPEG compression loop)
 *    Colors:       Realistic, muted, natural — no HDR / oversaturation
 *    Style:        Candid editorial photo, real city in background
 *
 *  COUNTRY-AGNOSTIC: Works for Italy, France, Spain, Portugal —
 *  any GeoBot-registered country. The city name comes from the
 *  AI-generated prompt (built by class-geobot-generator.php).
 * ═══════════════════════════════════════════════════════════════════
 */
class DBA_Image_Generator {

    private static $instance = null;
    private $api_key;
    private $image_api_url = 'https://api.openai.com/v1/images/generations';

    /** Target dimensions — 950px wide, landscape 16:9 */
    const TARGET_WIDTH  = 950;
    const TARGET_HEIGHT = 534;

    /** Maximum file weight in bytes (90 KB) */
    const MAX_FILE_BYTES = 92160; // 90 * 1024

    /** JPEG quality starting point — will decrease until ≤ MAX_FILE_BYTES */
    const JPEG_QUALITY_START = 82;
    const JPEG_QUALITY_MIN   = 45;

    public static function get_instance() {
        if (null === self::$instance) self::$instance = new self();
        return self::$instance;
    }

    private function __construct() {
        $settings = get_option('dba_settings', array());
        $this->api_key = isset($settings['openai_api_key']) ? $settings['openai_api_key'] : '';
    }

    /**
     * Generate image and upload to WP Media Library.
     * Returns: attachment ID or WP_Error
     *
     * @param array $image_data  Keys: prompt, filename, alt, title, caption, description
     * @param int   $post_id     The WP post this image is for.
     */
    public function generate_and_upload($image_data, $post_id = 0) {
        if (empty($this->api_key)) {
            return new WP_Error('no_api_key', 'OpenAI API key not configured. Set it in Blog Damalion AI → Settings.');
        }

        if (empty($image_data['prompt'])) {
            return new WP_Error('no_prompt', 'No image prompt provided in article JSON.');
        }

        $settings = get_option('dba_settings', array());
        $model = isset($settings['image_model']) ? $settings['image_model'] : 'dall-e-3';

        // Target dimensions (settings override, fallback to constants)
        $content_settings = get_option('dba_content_settings', array());
        $target_w = intval($content_settings['image_width'] ?? self::TARGET_WIDTH);
        $target_h = intval($content_settings['image_height'] ?? self::TARGET_HEIGHT);
        if ($target_w < 100) $target_w = self::TARGET_WIDTH;
        if ($target_h < 100) $target_h = self::TARGET_HEIGHT;

        // ── Build the final DALL-E prompt ──
        $final_prompt = $this->build_final_prompt($image_data['prompt'], $post_id);

        // ── API request body ──
        $api_body = array(
            'model'  => $model,
            'prompt' => $final_prompt,
            'n'      => 1,
        );

        if ($model === 'gpt-image-1') {
            $target_ratio = $target_w / $target_h;
            if ($target_ratio > 1.3) {
                $api_body['size'] = '1536x1024';
            } elseif ($target_ratio < 0.77) {
                $api_body['size'] = '1024x1536';
            } else {
                $api_body['size'] = '1024x1024';
            }
            $api_body['response_format'] = 'url';
        } else {
            // DALL-E 3
            $target_ratio = $target_w / $target_h;
            if ($target_ratio > 1.3) {
                $api_body['size'] = '1792x1024'; // Landscape
            } elseif ($target_ratio < 0.77) {
                $api_body['size'] = '1024x1792';
            } else {
                $api_body['size'] = '1024x1024';
            }
            $api_body['quality'] = 'hd';
            $api_body['style']   = 'natural';
            $api_body['response_format'] = 'url';
        }

        error_log('DBA Image: Using ' . $model . ' at ' . $api_body['size'] . ' -> resize to ' . $target_w . 'x' . $target_h . ' -> compress <=' . round(self::MAX_FILE_BYTES / 1024) . 'KB');

        // ── API call with retry on 429 ──
        $response    = null;
        $status_code = 0;

        for ($attempt = 0; $attempt < 3; $attempt++) {
            if ($attempt > 0) {
                $wait = min(pow(2, $attempt) * 15, 60);
                error_log('DBA Image: 429 rate limit — waiting ' . $wait . 's before retry #' . ($attempt + 1));
                sleep($wait);
            }

            $response = wp_remote_post($this->image_api_url, array(
                'timeout' => 120,
                'headers' => array(
                    'Authorization' => 'Bearer ' . $this->api_key,
                    'Content-Type'  => 'application/json',
                ),
                'body' => wp_json_encode($api_body),
            ));

            if (is_wp_error($response)) {
                error_log('DBA Image ERROR: ' . $response->get_error_message());
                return $response;
            }

            $status_code = wp_remote_retrieve_response_code($response);
            if ($status_code === 200) break;
            if ($status_code !== 429) break;
        }

        if ($status_code !== 200) {
            $body_text = wp_remote_retrieve_body($response);
            error_log('DBA Image API error ' . $status_code . ': ' . $body_text);

            // Parse API error for better feedback
            $api_error = json_decode($body_text, true);
            $error_msg = 'Image API returned HTTP ' . $status_code;
            if (!empty($api_error['error']['message'])) {
                $error_msg .= ': ' . $api_error['error']['message'];
            }
            if ($status_code === 429) {
                $error_msg = 'DALL-E rate limit (429). Check your OpenAI billing at https://platform.openai.com/settings/organization/billing';
            } elseif ($status_code === 401) {
                $error_msg = 'OpenAI API key invalid or expired (401). Check Blog Damalion AI → Settings.';
            } elseif ($status_code === 400) {
                $error_msg = 'DALL-E rejected the prompt (400). ' . ($api_error['error']['message'] ?? 'Check the prompt content.');
            }

            return new WP_Error('api_error', $error_msg);
        }

        $result = json_decode(wp_remote_retrieve_body($response), true);

        // ── Download image ──
        $tmp_file = false;

        if (!empty($result['data'][0]['url'])) {
            $tmp_file = download_url($result['data'][0]['url'], 60);
            if (is_wp_error($tmp_file)) {
                error_log('DBA Image download error: ' . $tmp_file->get_error_message());
                return $tmp_file;
            }
        } elseif (!empty($result['data'][0]['b64_json'])) {
            $tmp_file = wp_tempnam('dba-img-');
            $decoded  = base64_decode($result['data'][0]['b64_json']);
            if ($decoded === false) {
                return new WP_Error('decode_error', 'Failed to decode base64 image');
            }
            file_put_contents($tmp_file, $decoded);
        } else {
            error_log('DBA Image: No image in response — ' . wp_remote_retrieve_body($response));
            return new WP_Error('no_image', 'No image URL or data in API response');
        }

        // ── Resize + Compress to <= 90KB JPEG ──
        $final_file = $this->resize_and_compress($tmp_file, $target_w, $target_h);

        if ($final_file && $final_file !== $tmp_file) {
            @unlink($tmp_file);
            $tmp_file = $final_file;
        }

        $final_size = filesize($tmp_file);
        error_log('DBA Image: Final file ' . round($final_size / 1024) . 'KB (' . $target_w . 'x' . $target_h . ')');

        // ── Prepare filename (.jpg, not .webp) ──
        $filename = !empty($image_data['filename'])
            ? sanitize_file_name($image_data['filename'])
            : sanitize_file_name('damalion-' . wp_generate_uuid4() . '.jpg');

        // Force .jpg extension (we always output JPEG for compression control)
        $filename = preg_replace('/\.(webp|png)$/i', '.jpg', $filename);
        if (!preg_match('/\.(jpg|jpeg)$/i', $filename)) {
            $filename .= '.jpg';
        }

        $file_array = array(
            'name'     => $filename,
            'tmp_name' => $tmp_file,
        );

        // ── Upload to WP Media Library ──
        require_once ABSPATH . 'wp-admin/includes/media.php';
        require_once ABSPATH . 'wp-admin/includes/file.php';
        require_once ABSPATH . 'wp-admin/includes/image.php';

        $attachment_id = media_handle_sideload($file_array, $post_id);

        if (file_exists($tmp_file)) {
            @unlink($tmp_file);
        }

        if (is_wp_error($attachment_id)) {
            error_log('DBA Image upload error: ' . $attachment_id->get_error_message());
            return $attachment_id;
        }

        // ── Set image metadata (title, caption, description, alt) ──
        $attachment_update = array('ID' => $attachment_id);

        if (!empty($image_data['title'])) {
            $attachment_update['post_title'] = sanitize_text_field($image_data['title']);
        }
        if (!empty($image_data['caption'])) {
            $attachment_update['post_excerpt'] = sanitize_text_field($image_data['caption']);
        }
        if (!empty($image_data['description'])) {
            $attachment_update['post_content'] = sanitize_text_field($image_data['description']);
        }

        wp_update_post($attachment_update);

        if (!empty($image_data['alt'])) {
            update_post_meta($attachment_id, '_wp_attachment_image_alt', sanitize_text_field($image_data['alt']));
        }

        error_log('DBA Image: Uploaded attachment #' . $attachment_id . ' (' . $filename . ', ' . round($final_size / 1024) . 'KB)');

        return $attachment_id;
    }

    /* =================================================================
     *  PROMPT BUILDER — Entrepreneur-focused, country-agnostic
     * ================================================================= */

    /**
     * Build the final DALL-E prompt with style rules.
     *
     * The city/country context comes from the AI-generated prompt
     * (built by class-geobot-generator.php), which already contains
     * the city name. This method adds mandatory style instructions.
     *
     * @param string $raw_prompt  The prompt from the AI-generated article JSON.
     * @param int    $post_id     The WP post (unused here, reserved for future).
     * @return string
     */
    private function build_final_prompt($raw_prompt, $post_id = 0) {

        // Clean text/pose artifacts from AI-generated prompt
        $clean = $this->strip_text_descriptions($raw_prompt);

        // Minimal safety suffix — the full creative direction is already in the prompt
        // from class-geobot-generator.php (Prompt A: city, Prompt B: factory)
        $safety = "\n\n"
            . "CRITICAL RULES:\n"
            . "- The image MUST be in LANDSCAPE orientation (wider than tall), 16:9 horizontal format.\n"
            . "- ZERO text anywhere: no words, no letters, no signs, no signage, no logos, no brand names, "
            . "no watermarks, no captions, no overlays. The image must contain absolutely no readable characters.\n"
            . "- Must look like a real high-end editorial photograph — not AI-generated.\n"
            . "- NO stock photo aesthetics, NO HDR over-processing, NO floating objects, NO abstract elements.";

        return $clean . $safety;
    }

    /* =================================================================
     *  RESIZE + COMPRESS — Target <= 90KB JPEG
     * ================================================================= */

    /**
     * Resize to target dimensions, then compress JPEG until <= MAX_FILE_BYTES.
     *
     * @param string $file_path  Path to the source image.
     * @param int    $width      Target width.
     * @param int    $height     Target height.
     * @return string|false      Path to the compressed file, or false on failure.
     */
    private function resize_and_compress($file_path, $width, $height) {

        // Try WP image editor first
        $resized_path = $this->resize_with_wp_editor($file_path, $width, $height);

        // Fallback to raw GD
        if (!$resized_path) {
            $resized_path = $this->resize_with_gd($file_path, $width, $height);
        }

        if (!$resized_path) {
            error_log('DBA Image WARNING: Could not resize — uploading at original size');
            return false;
        }

        // Now compress until <= 90KB
        $compressed = $this->compress_to_target($resized_path, self::MAX_FILE_BYTES);

        return $compressed ?: $resized_path;
    }

    /**
     * Compress a JPEG file until it's <= target bytes.
     * Uses decreasing quality steps.
     *
     * @param string $file_path
     * @param int    $max_bytes
     * @return string|false  Path to compressed file (may be same path), or false.
     */
    private function compress_to_target($file_path, $max_bytes) {

        $current_size = filesize($file_path);

        // Already under target?
        if ($current_size <= $max_bytes) {
            error_log('DBA Image: Already ' . round($current_size / 1024) . 'KB <= ' . round($max_bytes / 1024) . 'KB target');
            return $file_path;
        }

        // Load image into GD
        $info = @getimagesize($file_path);
        if (!$info) return false;

        $mime = $info['mime'];
        switch ($mime) {
            case 'image/jpeg': $src = @imagecreatefromjpeg($file_path); break;
            case 'image/png':  $src = @imagecreatefrompng($file_path); break;
            case 'image/webp': $src = function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($file_path) : false; break;
            default: return false;
        }
        if (!$src) return false;

        $quality  = self::JPEG_QUALITY_START;
        $out_path = $file_path . '-compressed.jpg';

        while ($quality >= self::JPEG_QUALITY_MIN) {
            imagejpeg($src, $out_path, $quality);
            $new_size = filesize($out_path);

            error_log('DBA Image: Compression Q=' . $quality . ' -> ' . round($new_size / 1024) . 'KB');

            if ($new_size <= $max_bytes) {
                imagedestroy($src);
                return $out_path;
            }

            $quality -= 5;
        }

        // Even at minimum quality it's too large — return best effort
        error_log('DBA Image WARNING: Could not compress below ' . round($max_bytes / 1024) . 'KB (final: ' . round(filesize($out_path) / 1024) . 'KB at Q=' . self::JPEG_QUALITY_MIN . ')');
        imagedestroy($src);
        return $out_path;
    }

    /**
     * Resize using WP image editor (Imagick or GD).
     */
    private function resize_with_wp_editor($file_path, $width, $height) {
        if (!function_exists('wp_get_image_editor')) return false;

        $editor = wp_get_image_editor($file_path);
        if (is_wp_error($editor)) return false;

        $result = $editor->resize($width, $height, true); // Crop to exact
        if (is_wp_error($result)) return false;

        $new_path = $file_path . '-resized.jpg';
        $saved = $editor->save($new_path, 'image/jpeg', self::JPEG_QUALITY_START);
        if (is_wp_error($saved)) return false;

        error_log('DBA Image: Resized to ' . $width . 'x' . $height . ' via WP editor');
        return $saved['path'];
    }

    /**
     * Fallback: resize using raw GD.
     */
    private function resize_with_gd($file_path, $width, $height) {
        if (!function_exists('imagecreatetruecolor')) return false;

        $info = @getimagesize($file_path);
        if (!$info) return false;

        $mime  = $info['mime'];
        $src_w = $info[0];
        $src_h = $info[1];

        switch ($mime) {
            case 'image/jpeg': $src = @imagecreatefromjpeg($file_path); break;
            case 'image/png':  $src = @imagecreatefrompng($file_path); break;
            case 'image/webp': $src = function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($file_path) : false; break;
            default: return false;
        }
        if (!$src) return false;

        // Crop to target aspect ratio then resize
        $src_ratio = $src_w / $src_h;
        $dst_ratio = $width / $height;

        if ($src_ratio > $dst_ratio) {
            $crop_h = $src_h;
            $crop_w = intval($src_h * $dst_ratio);
            $crop_x = intval(($src_w - $crop_w) / 2);
            $crop_y = 0;
        } else {
            $crop_w = $src_w;
            $crop_h = intval($src_w / $dst_ratio);
            $crop_x = 0;
            $crop_y = intval(($src_h - $crop_h) / 2);
        }

        $dst = imagecreatetruecolor($width, $height);
        imagecopyresampled($dst, $src, 0, 0, $crop_x, $crop_y, $width, $height, $crop_w, $crop_h);

        $new_path = $file_path . '-gd-resized.jpg';
        imagejpeg($dst, $new_path, self::JPEG_QUALITY_START);

        imagedestroy($src);
        imagedestroy($dst);

        error_log('DBA Image: Resized to ' . $width . 'x' . $height . ' via GD fallback');
        return $new_path;
    }

    /**
     * Strip text-related and posed-portrait descriptions from prompts.
     * DALL-E always misspells text — remove any text references entirely.
     */
    private function strip_text_descriptions($prompt) {
        $patterns = array(
            // Text removal
            '/\b(?:with|showing|displaying|reading|saying|bearing|featuring)\s+(?:the\s+)?(?:word|text|sign|name|label|inscription|lettering)\s*["\']?[A-Za-z\s]+["\']?/i',
            '/\b(?:sign|plaque|banner|poster|nameplate|lettering|inscription|wall\s*text|engraved\s*text)\s+(?:that\s+)?(?:says?|reads?|showing|displaying|with)\s+["\']?[A-Za-z\s]+["\']?/i',
            '/\b[A-Z][a-z]+\s+(?:written|engraved|printed|inscribed|embossed)\s+on/i',
            '/with\s+["\'][A-Za-z\s]+["\']\s+(?:in|on|across)/i',
            '/\b(?:3D|large|bold|metal|gold|chrome|brass|wooden)\s+(?:letters?|text|words?)\s+(?:spelling|reading|saying)/i',
            // Portrait pose removal
            '/\b(?:smiling|grinning|looking|staring)\s+(?:at|into|toward)\s+(?:the\s+)?(?:camera|viewer|lens)/i',
            '/\b(?:arms?\s+crossed|thumbs?\s+up|power\s+pose|confident\s+pose)\b/i',
            '/\b(?:headshot|portrait|close-up|closeup)\s+(?:of|photo|photograph)/i',
        );

        $cleaned = $prompt;
        foreach ($patterns as $pattern) {
            $cleaned = preg_replace($pattern, '', $cleaned);
        }

        $cleaned = preg_replace('/\s{2,}/', ' ', $cleaned);
        $cleaned = preg_replace('/,\s*,/', ',', $cleaned);
        $cleaned = preg_replace('/,\s*\./', '.', $cleaned);
        $cleaned = trim($cleaned);

        if ($cleaned !== $prompt) {
            error_log('DBA Image: Stripped text/pose descriptions from prompt');
        }

        return $cleaned;
    }
}
