<?php
/**
 * Batched scanner service.
 *
 * @package CertaCart_EU_Product_Readiness
 */

namespace CertaCart\EU_Product_Readiness;

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

final class Scanner {
	private const LOCK_TTL = 600;
	private const START_LOCK = 'ccpr_scan_start_lock';
	private const START_LOCK_TTL = 30;
	private Scan_Repository $repository;

	public function __construct( ?Scan_Repository $repository = null ) {
		$this->repository = $repository ?? new Scan_Repository();
	}

	public function start( int $user_id, array $scope = array() ): array {
		$active = $this->repository->active();
		if ( $active ) {
			return $this->status( (int) $active['id'] );
		}

		if ( ! $this->acquire_named_lock( self::START_LOCK, self::START_LOCK_TTL ) ) {
			$active = $this->repository->active();
			if ( $active ) {
				return $this->status( (int) $active['id'] );
			}
			throw new \RuntimeException( esc_html__( 'Another request is starting a scan. Try again in a moment.', 'certacart-eu-product-readiness' ) );
		}

		try {
			$active = $this->repository->active();
			if ( $active ) {
				return $this->status( (int) $active['id'] );
			}
			$scan_scope = Settings::sanitize_scan_scope( $scope['default_targets_eu'] ?? Settings::get( 'default_targets_eu', 'review' ) );
			if ( '' !== $scan_scope ) {
				$scope['default_targets_eu'] = $scan_scope;
			}
			$scan_id = $this->repository->create( $user_id, $scope );
			if ( ! $scan_id ) {
				throw new \RuntimeException( __( 'Unable to create a scan job.', 'certacart-eu-product-readiness' ) );
			}
			$this->repository->mark_running( $scan_id );
			return $this->status( $scan_id );
		} finally {
			delete_option( self::START_LOCK );
		}
	}

	public function process( int $scan_id ): array {
		$scan = $this->repository->get( $scan_id );
		if ( ! $scan ) {
				throw new \InvalidArgumentException( esc_html__( 'Scan not found.', 'certacart-eu-product-readiness' ) );
		}
		if ( ! in_array( $scan['status'], array( 'queued', 'running' ), true ) ) {
			return $this->status( $scan_id );
		}

		if ( ! $this->acquire_lock( $scan_id ) ) {
			$status         = $this->status( $scan_id );
			$status['busy'] = true;
			return $status;
		}

		try {
			return $this->process_locked( $scan_id );
		} finally {
			$this->release_lock( $scan_id );
		}
	}

	private function process_locked( int $scan_id ): array {
		$scan = $this->repository->get( $scan_id );
		if ( ! $scan || ! in_array( $scan['status'], array( 'queued', 'running' ), true ) ) {
			return $this->status( $scan_id );
		}

		$scope       = json_decode( (string) $scan['scope_json'], true );
		$scope       = is_array( $scope ) ? $scope : array();
		$batch_size  = (int) Settings::get( 'batch_size', 1000 );
		$product_ids = $this->repository->next_product_ids( (int) $scan['last_product_id'], $batch_size, $scope );

		if ( empty( $product_ids ) ) {
			$this->repository->complete( $scan_id );
			return $this->status( $scan_id );
		}

		$processed = 0;
		$last_id   = (int) $scan['last_product_id'];
		$settings  = Settings::all();
		$scan_scope = Settings::sanitize_scan_scope( $scope['default_targets_eu'] ?? '' );
		if ( '' !== $scan_scope ) {
			$settings['default_targets_eu'] = $scan_scope;
		}
		$evaluations = array();
		$this->prime_product_data( $product_ids );

		try {
			foreach ( $product_ids as $product_id ) {
				$product = wc_get_product( $product_id );
				$last_id = $product_id;

				if ( $product instanceof \WC_Product ) {
					$evaluation = Rules::evaluate( $product, $settings );
				} else {
					$evaluation = array(
						'score'    => 0.0,
						'in_scope' => true,
						'critical' => 1,
						'warning'  => 0,
						'info'     => 0,
						'findings' => array(
							array(
								'rule'     => 'product_load_failed',
								'severity' => 'critical',
								'message'  => __( 'WooCommerce could not load this published product. Review the product record and extensions that alter product types.', 'certacart-eu-product-readiness' ),
								'field'    => 'product',
								'weight'   => 100,
							),
						),
					);
				}

				$evaluations[ $product_id ] = $evaluation;
				++$processed;
			}
			if ( ! $this->repository->save_results( $scan_id, $evaluations ) ) {
				throw new \RuntimeException( __( 'Unable to save this batch of scan results.', 'certacart-eu-product-readiness' ) );
			}
			$this->repository->advance( $scan_id, $processed, $last_id );
		} catch ( \Throwable $throwable ) {
			$this->repository->fail( $scan_id, $throwable->getMessage() );
			throw $throwable;
		}

		$updated = $this->repository->get( $scan_id );
		if ( $updated && in_array( $updated['status'], array( 'queued', 'running' ), true ) && (int) $updated['processed_products'] >= (int) $updated['total_products'] ) {
			$this->repository->complete( $scan_id );
		}
		return $this->status( $scan_id );
	}

