<?php

namespace Limb_Chatbot\Includes\Services\Export;

use Limb_Chatbot\Includes\Data_Objects\Dataset_Entry;
use Limb_Chatbot\Includes\Data_Objects\Message;
use Limb_Chatbot\Includes\Data_Objects\Message_Meta;
use Limb_Chatbot\Includes\Interfaces\Chat_Export_Formatter_Interface;
use Limb_Chatbot\Includes\Services\Knowledge\Knowledge_Source_Tracker;

/**
 * Class Abstract_Chat_Export_Formatter
 *
 * Base class for all chat export format strategies.
 * Provides shared helpers for extracting message data.
 *
 * @package Limb_Chatbot\Includes\Services\Export
 * @since 1.0.0
 */
abstract class Abstract_Chat_Export_Formatter implements Chat_Export_Formatter_Interface {

	/**
	 * Live agent source meta values.
	 *
	 * @var string[]
	 */
	const LIVE_AGENT_SOURCES = [ 'slack_agent', 'telegram_agent' ];

	/**
	 * Role label used for live agent messages in exports.
	 *
	 * @var string
	 */
	const ROLE_LIVE_AGENT = 'live_agent';

	/**
	 * Human-readable labels for special content types that carry no text.
	 *
	 * @var array<string, string>
	 */
	const CONTENT_TYPE_LABELS = [
		'live_agent_disconnection' => 'Live agent disconnected',
		'slack_connection'         => 'Slack connection',
		'action_cancellation'      => 'Action cancelled',
	];

	/**
	 * Extract a flat message record from a Message data object.
	 *
	 * @param Message $message The message data object.
	 *
	 * @return array{timestamp: string|null, role: string|null, type: string, content: string, sources: array}
	 */
	protected function extract_message_record( Message $message ): array {
		$content_type = $this->resolve_content_type( $message );
		$content      = $message->extract_text();

		// For special content types with no text body, use a descriptive label.
		if ( empty( trim( $content ) ) && isset( self::CONTENT_TYPE_LABELS[ $content_type ] ) ) {
			$content = '[' . self::CONTENT_TYPE_LABELS[ $content_type ] . ']';
		}

		return [
			'timestamp' => $message->get_created_at(),
			'role'      => $this->resolve_role( $message ),
			'type'      => $content_type,
			'content'   => $content,
			'sources'   => $this->resolve_sources( $message ),
		];
	}

	/**
	 * Resolve the display role for a message.
	 *
	 * Messages originating from a Slack or Telegram agent (identified by the
	 * 'source' meta) are labelled as 'live_agent' instead of 'assistant'.
	 *
	 * @param Message $message The message data object.
	 *
	 * @return string The resolved role string.
	 */
	protected function resolve_role( Message $message ): string {
		$source_meta = $message->get_meta( 'source' );

		if (
			$source_meta instanceof Message_Meta
			&& in_array( $source_meta->get_meta_value(), self::LIVE_AGENT_SOURCES, true )
		) {
			return self::ROLE_LIVE_AGENT;
		}

		return $message->get_role() ?? Message::ROLE_ASSISTANT;
	}

	/**
	 * Resolve the primary content type from a message's content array.
	 *
	 * @param Message $message The message data object.
	 *
	 * @return string The resolved content type.
	 */
	protected function resolve_content_type( Message $message ): string {
		$content = $message->get_content();
		if ( empty( $content ) || ! is_array( $content ) ) {
			return Message::CONTENT_TYPE_TEXT;
		}

		$first_part = reset( $content );

		return isset( $first_part['type'] ) ? sanitize_text_field( $first_part['type'] ) : Message::CONTENT_TYPE_TEXT;
	}

