<?php
/**
 * ArticleGenerator.php
 *
 * Environment variables used:
 *  - OPENAI_API_KEY
 *  - OPENAI_ENDPOINT (optional; default: https://api.openai.com/v1/responses)
 *  - OPENAI_MODEL (optional; default: gpt-4o-mini)
 *
 *  - GEMINI_API_KEY
 *  - GEMINI_ENDPOINT (optional)
 *  - GEMINI_MODEL (optional)
 *
 * Usage:
 *   $gen = new ArticleGenerator();
 *   $result = $gen->createArticle("OnePlus 13, camera, battery, display", "openai");
 *   // $result = ['title'=>'...', 'article'=>'...', 'slug'=>'...']
 */

class ArticleGenerator {
    public function createArticle($prompt, $aiEngine) {
        $aiEngine = strtolower((string)$aiEngine);
        if ($aiEngine !== 'openai' && $aiEngine !== 'gemini') {
            throw new \InvalidArgumentException('aiEngine must be "openai" or "gemini"');
        }

        // Parse prompt: comma-separated keywords
        $parts = array_map('trim', explode(',', $prompt));
        // remove empty entries
        $parts = array_filter($parts, function ($p) {
            return $p !== '';
        });
        $parts = array_values($parts);

        if (empty($parts)) {
            throw new \InvalidArgumentException('Prompt must contain at least one keyword/topic');
        }

        $mainTopic = $parts[0];
        $subtopics = array_slice($parts, 1);

        // Build instruction content for the model: request strict JSON output
        $structuredInstruction = $this->buildPromptTemplate($mainTopic, $subtopics);

        // Choose engine
        if ($aiEngine === 'openai') {
            $resp = $this->callOpenAI($structuredInstruction);
        } else {
            $resp = $this->callGemini($structuredInstruction);
        }

        // resp expected: array with keys 'title' and 'article' OR 'raw' text
        // If title present use it, else try to extract from article.
        $title = isset($resp['title']) && $resp['title'] ? trim($resp['title']) : null;
        $article = isset($resp['article']) && $resp['article'] ? $resp['article'] : (isset($resp['raw']) ? $resp['raw'] : '');

        // Fallback: if title is missing, try to extract first heading from article
        if (empty($title)) {
            $title = $mainTopic . ' — Article';
        }

        return [
            'title' => $title,
            'article' => $article,
            'slug' => $this->url_title(mb_substr($title, 0, 200), '-', true)
        ];
    }

    public function url_title($str, $separator = '-',$lowercase = false) {
        $qSeparator = preg_quote($separator, '#');

        $trans = [
            '&.+?;' => '',
            '[^\w\d\pL\pM _-]' => '',
            '\s+' => $separator,
            '(' . $qSeparator . ')+' => $separator,
        ];

        $str = strip_tags($str);

        foreach ($trans as $key => $val) {
            $str = preg_replace('#' . $key . '#iu', $val, $str);
        }

        if ($lowercase === true) {
            $str = mb_strtolower($str);
        }

        return trim(trim($str, $separator));
    }

    protected function buildPromptTemplate($mainTopic, $subtopics) {
        // Build user prompt: include main and subtopics; ask for JSON only
        $parts = array();
        $parts[] = "Generate a long-form article about the topic: \"" . $mainTopic . "\".";
        if (!empty($subtopics)) {
            $parts[] = "Include the following subtopics as H2/H3 headings in the article and cover them: " . implode(', ', $subtopics) . ".";
        } else {
            $parts[] = "Include relevant headings and subheadings for a thorough article.";
        }

        $parts[] = "Write a clear, human-readable article in HTML with headings and short paragraphs. Provide helpful, factual information and avoid invented facts.";

        // Strict output requirement: JSON only
        $parts[] = "IMPORTANT: Respond with valid JSON only, and nothing else. ";
        $parts[] = "Return a JSON object with exactly these keys: ";
        $parts[] = "  {\"title\": \"<short title>\", \"article\": \"<full article HTML>\"}";
        $parts[] = "Do not include any additional commentary, explanation, or surrounding text. If you cannot comply, return an object with 'title' and 'article' keys with empty strings.";

        $parts[] = "For 'title' keep it short (6-12 words). For 'article' include headings for each requested subtopic if provided.";

        return implode("\n\n", $parts);
    }

    protected function callOpenAI($instruction) {
        // Endpoint & key from env
        $endpoint = getenv('OPENAI_ENDPOINT') ?: 'https://api.openai.com/v1/responses';
        $key = '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';
        $model = getenv('OPENAI_MODEL') ?: 'gpt-4o-mini';

        if (empty($key)) {
            throw new \RuntimeException('OPENAI_API_KEY not configured in environment');
        }

        // Many new OpenAI "Responses" endpoints accept {"model":..., "input": "..."}.
        // Use that simple shape for compatibility with your curl example.
        $payload = array(
            'model' => $model,
            'input' => $instruction
        );

        $jsonBody = json_encode($payload);

        $curl = curl_init();
        curl_setopt_array($curl, array(
            CURLOPT_URL => $endpoint,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING => '',
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 60,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST => 'POST',
            CURLOPT_POSTFIELDS => $jsonBody,
            CURLOPT_HTTPHEADER => array(
                'Content-Type: application/json',
                'Authorization: Bearer ' . $key
            ),
        ));

        $body = curl_exec($curl);
        $err = curl_error($curl);
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);

        if ($err) {
            throw new \RuntimeException('OpenAI cURL error: ' . $err);
        }

