<?php

class SendyOpenAiClient
{
	private $api_key = '';
	private $api_base = 'https://api.openai.com/v1';
	private $model = 'gpt-5.4-mini';
	private $timeout = 45;
	private $ca_cert_path = '';

	public function __construct($api_key, $options = array())
	{
		$this->api_key = trim((string)$api_key);
		$this->ca_cert_path = dirname(dirname(__DIR__)).'/certs/cacert.pem';

		if(isset($options['api_base']) && trim($options['api_base'])!='')
			$this->api_base = rtrim($options['api_base'], '/');

		if(isset($options['model']) && trim($options['model'])!='')
			$this->model = trim($options['model']);

		if(isset($options['timeout']) && is_numeric($options['timeout']))
			$this->timeout = (int)$options['timeout'];
	}

	public function create_response($payload)
	{
		return $this->request('POST', '/responses', $payload);
	}

	public function generate_subject_line($html, $plain_text='', $current_subject='', $prompt_mode='compelling', $custom_prompt='')
	{
		$content = $this->build_subject_source($html, $plain_text);

		if($content=='')
			throw new Exception(_('Email content is required to generate a subject line.'));

		$payload = array(
			'model' => $this->model,
			'store' => false,
			'reasoning' => array('effort' => 'none'),
			'max_output_tokens' => 60,
			'temperature' => 0.8,
			'text' => array(
				'format' => array(
					'type' => 'text'
				)
			),
			'instructions' => 'You write email subject lines. Return exactly one subject line as plain text. Do not add quotation marks, labels, numbering, markdown, or commentary.',
			'input' => $this->build_subject_prompt($content, $current_subject, $prompt_mode, $custom_prompt)
		);

		$response = $this->create_response($payload);
		$subject = $this->extract_output_text($response);
		$subject = $this->sanitize_subject_line($subject);

		if($subject=='')
			throw new Exception(_('OpenAI returned an empty subject line.'));

		return $subject;
	}

	public function generate_campaign_report_analysis($metrics)
	{
		if(!is_array($metrics) || count($metrics)==0)
			throw new Exception(_('Campaign report metrics are required.'));

		$metrics_json = json_encode($metrics);
		if($metrics_json===false)
			throw new Exception(_('Unable to encode campaign report metrics.'));

		$payload = array(
			'model' => $this->model,
			'store' => false,
			'reasoning' => array('effort' => 'none'),
			'max_output_tokens' => 1800,
			'temperature' => 0.4,
			'text' => array(
				'format' => array(
					'type' => 'text'
				)
			),
			'instructions' => 'You are a friendly email marketing coach. Analyze campaign or autoresponder report metrics in a personal, conversational tone, like you are advising the sender directly. Be warm, clear, and practical rather than corporate or business-like. Include CTOR/click-to-open-rate analysis and geographic/country open data when present. Do not invent benchmarks or unsupported causes. If tracking is disabled or data is thin, say so and focus on what can still be learned. Return a safe HTML fragment only, using <p>, <strong>, <ul>, <ol>, and <li>. Section titles must be bold with <strong>. Put bullet or numbered points inside proper <ul> or <ol> lists. You may use a small number of relevant emojis. Do not include a "Campaign Analysis" title. Keep the analysis concise enough to finish completely, and make sure the final recommendation ends as a complete sentence.',
			'input' => "Analyze this Sendy email report. Include: (1) a short performance summary, (2) the main strengths and risks visible in the metrics, (3) CTOR/click-to-open-rate and what it suggests about content and calls to action, (4) what the country/open geography data suggests, and (5) specific recommendations for future campaigns.\n\nMetrics JSON:\n".$metrics_json
		);

		$response = $this->create_response($payload);
		$analysis = trim($this->extract_output_text($response));

		if($analysis=='')
			throw new Exception(_('OpenAI returned an empty campaign analysis.'));

		return $this->limit_text($analysis, 12000);
	}

