<?php
/**
 * Existing WooCommerce install handoff diagnostics.
 *
 * @package ScheduledChangesWooCommerce
 */

namespace ScheduledChangesWooCommerce\Handoff;

// phpcs:disable WordPress.DB -- Scheduler-owned custom tables require direct wpdb reads.
// phpcs:disable PluginCheck.Security.DirectDB -- Table names are resolved from scheduler-owned allowlists.

use ScheduledChanges\Database\TableNames;
use ScheduledChanges\Domain\Launch\LaunchVersionStatus;
use ScheduledChanges\Domain\Recording\RecordingSessionStatus;
use ScheduledChanges\Extension\ModuleDefinition;
use ScheduledChanges\Extension\ModuleRegistry;
use ScheduledChanges\Provider\ProviderManager;
use ScheduledChanges\Scheduler\WordPressActionSchedulerGateway;
use ScheduledChanges\Scheduler\QueueActionPayload;

/**
 * Reports whether legacy WooCommerce-owned data can safely move to the module.
 */
final class WooCommerceExtensionHandoff {
	public const PROVIDER_ID    = 'woocommerce-core';
	public const EXTENSION_SLUG = 'scheduled-changes-woocommerce-extension';
	public const OPTION         = 'scheduled_changes_woocommerce_extension_handoff_status';

	private \wpdb $wpdb;
	private TableNames $tables;
	private ModuleRegistry $modules;

	/**
	 * Creates the handoff reporter.
	 *
	 * @param \wpdb|null          $database WordPress database connection.
	 * @param TableNames|null     $tables Scheduler table resolver.
	 * @param ModuleRegistry|null $modules Module manifest registry.
	 */
	public function __construct( ?\wpdb $database = null, ?TableNames $tables = null, ?ModuleRegistry $modules = null ) {
		if ( null === $database ) {
			global $wpdb;

			$database = $wpdb;
		}

		$this->wpdb    = $database;
		$this->tables  = $tables ?? new TableNames();
		$this->modules = $modules ?? new ModuleRegistry();
	}

	/**
	 * Builds a live, redacted handoff report.
	 *
	 * @return array<string,mixed>
	 */
	public function report( ?ProviderManager $providers = null ): array {
		$counts            = $this->counts();
		$provider_versions = null === $providers ? array() : $providers->providers()->versions();
		$provider_loaded   = isset( $provider_versions[ self::PROVIDER_ID ] );
		$module            = $this->extensionModule();
		$module_status     = $module instanceof ModuleDefinition && '' !== $module->declaredLifecycleStatus()
			? $module->declaredLifecycleStatus()
			: ( $module instanceof ModuleDefinition ? ModuleDefinition::LIFECYCLE_UNKNOWN : ModuleDefinition::LIFECYCLE_MISSING );
		$detected          = $this->detected( $counts );
		$status            = $this->status( $detected, $provider_loaded, $module_status );

		return array(
			'providerId'      => self::PROVIDER_ID,
			'extensionSlug'   => self::EXTENSION_SLUG,
			'detected'        => $detected,
			'required'        => $detected,
			'status'          => $status,
			'safeToExecute'   => ! $detected || $provider_loaded,
			'providerLoaded'  => $provider_loaded,
			'providerVersion' => (string) ( $provider_versions[ self::PROVIDER_ID ] ?? '' ),
			'extension'       => array(
				'registered'      => $module instanceof ModuleDefinition,
				'version'         => $module instanceof ModuleDefinition ? $module->version() : '',
				'lifecycleStatus' => $module_status,
				'message'         => $module instanceof ModuleDefinition ? $module->lifecycleMessage() : '',
			),
			'counts'          => $counts,
			'message'         => $this->message( $status ),
			'recovery'        => array(
				'installOrActivateExtension',
				'finishOrAbandonActiveRecordings',
				'exportSupportEvidence',
				'followDocumentedRollbackOnly',
			),
		);
	}

	/**
	 * Persists the current report for migration and support history.
	 *
	 * @return array<string,mixed>
	 */
	public function recordReport( ?ProviderManager $providers = null ): array {
		$report               = $this->report( $providers );
		$report['recordedAt'] = gmdate( 'c' );

		update_option( self::OPTION, $report, false );

		return $report;
	}

	/**
	 * Returns the last stored report, when a migration has recorded one.
	 *
	 * @return array<string,mixed>
	 */
	public function storedReport(): array {
		$report = get_option( self::OPTION, array() );

		return is_array( $report ) ? $report : array();
	}

