<?php
/**
 * Composed Home status REST controller.
 *
 * @package Carticy\Conversions\Rest
 */

declare(strict_types=1);

namespace Carticy\Conversions\Rest;

use Carticy\Conversions\Client\ClientTrackingSettings;
use Carticy\Conversions\Diagnostics\DetectionResolutions;
use Carticy\Conversions\Diagnostics\ExistingTagScanner;
use Carticy\Conversions\Diagnostics\GatewayAccuracyReport;
use Carticy\Conversions\Diagnostics\VerificationStatus;
use Carticy\Conversions\Domain\ConsentState;
use Carticy\Conversions\Domain\ConversionEventType;
use Carticy\Conversions\Events\DispatchResult;
use Carticy\Conversions\Storage\OrderMetaStore;

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

/**
 * The single composed contract behind the Home screen: health state, last
 * tracked purchase, platform tiles, accuracy summary, and page-level notices.
 *
 * Every fact is DERIVED server-side from real stored state (recorded events,
 * dispatch rows, per-order consent records, configured settings, detected
 * tags) - the frontend renders `health.state`/`reasonCode` verbatim and never
 * computes tracking health itself (AGENTS.md architecture boundary). The
 * `blocked_consent` lifecycle semantics hold: waiting-for-consent is a paused,
 * recoverable state, never a failure; only `permanent_failed` rows are terminal.
 */
final class HomeStatusController extends AbstractRestController {

	/**
	 * Max recent orders scanned for consent-pause evidence.
	 */
	private const RECENT_ORDER_LIMIT = 25;

	/**
	 * Order meta store.
	 *
	 * @var OrderMetaStore
	 */
	private OrderMetaStore $store;

	/**
	 * Verification evidence evaluator.
	 *
	 * @var VerificationStatus
	 */
	private VerificationStatus $verification;

	/**
	 * Accuracy report builder (transient-cached).
	 *
	 * @var GatewayAccuracyReport
	 */
	private GatewayAccuracyReport $accuracy;

	/**
	 * Foreign tag scanner.
	 *
	 * @var ExistingTagScanner
	 */
	private ExistingTagScanner $scanner;

	/**
	 * Persisted detection resolutions.
	 *
	 * @var DetectionResolutions
	 */
	private DetectionResolutions $resolutions;

	/**
	 * Premium server bridge.
	 *
	 * @var PremiumServerBridge
	 */
	private PremiumServerBridge $server;

	/**
	 * Constructor.
	 *
	 * @param OrderMetaStore        $store Order meta store.
	 * @param VerificationStatus    $verification Verification evidence evaluator.
	 * @param GatewayAccuracyReport $accuracy Accuracy report builder.
	 * @param ExistingTagScanner    $scanner Foreign tag scanner.
	 * @param DetectionResolutions  $resolutions Persisted detection resolutions.
	 * @param PremiumServerBridge   $server Premium server bridge.
	 */
	public function __construct(
		OrderMetaStore $store,
		VerificationStatus $verification,
		GatewayAccuracyReport $accuracy,
		ExistingTagScanner $scanner,
		DetectionResolutions $resolutions,
		PremiumServerBridge $server
	) {
		$this->store        = $store;
		$this->verification = $verification;
		$this->accuracy     = $accuracy;
		$this->scanner      = $scanner;
		$this->resolutions  = $resolutions;
		$this->server       = $server;
	}

	/**
	 * Declared routes.
	 *
	 * @return array<int, array<string, mixed>>
	 */
	public function routes(): array {
		return array(
			array(
				'methods'  => 'GET',
				'route'    => '/status',
				'callback' => array( $this, 'get_status' ),
			),
		);
	}

