<?php

namespace EasyCriticalCSS;

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

class Failure_Patterns {
	const FAILURE_TIMEOUT_OR_NO_RESPONSE = 'timeout_or_no_response';
	const FAILURE_FETCH_BLOCKED          = 'fetch_blocked';
	const FAILURE_CALLBACK_BLOCKED       = 'callback_blocked';
	const FAILURE_GENERIC                = 'generic_failure';

	// Escalation threshold (after this many consecutive failures, show admin warning)
	const FAILURE_THRESHOLD = 3;

	// Cooldown window to pause auto-retries
	const COOLDOWN_SECONDS = 6 * HOUR_IN_SECONDS;

	// Docs link for troubleshooting
	const TROUBLESHOOTING_URL = 'https://criticalcss.net/docs/automatic-generation-failures';

	public static function init() {
		add_action( 'easy_cc_detect_timeouts', [ __CLASS__, 'detect_and_mark_timeouts' ] );
	}

	public static function classify_failure( $last_error ) {
		if ( empty( $last_error ) ) {
			return self::FAILURE_GENERIC;
		}

		$last_error = strtolower( $last_error );

		// Timeout indicators in error message: "timeout", "did not return", or "expired".
		if ( strpos( $last_error, 'timeout' ) !== false ||
			strpos( $last_error, 'did not return' ) !== false ||
			strpos( $last_error, 'expired' ) !== false ) {
			return self::FAILURE_TIMEOUT_OR_NO_RESPONSE;
		}

		// Fetch blocked indicators in error message: "fetch", "could not fetch", or "blocked".
		if ( strpos( $last_error, 'fetch' ) !== false ||
			strpos( $last_error, 'blocked' ) !== false ) {
			return self::FAILURE_FETCH_BLOCKED;
		}

		// Callback blocked indicators in error message: "POST" or "callback".
		if ( strpos( $last_error, 'post' ) !== false ||
			strpos( $last_error, 'callback' ) !== false ) {
			return self::FAILURE_CALLBACK_BLOCKED;
		}

		return self::FAILURE_GENERIC;
	}

	public static function record_success( $identifier ) {
		$url_hash = Helpers::get_url_hash( $identifier );
		if ( empty( $url_hash ) ) {
			return false;
		}

		$data = [
			'url_hash'             => $url_hash,
			'consecutive_failures' => 0,
			'last_failure_class'   => null,
			'last_failure_time'    => null,
			'paused_until'         => null,
		];

		return Database::upsert_row( $data );
	}

	public static function should_track_failure( $auto_mode_status = null ) {
		if ( ! is_array( $auto_mode_status ) ) {
			$auto_mode_status = Helpers::get_auto_mode_status();
		}

		$reason = ! empty( $auto_mode_status['reason'] )
			? $auto_mode_status['reason']
			: Helpers::get_auto_mode_status_reason( $auto_mode_status );

		return ! in_array( $reason, [ 'api_expired', 'no_api_key' ], true );
	}

	private static function get_row_for_identifier( $identifier ) {
		$url_hash = Helpers::get_url_hash( $identifier );
		if ( empty( $url_hash ) ) {
			return null;
		}

		return Database::get_row_by_url_hash( $url_hash );
	}

	public static function record_failure( $identifier, $failure_class ) {
		$row = self::get_row_for_identifier( $identifier );
		if ( empty( $row ) ) {
			return false;
		}

		$consecutive = 1;
		if ( ! empty( $row['last_failure_class'] ) && $row['last_failure_class'] === $failure_class ) {
			$consecutive = (int) $row['consecutive_failures'] + 1;
		}

		$paused_until = null;
		if ( $consecutive >= self::FAILURE_THRESHOLD ) {
			$paused_until = gmdate( 'Y-m-d H:i:s', time() + self::COOLDOWN_SECONDS );
		}

		$data = [
			'url_hash'             => $row['url_hash'],
			'consecutive_failures' => $consecutive,
			'last_failure_class'   => $failure_class,
			'last_failure_time'    => current_time( 'mysql' ),
			'paused_until'         => $paused_until,
		];

		return Database::upsert_row( $data );
	}

	public static function is_paused( $identifier ) {
		$row = self::get_row_for_identifier( $identifier );
		if ( empty( $row['paused_until'] ) ) {
			return false;
		}

		$paused_until = strtotime( $row['paused_until'] );

		return $paused_until !== false && $paused_until > time();
	}

	public static function has_escalated( $identifier ) {
		$row = self::get_row_for_identifier( $identifier );
		if ( empty( $row['consecutive_failures'] ) ) {
			return false;
		}

		return (int) $row['consecutive_failures'] >= self::FAILURE_THRESHOLD;
	}

	public static function get_failure_count( $identifier ) {
		$row = self::get_row_for_identifier( $identifier );
		if ( empty( $row ) ) {
			return 0;
		}

		return (int) $row['consecutive_failures'];
	}

	public static function detect_and_mark_timeouts() {
		global $wpdb;

		$table_name    = Database::get_table_name();
		$threshold     = gmdate( 'Y-m-d H:i:s', time() - ( 30 * MINUTE_IN_SECONDS ) );
		$failure_class = self::FAILURE_TIMEOUT_OR_NO_RESPONSE;

		// Find all pending rows older than 30 minutes and not yet marked as timed out.
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This method requires a direct DB query.
		$pending_rows = $wpdb->get_results(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT url_hash, last_failure_class FROM {$table_name}
				 WHERE processing_status = 'pending' AND requested_time < %s",
				$threshold
			),
			ARRAY_A
		);
		// phpcs:enable

		if ( empty( $pending_rows ) ) {
			return;
		}

		foreach ( $pending_rows as $row ) {
			// Only mark as timeout if not already marked or if it's a different failure class.
			if ( $row['last_failure_class'] !== $failure_class ) {
				self::record_failure( $row['url_hash'], $failure_class );
			}
		}
	}
}
