<?php
/**
 * Livada SEO — AI Suggestions
 *
 * Generates SEO title, meta description and focus-keyword suggestions for
 * existing posts using Anthropic's Claude API. The API key is shared with
 * the Image SEO module (`Livada_SEO_Image_Bulk::get_anthropic_key()`) so
 * the user only needs to configure it once.
 *
 * Hard caps applied client-side so the model can't blow past the SEO
 * recommendations:
 *   - title:       60 chars
 *   - description: 160 chars
 *   - keyword:     30 chars (single phrase)
 *
 * No content is stored or transmitted beyond the single API call.
 *
 * @package Livada_SEO
 */

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

class Livada_SEO_AI {

	const MODEL = 'claude-sonnet-4-5-20250929';

	/** Maximum tokens we ever request from the API. */
	const MAX_TOKENS = 200;

	/**
	 * Allowed `type` values from clients. Anything else returns 400.
	 *
	 * @var string[]
	 */
	const ALLOWED_TYPES = [ 'title', 'description', 'keyword', 'rewrite' ];

	// ── Bootstrap ─────────────────────────────────────────────────────────────

	public static function init(): void {
		add_action( 'wp_ajax_livada_seo_ai_suggest', [ self::class, 'ajax_suggest' ] );
	}

	// ── Public API (used by AJAX, REST and MCP) ──────────────────────────────

	/**
	 * Returns an AI-generated suggestion for the given post and type.
	 *
	 * @param  int    $post_id   Target post ID.
	 * @param  string $type      One of 'title', 'description', 'keyword', 'rewrite'.
	 * @param  string $extra     Optional source text for `rewrite` mode (defaults to the post content).
	 * @return string|WP_Error   Suggested string, or a WP_Error explaining why.
	 */
	public static function suggest( int $post_id, string $type, string $extra = '' ) {
		$post = get_post( $post_id );
		if ( ! $post ) {
			return new WP_Error( 'not_found', __( 'Post not found.', 'livada-seo' ), [ 'status' => 404 ] );
		}

		if ( ! in_array( $type, self::ALLOWED_TYPES, true ) ) {
			return new WP_Error(
				'invalid_type',
				__( 'Type must be one of: title, description, keyword.', 'livada-seo' ),
				[ 'status' => 400 ]
			);
		}

		$api_key = self::get_api_key();
		if ( '' === $api_key ) {
			return new WP_Error(
				'missing_api_key',
				__( 'Anthropic API key is not configured. Add it under General settings.', 'livada-seo' ),
				[ 'status' => 412 ]
			);
		}

		$context = self::build_post_context( $post );

		// Rewrite mode: take the user-provided text (or the post body when
		// $extra is empty) and inject it into the context.
		if ( 'rewrite' === $type ) {
			$context['rewrite_source'] = '' !== $extra ? $extra : $context['content_excerpt'];
		}

		$prompt = self::build_prompt( $type, $context );

		$body = [
			'model'      => self::MODEL,
			'max_tokens' => 'rewrite' === $type ? 1500 : self::MAX_TOKENS,
			'messages'   => [
				[
					'role'    => 'user',
					'content' => [
						[ 'type' => 'text', 'text' => $prompt ],
					],
				],
			],
		];

		$response = wp_remote_post(
			'https://api.anthropic.com/v1/messages',
			[
				'timeout' => 30,
				'headers' => [
					'x-api-key'         => $api_key,
					'anthropic-version' => '2023-06-01',
					'content-type'      => 'application/json',
				],
				'body'    => wp_json_encode( $body ),
			]
		);

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

		$code = (int) wp_remote_retrieve_response_code( $response );
		$raw  = (string) wp_remote_retrieve_body( $response );
		$json = json_decode( $raw, true );

		if ( $code >= 400 ) {
			$message = is_array( $json ) && isset( $json['error']['message'] )
				? (string) $json['error']['message']
				: 'HTTP ' . $code;
			return new WP_Error( 'ai_http_error', $message, [ 'status' => $code ] );
		}

		$out = '';
		if ( is_array( $json ) && isset( $json['content'] ) && is_array( $json['content'] ) ) {
			foreach ( $json['content'] as $block ) {
				if ( isset( $block['type'], $block['text'] ) && 'text' === $block['type'] ) {
					$out .= (string) $block['text'];
				}
			}
		}

		$out = self::clean_response( $out, $type );

		if ( '' === $out ) {
			return new WP_Error( 'empty_response', __( 'AI returned an empty response.', 'livada-seo' ), [ 'status' => 502 ] );
		}

		if ( class_exists( 'Livada_SEO_Activity_Log' ) ) {
			Livada_SEO_Activity_Log::log(
				'ai_suggest',
				sprintf( 'AI %s suggestion (%d chars)', $type, strlen( $out ) ),
				[ 'type' => $type, 'length' => strlen( $out ) ],
				[ 'type' => 'post', 'id' => $post_id ]
			);
		}

		return $out;
	}