	/**
	 * Build the composed Home contract.
	 *
	 * @return \WP_REST_Response
	 */
	public function get_status(): \WP_REST_Response {
		$client     = ClientTrackingSettings::from_options();
		$evidence   = $this->verification->evaluate();
		$dispatches = $this->store->get_recent_dispatches( 100 );
		$enabled    = 'yes' === get_option( 'carticy_conversions_enabled', 'yes' );
		$accuracy   = $this->accuracy->build( 30 );
		$duplicates = $this->unresolved_duplicates( $client );

		$health = $this->derive_health( $enabled, $client, $evidence, $dispatches );

		return $this->ok(
			array(
				'enabled'  => $enabled,
				'plan'     => array(
					'isPremium'  => function_exists( 'carticy_conversions_can_use_premium' ) && carticy_conversions_can_use_premium(),
					'upgradeUrl' => $this->upgrade_url(),
				),
				'health'   => $health,
				'tiles'    => $this->tiles( $client, $health, $dispatches ),
				'accuracy' => array(
					'windowDays' => (int) ( $accuracy['window']['days'] ?? 30 ),
					'paidOrders' => (int) ( $accuracy['totals']['paidOrders'] ?? 0 ),
					'gapOrders'  => (int) ( $accuracy['totals']['gapOrders'] ?? 0 ),
					'summary'    => $this->accuracy_summary( $accuracy ),
				),
				'notices'  => $this->notices( $health, $duplicates ),
				'advanced' => array(
					'cachePlugin' => $this->detect_cache_plugin(),
				),
			)
		);
	}

	/**
	 * Derive the health state from real signals, by precedence.
	 *
	 * @param bool                             $enabled Runtime enabled flag.
	 * @param ClientTrackingSettings           $client Client settings.
	 * @param array<string, mixed>             $evidence Verification evidence.
	 * @param array<int, array<string, mixed>> $dispatches Recent dispatch rows.
	 * @return array<string, mixed>
	 */
	private function derive_health( bool $enabled, ClientTrackingSettings $client, array $evidence, array $dispatches ): array {
		$purchase   = is_array( $evidence['evidence']['purchase'] ?? null ) ? $evidence['evidence']['purchase'] : null;
		$configured = $client->has_active_destinations() || $this->server->available();

		$state  = 'healthy';
		$reason = '';

		if ( ! $enabled ) {
			$state  = 'disabled';
			$reason = 'tracking_disabled';
		} elseif ( $this->has_permanent_failures( $dispatches ) ) {
			$state  = 'needs_attention';
			$reason = 'permanent_send_failures';
		} elseif ( ! $configured ) {
			$state  = 'not_configured';
			$reason = 'no_destination_configured';
		} elseif ( $this->is_paused_for_consent( $dispatches, null !== $purchase ) ) {
			$state  = 'paused_consent';
			$reason = 'orders_waiting_for_consent';
		} elseif ( null === $purchase ) {
			$state  = 'awaiting_first_purchase';
			$reason = 'no_recorded_purchase_yet';
		}

		return array(
			'state'               => $state,
			'reasonCode'          => $reason,
			'lastTrackedPurchase' => $this->last_tracked_purchase( $purchase, $evidence ),
			'evaluatedAt'         => gmdate( 'Y-m-d H:i:s' ),
		);
	}

	/**
	 * Whether recent dispatch rows contain UNRECOVERED terminal failures.
	 *
	 * @param array<int, array<string, mixed>> $dispatches Recent dispatch rows.
	 * @return bool
	 */
	private function has_permanent_failures( array $dispatches ): bool {
		return array() !== self::unrecovered_failed_destinations( $dispatches );
	}

	/**
	 * Destinations whose most recent terminal outcome is a permanent failure.
	 *
	 * Health is a rolling verdict, not a permanent stain: once the merchant
	 * fixes the setup and the destination delivers again, a newer `sent` row
	 * recovers it. Lost events stay visible in Recent events, while live status
	 * reflects current delivery ability.
	 * Rows arrive newest-first ({@see OrderMetaStore::get_recent_dispatches}).
	 *
	 * @param array<int, array<string, mixed>> $dispatches Recent dispatch rows, newest first.
	 * @return array<int, string>
	 */
	public static function unrecovered_failed_destinations( array $dispatches ): array {
		$recovered = array();
		$failed    = array();

		foreach ( $dispatches as $row ) {
			$status      = sanitize_key( (string) ( $row['status'] ?? '' ) );
			$destination = sanitize_key( (string) ( $row['destination'] ?? '' ) );

			if ( DispatchResult::STATUS_SENT === $status ) {
				$recovered[ $destination ] = true;
				continue;
			}

			if ( DispatchResult::STATUS_PERMANENT_FAILED === $status && empty( $recovered[ $destination ] ) ) {
				$failed[] = $destination;
			}
		}

		return array_values( array_unique( $failed ) );
	}