	public function generate_campaign_report_score($metrics)
	{
		if(!is_array($metrics) || count($metrics)==0)
			throw new Exception(_('Campaign report metrics are required.'));

		$metrics_json = json_encode($metrics);
		if($metrics_json===false)
			throw new Exception(_('Unable to encode campaign report metrics.'));

		$payload = array(
			'model' => $this->model,
			'store' => false,
			'reasoning' => array('effort' => 'none'),
			'max_output_tokens' => 20,
			'temperature' => 0.2,
			'text' => array(
				'format' => array(
					'type' => 'text'
				)
			),
			'instructions' => 'You score email marketing campaign reports. Return exactly one integer from 0 to 100 and nothing else. Score the campaign using only the supplied metrics: engagement, click-to-open rate, deliverability, unsubscribes, complaints, bounces, tracking availability, and amount of data. Do not invent industry benchmarks.',
			'input' => "Give this Sendy email report a campaign score out of 100.\n\nMetrics JSON:\n".$metrics_json
		);

		$response = $this->create_response($payload);
		$score_text = trim($this->extract_output_text($response));

		if(!preg_match('/\b(100|[1-9]?[0-9])\b/', $score_text, $matches))
			throw new Exception(_('OpenAI returned an invalid campaign score.'));

		return max(0, min(100, (int)$matches[1]));
	}

	public function generate_email_content_improvements($html, $plain_text='', $current_subject='')
	{
		$content = $this->build_subject_source($html, $plain_text);
		$current_subject = $this->sanitize_subject_line($current_subject);

		if($content=='')
			throw new Exception(_('Write some email content first, then I can suggest improvements.'));

		$subject_context = $current_subject!='' ? "Current subject line:\n".$current_subject."\n\n" : '';

		$payload = array(
			'model' => $this->model,
			'store' => false,
			'reasoning' => array('effort' => 'none'),
			'max_output_tokens' => 1800,
			'temperature' => 0.4,
			'text' => array(
				'format' => array(
					'type' => 'text'
				)
			),
			'instructions' => 'You are a friendly email marketing coach. Review the current email content and suggest practical improvements in a personal, conversational tone, like you are advising the sender directly. Focus only on what is visible in the email content. Do not invent audience details, goals, metrics, product claims, or benchmarks. For each meaningful improvement, include a concrete example of how you would rewrite the relevant portion, while preserving the sender\'s likely intent. Return a safe HTML fragment only, using <p>, <strong>, <ul>, <ol>, and <li>. Section titles must be bold with <strong>. Put bullet or numbered points inside proper <ul> or <ol> lists. Do not include an "Email Analysis" title. Keep the response concise and actionable, and make sure the final recommendation ends as a complete sentence.',
			'input' => "Suggest improvements for this email before it is sent. Include: (1) a short summary of what the email is trying to do, (2) strengths worth keeping, (3) specific improvements to clarity, structure, persuasion, and calls to action, (4) rewrite examples for the portions you say can be improved, (5) any deliverability or readability risks visible in the content, and (6) a short prioritized checklist of changes to make next. When giving rewrite examples, quote or describe the original portion briefly, then provide a better version the sender could use.\n\n".$subject_context."Email content:\n".$content
		);

		$response = $this->create_response($payload);
		$analysis = trim($this->extract_output_text($response));

		if($analysis=='')
			throw new Exception(_('OpenAI returned an empty improvement suggestion.'));

		return $this->limit_text($analysis, 12000);
	}

	public function generate_email_newsletter_html($prompt)
	{
		$prompt = trim((string)$prompt);

		if($prompt=='')
			throw new Exception(_('A prompt is required to generate an email.'));

		$payload = array(
			'model' => $this->model,
			'store' => false,
			'reasoning' => array('effort' => 'none'),
			'max_output_tokens' => 8000,
			'temperature' => 0.5,
			'text' => array(
				'format' => array(
					'type' => 'text'
				)
			),
			'instructions' => 'Create the email requested by the user. If reference email context is provided, use it only when it matches the user\'s requested visual direction, color scheme, spacing, or layout inspiration. When the user asks to follow the reference color scheme, palette, colors, background, or dark/light theme, the reference palette must override generic defaults such as clean white background, simple white layout, or neutral styling. For HTML emails, web version links must use href="[webversion]" exactly and unsubscribe links must use href="[unsubscribe]" exactly. If the user requests HTML, return complete HTML only. If the user requests plain text, return plain text only. Do not include markdown fences, explanations, labels, or commentary.',
			'input' => $prompt
		);

		$response = $this->create_response($payload);
		$html = trim($this->extract_output_text($response));
		$html = $this->strip_markdown_code_fence($html);

		if($html=='')
			throw new Exception(_('OpenAI returned empty email HTML.'));

		return $html;
	}