	/**
	 * Load product posts, metadata, and terms in bulk before WooCommerce builds
	 * individual product objects. This keeps large scans from issuing thousands
	 * of small database queries while preserving WooCommerce's normal getters.
	 *
	 * @param int[] $product_ids Product IDs in this batch.
	 */
	private function prime_product_data( array $product_ids ): void {
		if ( empty( $product_ids ) || ! function_exists( 'get_posts' ) ) {
			return;
		}

		get_posts(
			array(
				'post_type'              => 'product',
				'post_status'            => 'publish',
				'post__in'               => array_map( 'absint', $product_ids ),
				'orderby'                => 'post__in',
				'posts_per_page'         => count( $product_ids ),
				'no_found_rows'          => true,
				'update_post_meta_cache' => true,
				'update_post_term_cache' => true,
			)
		);
	}

	public function cancel( int $scan_id ): array {
		$scan = $this->repository->get( $scan_id );
		if ( ! $scan ) {
			throw new \InvalidArgumentException( esc_html__( 'Scan not found.', 'certacart-eu-product-readiness' ) );
		}
		if ( in_array( $scan['status'], array( 'queued', 'running' ), true ) ) {
			$this->repository->cancel( $scan_id );
		}
		return $this->status( $scan_id );
	}

	public function status( int $scan_id ): array {
		$scan = $this->repository->get( $scan_id );
		if ( ! $scan ) {
			throw new \InvalidArgumentException( esc_html__( 'Scan not found.', 'certacart-eu-product-readiness' ) );
		}
		$total    = max( 0, (int) $scan['total_products'] );
		$progress = $total > 0 ? min( 100, round( ( (int) $scan['processed_products'] / $total ) * 100, 1 ) ) : 100;
		if ( 'completed' === $scan['status'] ) {
			$progress = 100;
		}
		$status = array(
			'id'                 => (int) $scan['id'],
			'status'             => $scan['status'],
			'total_products'     => $total,
			'processed_products' => (int) $scan['processed_products'],
			'progress'           => $progress,
			'overall_score'      => (float) $scan['overall_score'],
			'critical_count'     => (int) $scan['critical_count'],
			'warning_count'      => (int) $scan['warning_count'],
			'info_count'         => (int) $scan['info_count'],
			'in_scope_products'    => (int) ( $scan['in_scope_products'] ?? 0 ),
			'score_available'      => self::is_score_available( $scan ),
			'scope_confirmation_required' => self::needs_scope_confirmation( $scan ),
			'error_message'      => $scan['error_message'],
			'started_at'         => $scan['started_at'],
			'completed_at'       => $scan['completed_at'],
		);
		$status['top_issues'] = 'completed' === $scan['status'] ? $this->repository->issue_summary( $scan_id ) : array();
		return $status;
	}

	/** Whether a scan has an intentionally graded EU scope. */
	public static function is_score_available( array $scan ): bool {
		$scope = json_decode( (string) ( $scan['scope_json'] ?? '' ), true );
		$scope = is_array( $scope ) ? $scope : array();
		$scan_scope = Settings::sanitize_scan_scope( $scope['default_targets_eu'] ?? '' );
		if ( 'review' === $scan_scope ) {
			return false;
		}
		return 0 < (int) ( $scan['in_scope_products'] ?? 0 );
	}

	/** Whether the scan should offer a clear route to confirm EU sales scope. */
	public static function needs_scope_confirmation( array $scan ): bool {
		$scope = json_decode( (string) ( $scan['scope_json'] ?? '' ), true );
		$scope = is_array( $scope ) ? $scope : array();
		return 'review' === Settings::sanitize_scan_scope( $scope['default_targets_eu'] ?? '' );
	}

	private function acquire_lock( int $scan_id ): bool {
		return $this->acquire_named_lock( 'ccpr_scan_lock_' . $scan_id, self::LOCK_TTL );
	}

	private function acquire_named_lock( string $key, int $ttl ): bool {
		$now     = time();
		$expires = (int) get_option( $key, 0 );

		if ( $expires > $now ) {
			return false;
		}
		if ( $expires ) {
			delete_option( $key );
		}
		return add_option( $key, $now + max( 1, $ttl ), '', false );
	}

	private function release_lock( int $scan_id ): void {
		delete_option( 'ccpr_scan_lock_' . $scan_id );
	}
}
