<?php
/**
 * Converts stored scan findings into ranked, task-first summaries.
 *
 * @package CertaCart_EU_Product_Readiness
 */

namespace CertaCart\EU_Product_Readiness;

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Completed scan results live in a dedicated table and must be read in bounded pages.
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter -- The table identifier comes only from Schema and all runtime values use wpdb placeholders.

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

final class Task_Aggregator {
	private const PAGE_SIZE = 500;
	private const TASK_RULES = array(
		'manufacturer_details'          => array( 'manufacturer_name', 'manufacturer_address', 'manufacturer_contact' ),
		'eu_responsible_person'         => array( 'responsible_person_name', 'responsible_person_address', 'responsible_person_contact' ),
		'manufacturer_location_unknown' => array( 'manufacturer_location_unknown' ),
		'product_identifier_missing'    => array( 'product_identifier_missing' ),
		'product_image_missing'         => array( 'product_image_missing' ),
		'safety_information_missing'    => array( 'safety_information_missing' ),
		'storefront_display_disabled'   => array( 'storefront_display_disabled' ),
		'product_load_failed'           => array( 'product_load_failed' ),
	);

	/**
	 * Build task summaries for one stored scan.
	 *
	 * A limit of zero returns every task. Results are read in bounded pages so a
	 * large catalog does not need to load every product result at once.
	 *
	 * @param int $scan_id Scan ID.
	 * @param int $limit Maximum tasks to return; zero returns all tasks.
	 * @return array<int,array<string,mixed>>
	 */
	public function for_scan( int $scan_id, int $limit = 0 ): array {
		if ( $scan_id < 1 ) {
			return array();
		}
		$cache_key = 'ccpr_scan_tasks_' . $scan_id;
		$cached    = get_transient( $cache_key );
		if ( is_array( $cached ) ) {
			return $limit > 0 ? array_slice( $cached, 0, $limit ) : $cached;
		}

		global $wpdb;
		$table    = Schema::results_table();
		$tasks    = array();
		$after_id = 0;

		do {
			$rows = $wpdb->get_results(
				$wpdb->prepare(
					'SELECT id,product_id,in_scope,findings_json FROM %i WHERE scan_id = %d AND in_scope = 1 AND id > %d ORDER BY id ASC LIMIT %d',
					$table,
					$scan_id,
					$after_id,
					self::PAGE_SIZE
				),
				ARRAY_A
			);
			$rows = is_array( $rows ) ? $rows : array();
			if ( ! empty( $rows ) ) {
				$last_row = end( $rows );
				$after_id = (int) ( $last_row['id'] ?? $after_id );
				$this->collect( $rows, $tasks );
			}
		} while ( self::PAGE_SIZE === count( $rows ) );

		$ranked = $this->rank( $tasks, 0 );
		set_transient( $cache_key, $ranked, DAY_IN_SECONDS );
		return $limit > 0 ? array_slice( $ranked, 0, $limit ) : $ranked;
	}

	/**
	 * Aggregate result rows already available to the caller.
	 *
	 * Rows may contain a decoded `findings` array or the stored `findings_json`
	 * value. Related rules are grouped and each task is counted at most once
	 * within each product result;
	 * the scan result schema guarantees one stored row per product.
	 *
	 * @param array $result_rows Scan result rows.
	 * @param int   $limit Maximum tasks to return; zero returns all tasks.
	 * @return array<int,array<string,mixed>>
	 */
	public function aggregate( array $result_rows, int $limit = 0 ): array {
		$tasks = array();
		$this->collect( $result_rows, $tasks );
		return $this->rank( $tasks, $limit );
	}

	/**
	 * Return the stored finding rules represented by a user-facing task.
	 *
	 * Standalone and extension-provided rules map to themselves. This keeps REST
	 * filtering useful without requiring the aggregator to know every extension.
	 *
	 * @param string $task_key User-facing task key.
	 * @return string[]
	 */
	public function rules_for_task( string $task_key ): array {
		$task_key = sanitize_key( $task_key );
		if ( '' === $task_key ) {
			return array();
		}
		return self::TASK_RULES[ $task_key ] ?? array( $task_key );
	}

