<?php
/**
 * AI Content Generator Service
 * 
 * Generates product titles, descriptions using AI.
 * Integrated into product edit form. Requires 'ai_content' addon to be active.
 * 
 * @since 1.5.5
 * @since 2.0.43 - Integrated into product edit form, removed standalone page
 */

namespace Instinctor\Services;

use Instinctor\Core\Config;
use Instinctor\Core\DB;
use Instinctor\Core\Auth;
use Instinctor\Core\Session;

if ( ! defined( 'ABSPATH' ) ) exit;

class AIContentService
{
    private static ?self $instance = null;
    
    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    public function __construct()
    {
        // Register AJAX handler (even if addon not active - we check in the handler)
        $this->registerAjax('instinctor_ai_generate_content', [$this, 'ajaxGenerateContent']);
    }
    
    /**
     * Get license key
     */
    private function getLicenseKey(): string
    {
        $licenses = Config::get('instinctor_addon_licenses', []);
        return Config::get('instinctor_license_key', '')
            ?: ($licenses['ai_content']['key'] ?? $licenses['all_access']['key'] ?? '');
    }
    
    /**
     * Call AI API via License Manager REST endpoint
     */
    private function callAI(string $prompt, string $context = ''): ?string
    {
        $result = self::generateContent($prompt);
        if (is_wp_error($result)) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log('Instinctor AI Content: ' . $result->get_error_message());
            return null;
        }
        return $result;
    }

    /**
     * Static relay call for use by REST endpoints.
     * POSTs to the Instinctor License Manager relay which proxies to OpenAI.
     *
     * @param  string   $prompt     Prompt text
     * @param  int      $maxTokens  Unused — relay controls token limit; kept for signature compat
     * @return string|\WP_Error     Generated text or WP_Error on failure
     */
    /**
     * Generate alt text for an image via vision relay.
     * Accepts base64-encoded image data (without data URI prefix).
     *
     * @param  string       $imageBase64  Raw base64 image data
     * @param  string       $mimeType     e.g. 'image/jpeg'
     * @return string|\WP_Error           Alt text string or WP_Error on failure
     */
    public static function generateAltText(string $imageBase64, string $mimeType = 'image/jpeg', string $filename = '', int $width = 0, int $height = 0, array $siteContext = []): array|\WP_Error
    {
        $licenses   = Config::get('instinctor_addon_licenses', []);
        $licenseKey = Config::get('instinctor_license_key', '')
            ?: ($licenses['ai_content']['key'] ?? $licenses['all_access']['key'] ?? '');

        // Unpack site context — store name/description come from Store > Settings
        $storeName = trim($siteContext['store_name']        ?? Config::get('instinctor_store_name',        ''));
        $storeDesc = trim($siteContext['store_description'] ?? Config::get('instinctor_store_description', ''));
        $pageTitle = trim($siteContext['page_title']        ?? '');
        $pageDesc  = trim($siteContext['page_description']  ?? '');
        $keyword   = trim($siteContext['focus_keyword']     ?? '');

        // Build shared context block
        $ctxLines = [];
        if ($storeName) $ctxLines[] = "Store: {$storeName}";
        if ($storeDesc) $ctxLines[] = "Store description: {$storeDesc}";
        if ($pageTitle) $ctxLines[] = "Page this image appears on: {$pageTitle}";
        if ($keyword)   $ctxLines[] = "SEO focus keyword: {$keyword}";
        if ($pageDesc)  $ctxLines[] = "Page description: " . mb_substr($pageDesc, 0, 200);
        $ctxBlock = implode("\n", $ctxLines);

        // SEO-aware vision prompt
        $visionPrompt = ($ctxBlock ? $ctxBlock . "\n\n" : '')
            . "Analyze this image in the context above.\n"
            . "Return a JSON object with exactly two fields:\n"
            . '"alt": SEO-optimised alt text under 125 chars. Describe what the image shows as it relates '
            .       'to the store and page topic. Include the focus keyword naturally if it fits. '
            .       "Ignore decorative elements, background colours, gradients, and stock-photo cliches. "
            .       "Do not start with 'Image of' or 'Photo of'. No quotes.\n"
            . '"title": Short descriptive image title, 3-6 words, Title Case. '
            .          "Specific to the subject shown, not a generic label like 'Product Image'.\n"
            . "Return only valid JSON, nothing else.";

        // Try vision relay first — through unified callAIRelay so the chat +
        // section-generator + alt-text all share the same transport and
        // error-handling code path.
        $body = self::callAIRelay('ai-vision', [
            'task'       => 'alt_text',
            'image_data' => $imageBase64,
            'mime_type'  => $mimeType,
            'prompt'     => $visionPrompt,
        ], 30);
        if (!is_wp_error($body) && !empty($body['success']) && !empty($body['content'])) {
            return [
                'alt'   => trim($body['content'], "\"' "),
                'title' => trim($body['title']   ?? '', "\"' "),
            ];
        }

        // Vision relay unavailable — fall back to text relay using filename + context
        $textCtx = $ctxBlock;
        if ($filename) {
            $clean    = pathinfo($filename, PATHINFO_FILENAME);
            $clean    = trim(preg_replace('/\s+/', ' ', preg_replace('/[_\-]+/', ' ', $clean)));
            $textCtx .= ($textCtx ? "\n" : '') . "Image filename: {$clean}";
        }
        if ($width && $height) $textCtx .= " ({$width}x{$height}px)";

        if (!trim($textCtx)) {
            return new \WP_Error('ai_vision_error', 'No image context available for alt text generation');
        }

        $altPrompt = "You are an SEO specialist writing image alt text for a website.\n"
            . $textCtx . "\n\n"
            . "Write alt text under 125 chars describing what the image likely shows as it relates to "
            . "the store and page topic. Include the focus keyword naturally if provided. "
            . "Skip generic descriptions like background colour or stock-photo cliches. "
            . "Do not start with 'Image of' or 'Photo of'. Return only the alt text, no quotes.";

        $titlePrompt = "You are an SEO specialist writing image titles for a website.\n"
            . $textCtx . "\n\n"
            . "Write a short image title (3-6 words, Title Case) specific to the subject shown, "
            . "not a generic label. Return only the title, no quotes.";

        $alt   = self::generateContent($altPrompt, 80);
        $title = self::generateContent($titlePrompt, 30);
        return [
            'alt'   => is_wp_error($alt)   ? '' : trim($alt),
            'title' => is_wp_error($title) ? '' : trim($title),
        ];
    }


    /**
     * Single relay entry point for ALL AI calls (text generation, vision,
     * chat replies). Both the section-generator Instagent and the chat
     * Instagent funnel through here so there's one place that handles
     * auth, transport, decoding, and error normalisation.
     *
     * @param string $task    Relay endpoint suffix: 'ai-content' | 'ai-vision' | 'ai-reply'
     * @param array  $params  Body params merged into license_key + site_url
     * @param int    $timeout Request timeout in seconds
     * @return array|\WP_Error  Decoded relay response or WP_Error
     */
    public static function callAIRelay(string $task, array $params, int $timeout = 60): array|\WP_Error
    {
        $licenses   = Config::get('instinctor_addon_licenses', []);
        $licenseKey = Config::get('instinctor_license_key', '')
            ?: ($licenses['ai_content']['key'] ?? $licenses['all_access']['key'] ?? '');

        // WP.org guideline 7/9 — gated on opt-in. AI relay forwards
        // license_key + site_url + the model prompt to platform.
        if (!\Instinctor\Core\PlatformConsent::granted()) {
            return new \WP_Error('platform_consent_required', 'Instinctor platform features are disabled. Enable them in Settings → Instinctor.');
        }
        $body = array_merge([
            'license_key' => $licenseKey,
            'site_url'    => home_url(),
        ], $params);

        $response = wp_remote_post('https://platform.instinctor.com/api/relay/' . $task, [
            'body'    => $body,
            'timeout' => $timeout,
        ]);

        if (is_wp_error($response)) {
            return new \WP_Error('ai_request_failed', $response->get_error_message());
        }

        $decoded = json_decode(wp_remote_retrieve_body($response), true);
        if (!is_array($decoded)) {
            return new \WP_Error('ai_relay_invalid', 'Malformed relay response');
        }

        $code = wp_remote_retrieve_response_code($response);
        if ($code >= 400 && empty($decoded['error'])) {
            $decoded['error'] = $decoded['message'] ?? "Relay returned HTTP {$code}";
        }

        return $decoded;
    }

    /**
     * Section-generator / product-content text generation.
     */
    public static function generateContent(string $prompt, int $maxTokens = 200): string|\WP_Error
    {
        $r = self::callAIRelay('ai-content', ['prompt' => $prompt, 'context' => '']);
        if (is_wp_error($r)) return $r;
        if (!empty($r['success']) && !empty($r['content'])) {
            return trim($r['content']);
        }
        return new \WP_Error('ai_relay_error', $r['error'] ?? 'Unknown relay error');
    }

    /**
     * Chat Instagent reply. ChatService::getAIReply delegates here so both
     * Instagents share one transport. Returns the full decoded payload so
     * the chat layer can read `reply`, `success`, plus any extras (handoff
     * markers, suggested actions, etc.) the relay may add later.
     */
    public static function chatReply(array $params): array|\WP_Error
    {
        $r = self::callAIRelay('ai-reply', $params, 30);
        if (is_wp_error($r)) return $r;
        if (!empty($r['success']) && !empty($r['reply'])) return $r;
        return new \WP_Error('ai_relay_error', $r['error'] ?? 'Chat AI unavailable');
    }

    /**
     * Help-mode reply — KB-augmented. Used by the chat on instinctor.com
     * (and anywhere else `instinctor_chat_mode='help'` is set in config) so
     * questions about the product get answered from the same KB the editor's
     * Help mode uses, instead of the generic store-support template that
     * handles "where's my order".
     *
     * Calls /api/relay/ai-help (HelpController::ask). The platform side
     * does KB retrieval (KBService::retrieve) + Claude with Instagent voice
     * rules baked in. Returns shape { success, answer, sources } — chat
     * layer normalises to its own { success, reply } contract before
     * showing.
     */
    public static function helpAsk(string $question, array $context = []): array|\WP_Error
    {
        $r = self::callAIRelay('ai-help', [
            'question' => $question,
            'context'  => $context,
        ], 30);
        if (is_wp_error($r)) return $r;
        if (!empty($r['success']) && !empty($r['answer'])) {
            // Normalise to chat-side shape so the existing reply-handling
            // path in ChatService doesn't need a second branch.
            return [
                'success'      => true,
                'reply'        => $r['answer'],
                'sources'      => $r['sources'] ?? [],
                'needs_handoff' => false,
            ];
        }
        return new \WP_Error('ai_help_error', $r['error'] ?? 'Help AI unavailable');
    }
    
    /**
     * Get Instinctor product context for AI
     */
    private function getInstinctorProductContext(int $productId): array
    {
        $table = DB::prefix() . 'instinctor_products';
        
        $product = DB::get_row(DB::prepare(
            "SELECT * FROM $table WHERE id = %d",
            $productId
        ));
        
        if (!$product) {
            return [];
        }
        
        $context = [
            'name' => $product->name ?? '',
            'short_description' => $product->short_description ?? '',
            'description' => wp_strip_all_tags($product->description ?? ''),
            'price' => $product->price ?? '',
            'sku' => $product->sku ?? '',
            'product_type' => $product->product_type ?? 'physical',
        ];
        
        // Get categories
        $categories = CategoryService::getProductCategories($productId);
        if (!empty($categories)) {
            $context['categories'] = array_map(function($cat) {
                return $cat->name;
            }, $categories);
        }
        
        return $context;
    }
    
    /**
     * AJAX: Generate content for a field
     */
    public function ajaxGenerateContent(): void
    {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- check_ajax_referer() IS the nonce verification
        check_ajax_referer('instinctor_ai_content', '_wpnonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(__('Permission denied', 'instinctor'));
        }
        
        // phpcs:disable WordPress.Security.NonceVerification -- request authenticated via the plugin's check_ajax_referer()/verify() HMAC nonce (standalone auth lifecycle; WPCS does not recognise Session::check as a nonce function); all inputs are sanitize_*'d + wp_unslash'd.
        $field = isset($_POST['field']) ? sanitize_text_field(wp_unslash($_POST['field'])) : '';
        // phpcs:enable WordPress.Security.NonceVerification
        // phpcs:disable WordPress.Security.NonceVerification -- request authenticated via the plugin's check_ajax_referer()/verify() HMAC nonce (standalone auth lifecycle; WPCS does not recognise Session::check as a nonce function); all inputs are sanitize_*'d + wp_unslash'd.
        $productId = isset($_POST['product_id']) ? intval($_POST['product_id']) : 0;
        // phpcs:enable WordPress.Security.NonceVerification
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON needs to preserve structure, individual values sanitized after decode
        // phpcs:disable WordPress.Security.NonceVerification -- request authenticated via the plugin's check_ajax_referer()/verify() HMAC nonce (standalone auth lifecycle; WPCS does not recognise Session::check as a nonce function); all inputs are sanitize_*'d + wp_unslash'd.
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON string, decoded and validated field-by-field below.
        $contextJson = isset($_POST['context']) ? wp_unslash($_POST['context']) : '{}';
        // phpcs:enable WordPress.Security.NonceVerification
        
        // Parse the context from the form (current field values)
        $formContext = json_decode($contextJson, true);
        if (!is_array($formContext)) {
            $formContext = [];
        }
        
        // Sanitize form context
        $formContext = array_map('sanitize_text_field', $formContext);
        
        // If we have a product ID, merge with database values
        if ($productId > 0) {
            $dbContext = $this->getInstinctorProductContext($productId);
            // Form values take precedence over DB values
            $context = array_merge($dbContext, array_filter($formContext));
        } else {
            $context = $formContext;
        }
        
        // Get store info for context
        $storeName = Config::get('instinctor_store_name', get_bloginfo('name'));
        $storeDescription = Config::get('instinctor_store_description', get_bloginfo('description'));
        
        $context['store_name'] = $storeName;
        $context['store_description'] = $storeDescription;
        
        // Generate based on field type
        $prompt = '';
        switch ($field) {
            case 'name':
                $prompt = $this->buildNamePrompt($context);
                break;
            case 'short_description':
                $prompt = $this->buildShortDescriptionPrompt($context);
                break;
            case 'description':
                $prompt = $this->buildDescriptionPrompt($context);
                break;
            default:
                wp_send_json_error(__('Invalid field type', 'instinctor'));
        }
        
        $result = $this->callAI($prompt, wp_json_encode($context));
        
        if ($result) {
            // Clean up the result
            $result = trim($result, '"\'');
            wp_send_json_success(['content' => $result]);
        } else {
            wp_send_json_error(__('Failed to generate content. Please try again.', 'instinctor'));
        }
    }
    
    /**
     * Build prompt for product name generation
     */
    private function buildNamePrompt(array $context): string
    {
        $prompt = "Generate a compelling, SEO-friendly product name/title.\n\n";
        $prompt .= "Guidelines:\n";
        $prompt .= "- Keep it concise (3-8 words typically)\n";
        $prompt .= "- Make it descriptive and keyword-rich\n";
        $prompt .= "- Avoid generic words like 'Amazing' or 'Best'\n";
        $prompt .= "- Don't use quotes around the name\n\n";
        
        if (!empty($context['short_description'])) {
            $prompt .= "Product summary: " . $context['short_description'] . "\n";
        }
        if (!empty($context['description'])) {
            $prompt .= "Product details: " . substr($context['description'], 0, 500) . "\n";
        }
        if (!empty($context['categories'])) {
            $prompt .= "Category: " . implode(', ', (array)$context['categories']) . "\n";
        }
        if (!empty($context['price'])) {
            $prompt .= "Price point: $" . $context['price'] . "\n";
        }
        
        $prompt .= "\nGenerate only the product name, nothing else.";
        
        return $prompt;
    }
    
    /**
     * Build prompt for short description generation
     */
    private function buildShortDescriptionPrompt(array $context): string
    {
        $prompt = "Write a compelling short product description (1-2 sentences, max 150 characters).\n\n";
        $prompt .= "Guidelines:\n";
        $prompt .= "- Be concise and punchy\n";
        $prompt .= "- Highlight the main benefit\n";
        $prompt .= "- Create urgency or desire\n";
        $prompt .= "- Use active voice\n\n";
        
        if (!empty($context['name'])) {
            $prompt .= "Product name: " . $context['name'] . "\n";
        }
        if (!empty($context['description'])) {
            $prompt .= "Full description: " . substr($context['description'], 0, 500) . "\n";
        }
        if (!empty($context['categories'])) {
            $prompt .= "Category: " . implode(', ', (array)$context['categories']) . "\n";
        }
        if (!empty($context['price'])) {
            $prompt .= "Price: $" . $context['price'] . "\n";
        }
        
        $prompt .= "\nGenerate only the short description, nothing else.";
        
        return $prompt;
    }
    
    /**
     * Build prompt for full description generation
     */
    private function buildDescriptionPrompt(array $context): string
    {
        $prompt = "Write a compelling, SEO-optimized product description (150-300 words).\n\n";
        $prompt .= "Guidelines:\n";
        $prompt .= "- Start with a hook that grabs attention\n";
        $prompt .= "- Highlight key features and benefits\n";
        $prompt .= "- Use bullet points for scanability if appropriate\n";
        $prompt .= "- Include a call to action\n";
        $prompt .= "- Use natural, conversational language\n";
        $prompt .= "- Include relevant keywords naturally\n\n";
        
        if (!empty($context['name'])) {
            $prompt .= "Product name: " . $context['name'] . "\n";
        }
        if (!empty($context['short_description'])) {
            $prompt .= "Summary: " . $context['short_description'] . "\n";
        }
        if (!empty($context['categories'])) {
            $prompt .= "Category: " . implode(', ', (array)$context['categories']) . "\n";
        }
        if (!empty($context['price'])) {
            $prompt .= "Price: $" . $context['price'] . "\n";
        }
        if (!empty($context['sku'])) {
            $prompt .= "SKU: " . $context['sku'] . "\n";
        }
        if (!empty($context['store_name'])) {
            $prompt .= "Store: " . $context['store_name'] . "\n";
        }
        
        $prompt .= "\nGenerate only the product description, nothing else.";
        
        return $prompt;
    }

    /**
     * Register an AJAX handler for both WordPress and standalone modes.
     * On WordPress: add_action('wp_ajax_' . $action)
     * On standalone: AjaxDispatcher::register($action)
     */
    private function registerAjax(string $action, callable $handler): void
    {
        if (!defined('INSTINCTOR_ENV') || INSTINCTOR_ENV === 'wordpress') {
            add_action('wp_ajax_' . $action, $handler);
        } else {
            \Instinctor\Core\AjaxDispatcher::register($action, $handler);
        }
    }

}