        if ($httpCode < 200 || $httpCode >= 300) {
            throw new \RuntimeException('OpenAI returned HTTP ' . $httpCode . ': ' . $body);
        }

        // Try to parse structured text from common OpenAI response shapes
        // Attempt decode
        $json = json_decode($body, true);

        $text = null;
        if (is_array($json)) {
            // /v1/responses shape: may have 'output' or 'items' or 'content'
            if (!empty($json['output']) && is_string($json['output'])) {
                $text = $json['output'];
            } elseif (!empty($json['output']) && is_array($json['output'])) {
                // sometimes 'output' is an array of objects; join text parts
                $collected = '';
                foreach ($json['output'] as $o) {
                    if (is_array($o) && !empty($o['content'])) {
                        if (is_string($o['content'])) {
                            $collected .= $o['content'] . "\n";
                        } elseif (is_array($o['content'])) {
                            // nested contents
                            foreach ($o['content'] as $c) {
                                if (is_string($c)) $collected .= $c;
                                elseif (is_array($c) && !empty($c['text'])) $collected .= $c['text'];
                            }
                        }
                    } elseif (is_string($o)) {
                        $collected .= $o . "\n";
                    }
                }
                $text = trim($collected);
            } elseif (!empty($json['choices'][0]['message']['content'])) {
                $text = $json['choices'][0]['message']['content'];
            } elseif (!empty($json['choices'][0]['text'])) {
                $text = $json['choices'][0]['text'];
            } elseif (!empty($json['candidates'][0]['content'])) {
                $text = $json['candidates'][0]['content'];
            }
        }

        // Fallback: if not parsed, use raw body
        if (empty($text)) {
            $text = is_string($body) ? $body : '';
        }

        // Try decode JSON from the model output
        $decoded = json_decode($text, true);
        if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
            return array(
                'title' => isset($decoded['title']) ? $decoded['title'] : null,
                'article' => isset($decoded['article']) ? $decoded['article'] : (isset($decoded['content']) ? $decoded['content'] : null),
                'raw' => $text
            );
        }

        // If model didn't return JSON, heuristically try to find JSON inside the text
        $embedded = $this->extractJsonFromText($text);
        if ($embedded !== null) {
            return array(
                'title' => isset($embedded['title']) ? $embedded['title'] : null,
                'article' => isset($embedded['article']) ? $embedded['article'] : (isset($embedded['content']) ? $embedded['content'] : null),
                'raw' => $text
            );
        }

        // Return as raw article text
        return array('title' => null, 'article' => $text, 'raw' => $text);
    }

    protected function callGemini($instruction) {
        $key = 'AIzaSyBu-GH4iTVDn3-k3UKSw2d-lhHyNcefykk';
        $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=' . $key;

        $payload = ['contents' => [['parts' => [['text' => $instruction]]]]];;

        $jsonBody = json_encode($payload);

        $curl = curl_init();
        curl_setopt_array($curl, array(
            CURLOPT_URL => $endpoint,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING => '',
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 60,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST => 'POST',
            CURLOPT_POSTFIELDS => $jsonBody,
            CURLOPT_HTTPHEADER => array(
                'Content-Type: application/json'
            ),
        ));

        $body = curl_exec($curl);
        $err = curl_error($curl);
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);

        if ($err) {
            throw new \RuntimeException('Gemini cURL error: ' . $err);
        }
        if ($httpCode < 200 || $httpCode >= 300) {
            throw new \RuntimeException('Gemini returned HTTP ' . $httpCode . ': ' . $body);
        }

        $json = json_decode($body, true);

        $text = null;
        if (is_array($json)) {
            if (!empty($json['candidates'][0]['content']['parts'][0]['text'])) {
                $text = $json['candidates'][0]['content']['parts'][0]['text'];
            } elseif (!empty($json['output'][0]['content'])) {
                $text = is_string($json['output'][0]['content']) ? $json['output'][0]['content'] : json_encode($json['output'][0]['content']);
            } elseif (!empty($json['output'])) {
                $text = is_string($json['output']) ? $json['output'] : json_encode($json['output']);
            } elseif (!empty($json['response'])) {
                $text = is_string($json['response']) ? $json['response'] : json_encode($json['response']);
            }
        }

        if (empty($text)) {
            $text = is_string($body) ? $body : '';
        }

        $decoded = json_decode($text, true);
        if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
            return array(
                'title' => isset($decoded['title']) ? $decoded['title'] : null,
                'article' => isset($decoded['article']) ? $decoded['article'] : (isset($decoded['content']) ? $decoded['content'] : null),
                'raw' => $text
            );
        }

        $embedded = $this->extractJsonFromText($text);
        if ($embedded !== null) {
            return array(
                'title' => isset($embedded['title']) ? $embedded['title'] : null,
                'article' => isset($embedded['article']) ? $embedded['article'] : (isset($embedded['content']) ? $embedded['content'] : null),
                'raw' => $text
            );
        }

        return array('title' => null, 'article' => $text, 'raw' => $text);
    }

    protected function extractJsonFromText($text) {
        if (!is_string($text) || trim($text) === '') return null;

        // Try to find a JSON object substring inside $text
        // Simple heuristic: find first "{" and last "}" and attempt decode
        $first = strpos($text, '{');
        $last = strrpos($text, '}');
        if ($first === false || $last === false || $last <= $first) return null;

        $sub = substr($text, $first, $last - $first + 1);
        $decoded = json_decode($sub, true);
        if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
            return $decoded;
        }

        return null;
    }
}