	/**
	 * Resolve knowledge sources attached to a message.
	 *
	 * Reads the dataset_entry_sources meta, looks up each Dataset_Entry,
	 * and returns an array of source records with similarity, title, and URL.
	 *
	 * @param Message $message The message data object.
	 *
	 * @return array<int, array{similarity: float, source: string, source_url: string}> Source records.
	 */
	protected function resolve_sources( Message $message ): array {
		$sources_meta = $message->get_meta( Knowledge_Source_Tracker::META_KEY_DATASET_ENTRY_SOURCES );

		if (
			! $sources_meta instanceof Message_Meta
			|| empty( $sources_meta->get_meta_value() )
			|| ! is_array( $sources_meta->get_meta_value() )
		) {
			return [];
		}

		$sources = [];
		foreach ( $sources_meta->get_meta_value() as $source_ref ) {
			if ( empty( $source_ref['id'] ) ) {
				continue;
			}

			$dataset_entry = Dataset_Entry::find( (int) $source_ref['id'] );
			if ( ! $dataset_entry instanceof Dataset_Entry ) {
				continue;
			}

			$entry_data     = $dataset_entry->get_entry();
			$entry_metadata = $entry_data['entry_metadata'] ?? [];
			$similarity     = isset( $source_ref['similarity'] ) ? round( (float) $source_ref['similarity'], 4 ) : null;
			$source_title   = $entry_metadata['post_title'] ?? $entry_metadata['heading'] ?? '';
			$source_url     = $entry_metadata['source_url'] ?? '';

			$sources[] = [
				'similarity' => $similarity,
				'source'     => sanitize_text_field( $source_title ),
				'source_url' => esc_url_raw( $source_url ),
			];
		}

		return $sources;
	}

	/**
	 * Sanitize a string for safe plain-text output in export files.
	 *
	 * Strips HTML tags, markdown formatting, and non-renderable symbols (emojis).
	 *
	 * @param string $value The value to sanitize.
	 *
	 * @return string Sanitized plain-text value.
	 */
	protected function sanitize_export_value( string $value ): string {
		$value = wp_strip_all_tags( $value );
		$value = $this->strip_markdown( $value );
		$value = $this->strip_emojis( $value );

		return trim( $value );
	}

	/**
	 * Convert markdown content to basic HTML for PDF rendering.
	 *
	 * Handles: bold, italic, horizontal rules, unordered lists,
	 * headings (h1–h3), links, and line breaks.
	 *
	 * @param string $text Raw markdown text.
	 *
	 * @return string HTML string.
	 */
	protected function markdown_to_html( string $text ): string {
		// Normalise line endings.
		$text = str_replace( [ "\r\n", "\r" ], "\n", $text );

		// Horizontal rules.
		$text = preg_replace( '/^-{3,}$/m', '<hr>', $text );

		// Bold + italic.
		$text = preg_replace( '/\*\*\*(.+?)\*\*\*/s', '<strong><em>$1</em></strong>', $text );

		// Bold.
		$text = preg_replace( '/\*\*(.+?)\*\*/s', '<strong>$1</strong>', $text );

		// Italic.
		$text = preg_replace( '/\*(.+?)\*/s', '<em>$1</em>', $text );

		// Headings (### h3, ## h2, # h1).
		$text = preg_replace( '/^###\s+(.+)$/m', '<h3>$1</h3>', $text );
		$text = preg_replace( '/^##\s+(.+)$/m', '<h2>$1</h2>', $text );
		$text = preg_replace( '/^#\s+(.+)$/m', '<h1>$1</h1>', $text );

		// Unordered list items (- item or * item).
		$text = preg_replace( '/^[-*]\s+(.+)$/m', '<li>$1</li>', $text );
		$text = preg_replace( '/(<li>.*<\/li>\n?)+/s', '<ul>$0</ul>', $text );

		// Links [text](url).
		$text = preg_replace( '/\[([^]]+)]\(([^)]+)\)/', '<a href="$2">$1</a>', $text );

		// Line breaks (double newline → paragraph, single → <br>).
		$text = preg_replace( '/\n{2,}/', '</p><p>', $text );
		$text = str_replace( "\n", '<br>', $text );

		// Wrap in paragraph if not already wrapped.
		if ( strpos( $text, '<p>' ) === false && strpos( $text, '<h' ) === false ) {
			$text = '<p>' . $text . '</p>';
		}

		$text = $this->strip_unsupported_emoji( $text );

		return $text;
	}