	/**
	 * Whether tracking is paused waiting for consent.
	 *
	 * Real signals only: server rows blocked on consent with nothing delivered,
	 * or (free build) recent PAID orders whose STORED consent record is
	 * denied/unset while no purchase event was recorded for them.
	 *
	 * @param array<int, array<string, mixed>> $dispatches Recent dispatch rows.
	 * @param bool                             $has_purchase Whether any purchase was recorded.
	 * @return bool
	 */
	private function is_paused_for_consent( array $dispatches, bool $has_purchase ): bool {
		$blocked = false;
		foreach ( $dispatches as $row ) {
			$status = sanitize_key( (string) ( $row['status'] ?? '' ) );
			if ( DispatchResult::STATUS_SENT === $status ) {
				return false;
			}
			if ( DispatchResult::STATUS_BLOCKED_CONSENT === $status ) {
				$blocked = true;
			}
		}

		if ( $blocked ) {
			return true;
		}

		if ( $has_purchase ) {
			return false;
		}

		return $this->recent_paid_orders_blocked_by_consent();
	}

	/**
	 * Whether recent paid orders exist whose stored consent blocks tracking and
	 * which recorded no purchase event.
	 *
	 * @return bool
	 */
	private function recent_paid_orders_blocked_by_consent(): bool {
		if ( ! function_exists( 'wc_get_orders' ) ) {
			return false;
		}

		$orders = wc_get_orders(
			array(
				'limit'   => self::RECENT_ORDER_LIMIT,
				'orderby' => 'date',
				'order'   => 'DESC',
				'return'  => 'objects',
			)
		);

		if ( is_object( $orders ) && isset( $orders->orders ) && is_array( $orders->orders ) ) {
			$orders = $orders->orders;
		}

		// Orders that predate the plugin never had consent captured - reading
		// their (absent) record as "blocked" would flag every fresh install on
		// an existing store as paused_consent.
		$activated_at = (int) get_option( 'carticy_conversions_activated_at', 0 );

		foreach ( is_array( $orders ) ? $orders : array() as $order ) {
			if ( ! is_object( $order ) || ! method_exists( $order, 'is_paid' ) || ! $order->is_paid() ) {
				continue;
			}

			if ( $activated_at > 0 && method_exists( $order, 'get_date_created' ) ) {
				$created = $order->get_date_created();
				if ( is_object( $created ) && method_exists( $created, 'getTimestamp' ) && (int) $created->getTimestamp() < $activated_at ) {
					continue;
				}
			}

			$has_purchase_event = false;
			foreach ( $this->store->get_events( $order ) as $event ) {
				if ( ConversionEventType::PURCHASE === $event->event_type() ) {
					$has_purchase_event = true;
					break;
				}
			}

			if ( ! $has_purchase_event && ConsentState::GRANTED !== $this->store->get_consent( $order )->state() ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Shape the last-tracked-purchase fact from the verification evidence.
	 *
	 * @param array<string, mixed>|null $purchase Latest recorded purchase.
	 * @param array<string, mixed>      $evidence Full verification evidence.
	 * @return array<string, mixed>|null
	 */
	private function last_tracked_purchase( ?array $purchase, array $evidence ): ?array {
		if ( null === $purchase ) {
			return null;
		}

		$server_sends = is_array( $evidence['evidence']['serverSends'] ?? null ) ? $evidence['evidence']['serverSends'] : array();
		$via          = ! empty( $evidence['client']['verified'] ) ? 'client' : ( array() !== $server_sends ? 'server' : 'client' );

		return array(
			'orderId'     => (int) ( $purchase['orderId'] ?? 0 ),
			'occurredAt'  => (string) ( $purchase['occurredAt'] ?? '' ),
			'via'         => $via,
			'destination' => array() !== $server_sends ? (string) ( $server_sends[0]['destination'] ?? '' ) : '',
		);
	}

	/**
	 * Build the Home platform tiles from configured settings + dispatch reality.
	 *
	 * @param ClientTrackingSettings           $client Client settings.
	 * @param array<string, mixed>             $health Derived health.
	 * @param array<int, array<string, mixed>> $dispatches Recent dispatch rows.
	 * @return array<string, mixed>
	 */
	private function tiles( ClientTrackingSettings $client, array $health, array $dispatches ): array {
		$config = $client->to_array();
		$failed = $this->permanently_failed_destinations( $dispatches );

		$google_parts = array();
		if ( $client->provider_enabled( 'ga4' ) ) {
			$google_parts[] = 'ga4';
		}
		if ( $client->provider_enabled( 'google_ads' ) ) {
			$google_parts[] = 'google_ads';
		}

		$gtm_enabled = ! empty( $config['gtm']['enabled'] );

		// The Google tile covers GA4 AND Google Ads destinations.
		$google_attention = $this->destinations_match( $failed, 'ga4' ) || $this->destinations_match( $failed, 'google' );

		return array(
			'google'     => array(
				'status'     => $this->tile_status( array() !== $google_parts, $google_attention ),
				'parts'      => $google_parts,
				'reasonCode' => $google_attention ? 'permanent_send_failures' : '',
			),
			'meta'       => array(
				'status'     => $this->tile_status( $client->provider_enabled( 'meta' ), $this->destinations_match( $failed, 'meta' ) ),
				'parts'      => $client->provider_enabled( 'meta' ) ? array( 'pixel' ) : array(),
				'reasonCode' => $this->destinations_match( $failed, 'meta' ) ? 'permanent_send_failures' : '',
			),
			'delivery'   => array(
				'mode'             => $gtm_enabled ? 'gtm' : ( $client->has_active_destinations() ? 'direct' : 'off' ),
				'gtmContainerId'   => (string) ( $config['gtm']['containerId'] ?? '' ),
				'pausedForConsent' => 'paused_consent' === (string) ( $health['state'] ?? '' ),
			),
			'serverSide' => array(
				'available' => $this->server->available(),
			),
		);
	}

	/**
	 * Destinations of unrecovered permanently failed dispatch rows.
	 *
	 * @param array<int, array<string, mixed>> $dispatches Recent dispatch rows.
	 * @return array<int, string>
	 */
	private function permanently_failed_destinations( array $dispatches ): array {
		return self::unrecovered_failed_destinations( $dispatches );
	}

	/**
	 * Whether any failed destination belongs to a provider prefix.
	 *
	 * @param array<int, string> $failed Failed destination keys.
	 * @param string             $prefix Provider prefix (e.g. 'ga4', 'meta').
	 * @return bool
	 */
	private function destinations_match( array $failed, string $prefix ): bool {
		foreach ( $failed as $destination ) {
			if ( 0 === strpos( $destination, $prefix ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Map configured/failed flags to a tile status.
	 *
	 * @param bool $configured Whether the platform is configured.
	 * @param bool $attention Whether real failures implicate it.
	 * @return string
	 */
	private function tile_status( bool $configured, bool $attention ): string {
		if ( $attention ) {
			return 'attention';
		}

		return $configured ? 'connected' : 'not_configured';
	}

	/**
	 * Foreign duplicate tags for platforms the plugin also fires, minus the
	 * ones the merchant already resolved.
	 *
	 * @param ClientTrackingSettings $client Client settings.
	 * @return array<int, array<string, string>>
	 */
	private function unresolved_duplicates( ClientTrackingSettings $client ): array {
		$scan       = $this->scanner->scan();
		$duplicates = array();

		foreach ( is_array( $scan['found'] ?? null ) ? $scan['found'] : array() as $item ) {
			if ( ! empty( $item['ownedByPlugin'] ) ) {
				continue;
			}

			$type = (string) ( $item['type'] ?? '' );
			if ( ! in_array( $type, array( 'ga4', 'google_ads', 'meta' ), true ) || ! $client->provider_enabled( $type ) ) {
				continue;
			}

			$id = (string) ( $item['id'] ?? '' );
			if ( '' !== $this->resolutions->action_for( $type, $id ) ) {
				continue;
			}

			$duplicates[] = array(
				'provider'  => $type,
				'foreignId' => $id,
			);
		}

		return $duplicates;
	}

	/**
	 * Page-level notices derived from real state.
	 *
	 * @param array<string, mixed>              $health Derived health.
	 * @param array<int, array<string, string>> $duplicates Unresolved duplicate tags.
	 * @return array<int, array<string, mixed>>
	 */
	private function notices( array $health, array $duplicates ): array {
		$notices = array();

		foreach ( $duplicates as $duplicate ) {
			$notices[] = array(
				'id'       => 'duplicate_pixel:' . $duplicate['provider'] . ':' . $duplicate['foreignId'],
				'type'     => 'duplicate_pixel',
				'severity' => 'warning',
				'context'  => $duplicate,
			);
		}

		$state = (string) ( $health['state'] ?? '' );
		if ( 'needs_attention' === $state ) {
			$notices[] = array(
				'id'       => 'needs_attention:' . (string) ( $health['reasonCode'] ?? '' ),
				'type'     => 'needs_attention',
				'severity' => 'critical',
				'context'  => array( 'reasonCode' => (string) ( $health['reasonCode'] ?? '' ) ),
			);
		}
		if ( 'paused_consent' === $state ) {
			$notices[] = array(
				'id'       => 'consent_paused',
				'type'     => 'consent_paused',
				'severity' => 'info',
				'context'  => array(),
			);
		}

		return $notices;
	}

	/**
	 * Map the accuracy totals to a one-word summary.
	 *
	 * @param array<string, mixed> $accuracy Accuracy report.
	 * @return string
	 */
	private function accuracy_summary( array $accuracy ): string {
		$paid = (int) ( $accuracy['totals']['paidOrders'] ?? 0 );
		$gap  = (int) ( $accuracy['totals']['gapOrders'] ?? 0 );

		if ( 0 === $paid ) {
			return 'no_data';
		}

		return $gap > 0 ? 'gaps' : 'all_matched';
	}

	/**
	 * The real upgrade destination: the Freemius checkout when the SDK is
	 * active, otherwise the public plugin page.
	 *
	 * @return string
	 */
	private function upgrade_url(): string {
		if ( function_exists( 'carticy_conversions_fs' ) ) {
			$freemius = carticy_conversions_fs();
			if ( is_object( $freemius ) && method_exists( $freemius, 'get_upgrade_url' ) ) {
				return (string) $freemius->get_upgrade_url();
			}
		}

		return 'https://carticy.com/plugins/carticy-conversions-for-woocommerce/';
	}

	/**
	 * Detect a known page-cache/optimization plugin from verifiable runtime
	 * markers (the Advanced tab renders this as a read-only fact).
	 *
	 * @return array<string, mixed>
	 */
	private function detect_cache_plugin(): array {
		$known = array(
			'WP_ROCKET_VERSION'            => 'WP Rocket',
			'W3TC'                         => 'W3 Total Cache',
			'WPCACHEHOME'                  => 'WP Super Cache',
			'LSCWP_V'                      => 'LiteSpeed Cache',
			'WPO_VERSION'                  => 'WP-Optimize',
			'AUTOPTIMIZE_PLUGIN_VERSION'   => 'Autoptimize',
			'BREEZE_VERSION'               => 'Breeze',
			'WP_FASTEST_CACHE_PLUGIN_PATH' => 'WP Fastest Cache',
		);

		foreach ( $known as $constant => $name ) {
			if ( defined( $constant ) ) {
				return array(
					'detected' => true,
					'name'     => $name,
				);
			}
		}

		return array(
			'detected' => false,
			'name'     => '',
		);
	}
}