	public function build_subject_source($html, $plain_text='')
	{
		$plain_text = $this->normalize_whitespace($plain_text);
		$html_text = $this->html_to_text($html);

		if($plain_text!='' && $html_text!='')
		{
			if($plain_text == $html_text) $content = $plain_text;
			else $content = "Plain text version:\n".$plain_text."\n\nHTML version:\n".$html_text;
		}
		else if($plain_text!='') $content = $plain_text;
		else $content = $html_text;

		return $this->limit_text($content, 12000);
	}

	public function get_subject_prompt_template($prompt_mode='compelling')
	{
		switch($prompt_mode)
		{
			case 'curiosity':
				return "Generate one curiosity-based email subject line for the following email.\n\nRequirements:\n- Plain text only.\n- One line only.\n- Create an open loop that makes the reader want to learn more.\n- Keep the curiosity specific to the email content.\n- Prefer roughly 40 to 70 characters when possible.\n- Do not use quotation marks.\n- Avoid misleading clickbait, emojis, spammy words, or all caps.\n\nCurrent subject draft (optional context): {{current_subject}}\n\nEmail content:\n{{email_content}}";
			
			case 'value':
				return "Generate one value-based email subject line for the following email.\n\nRequirements:\n- Plain text only.\n- One line only.\n- Lead with the concrete benefit, outcome, offer, or useful takeaway.\n- Keep it concise and specific.\n- Prefer roughly 40 to 70 characters when possible.\n- Do not use quotation marks.\n- Avoid vague claims, emojis, spammy words, or all caps.\n\nCurrent subject draft (optional context): {{current_subject}}\n\nEmail content:\n{{email_content}}";
			
			case 'personal':
				return "Generate one email subject line in a personal, conversational tone for the following email.\n\nRequirements:\n- Plain text only.\n- One line only.\n- Sound warm, direct, and human, like a note from one person to another.\n- Keep it natural and specific to the email content.\n- Prefer roughly 40 to 70 characters when possible.\n- Do not use quotation marks.\n- Avoid over-familiar language, emojis, spammy words, or all caps.\n\nCurrent subject draft (optional context): {{current_subject}}\n\nEmail content:\n{{email_content}}";
			
			case 'compelling':
			default:
				return "Generate one email subject line for the following email.\n\nRequirements:\n- Plain text only.\n- One line only.\n- Keep it concise and specific.\n- Prefer roughly 40 to 70 characters when possible.\n- Do not use quotation marks.\n- Avoid emojis unless the email clearly calls for them.\n- Avoid spammy words or clickbait.\n\nCurrent subject draft (optional context): {{current_subject}}\n\nEmail content:\n{{email_content}}";
		}
	}

	public function get_subject_context_template()
	{
		return "Current subject draft (optional context): {{current_subject}}\n\nEmail content:\n{{email_content}}";
	}

	private function build_subject_prompt($content, $current_subject='', $prompt_mode='compelling', $custom_prompt='')
	{
		$current_subject = $this->sanitize_subject_line($current_subject);
		$template = trim($custom_prompt)!='' ? trim($custom_prompt) : $this->get_subject_prompt_template($prompt_mode);

		$prompt = str_replace(
			array('{{current_subject}}', '{{email_content}}'),
			array($current_subject, $content),
			$template
		);

		if(strpos($template, '{{email_content}}')===false)
		{
			$prompt .= "\n\n".str_replace(
				array('{{current_subject}}', '{{email_content}}'),
				array($current_subject, $content),
				$this->get_subject_context_template()
			);
		}

		return $prompt;
	}

	private function request($method, $path, $payload=null)
	{
		if($this->api_key=='')
			throw new Exception(_('OpenAI API key is missing.'));

		if(!function_exists('curl_init'))
			throw new Exception(_('cURL is required to call the OpenAI API.'));

		$url = $this->api_base.$path;
		$ch = curl_init();
		$headers = array(
			'Content-Type: application/json',
			'Authorization: Bearer '.$this->api_key
		);

		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
		curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_HEADER, false);
		curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
		curl_setopt($ch, CURLOPT_USERAGENT, 'Sendy AI/1.0');

		if(file_exists($this->ca_cert_path))
			curl_setopt($ch, CURLOPT_CAINFO, $this->ca_cert_path);

		if($payload!==null)
		{
			$payload_json = json_encode($payload);
			if($payload_json===false)
				throw new Exception(_('Unable to encode the OpenAI API request.'));

			curl_setopt($ch, CURLOPT_POSTFIELDS, $payload_json);
		}