	/**
	 * Add rows to the internal task map.
	 *
	 * @param array $rows Result rows.
	 * @param array $tasks Task map, passed by reference.
	 */
	private function collect( array $rows, array &$tasks ): void {
		foreach ( $rows as $row ) {
			if ( ! is_array( $row ) || ( array_key_exists( 'in_scope', $row ) && empty( $row['in_scope'] ) ) ) {
				continue;
			}

			$findings = $row['findings'] ?? null;
			if ( ! is_array( $findings ) ) {
				$findings = json_decode( (string) ( $row['findings_json'] ?? '' ), true );
			}
			if ( ! is_array( $findings ) ) {
				continue;
			}

			$seen_tasks = array();

			foreach ( $findings as $finding ) {
				if ( ! is_array( $finding ) ) {
					continue;
				}
				$rule     = sanitize_key( $finding['rule'] ?? '' );
				$severity = sanitize_key( $finding['severity'] ?? '' );
				if ( '' === $rule || ! in_array( $severity, array( 'critical', 'warning' ), true ) ) {
					continue;
				}
				$task_key = $this->task_for_rule( $rule );

				if ( ! isset( $tasks[ $task_key ] ) ) {
					$tasks[ $task_key ] = array(
						'rule'              => $task_key,
						'rules'             => array(),
						'severity'          => $severity,
						'finding_message'   => sanitize_text_field( $finding['message'] ?? '' ),
						'affected_products' => 0,
					);
				} elseif ( $this->severity_rank( $severity ) > $this->severity_rank( $tasks[ $task_key ]['severity'] ) ) {
					$tasks[ $task_key ]['severity']        = $severity;
					$tasks[ $task_key ]['finding_message'] = sanitize_text_field( $finding['message'] ?? '' );
				}
				$tasks[ $task_key ]['rules'][ $rule ] = true;

				if ( isset( $seen_tasks[ $task_key ] ) ) {
					continue;
				}
				$seen_tasks[ $task_key ] = true;
				++$tasks[ $task_key ]['affected_products'];
			}
		}
	}

	/**
	 * Rank and format collected tasks.
	 *
	 * @param array $tasks Internal task map.
	 * @param int   $limit Maximum tasks; zero returns all tasks.
	 * @return array<int,array<string,mixed>>
	 */
	private function rank( array $tasks, int $limit ): array {
		$catalog = $this->catalog();
		$items   = array();

		foreach ( $tasks as $task ) {
			$rule       = $task['rule'];
			$definition = $catalog[ $rule ] ?? array();
			$message    = $task['finding_message'];
			$rules      = array_keys( $task['rules'] );
			$rule_order = array_flip( $this->rules_for_task( $rule ) );
			usort(
				$rules,
				static function ( string $left, string $right ) use ( $rule_order ): int {
					return ( $rule_order[ $left ] ?? PHP_INT_MAX ) <=> ( $rule_order[ $right ] ?? PHP_INT_MAX )
						?: strcmp( $left, $right );
				}
			);
			$title      = $definition['title'] ?? $message;
			if ( '' === $title ) {
				$title = ucwords( str_replace( array( '_', '-' ), ' ', $rule ) );
			}

			$items[] = array(
				'rank'                => 0,
				'rule'                => $rule,
				'rules'               => $rules,
				'title'               => $title,
				'severity'            => $task['severity'],
				'affected_products'   => $task['affected_products'],
				'reason'              => $definition['reason'] ?? ( '' !== $message ? $message : __( 'This supported information gap affects the listed products.', 'certacart-eu-product-readiness' ) ),
				'best_next_action'    => $definition['action'] ?? __( 'Review the recorded finding on the affected products.', 'certacart-eu-product-readiness' ),
				'estimated_minutes'   => $definition['estimated_minutes'] ?? null,
				'confidence'          => $definition['confidence'] ?? null,
				'finding_message'     => $message,
			);
		}

		usort(
			$items,
			function ( array $left, array $right ): int {
				return $this->severity_rank( $right['severity'] ) <=> $this->severity_rank( $left['severity'] )
					?: $right['affected_products'] <=> $left['affected_products']
					?: strcmp( $left['rule'], $right['rule'] );
			}
		);

		if ( $limit > 0 ) {
			$items = array_slice( $items, 0, $limit );
		}
		foreach ( $items as $index => &$item ) {
			$item['rank'] = $index + 1;
		}
		unset( $item );

		return $items;
	}