	// ── AJAX handler ──────────────────────────────────────────────────────────

	public static function ajax_suggest(): void {
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'edit_posts' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'livada-seo' ) ], 403 );
		}

		$post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
		$type    = isset( $_POST['type'] )    ? sanitize_key( (string) $_POST['type'] ) : '';
		$extra   = isset( $_POST['extra'] )   ? wp_kses_post( wp_unslash( (string) $_POST['extra'] ) ) : '';

		if ( $post_id <= 0 || ! current_user_can( 'edit_post', $post_id ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid post or permissions.', 'livada-seo' ) ], 403 );
		}

		$result = self::suggest( $post_id, $type, $extra );

		if ( is_wp_error( $result ) ) {
			$status = (int) ( $result->get_error_data()['status'] ?? 500 );
			wp_send_json_error( [ 'message' => $result->get_error_message() ], $status );
		}

		wp_send_json_success( [
			'post_id' => $post_id,
			'type'    => $type,
			'value'   => (string) $result,
		] );
	}

	// ── Helpers ───────────────────────────────────────────────────────────────

	/**
	 * Returns the Anthropic API key from the shared settings, decrypted.
	 */
	private static function get_api_key(): string {
		if ( class_exists( 'Livada_SEO_Image_Bulk' ) ) {
			return Livada_SEO_Image_Bulk::get_anthropic_key();
		}
		return (string) Livada_SEO_Loader::get_setting( 'anthropic_api_key', '' );
	}

	/**
	 * Builds the structured context blob fed to the model. Includes the post
	 * title, the existing meta (when set), an excerpt, the focus keyword, and
	 * the WordPress locale so the model picks the right language automatically.
	 *
	 * @return array<string,string>
	 */
	private static function build_post_context( WP_Post $post ): array {
		$id = (int) $post->ID;

		$meta_title       = (string) get_post_meta( $id, '_livada_seo_title', true );
		$meta_description = (string) get_post_meta( $id, '_livada_seo_description', true );
		$focus_keyword    = (string) get_post_meta( $id, '_livada_seo_focus_keyword', true );

		// Build a content excerpt that's long enough to give context but not
		// so long it blows the model's context budget. Strip HTML, shortcodes,
		// and consecutive whitespace.
		$content = (string) $post->post_content;
		$content = strip_shortcodes( $content );
		$content = wp_strip_all_tags( $content );
		$content = html_entity_decode( $content, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$content = preg_replace( '/\s+/u', ' ', trim( $content ) ) ?? '';
		$content = mb_substr( $content, 0, 1500 );

		$locale = get_locale();
		$lang   = strtolower( substr( $locale, 0, 2 ) );

		return [
			'post_title'       => (string) $post->post_title,
			'meta_title'       => $meta_title,
			'meta_description' => $meta_description,
			'focus_keyword'    => $focus_keyword,
			'content_excerpt'  => $content,
			'permalink'        => (string) get_permalink( $id ),
			'language'         => $lang,
			'language_full'    => $locale,
		];
	}

	/**
	 * Builds the per-type prompt sent to the model. Kept short and explicit
	 * because we only want a single line back, never a markdown wrapper.
	 *
	 * @param string                $type
	 * @param array<string,string>  $ctx
	 */
	private static function build_prompt( string $type, array $ctx ): string {
		$header  = "You write SEO copy for an established WordPress site.\n";
		$header .= "Match the language of the source material; do NOT translate (locale: " . $ctx['language_full'] . ").\n";
		$header .= "Reply with the requested string ONLY — no quotes, no preamble, no markdown, no trailing punctuation unless natural.\n\n";

		$body  = "POST TITLE: " . $ctx['post_title'] . "\n";
		if ( '' !== $ctx['focus_keyword'] ) {
			$body .= "FOCUS KEYWORD: " . $ctx['focus_keyword'] . "\n";
		}
		if ( '' !== $ctx['meta_title'] ) {
			$body .= "CURRENT SEO TITLE: " . $ctx['meta_title'] . "\n";
		}
		if ( '' !== $ctx['meta_description'] ) {
			$body .= "CURRENT META DESCRIPTION: " . $ctx['meta_description'] . "\n";
		}
		$body .= "PERMALINK: " . $ctx['permalink'] . "\n";
		$body .= "CONTENT EXCERPT:\n" . $ctx['content_excerpt'] . "\n\n";

		switch ( $type ) {
			case 'title':
				$task = "Write a compelling SEO title between 50 and 60 characters. "
					. "Front-load the most relevant keyword. "
					. "If a focus keyword is set, include it verbatim. "
					. "Hard cap: 60 characters.";
				break;

			case 'description':
				$task = "Write a meta description between 120 and 160 characters that "
					. "summarises the page benefit and entices the click. "
					. "If a focus keyword is set, include it once, naturally. "
					. "Avoid quotes. Hard cap: 160 characters.";
				break;

			case 'rewrite':
				$task = "Rewrite the SOURCE TEXT below to read more clearly and rank "
					. "better in search engines. Keep the same language, structure, "
					. "and approximate length. Preserve every fact and proper noun. "
					. "If a focus keyword is set, weave it in naturally 1–2 times. "
					. "Output ONLY the rewritten text — no preamble, no markdown.\n\n"
					. "SOURCE TEXT:\n" . ( $ctx['rewrite_source'] ?? '' );
				break;

			case 'keyword':
			default:
				$task = "Suggest ONE primary focus keyword (1–4 words) the page is "
					. "best positioned to rank for. Output the phrase only. "
					. "Hard cap: 30 characters.";
				break;
		}

		return $header . $body . "TASK: " . $task;
	}

	/**
	 * Strips wrapping quotes / markdown / boilerplate the model sometimes
	 * produces despite the prompt, then enforces our SEO-safe length cap.
	 */
	private static function clean_response( string $raw, string $type ): string {
		// Rewrite mode keeps newlines and full structure — only trim wrapping
		// whitespace + quotes.
		if ( 'rewrite' === $type ) {
			$out = trim( $raw );
			$out = trim( $out, " \t\n\r\0\x0B\"'`*" );
			return $out;
		}

		$out = trim( $raw );

		// First non-empty line only — defensive against multi-line responses.
		if ( false !== strpos( $out, "\n" ) ) {
			foreach ( preg_split( '/\r?\n/', $out ) as $line ) {
				$line = trim( $line );
				if ( '' !== $line ) {
					$out = $line;
					break;
				}
			}
		}

		// Strip wrapping quotes / backticks / markdown bullets.
		$out = trim( $out, " \t\n\r\0\x0B\"'`*-•· " );

		// Normalise whitespace.
		$out = preg_replace( '/\s+/u', ' ', $out ) ?? $out;

		switch ( $type ) {
			case 'title':
				$out = mb_substr( $out, 0, 60 );
				break;
			case 'description':
				$out = mb_substr( $out, 0, 160 );
				break;
			case 'keyword':
				$out = mb_substr( $out, 0, 30 );
				break;
			case 'rewrite':
				// No hard cap on rewrite — the model self-limits via max_tokens.
				// Keep the multi-line structure (don't collapse newlines like the
				// other modes do).
				$out = trim( $raw );
				$out = trim( $out, " \t\n\r\0\x0B\"'`*" );
				break;
		}

		return $out;
	}
}
