<?php

namespace Limb_Chatbot\Includes\Repositories;

use Limb_Chatbot\Includes\Data_Objects\Conversation_State;

/**
 * Repository class for managing Conversation_State objects.
 *
 * Handles storage, retrieval, and intelligent merging of conversation states.
 *
 * @package Limb_Chatbot\Includes\Repositories
 * @since 1.0.0
 */
class Conversation_State_Repository {

	/**
	 * Inserts or updates a conversation state in the repository.
	 *
	 * Performs intelligent merging if a state with the same chat UUID exists.
	 *
	 * @param  Conversation_State  $state  Conversation state object to persist.
	 *
	 * @throws \Exception When saving or merging fails.
	 * @since 1.0.0
	 */
	public function upsert( Conversation_State $state ): void {
		try {
			$payload = [
				'chat_uuid'           => $state->get_chat_uuid(),
				'topic'               => $state->get_topic(),
				'entities'            => $state->get_entities() ?? [],
				'summary'             => $state->get_summary(),
				'confidence'          => $state->get_confidence() !== null
					? number_format( (float) $state->get_confidence(), 4, '.', '' )
					: Conversation_State::DEFAULT_CONFIDENCE,
				'sentiment'           => $state->get_sentiment(),
				'user_preferences'    => $state->get_user_preferences() ?? [],
				'user_intent_history' => $state->get_user_intent_history() ?? [],
			];

			$existing  = $this->get_by_chat_uuid( $state->get_chat_uuid() );
			$new_state = Conversation_State::make( $payload );

			if ( $existing ) {
				$merged = $this->merge( $existing, $new_state );
				$existing->refresh( $merged );
				$existing->save();
			} else {
				$new_state->save();
			}
		} catch ( \Exception $e ) {
			throw $e;
		}
	}

	/**
	 * Retrieve a conversation state by chat UUID.
	 *
	 * @param  string  $chat_uuid  Unique chat identifier.
	 *
	 * @return Conversation_State|null The conversation state object if found, null otherwise.
	 * @since 1.0.0
	 */
	public function get_by_chat_uuid( string $chat_uuid ): ?Conversation_State {
		return Conversation_State::where( [ 'chat_uuid' => $chat_uuid ] )->first();
	}

	/**
	 * Merge two conversation state objects intelligently.
	 *
	 * Combines array fields with deduplication and updates textual fields preserving continuity.
	 * Adjusts confidence values smoothly.
	 *
	 * @param  Conversation_State  $existing  Existing state in repository.
	 * @param  Conversation_State  $incoming  Incoming state to merge.
	 *
	 * @return Conversation_State Merged conversation state.
	 * @since 1.0.0
	 */
	public function merge( Conversation_State $existing, Conversation_State $incoming ): Conversation_State {
		$base = $existing->to_array();
		$inc  = $incoming->to_array();

		$merged = $base;

		// Merge array fields intelligently
		foreach ( [ 'entities', 'user_intent_history', 'user_preferences' ] as $field ) {
			$merged[ $field ] = $this->merge_arrays( $base[ $field ] ?? [], $inc[ $field ] ?? [] );
		}

		// Merge text fields with continuity
		foreach ( [ 'chat_uuid', 'topic', 'summary', 'sentiment' ] as $field ) {
			if ( ! empty( $inc[ $field ] ) ) {
				if ( $field === 'topic' && ! empty( $base[ $field ] ) ) {
					if ( $this->is_topic_shift( $base[ $field ], $inc[ $field ] ) ) {
						$merged[ $field ] = $inc[ $field ];
					}
				} elseif ( $field === 'summary' && ! empty( $base[ $field ] ) ) {
					$merged[ $field ] = $this->evolve_summary( $base[ $field ], $inc[ $field ] );
				} else {
					$merged[ $field ] = $inc[ $field ];
				}
			}
		}

		// Smoothly adjust confidence
		$merged['confidence'] = $this->adjust_confidence(
			$base['confidence'] ?? Conversation_State::DEFAULT_CONFIDENCE,
			$inc['confidence'] ?? Conversation_State::DEFAULT_CONFIDENCE
		);

		return Conversation_State::make( $merged );
	}