		$body = curl_exec($ch);
		$curl_error = curl_error($ch);
		$status_code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
		curl_close($ch);

		if($body===false)
			throw new Exception($curl_error!='' ? $curl_error : _('Unable to reach the OpenAI API.'));

		$data = json_decode($body, true);
		if(!is_array($data))
		{
			$json_error = function_exists('json_last_error_msg') ? json_last_error_msg() : _('Unable to decode JSON.');
			throw new Exception($json_error);
		}

		if($status_code<200 || $status_code>=300)
		{
			$error_message = $this->extract_error_message($data);
			if($error_message=='') $error_message = _('The OpenAI API request failed.');
			throw new Exception($error_message);
		}

		return $data;
	}

	private function extract_output_text($response)
	{
		if(isset($response['output_text']) && trim($response['output_text'])!='')
			return trim($response['output_text']);

		if(isset($response['output']) && is_array($response['output']))
		{
			foreach($response['output'] as $item)
			{
				if(!isset($item['content']) || !is_array($item['content'])) continue;

				foreach($item['content'] as $content)
				{
					if(isset($content['type']) && $content['type']=='output_text' && isset($content['text']))
						return trim($content['text']);

					if(isset($content['type']) && $content['type']=='refusal' && isset($content['refusal']))
						throw new Exception(trim($content['refusal']));
				}
			}
		}

		throw new Exception(_('OpenAI returned an unexpected response.'));
	}

	private function extract_error_message($data)
	{
		if(isset($data['error']) && is_array($data['error']) && isset($data['error']['message']))
			return trim($data['error']['message']);

		if(isset($data['error']) && is_string($data['error']))
			return trim($data['error']);

		return '';
	}

	private function html_to_text($html)
	{
		$html = (string)$html;
		if(trim($html)=='') return '';

		$html = preg_replace('/<script\b[^>]*>.*?<\/script>/is', ' ', $html);
		$html = preg_replace('/<style\b[^>]*>.*?<\/style>/is', ' ', $html);
		$html = preg_replace('/<head\b[^>]*>.*?<\/head>/is', ' ', $html);
		$html = str_ireplace(array('</p>', '</div>', '</li>', '<br>', '<br/>', '<br />', '</tr>', '</table>', '</h1>', '</h2>', '</h3>', '</h4>', '</h5>', '</h6>'), "\n", $html);
		$html = strip_tags($html);
		$html = html_entity_decode($html, ENT_QUOTES, 'UTF-8');

		return $this->normalize_whitespace($html);
	}

	private function normalize_whitespace($text)
	{
		$text = html_entity_decode((string)$text, ENT_QUOTES, 'UTF-8');
		$text = str_replace(array("\r\n", "\r"), "\n", $text);
		$text = preg_replace("/\n{3,}/", "\n\n", $text);
		$text = preg_replace('/[ \t]+/', ' ', $text);
		$text = preg_replace('/[ ]*\n[ ]*/', "\n", $text);
		return trim($text);
	}

	private function sanitize_subject_line($subject)
	{
		$subject = trim((string)$subject);
		$subject = str_replace(array("\r\n", "\r", "\n"), ' ', $subject);
		$subject = preg_replace('/^\s*(?:subject)\s*:\s*/i', '', $subject);
		$subject = preg_replace('/^\s*(?:[-*]\s+|\d+[\.\)]\s+)/', '', $subject);
		$subject = trim($subject, " \t\n\r\0\x0B\"'");
		$subject = preg_replace('/\s{2,}/', ' ', $subject);
		return $this->limit_text($subject, 255);
	}

	private function strip_markdown_code_fence($text)
	{
		$text = trim((string)$text);

		if(preg_match('/^```(?:html)?\s*(.*?)\s*```$/is', $text, $matches))
			return trim($matches[1]);

		return $text;
	}

	private function limit_text($text, $limit)
	{
		$text = trim((string)$text);
		if($text=='') return '';

		if(function_exists('mb_strlen') && function_exists('mb_substr'))
		{
			if(mb_strlen($text, 'UTF-8') > $limit)
				return trim(mb_substr($text, 0, $limit, 'UTF-8'));
		}
		else if(strlen($text) > $limit)
			return trim(substr($text, 0, $limit));

		return $text;
	}
}

?>
