<?php
/**
 * AI hotspot suggestion service.
 *
 * @package Ugcify\AI
 */

namespace Ugcify\AI;

use Ugcify\Database\ProductTagRepository;
use Ugcify\Database\ReviewRepository;
use Ugcify\Review\ReviewMeta;
use WP_Error;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class HotspotSuggestionService
 */
class HotspotSuggestionService {
	/**
	 * Job repository.
	 *
	 * @var AIJobRepository
	 */
	private $jobs;

	/**
	 * Candidate service.
	 *
	 * @var CandidateProductService
	 */
	private $candidates;

	/**
	 * Provider factory.
	 *
	 * @var AIProviderFactory
	 */
	private $provider_factory;

	/**
	 * Review repository.
	 *
	 * @var ReviewRepository
	 */
	private $reviews;

	/**
	 * Review meta.
	 *
	 * @var ReviewMeta
	 */
	private $meta;

	/**
	 * Product tag repository.
	 *
	 * @var ProductTagRepository
	 */
	private $tags;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->jobs       = new AIJobRepository();
		$this->candidates = new CandidateProductService();
		$this->provider_factory = new AIProviderFactory();
		$this->reviews    = new ReviewRepository();
		$this->meta       = new ReviewMeta();
		$this->tags       = new ProductTagRepository();
	}

	/**
	 * Create an AI job.
	 *
	 * @param int    $review_id Review ID.
	 * @param int    $media_index Media index.
	 * @param string $image_url Image URL.
	 * @param int    $attachment_id Attachment ID.
	 * @return array<string,mixed>|WP_Error
	 */
	public function create_job( int $review_id, int $media_index, string $image_url = '', int $attachment_id = 0 ) {
		$review = $this->reviews->find( $review_id );
		if ( ! $review ) {
			return new WP_Error( 'review_not_found', __( 'Review not found.', 'ugcify' ) );
		}

		if ( $media_index < 0 ) {
			return new WP_Error( 'invalid_media_index', __( 'Invalid media index.', 'ugcify' ) );
		}

		$image_url = esc_url_raw( $image_url );
		if ( '' === $image_url ) {
			$image_url = $this->resolve_review_image_url( $review_id, $media_index );
		}

		if ( '' === $image_url && $attachment_id > 0 ) {
			$image_url = wp_get_attachment_url( $attachment_id ) ?: '';
		}

		if ( '' === $image_url ) {
			return new WP_Error( 'missing_image_url', __( 'Image URL is missing.', 'ugcify' ) );
		}

		if ( $attachment_id < 1 && function_exists( 'attachment_url_to_postid' ) ) {
			$attachment_id = (int) attachment_url_to_postid( $image_url );
		}

		$settings = AISettings::get_settings();
		if ( empty( $settings['enabled'] ) ) {
			return new WP_Error( 'ai_hotspots_disabled', __( 'AI hotspot suggestions are disabled.', 'ugcify' ) );
		}
		$provider = (string) $settings['provider'];

		return $this->jobs->create_or_reset(
			array(
				'review_id'      => $review_id,
				'media_id'       => $this->get_media_id( $review_id, $media_index ),
				'media_index'    => $media_index,
				'attachment_id'  => $attachment_id,
				'image_url_hash' => hash( 'sha256', $image_url ),
				'provider'       => $provider,
				'model'          => AISettings::get_model( $provider, $settings ),
			)
		);
	}

	/**
	 * Run a job.
	 *
	 * @param int $job_id Job ID.
	 * @return array<string,mixed>|WP_Error
	 */
	public function run_job( int $job_id ) {
		$job = $this->jobs->find( $job_id );
		if ( ! $job ) {
			return new WP_Error( 'job_not_found', __( 'AI hotspot job not found.', 'ugcify' ) );
		}

		if ( 'needs_review' === $job['status'] ) {
			return $job;
		}

		if ( ! $this->jobs->claim( $job_id ) ) {
			$current = $this->jobs->find( $job_id );
			return $current ? $current : new WP_Error( 'job_claim_failed', __( 'AI hotspot job is already running.', 'ugcify' ) );
		}

		$image_source = $this->resolve_image_source( $job );
		if ( is_wp_error( $image_source ) ) {
			$this->jobs->mark_status( $job_id, 'failed', $image_source->get_error_code() );
			return $image_source;
		}

		$settings = AISettings::get_settings();
		if ( empty( $settings['enabled'] ) ) {
			$this->jobs->mark_status( $job_id, 'failed', 'ai_hotspots_disabled' );
			return new WP_Error( 'ai_hotspots_disabled', __( 'AI hotspot suggestions are disabled.', 'ugcify' ) );
		}
		$job_provider = ! empty( $job['provider'] ) ? sanitize_key( (string) $job['provider'] ) : (string) $settings['provider'];
		if ( isset( $settings['providers'][ $job_provider ] ) ) {
			$settings['provider'] = $job_provider;
			if ( ! empty( $job['model'] ) ) {
				$settings['providers'][ $job_provider ]['model'] = (string) $job['model'];
			}
		}

		$candidates = $this->candidates->get_candidates( (int) $job['review_id'], (int) $settings['max_candidates'] );
		if ( empty( $candidates ) ) {
			$this->jobs->mark_status( $job_id, 'no_match', null );
			return $this->jobs->find( $job_id );
		}

		$review = $this->reviews->find( (int) $job['review_id'] );
		$provider = $this->provider_factory->make( $settings );
		$result = $provider->analyze_hotspots(
			$image_source,
			$candidates,
			array(
				'review_product_id' => $review ? (int) $review->product_id : 0,
				'review_text'       => $review ? (string) $review->content : '',
			)
		);

		if ( is_wp_error( $result ) ) {
			$this->jobs->mark_status( $job_id, 'failed', $result->get_error_code() );
			return $result;
		}

		$suggestions = $this->normalize_suggestions( $result, $candidates, (float) $settings['min_confidence'], (int) $job['media_id'] );
		$candidate_ids = array_map(
			static function ( array $candidate ) {
				return (int) $candidate['id'];
			},
			$candidates
		);

		if ( empty( $suggestions ) ) {
			$this->jobs->mark_needs_review( $job_id, $candidate_ids, array() );
			$this->jobs->mark_status( $job_id, 'no_match', null );
			return $this->jobs->find( $job_id );
		}

		$this->jobs->mark_needs_review( $job_id, $candidate_ids, $suggestions );
		return $this->jobs->find( $job_id );
	}

	/**
	 * Approve a suggestion.
	 *
	 * @param int                 $job_id Job ID.
	 * @param int                 $suggestion_index Suggestion index.
	 * @param array<string,mixed> $overrides Optional approved values.
	 * @return array<string,mixed>|WP_Error
	 */
	public function approve_suggestion( int $job_id, int $suggestion_index, array $overrides = array() ) {
		$job = $this->jobs->find( $job_id );
		if ( ! $job ) {
			return new WP_Error( 'job_not_found', __( 'AI hotspot job not found.', 'ugcify' ) );
		}

		$suggestions = is_array( $job['suggestions'] ) ? $job['suggestions'] : array();
		if ( ! isset( $suggestions[ $suggestion_index ] ) || ! is_array( $suggestions[ $suggestion_index ] ) ) {
			return new WP_Error( 'suggestion_not_found', __( 'AI hotspot suggestion not found.', 'ugcify' ) );
		}

		$suggestion = $suggestions[ $suggestion_index ];
		if ( 'approved' === ( $suggestion['status'] ?? '' ) ) {
			return new WP_Error( 'suggestion_already_approved', __( 'AI hotspot suggestion is already approved.', 'ugcify' ) );
		}

		$approved = $this->apply_approval_overrides( $suggestion, $overrides );
		if ( is_wp_error( $approved ) ) {
			return $approved;
		}

		$tag = $this->tags->create(
			(int) $job['media_id'],
			(int) $approved['product_id'],
			(float) $approved['x_coordinate'],
			(float) $approved['y_coordinate'],
			'',
			'ai',
			$job_id,
			isset( $approved['confidence'] ) ? (float) $approved['confidence'] : null
		);

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

		$tag['product'] = $this->build_product_payload( (int) $tag['product_id'] );

		$suggestions[ $suggestion_index ]           = array_merge( $suggestion, $approved );
		$suggestions[ $suggestion_index ]['status'] = 'approved';
		$this->jobs->update_suggestions( $job_id, $suggestions, 'approved' );

		return array(
			'job' => $this->jobs->find( $job_id ),
			'tag' => $tag,
		);
	}

	/**
	 * Reject a suggestion.
	 *
	 * @param int $job_id Job ID.
	 * @param int $suggestion_index Suggestion index.
	 * @return array<string,mixed>|WP_Error
	 */
	public function reject_suggestion( int $job_id, int $suggestion_index ) {
		$job = $this->jobs->find( $job_id );
		if ( ! $job ) {
			return new WP_Error( 'job_not_found', __( 'AI hotspot job not found.', 'ugcify' ) );
		}

		$suggestions = is_array( $job['suggestions'] ) ? $job['suggestions'] : array();
		if ( ! isset( $suggestions[ $suggestion_index ] ) || ! is_array( $suggestions[ $suggestion_index ] ) ) {
			return new WP_Error( 'suggestion_not_found', __( 'AI hotspot suggestion not found.', 'ugcify' ) );
		}

		$suggestions[ $suggestion_index ]['status'] = 'rejected';
		$this->jobs->update_suggestions( $job_id, $suggestions );

		return $this->jobs->find( $job_id );
	}

	/**
	 * Apply owner edits before approval.
	 *
	 * @param array<string,mixed> $suggestion Suggestion.
	 * @param array<string,mixed> $overrides Owner edits.
	 * @return array<string,mixed>|WP_Error
	 */
	private function apply_approval_overrides( array $suggestion, array $overrides ) {
		$product_id = isset( $overrides['product_id'] ) ? absint( $overrides['product_id'] ) : (int) ( $suggestion['product_id'] ?? 0 );
		$x          = array_key_exists( 'x_coordinate', $overrides ) ? (float) $overrides['x_coordinate'] : (float) ( $suggestion['x_coordinate'] ?? -1 );
		$y          = array_key_exists( 'y_coordinate', $overrides ) ? (float) $overrides['y_coordinate'] : (float) ( $suggestion['y_coordinate'] ?? -1 );

		if ( $product_id < 1 ) {
			return new WP_Error( 'invalid_product_id', __( 'A valid product is required before approving the AI hotspot.', 'ugcify' ) );
		}

		if ( ! function_exists( 'wc_get_product' ) || ! wc_get_product( $product_id ) ) {
			return new WP_Error( 'product_not_found', __( 'Selected product does not exist.', 'ugcify' ) );
		}

		if ( $x < 0 || $x > 100 || $y < 0 || $y > 100 ) {
			return new WP_Error( 'invalid_coordinates', __( 'Hotspot coordinates must be between 0 and 100.', 'ugcify' ) );
		}

		$product = wc_get_product( $product_id );

		return array(
			'product_id'     => $product_id,
			'x_coordinate'   => round( $x, 4 ),
			'y_coordinate'   => round( $y, 4 ),
			'confidence'     => isset( $suggestion['confidence'] ) ? (float) $suggestion['confidence'] : null,
			'product_title'  => $product ? sanitize_text_field( $product->get_name() ) : '',
			'label'          => isset( $suggestion['label'] ) ? sanitize_text_field( (string) $suggestion['label'] ) : '',
			'reason'         => ! empty( $overrides ) ? 'owner_edited' : ( isset( $suggestion['reason'] ) ? sanitize_key( (string) $suggestion['reason'] ) : 'visual_match' ),
			'bbox'           => isset( $suggestion['bbox'] ) ? $suggestion['bbox'] : null,
		);
	}

	/**
	 * Build product payload for an approved hotspot.
	 *
	 * @param int $product_id Product ID.
	 * @return array<string,mixed>|null
	 */
	private function build_product_payload( int $product_id ): ?array {
		if ( ! function_exists( 'wc_get_product' ) || $product_id < 1 ) {
			return null;
		}

		$product = wc_get_product( $product_id );
		if ( ! $product ) {
			return null;
		}

		return array(
			'id'         => $product->get_id(),
			'title'      => $product->get_name(),
			'url'        => get_permalink( $product->get_id() ),
			'permalink'  => get_permalink( $product->get_id() ),
			'thumbnail'  => get_the_post_thumbnail_url( $product->get_id(), 'thumbnail' ),
			'price_html' => $product->get_price_html(),
			'type'       => $product->get_type(),
		);
	}

	/**
	 * Normalize suggestions.
	 *
	 * @param array<string,mixed>             $result Provider result.
	 * @param array<int,array<string,mixed>>  $candidates Candidate products.
	 * @param float                          $min_confidence Minimum confidence.
	 * @param int                            $media_id Media ID.
	 * @return array<int,array<string,mixed>>
	 */
	private function normalize_suggestions( array $result, array $candidates, float $min_confidence, int $media_id ): array {
		$allowed_ids = array();
		$candidate_map = array();
		foreach ( $candidates as $candidate ) {
			$candidate_id = (int) $candidate['id'];
			$allowed_ids[] = $candidate_id;
			$candidate_map[ $candidate_id ] = $candidate;
		}

		$existing = $this->tags->get_by_media_id( $media_id );
		$rows     = isset( $result['suggestions'] ) && is_array( $result['suggestions'] ) ? $result['suggestions'] : array();
		$output   = array();

		foreach ( $rows as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}

			$product_id  = isset( $row['product_id'] ) ? (int) $row['product_id'] : 0;
			$confidence  = isset( $row['confidence'] ) ? (float) $row['confidence'] : 0.0;
			$x           = isset( $row['x_coordinate'] ) ? (float) $row['x_coordinate'] : -1.0;
			$y           = isset( $row['y_coordinate'] ) ? (float) $row['y_coordinate'] : -1.0;

			if ( ! in_array( $product_id, $allowed_ids, true ) || $confidence < $min_confidence || $x < 0 || $x > 100 || $y < 0 || $y > 100 ) {
				continue;
			}

			if ( $this->has_nearby_existing_tag( $existing, $product_id, $x, $y ) ) {
				continue;
			}

			$output[] = array(
				'product_id'   => $product_id,
				'x_coordinate' => round( $x, 4 ),
				'y_coordinate' => round( $y, 4 ),
				'confidence'   => round( max( 0, min( 1, $confidence ) ), 4 ),
				'label'        => isset( $row['label'] ) ? sanitize_text_field( (string) $row['label'] ) : '',
				'product_title' => isset( $candidate_map[ $product_id ]['title'] ) ? sanitize_text_field( (string) $candidate_map[ $product_id ]['title'] ) : '',
				'thumbnail'     => isset( $candidate_map[ $product_id ]['image_url'] ) ? esc_url_raw( (string) $candidate_map[ $product_id ]['image_url'] ) : '',
				'reason'       => isset( $row['reason'] ) ? sanitize_key( (string) $row['reason'] ) : 'visual_match',
				'bbox'         => isset( $row['bbox'] ) && is_array( $row['bbox'] ) ? $row['bbox'] : null,
				'status'       => 'pending',
			);
		}

		return $output;
	}

	/**
	 * Check duplicate tag near suggestion.
	 *
	 * @param array<int,array<string,mixed>> $existing Existing tags.
	 * @param int                           $product_id Product ID.
	 * @param float                         $x X coordinate.
	 * @param float                         $y Y coordinate.
	 * @return bool
	 */
	private function has_nearby_existing_tag( array $existing, int $product_id, float $x, float $y ): bool {
		foreach ( $existing as $tag ) {
			if ( (int) ( $tag['product_id'] ?? 0 ) !== $product_id ) {
				continue;
			}

			if ( abs( (float) $tag['x_coordinate'] - $x ) <= 3 && abs( (float) $tag['y_coordinate'] - $y ) <= 3 ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Resolve image source for Anthropic.
	 *
	 * @param array<string,mixed> $job Job.
	 * @return array<string,mixed>|WP_Error
	 */
	private function resolve_image_source( array $job ) {
		$attachment_id = isset( $job['attachment_id'] ) ? (int) $job['attachment_id'] : 0;
		if ( $attachment_id > 0 ) {
			$path = get_attached_file( $attachment_id );
			if ( $path && is_readable( $path ) ) {
				$mime = get_post_mime_type( $attachment_id );
				return array(
					'path'      => $path,
					'mime_type' => $mime ?: 'image/jpeg',
				);
			}
		}

		$url = $this->resolve_review_image_url( (int) $job['review_id'], (int) $job['media_index'] );
		if ( '' !== $url ) {
			return array( 'url' => $url );
		}

		return new WP_Error( 'image_not_found', __( 'Review image could not be resolved.', 'ugcify' ) );
	}

	/**
	 * Resolve review image URL from meta.
	 *
	 * @param int $review_id Review ID.
	 * @param int $media_index Media index.
	 * @return string
	 */
	private function resolve_review_image_url( int $review_id, int $media_index ): string {
		$images = $this->meta->get_meta( $review_id, 'images', true );
		if ( ! is_array( $images ) ) {
			$images = $images ? array( $images ) : array();
		}

		$images = array_values(
			array_filter(
				array_map(
					static function ( $item ) {
						if ( is_array( $item ) ) {
							return esc_url_raw( (string) ( $item['url'] ?? $item['src'] ?? '' ) );
						}
						return esc_url_raw( (string) $item );
					},
					$images
				)
			)
		);

		return isset( $images[ $media_index ] ) ? (string) $images[ $media_index ] : '';
	}

	/**
	 * Build existing synthetic media ID.
	 *
	 * @param int $review_id Review ID.
	 * @param int $media_index Media index.
	 * @return int
	 */
	private function get_media_id( int $review_id, int $media_index ): int {
		return ( $review_id * 1000 ) + $media_index;
	}
}