	/**
	 * Merge two arrays intelligently with deduplication and cleanup.
	 * Handles both flat arrays and structured objects (entities, preferences).
	 *
	 * @param  array  $base  Base array.
	 * @param  array  $incoming  Incoming array to merge.
	 *
	 * @return array Merged and cleaned array.
	 * @since 1.0.0
	 */
	private function merge_arrays( array $base, array $incoming ): array {
		// Handle structured objects (entities, preferences)
		if ( $this->is_structured_object( $base ) || $this->is_structured_object( $incoming ) ) {
			return $this->merge_structured_objects( $base, $incoming );
		}

		// Handle flat arrays (user_intent_history)
		$merged = $base;
		foreach ( $incoming as $item ) {
			if ( ! $this->array_contains( $merged, $item ) ) {
				$merged[] = $item;
			}
		}

		return $this->cleanup_array( $merged );
	}

	/**
	 * Check if an array contains a similar item.
	 *
	 * @param  array  $array  Array to search.
	 * @param  mixed  $item  Item to check.
	 *
	 * @return bool True if a similar item exists, false otherwise.
	 * @since 1.0.0
	 */
	private function array_contains( array $array, $item ): bool {
		foreach ( $array as $existing ) {
			if ( $this->items_are_similar( $existing, $item ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Determine if two items are similar (strings, arrays, exact match).
	 *
	 * @param  mixed  $item1  First item.
	 * @param  mixed  $item2  Second item.
	 *
	 * @return bool True if items are considered similar.
	 * @since 1.0.0
	 */
	private function items_are_similar( $item1, $item2 ): bool {
		if ( $item1 === $item2 ) {
			return true;
		}

		if ( is_string( $item1 ) && is_string( $item2 ) ) {
			return $this->string_similarity( $item1, $item2 ) > Conversation_State::STRING_SIMILARITY_THRESHOLD;
		}

		if ( is_array( $item1 ) && is_array( $item2 ) ) {
			return $this->arrays_are_similar( $item1, $item2 );
		}

		return false;
	}

	/**
	 * Compute string similarity (0-1) using Levenshtein distance.
	 *
	 * @param  string  $a  First string.
	 * @param  string  $b  Second string.
	 *
	 * @return float Similarity ratio.
	 * @since 1.0.0
	 */
	private function string_similarity( string $a, string $b ): float {
		$a = strtolower( trim( $a ) );
		$b = strtolower( trim( $b ) );

		if ( $a === $b ) {
			return 1.0;
		}

		$distance = levenshtein( $a, $b );

		return 1 - ( $distance / max( strlen( $a ), strlen( $b ) ) );
	}

	/**
	 * Compare two arrays for similarity.
	 *
	 * @param  array  $arr1  First array.
	 * @param  array  $arr2  Second array.
	 *
	 * @return bool True if arrays are similar.
	 * @since 1.0.0
	 */
	private function arrays_are_similar( array $arr1, array $arr2 ): bool {
		if ( count( $arr1 ) !== count( $arr2 ) ) {
			return false;
		}

		foreach ( $arr1 as $k => $v ) {
			if ( ! isset( $arr2[ $k ] ) || ! $this->items_are_similar( $v, $arr2[ $k ] ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Clean up array by removing empty/null values and limiting length.
	 *
	 * @param  array  $array  Input array.
	 *
	 * @return array Cleaned array.
	 * @since 1.0.0
	 */
	private function cleanup_array( array $array ): array {
		$array = array_filter( $array, fn( $v ) => $v !== null && $v !== '' && $v !== [] );

		if ( count( $array ) > Conversation_State::MAX_ARRAY_LENGTH ) {
			$array = array_slice( $array, - Conversation_State::MAX_ARRAY_LENGTH );
		}

		return $array;
	}

	/**
	 * Detect whether a topic has shifted significantly.
	 *
	 * @param  string  $old  Old topic.
	 * @param  string  $new  New topic.
	 *
	 * @return bool True if topic has shifted, false otherwise.
	 * @since 1.0.0
	 */
	private function is_topic_shift( string $old, string $new ): bool {
		$old_words  = array_map( 'strtolower', explode( ' ', $old ) );
		$new_words  = array_map( 'strtolower', explode( ' ', $new ) );
		$common     = count( array_intersect( $old_words, $new_words ) );
		$similarity = $common / max( count( $old_words ), count( $new_words ) );

		return $similarity < Conversation_State::TOPIC_SHIFT_THRESHOLD;
	}

	/**
	 * Evolve conversation summary intelligently by replacing with updated version.
	 *
	 * Uses the new summary as the primary source, ensuring it's concise and current.
	 *
	 * @param  string  $old  Old summary.
	 * @param  string  $new  New summary.
	 *
	 * @return string Evolved summary.
	 * @since 1.0.0
	 */
	private function evolve_summary( string $old, string $new ): string {
		// Use the new summary as the primary source
		$summary = trim( $new );
		
		// If new summary is empty or too short, keep the old one
		if ( empty( $summary ) || strlen( $summary ) < 10 ) {
			return $old;
		}
		
		// Ensure summary is not too long (max 2 sentences)
		$sentences = preg_split( '/(?<=[.!?])\s+/', $summary );
		if ( count( $sentences ) > 2 ) {
			$summary = implode( ' ', array_slice( $sentences, 0, 2 ) );
		}
		
		return $summary;
	}

	/**
	 * Adjust confidence smoothly between old and new values.
	 *
	 * @param  float  $old  Previous confidence value.
	 * @param  float  $new  New confidence value.
	 *
	 * @return float Adjusted confidence.
	 * @since 1.0.0
	 */
	private function adjust_confidence( float $old, float $new ): float {
		return abs( $new - $old ) > 0.3 ? $new : ( $old * 0.7 + $new * 0.3 );
	}

	/**
	 * Check if an array is a structured object (has string keys).
	 *
	 * @param  array  $array  Array to check.
	 *
	 * @return bool True if structured object, false if flat array.
	 * @since 1.0.0
	 */
	private function is_structured_object( array $array ): bool {
		if ( empty( $array ) ) {
			return false;
		}

		// Check if all keys are strings (structured object)
		foreach ( array_keys( $array ) as $key ) {
			if ( ! is_string( $key ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Merge structured objects (entities, preferences) intelligently.
	 *
	 * @param  array  $base  Base structured object.
	 * @param  array  $incoming  Incoming structured object.
	 *
	 * @return array Merged structured object.
	 * @since 1.0.0
	 */
	private function merge_structured_objects( array $base, array $incoming ): array {
		$merged = $base;

		foreach ( $incoming as $key => $value ) {
			if ( is_array( $value ) && isset( $merged[ $key ] ) && is_array( $merged[ $key ] ) ) {
				// Merge array values with deduplication
				$merged[ $key ] = $this->merge_array_values( $merged[ $key ], $value );
			} else {
				// Replace or add new key-value pair
				$merged[ $key ] = $value;
			}
		}

		return $this->cleanup_structured_object( $merged );
	}

	/**
	 * Merge array values with deduplication.
	 *
	 * @param  array  $base  Base array values.
	 * @param  array  $incoming  Incoming array values.
	 *
	 * @return array Merged array values.
	 * @since 1.0.0
	 */
	private function merge_array_values( array $base, array $incoming ): array {
		$merged = $base;

		foreach ( $incoming as $item ) {
			if ( ! $this->array_contains( $merged, $item ) ) {
				$merged[] = $item;
			}
		}

		return array_filter( $merged, fn( $v ) => $v !== null && $v !== '' );
	}

	/**
	 * Clean up structured object by removing empty values and limiting array lengths.
	 *
	 * @param  array  $object  Structured object to clean.
	 *
	 * @return array Cleaned structured object.
	 * @since 1.0.0
	 */
	private function cleanup_structured_object( array $object ): array {
		foreach ( $object as $key => $value ) {
			if ( is_array( $value ) ) {
				// Clean array values
				$object[ $key ] = array_filter( $value, fn( $v ) => $v !== null && $v !== '' );
				
				// Limit array length
				if ( count( $object[ $key ] ) > Conversation_State::MAX_ARRAY_LENGTH ) {
					$object[ $key ] = array_slice( $object[ $key ], - Conversation_State::MAX_ARRAY_LENGTH );
				}
			} elseif ( $value === null || $value === '' ) {
				// Remove empty values
				unset( $object[ $key ] );
			}
		}

		return $object;
	}
}