	/**
	 * @return array<string,int>
	 */
	private function counts(): array {
		return array(
			'launchVersions'            => $this->launchVersionCount( false ),
			'scheduledLaunchVersions'   => $this->launchVersionCount( true ),
			'launchTargets'             => $this->countProviderRows( 'launch_targets', 'provider_id' ),
			'operations'                => $this->countProviderRows( 'operations', 'provider_id' ),
			'snapshots'                 => $this->countProviderRows( 'snapshots', 'provider_id' ),
			'recordingSources'          => $this->countProviderRows( 'recording_sessions', 'source_provider_id' ),
			'recordingWorkspaces'       => $this->countProviderRows( 'recording_sessions', 'workspace_provider_id' ),
			'activeRecordings'          => $this->activeRecordingCount(),
			'pendingWooActions'         => $this->pendingWooActionCount( false ),
			'legacyQueueActions'        => $this->pendingWooActionCount( true ),
			'legacyRowsNeedingBackfill' => $this->legacyRowsNeedingBackfill(),
		);
	}

	/**
	 * Counts launch versions that reference the WooCommerce provider.
	 */
	private function launchVersionCount( bool $scheduled_only ): int {
		$tables = $this->tables->all();

		if ( ! $this->tableExists( $tables['launch_versions'] ) ) {
			return 0;
		}

		$join  = '';
		$where = 'v.required_providers_json LIKE %s OR v.plan_json LIKE %s';
		$args  = array(
			'%' . self::PROVIDER_ID . '%',
			'%' . self::PROVIDER_ID . '%',
		);

		if ( $this->tableExists( $tables['operations'] ) ) {
			$join   = "LEFT JOIN {$tables['operations']} o ON o.launch_version_id = v.id";
			$where .= ' OR o.provider_id = %s';
			$args[] = self::PROVIDER_ID;
		}

		$status = $scheduled_only ? $this->wpdb->prepare( 'v.status = %s AND ', LaunchVersionStatus::SCHEDULED ) : '';
		$sql    = "SELECT COUNT(DISTINCT v.id)
			FROM {$tables['launch_versions']} v
			{$join}
			WHERE {$status}({$where})";

		return (int) $this->wpdb->get_var( $this->wpdb->prepare( $sql, ...$args ) );
	}

	/**
	 * Counts rows owned by a provider column.
	 */
	private function countProviderRows( string $table_key, string $column ): int {
		$table = $this->tables->get( $table_key );

		if ( ! $this->tableExists( $table ) || ! $this->columnExists( $table, $column ) ) {
			return 0;
		}

		return (int) $this->wpdb->get_var(
			$this->wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE {$column} = %s", self::PROVIDER_ID )
		);
	}

	/**
	 * Counts active WooCommerce-owned recording sessions.
	 */
	private function activeRecordingCount(): int {
		$table = $this->tables->get( 'recording_sessions' );

		if ( ! $this->tableExists( $table ) ) {
			return 0;
		}

		$clauses = array( 'source_product_id > 0' );
		$args    = array( RecordingSessionStatus::ACTIVE );

		if ( $this->columnExists( $table, 'source_provider_id' ) ) {
			$clauses[] = 'source_provider_id = %s';
			$args[]    = self::PROVIDER_ID;
		}

		return (int) $this->wpdb->get_var(
			$this->wpdb->prepare(
				"SELECT COUNT(*) FROM {$table} WHERE status = %s AND (" . implode( ' OR ', $clauses ) . ')',
				...$args
			)
		);
	}

	/**
	 * Counts pending WooCommerce scheduler actions, optionally only legacy arg shapes.
	 */
	private function pendingWooActionCount( bool $legacy_only ): int {
		if (
			! function_exists( 'as_get_scheduled_actions' )
			|| ! class_exists( 'ActionScheduler' )
			|| ! class_exists( 'ActionScheduler_Store' )
		) {
			return 0;
		}

		$store      = \ActionScheduler::store();
		$action_ids = as_get_scheduled_actions(
			array(
				'hook'     => WordPressActionSchedulerGateway::HOOK,
				'group'    => WordPressActionSchedulerGateway::GROUP,
				'status'   => \ActionScheduler_Store::STATUS_PENDING,
				'per_page' => -1,
			),
			'ids'
		);
		$count      = 0;

		foreach ( $action_ids as $action_id ) {
			$args      = $store->fetch_action( (int) $action_id )->get_args();
			$is_legacy = 1 !== count( $args ) || ! is_array( $args[0] );

			try {
				$version_id = QueueActionPayload::fromActionArguments( $args )->versionId();
			} catch ( \UnexpectedValueException $exception ) {
				continue;
			}

			if ( $version_id <= 0 || ! $this->versionUsesWooCommerce( $version_id ) ) {
				continue;
			}

			if ( $legacy_only && ! $is_legacy ) {
				continue;
			}

			++$count;
		}

		return $count;
	}

