<?php

namespace AIPT\Api\BulkGenerator\Desc;

use AIPT\Api\BulkGenerator\Desc\Core\ProductDataProvider;
use AIPT\Api\BulkGenerator\Desc\Core\PromptFactory;
use AIPT\Api\BulkGenerator\Desc\Core\ResponseParser;
use AIPT\Api\BulkGenerator\Desc\Core\OutputValidator;
use AIPT\Api\BulkGenerator\Common\AIClientFactory;
use AIPT\Api\BulkGenerator\Common\DatabaseManager;
use AIPT\Utils\ImagePreserver;
use AIPT\Utils\PromptFormatter;

class ContentGenerator {

	private const MAX_RETRIES = 2;
	private const BATCH_SIZE = 10;

	private DatabaseManager $db;
	private array $options;

	public function __construct( ?DatabaseManager $db = null ) {
		$this->db      = $db ?? new DatabaseManager();
		$this->options = array();
	}

	/**
	 * Log debug message only when WP_DEBUG is enabled.
	 *
	 * @param string $message Debug message to log.
	 * @return void
	 */
	private function debugLog( string $message ): void {
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
		}
	}

	public function generate( array $productIds, array $contentTypes, array $options = array() ): GenerationResult {
		$result    = new GenerationResult();
		$startTime = microtime( true );

		$this->options = $this->normalizeOptions( $options );

		$productIds   = array_unique( array_map( 'absint', $productIds ) );
		$contentTypes = $this->normalizeContentTypes( $contentTypes );

		if ( empty( $productIds ) || empty( $contentTypes ) ) {
			$result->errors[] = 'No products or content types specified';
			return $result;
		}

		$chunks = array_chunk( $productIds, self::BATCH_SIZE );

		foreach ( $chunks as $chunk ) {
			$productsData = ProductDataProvider::bulkGet( $chunk );

			foreach ( $chunk as $productId ) {
				$productData = $productsData[ $productId ] ?? null;

				if ( null === $productData ) {
					$result->results[ $productId ] = new ProductResult( $productId );
					$post_type   = get_post_type( $productId );
					$post_status = get_post_status( $productId );
					if ( ! $post_type ) {
						$result->results[ $productId ]->errors[] = sprintf(
							'Product ID %d does not exist in the database.',
							$productId
						);
					} elseif ( $post_type === 'product_variation' ) {
						$parent_id = wp_get_post_parent_id( $productId );
						$result->results[ $productId ]->errors[] = sprintf(
							'Product ID %d is a variation (parent: %d). Use the parent product instead.',
							$productId,
							$parent_id ?: 0
						);
					} elseif ( ! in_array( $post_status, array( 'publish', 'draft', 'pending', 'private' ), true ) ) {
						$result->results[ $productId ]->errors[] = sprintf(
							'Product ID %d has status "%s" which is not supported. Supported statuses: publish, draft, pending, private.',
							$productId,
							$post_status ?: 'unknown'
						);
					} else {
						$result->results[ $productId ]->errors[] = sprintf(
							'Product ID %d could not be loaded (type: %s, status: %s). This may be a data integrity issue.',
							$productId,
							$post_type,
							$post_status ?: 'unknown'
						);
					}
					$result->failureCount++;
					continue;
				}

				$productResult = $this->processProduct( $productId, $productData, $contentTypes );
				$result->results[ $productId ] = $productResult;

				if ( $productResult->success ) {
					$result->successCount++;
				} else {
					$result->failureCount++;
				}
			}

			unset( $productsData );
			if ( function_exists( 'gc_collect_cycles' ) ) {
				gc_collect_cycles();
			}
		}

		$result->processingTime = microtime( true ) - $startTime;
		return $result;
	}

	public function generateForProduct( int $productId, array $contentTypes, array $options = array() ): ProductResult {
		$result = $this->generate( array( $productId ), $contentTypes, $options );
		return $result->results[ $productId ] ?? new ProductResult( $productId );
	}

	private function processProduct( int $productId, array $productData, array $contentTypes ): ProductResult {
		$result  = new ProductResult( $productId );
		$factory = new PromptFactory( $productData, $this->options );

		$client = $this->createAIClient( $productData );
		if ( null === $client ) {
			$result->errors[] = 'Failed to create AI client';
			return $result;
		}

		$unifiedContent = null;
		try {
			$unifiedContent = $this->generateUnified( $client, $factory, $contentTypes );
		} catch ( \Exception $e ) {
			$result->errors[] = $e->getMessage();
		}

		if ( null !== $unifiedContent ) {
			foreach ( $contentTypes as $type ) {
				if ( isset( $unifiedContent[ $type ] ) ) {
					$content       = $unifiedContent[ $type ];
					$fieldImageMap = $factory->getImageMapForField( (string) $type );
					$content       = $this->restoreImagePlaceholders( $content, $fieldImageMap, $productId, (string) $type );

					$validation = OutputValidator::validate( $content, $type, $this->options );
					if ( $validation->isValid ) {
						$sanitized              = OutputValidator::sanitize( $content, $type );
						$result->content[ $type ] = $sanitized;
						$this->saveToHistory( $productId, $type, $sanitized );
					} else {
						$fixed = $this->tryFixContent( $content, $type );
						if ( null !== $fixed ) {
							$result->content[ $type ] = $fixed;
							$this->saveToHistory( $productId, $type, $fixed );
						} else {

							$result->errors[] = sprintf(
								/* translators: 1: content type, 2: validation errors, 3: response length, 4: validation strategy */
								__( 'Validation failed for %1$s: %2$s (response length: %3$d bytes, validator: %4$s)', 'ai-product-tools' ),
								$type,
								implode( ', ', $validation->errors ),
								strlen( (string) $content ),
								$type
							);
						}
					}
				}
			}
		}

		$missingTypes = array_diff( $contentTypes, array_keys( $result->content ) );

		if ( ! empty( $missingTypes ) ) {
			$sequentialResult = array( 'results' => array(), 'errors' => array(), 'imageMaps' => array() );
			try {
				$sequentialResult = $this->generateSequential( $client, $factory, $missingTypes );
			} catch ( \Exception $e ) {
				$result->errors[] = $e->getMessage();
			}

			if ( ! empty( $sequentialResult['errors'] ) ) {
				foreach ( $sequentialResult['errors'] as $error ) {
					$result->errors[] = sprintf( '%s: %s', $error['type'], $error['message'] );
				}
			}

			foreach ( $sequentialResult['results'] as $type => $content ) {
				if ( ! isset( $result->content[ $type ] ) && ! empty( $content ) ) {
					$fieldImageMap          = $sequentialResult['imageMaps'][ $type ] ?? array();
					$content                = $this->restoreImagePlaceholders( $content, $fieldImageMap, $productId, (string) $type );
					$sanitized              = OutputValidator::sanitize( $content, $type );
					$result->content[ $type ] = $sanitized;
					$this->saveToHistory( $productId, $type, $sanitized );
				}
			}
		}

		$result->success = ! empty( $result->content );
		return $result;
	}

	private function generateUnified( $client, PromptFactory $factory, array $contentTypes ): ?array {
		$prompts = $factory->buildUnified( $contentTypes, $this->options );

		$model = $this->options['model'] ?? null;

		try {
			$response = $client->generate_description(
				$prompts['system'],
				$prompts['user'],
				$model
			);

			if ( empty( $response ) ) {
				return null;
			}

			$parseResult = ResponseParser::parseUnified( $response, $contentTypes );

			if ( $parseResult->success && ! empty( $parseResult->content ) ) {
				return $parseResult->content;
			}

			return null;
		} catch ( \Exception $e ) {
			$sanitized_message = \AIPT\Core\ErrorHandler::sanitize_error_for_logging( $e->getMessage() );
			$this->debugLog( sprintf( 'generateUnified failed: %s - %s (code: %d)', get_class( $e ), $sanitized_message, $e->getCode() ) );
			throw $e;
		}
	}

	private function generateSequential( $client, PromptFactory $factory, array $contentTypes ): array {
		$results   = array();
		$errors    = array();
		$imageMaps = array();
		$model     = $this->options['model'] ?? null;

		foreach ( $contentTypes as $type ) {
			$prompts              = $factory->buildSingle( $type, $this->options );
			$imageMaps[ $type ] = $factory->getImageMapForField( (string) $type );

			try {
				$response = $client->generate_description(
					$prompts['system'],
					$prompts['user'],
					$model
				);

				if ( ! empty( $response ) ) {
					$parseResult = ResponseParser::parseUnified( $response, array( $type ) );

					if ( $parseResult->success && isset( $parseResult->content[ $type ] ) ) {
						$results[ $type ] = $parseResult->content[ $type ];
					} else {
						$normalized = ResponseParser::normalizeContent( $response, $type );
						if ( ! empty( $normalized ) ) {
							$results[ $type ] = $normalized;
						}
					}
				}
			} catch ( \Exception $e ) {
				$sanitized_message = \AIPT\Core\ErrorHandler::sanitize_error_for_logging( $e->getMessage() );
				$this->debugLog( sprintf( 'generateSequential failed for type %s: %s - %s (code: %d)', $type, get_class( $e ), $sanitized_message, $e->getCode() ) );
				$errors[] = array(
					'type' => $type,
					'message' => $e->getMessage(),
					'code' => (int) $e->getCode(),
				);
			}
		}

		if ( ! empty( $errors ) ) {
			$this->debugLog( sprintf( 'generateSequential completed with %d error(s)', count( $errors ) ) );
		}

		return array(
			'results'   => $results,
			'errors'    => $errors,
			'imageMaps' => $imageMaps,
		);
	}

	/**
	 * Restore image placeholders in AI-generated content back to original <img> tags.
	 * Logs dropped placeholders when the AI fails to return them verbatim.
	 *
	 * @param string                $content   AI response content (may contain placeholders).
	 * @param array<string, string> $imageMap  Placeholder-to-tag map from PromptFactory::getImageMap().
	 * @param int                   $productId Product ID, for logging context.
	 * @param string                $type      Content type (description | short_description | tags), for logging context.
	 * @return string Content with placeholders restored (and missing images appended as fallback).
	 */
	private function restoreImagePlaceholders( string $content, array $imageMap, int $productId, string $type ): string {
		if ( empty( $imageMap ) ) {
			return $content;
		}

		$result = ImagePreserver::unmask( $content, $imageMap );

		if ( ! empty( $result['missing'] ) ) {
			$this->debugLog(
				sprintf(
					'Image preservation: %d of %d placeholders dropped by AI for product %d (type: %s), appended as fallback',
					count( $result['missing'] ),
					count( $imageMap ),
					$productId,
					$type
				)
			);
		}

		return $result['content'];
	}

	private function tryFixContent( string $content, string $type ): ?string {
		$sanitized = OutputValidator::sanitize( $content, $type );

		$validation = OutputValidator::validate( $sanitized, $type, $this->options );
		if ( $validation->isValid ) {
			return $sanitized;
		}

		$truncated  = OutputValidator::truncateToLimit( $sanitized, $type, $this->options );
		$validation = OutputValidator::validate( $truncated, $type, $this->options );
		if ( $validation->isValid ) {
			return $truncated;
		}

		if ( empty( $validation->errors ) && ! empty( $validation->warnings ) ) {
			return $truncated;
		}

		return null;
	}

	private function saveToHistory( int $productId, string $type, string $content ): void {
		$dbType = $this->mapTypeToDbType( $type );
		$this->db->insert_history_item( $productId, $dbType, $content );
	}

	private function mapTypeToDbType( string $type ): string {
		$mapping = array(
			'short_description' => 'short',
			'description'       => 'full',
			'tags'              => 'tags',
		);

		return $mapping[ $type ] ?? $type;
	}

	private function createAIClient( array $productData ): ?object {
		$provider = $this->options['provider'] ?? get_option( 'aipt_api_provider', 'openai' );

		try {
			return AIClientFactory::create( $provider, array(
				'category'        => $productData['categories'] ?? '',
				'generation_type' => 'unified',
				'model'           => $this->options['model'] ?? null,
			) );
		} catch ( \Exception $e ) {
			$sanitized_message = \AIPT\Core\ErrorHandler::sanitize_error_for_logging( $e->getMessage() );
			$this->debugLog( sprintf( 'createAIClient failed for provider %s: %s - %s (code: %d)', $provider, get_class( $e ), $sanitized_message, $e->getCode() ) );
			return null;
		}
	}

	private function normalizeOptions( array $options ): array {
		$defaults = array(
			'provider'                   => get_option( 'aipt_api_provider', 'openai' ),
			'model'                      => get_option( 'aipt_model', '' ),
			'language'                   => get_option( 'aipt_language', 'English' ),
			'writing_style'              => get_option( 'aipt_writing_style', 'Professional' ),
			'content_length_preset'      => get_option( 'aipt_content_length_preset', 'long' ),
			'short_length_preset'        => get_option( 'aipt_short_length_preset', 'standard' ),
			'max_tags_count'             => (int) get_option( 'aipt_max_tags_count', 3 ),
			'improveExistingDescription' => false,
			'keepExistingDescription'    => false,
			'additionalTagsCount'        => 2,
		);

		return array_merge( $defaults, $options );
	}

	private function normalizeContentTypes( array $types ): array {
		$mapping = array(
			'generateShortDescription' => 'short_description',
			'generateDescription'      => 'description',
			'generateTags'             => 'tags',
			'short'                    => 'short_description',
			'full'                     => 'description',
		);

		$normalized = array();

		foreach ( $types as $key => $value ) {
			if ( is_string( $key ) && $value === true ) {
				$type = $mapping[ $key ] ?? $key;
				if ( in_array( $type, array( 'short_description', 'description', 'tags' ), true ) ) {
					$normalized[] = $type;
				}
			} elseif ( is_string( $value ) ) {
				$type = $mapping[ $value ] ?? $value;
				if ( in_array( $type, array( 'short_description', 'description', 'tags' ), true ) ) {
					$normalized[] = $type;
				}
			}
		}

		return array_unique( $normalized );
	}

	public function applyContent( int $productId, array $content, array $options = array() ): bool {
		$this->debugLog( "applyContent: START - productId={$productId}, content_keys=" . implode( ',', array_keys( $content ) ) );

		try {
			$product = wc_get_product( $productId );
			if ( ! $product ) {
				$this->debugLog( "applyContent: Product not found for ID {$productId}" );
				return false;
			}

			$this->debugLog( "applyContent: Product found, type=" . get_class( $product ) );

			$keepExisting = ! empty( $options['keepExistingDescription'] );
			$position     = $options['existingDescriptionPosition'] ?? 'after';
			$beforeText   = $options['beforeContentText'] ?? '';
			$afterText    = $options['afterContentText'] ?? '';

			$appliedAny = false;

			foreach ( $content as $type => $text ) {
				$this->debugLog( "applyContent: Processing type={$type}, content_length=" . strlen( $text ) );

				if ( empty( $text ) ) {
					$this->debugLog( "applyContent: Skipping empty content for type={$type}" );
					continue;
				}

				if ( $type === 'tags' ) {
					$this->applyTags( $productId, $text, $options );
					$appliedAny = true;
					continue;
				}

				$finalContent = $text;

				if ( ! empty( $beforeText ) || ! empty( $afterText ) ) {
					$finalContent = $beforeText . $finalContent . $afterText;
				}

				if ( $keepExisting ) {
					$existing = $type === 'short_description'
						? $product->get_short_description()
						: $product->get_description();

					if ( ! empty( $existing ) ) {
						$finalContent = $position === 'before'
							? $finalContent . "\n\n" . $existing
							: $existing . "\n\n" . $finalContent;
					}
				}

				if ( $type === 'short_description' ) {
					$this->debugLog( "applyContent: Setting short_description for product {$productId}, content_length=" . strlen( $finalContent ) );
					$product->set_short_description( $finalContent );
					$appliedAny = true;
				} elseif ( $type === 'description' ) {
					$this->debugLog( "applyContent: Setting description for product {$productId}, content_length=" . strlen( $finalContent ) );
					$product->set_description( $finalContent );
					$appliedAny = true;
				} else {
					$this->debugLog( "applyContent: Unknown type={$type}, skipping" );
				}
			}

			if ( $appliedAny ) {
				$this->debugLog( "applyContent: About to save product {$productId}" );
				$save_result = $product->save();
				$this->debugLog( "applyContent: Product saved, result=" . wp_json_encode( $save_result ) );

				ProductDataProvider::clearCache( $productId );
				$this->debugLog( "applyContent: Cache cleared for product {$productId}" );

				return true;
			}

			$this->debugLog( "applyContent: Nothing was applied for product {$productId}" );
			return false;
		} catch ( \Exception $e ) {
			$sanitized_message = \AIPT\Core\ErrorHandler::sanitize_error_for_logging( $e->getMessage() );
			$sanitized_trace = \AIPT\Core\ErrorHandler::sanitize_error_for_logging( $e->getTraceAsString() );
			$this->debugLog( "applyContent: EXCEPTION - " . $sanitized_message );
			$this->debugLog( "applyContent: Stack trace - " . $sanitized_trace );
			return false;
		}
	}

	private function applyTags( int $productId, string $tagsString, array $options ): void {
		$tags = ResponseParser::parseTags( $tagsString, $this->options['max_tags_count'] ?? 5 );

		if ( empty( $tags ) ) {
			return;
		}

		$append = ! empty( $options['keepExistingTags'] );

		wp_set_object_terms( $productId, $tags, 'product_tag', $append );
		clean_object_term_cache( $productId, 'product' );
	}

	public function getHistory(): array {
		return $this->db->get_history_items();
	}
}

class GenerationResult {
	public array $results        = array();
	public int $successCount     = 0;
	public int $failureCount     = 0;
	public array $errors         = array();
	public float $processingTime = 0.0;
}

class ProductResult {
	public int $productId;
	public array $content = array();
	public array $errors  = array();
	public bool $success  = false;

	public function __construct( int $productId ) {
		$this->productId = $productId;
	}
}