	/**
	 * Strip only supplementary-plane emojis that no standard font can render.
	 *
	 * Unlike {@see strip_emojis()}, this method preserves BMP symbols (arrows,
	 * dashes, bullets, etc.) that Unicode-capable fonts like DejaVu Sans support.
	 * Use this for PDF output; use {@see strip_emojis()} for plain-text formats.
	 *
	 * @param string $text Input text.
	 *
	 * @return string Text with unsupported emojis removed.
	 */
	protected function strip_unsupported_emoji( string $text ): string {
		// Remove characters from Supplementary planes (U+10000+) — covers most emojis.
		$text = preg_replace( '/[\x{10000}-\x{10FFFF}]/u', '', $text );

		// Remove variation selectors and zero-width joiners (emoji modifiers).
		$text = preg_replace( '/[\x{FE00}-\x{FE0F}]/u', '', $text );
		$text = preg_replace( '/[\x{200D}]/u', '', $text );
		$text = preg_replace( '/[\x{20E3}]/u', '', $text );

		// Clean up leftover whitespace.
		$text = preg_replace( '/\s{2,}/', ' ', $text );

		return $text;
	}

	/**
	 * Strip markdown formatting from text, leaving plain readable content.
	 *
	 * @param string $text Markdown text.
	 *
	 * @return string Plain text without markdown syntax.
	 */
	protected function strip_markdown( string $text ): string {
		// Bold + italic.
		$text = preg_replace( '/\*{1,3}(.+?)\*{1,3}/s', '$1', $text );

		// Headings.
		$text = preg_replace( '/^#{1,6}\s+/m', '', $text );

		// Horizontal rules.
		$text = preg_replace( '/^-{3,}$/m', '', $text );

		// Unordered list markers.
		$text = preg_replace( '/^[-*]\s+/m', '• ', $text );

		// Links [text](url) → text.
		$text = preg_replace( '/\[([^]]+)]\([^)]+\)/', '$1', $text );

		return $text;
	}

	/**
	 * Remove emoji and other non-renderable Unicode symbols.
	 *
	 * Strips characters from Supplementary Multilingual Plane and common emoji ranges
	 * that cannot be rendered by standard PDF fonts (Helvetica, Courier, etc.).
	 *
	 * @param string $text Input text.
	 *
	 * @return string Text with emojis removed.
	 */
	protected function strip_emojis( string $text ): string {
		// Replace known symbols with ASCII equivalents first.

		// Remove characters from Supplementary planes (U+10000 and above) — covers most emojis.
		$text = preg_replace( '/[\x{10000}-\x{10FFFF}]/u', '', $text );

		// Remove common emoji/symbol ranges in BMP.
		$text = preg_replace( '/[\x{2600}-\x{27BF}]/u', '', $text );  // Misc symbols, dingbats.
		$text = preg_replace( '/[\x{FE00}-\x{FE0F}]/u', '', $text );  // Variation selectors.
		$text = preg_replace( '/[\x{200D}]/u', '', $text );           // Zero-width joiner.
		$text = preg_replace( '/[\x{20E3}]/u', '', $text );           // Combining enclosing keycap.
		$text = preg_replace( '/[\x{2190}-\x{21FF}]/u', '', $text );  // Remaining arrows not already replaced.
		$text = preg_replace( '/[\x{2200}-\x{22FF}]/u', '', $text );  // Mathematical operators.
		$text = preg_replace( '/[\x{2300}-\x{23FF}]/u', '', $text );  // Miscellaneous technical.
		$text = preg_replace( '/[\x{2500}-\x{257F}]/u', '', $text );  // Box drawing.
		$text = preg_replace( '/[\x{2580}-\x{259F}]/u', '', $text );  // Block elements.
		$text = preg_replace( '/[\x{25A0}-\x{25FF}]/u', '', $text );  // Geometric shapes.

		// Clean up leftover whitespace.
		$text = preg_replace( '/\s{2,}/', ' ', $text );

		return $text;
	}
}