	/**
	 * Counts legacy rows that still need additive provider identity backfills.
	 */
	private function legacyRowsNeedingBackfill(): int {
		$tables = $this->tables->all();
		$count  = 0;

		if ( $this->tableExists( $tables['launch_targets'] ) ) {
			$count += (int) $this->wpdb->get_var(
				"SELECT COUNT(*) FROM {$tables['launch_targets']}
				WHERE (provider_id IS NULL OR provider_id = '')
					AND (
						object_type IN ('product', 'variation')
						OR target_reference LIKE 'product:%'
						OR target_reference LIKE 'variation:%'
					)"
			);
		}

		if ( $this->tableExists( $tables['operations'] ) && $this->tableExists( $tables['launch_targets'] ) ) {
			$count += (int) $this->wpdb->get_var(
				"SELECT COUNT(*) FROM {$tables['operations']}
				WHERE (provider_id IS NULL OR provider_id = '')
					AND (
						target_uuid IN (
							SELECT target_uuid FROM {$tables['launch_targets']}
							WHERE object_type IN ('product', 'variation')
								OR target_reference LIKE 'product:%'
								OR target_reference LIKE 'variation:%'
						)
						OR field_path LIKE 'product.%'
					)"
			);
		}

		if ( $this->tableExists( $tables['recording_sessions'] ) ) {
			foreach ( array( 'source_provider_id', 'workspace_provider_id' ) as $column ) {
				if ( ! $this->columnExists( $tables['recording_sessions'], $column ) ) {
					continue;
				}

				$id_column = 'source_provider_id' === $column ? 'source_product_id' : 'shadow_product_id';
				$count    += (int) $this->wpdb->get_var(
					"SELECT COUNT(*) FROM {$tables['recording_sessions']}
					WHERE ({$column} IS NULL OR {$column} = '')
						AND {$id_column} IS NOT NULL
						AND {$id_column} > 0"
				);
			}
		}

		return $count;
	}

	/**
	 * Checks whether a stored launch version references WooCommerce provider data.
	 */
	private function versionUsesWooCommerce( int $version_id ): bool {
		$tables = $this->tables->all();

		if ( ! $this->tableExists( $tables['launch_versions'] ) ) {
			return false;
		}

		$join  = '';
		$where = 'v.required_providers_json LIKE %s OR v.plan_json LIKE %s';
		$args  = array(
			$version_id,
			'%' . self::PROVIDER_ID . '%',
			'%' . self::PROVIDER_ID . '%',
		);

		if ( $this->tableExists( $tables['operations'] ) ) {
			$join   = "LEFT JOIN {$tables['operations']} o ON o.launch_version_id = v.id";
			$where .= ' OR o.provider_id = %s';
			$args[] = self::PROVIDER_ID;
		}

		$sql = "SELECT COUNT(DISTINCT v.id)
			FROM {$tables['launch_versions']} v
			{$join}
			WHERE v.id = %d AND ({$where})";

		return (int) $this->wpdb->get_var( $this->wpdb->prepare( $sql, ...$args ) ) > 0;
	}

	/**
	 * Returns the registered WooCommerce module manifest, when present.
	 */
	private function extensionModule(): ?ModuleDefinition {
		foreach ( $this->modules->definitions() as $module ) {
			if ( self::PROVIDER_ID === $module->providerId() || self::EXTENSION_SLUG === $module->slug() ) {
				return $module;
			}
		}

		return null;
	}

	/**
	 * @param array<string,int> $counts Handoff counts.
	 */
	private function detected( array $counts ): bool {
		foreach (
			array(
				'launchVersions',
				'launchTargets',
				'operations',
				'snapshots',
				'recordingSources',
				'recordingWorkspaces',
				'activeRecordings',
				'pendingWooActions',
				'legacyRowsNeedingBackfill',
			) as $key
		) {
			if ( (int) ( $counts[ $key ] ?? 0 ) > 0 ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Resolves the live handoff status slug.
	 */
	private function status( bool $detected, bool $provider_loaded, string $module_status ): string {
		if ( ! $detected ) {
			return 'no_woocommerce_data';
		}

		if ( $provider_loaded ) {
			return 'ready';
		}

		if ( ModuleDefinition::LIFECYCLE_INCOMPATIBLE === $module_status ) {
			return 'blocked_incompatible_extension';
		}

		return 'blocked_missing_extension';
	}

	/**
	 * Returns the user-facing status message.
	 */
	private function message( string $status ): string {
		switch ( $status ) {
			case 'no_woocommerce_data':
				return __( 'No WooCommerce-owned scheduler data was detected.', 'jjs-post-scheduler' );
			case 'ready':
				return __( 'WooCommerce-owned scheduler data can execute because the WooCommerce provider module is loaded.', 'jjs-post-scheduler' );
			case 'blocked_incompatible_extension':
				return __( 'WooCommerce-owned scheduler data is preserved but paused until a compatible WooCommerce provider module is available.', 'jjs-post-scheduler' );
			default:
				return __( 'WooCommerce-owned scheduler data is preserved but paused until the WooCommerce provider module is installed and active.', 'jjs-post-scheduler' );
		}
	}

	/**
	 * Checks whether a table exists.
	 */
	private function tableExists( string $table ): bool {
		return $table === $this->wpdb->get_var( $this->wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
	}

	/**
	 * Checks whether a column exists.
	 */
	private function columnExists( string $table, string $column ): bool {
		return $column === $this->wpdb->get_var(
			$this->wpdb->prepare( "SHOW COLUMNS FROM {$table} LIKE %s", $column )
		);
	}
}