	private function severity_rank( string $severity ): int {
		return 'critical' === $severity ? 2 : ( 'warning' === $severity ? 1 : 0 );
	}

	private function task_for_rule( string $rule ): string {
		foreach ( self::TASK_RULES as $task_key => $rules ) {
			if ( in_array( $rule, $rules, true ) ) {
				return $task_key;
			}
		}
		return $rule;
	}

	/**
	 * Human actions backed by fields and settings that already exist in Free.
	 *
	 * No time estimate is supplied until real task-duration data exists. The
	 * confidence value describes certainty in the detected gap, not certainty
	 * that CertaCart can repair it automatically.
	 *
	 * @return array<string,array<string,mixed>>
	 */
	private function catalog(): array {
		return array(
			'manufacturer_details' => array(
				'title'      => __( 'Complete manufacturer details', 'certacart-eu-product-readiness' ),
				'reason'     => __( 'The affected products are missing one or more manufacturer identity, address, or contact details.', 'certacart-eu-product-readiness' ),
				'action'     => __( 'Complete the missing manufacturer name, postal address, and contact details on the affected products.', 'certacart-eu-product-readiness' ),
				'confidence' => 'high',
			),
			'manufacturer_location_unknown' => array(
				'title'      => __( 'Confirm manufacturer location', 'certacart-eu-product-readiness' ),
				'reason'     => __( 'The manufacturer’s EU location has not been confirmed.', 'certacart-eu-product-readiness' ),
				'action'     => __( 'Confirm whether the manufacturer is established in the EU for each affected product.', 'certacart-eu-product-readiness' ),
				'confidence' => 'medium',
			),
			'eu_responsible_person' => array(
				'title'      => __( 'Complete EU responsible person details', 'certacart-eu-product-readiness' ),
				'reason'     => __( 'The affected products have a non-EU manufacturer and are missing one or more EU responsible person details.', 'certacart-eu-product-readiness' ),
				'action'     => __( 'Complete the missing EU responsible person name, postal address, and contact details on the affected products.', 'certacart-eu-product-readiness' ),
				'confidence' => 'high',
			),
			'product_identifier_missing' => array(
				'title'      => __( 'Add product identifiers', 'certacart-eu-product-readiness' ),
				'reason'     => __( 'The affected products have no model, batch, type, or SKU identifier.', 'certacart-eu-product-readiness' ),
				'action'     => __( 'Add a model, batch, type, or SKU identifier to each affected product.', 'certacart-eu-product-readiness' ),
				'confidence' => 'high',
			),
			'product_image_missing' => array(
				'title'      => __( 'Add product images', 'certacart-eu-product-readiness' ),
				'reason'     => __( 'The affected products have no featured product image.', 'certacart-eu-product-readiness' ),
				'action'     => __( 'Add a clear featured image to each affected product.', 'certacart-eu-product-readiness' ),
				'confidence' => 'high',
			),
			'safety_information_missing' => array(
				'title'      => __( 'Complete safety information', 'certacart-eu-product-readiness' ),
				'reason'     => __( 'Safety information is missing or its applicability has not been confirmed for the affected products.', 'certacart-eu-product-readiness' ),
				'action'     => __( 'Add the applicable warning, or mark safety information as not applicable when that is correct.', 'certacart-eu-product-readiness' ),
				'confidence' => 'medium',
			),
			'storefront_display_disabled' => array(
				'title'      => __( 'Show product information on the storefront', 'certacart-eu-product-readiness' ),
				'reason'     => __( 'CertaCart product information is stored, but storefront display is turned off for these products.', 'certacart-eu-product-readiness' ),
				'action'     => __( 'Enable CertaCart storefront display for the affected products.', 'certacart-eu-product-readiness' ),
				'confidence' => 'high',
			),
			'product_load_failed' => array(
				'title'      => __( 'Review products CertaCart could not scan', 'certacart-eu-product-readiness' ),
				'reason'     => __( 'WooCommerce could not load these product records.', 'certacart-eu-product-readiness' ),
				'action'     => __( 'Check each product, then scan again.', 'certacart-eu-product-readiness' ),
				'confidence' => 'medium',
			),
		);
	}
}
