<?php
namespace Actirise\Includes;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

use Actirise\Includes\Options;
use Actirise\Includes\Helpers;
use Actirise\Includes\Api;
use Actirise\Includes\Logger;
use Actirise\Includes\Cron;

/**
 * Event annotation management class.
 *
 * This class allows creating, managing and sending event annotations
 * to the Actirise API. It handles predefined event categories and
 * allows tracking state changes of events.
 *
 * @phpstan-type EventData array{
 *     key: string,
 *     title: string,
 *     message: string,
 *     category: string,
 *     startDate: string,
 *     endDate: string,
 *     timezone: string,
 *     scope: string,
 *     initial_state: mixed,
 *     new_state: mixed
 * }
 *
 * @final
 * @package Actirise\Includes
 * @since 3.2.0
 */
final class EventAnnotation {
	/**
	 * Adds or updates an event in the queue.
	 *
	 * This method allows adding a new event or updating an existing event
	 * based on its title. If an event with the same title already exists,
	 * only the new state and message will be updated.
	 *
	 * @param string $title Event title.
	 * @param string $message Message describing the event.
	 * @param string $category Event category ('website_update' or 'config_changes').
	 * @param mixed  $initial_state Initial state of the event (optional).
	 * @param mixed  $new_state New state of the event (optional).
	 * @param bool   $direct If true, the event will be sent immediately via cron (default: false).
	 *
	 * @return void
	 *
	 * @since 3.2.0
	 */
	public static function add_event( $title, $message, $category, $initial_state = null, $new_state = null, $direct = false ) {
		/** @var array<int, EventData> $options */
		$options = Options::get( 'events', array() );

		if ( ! is_array( $options ) ) {
			$options = array();
		}

		$categories = self::get_categories_id();

		if ( ! is_array( $categories ) || ! array_key_exists( $category, $categories ) ) {
			return;
		}

		$category = $categories[$category];

		$event_key   = md5( $title );
		$event_found = false;

		foreach ( $options as $index => $option_event ) {
			if ( isset( $option_event['key'] ) && $option_event['key'] === $event_key ) {
				$options[$index]['new_state'] = $new_state;
				$options[$index]['message']   = $message;
				$event_found                  = true;
				break;
			}
		}

		if ( ! $event_found ) {
			$dates = self::get_dates();

			$options[] = array(
				'key'           => $event_key,
				'title'         => 'WP - ' . $title,
				'message'       => $message,
				'category'      => $category,
				'startDate'     => $dates['date_now'],
				'endDate'       => $direct ? $dates['date_now'] : $dates['date_five'],
				'timezone'      => $dates['timezone'],
				'initial_state' => $initial_state,
				'new_state'     => $new_state,
			);
		}

		Options::update( 'events', $options );

		if ( $direct ) {
			self::send_events();
		}
	}

	/**
	 * Processes and sends events.
	 *
	 * This method can be called either by the WordPress cron system or directly
	 * to check pending events and send them to the API.
	 * It first performs an event check then attempts to send them.
	 *
	 * @return void
	 *
	 * @since 3.2.0
	 */
	public static function send_events() {
		self::check_events();
		self::send_to_api();
	}

	/**
	 * Updates event annotation scheduling via cron.
	 *
	 * This method creates a cron instance and checks for scheduled tasks
	 * using transients to upload event annotations. It ensures that the
	 * upload_event_annotation task is properly scheduled.
	 *
	 * @return void
	 *
	 * @since 3.2.0
	 */
	public static function update_event_annotation() {
		$cron = new Cron();
		$cron->check_scheduled_task_with_transient(
			'upload_event_annotation',
			array(
				$cron,
				'upload_event_annotation',
			)
		);
	}

	/**
	 * Checks and cleans the list of pending events.
	 *
	 * This method goes through the event list and removes those
	 * where the initial state and new state are identical, considering
	 * that no real change has occurred.
	 *
	 * @return void
	 *
	 * @since 3.2.0
	 */
	private static function check_events() {
		/** @var array<int, EventData> $events */
		$events = Options::get( 'events', array() );

		if ( ! is_array( $events ) ) {
			$events = array();
		}

		if ( count( $events ) === 0 ) {
			return;
		}

		foreach ( $events as $index => $event ) {
			if ( array_key_exists( 'initial_state', $event )
				&& array_key_exists( 'new_state', $event )
				&& $event['initial_state'] !== null
				&& $event['new_state'] !== null
				&& $event['initial_state'] === $event['new_state'] ) {
				unset( $events[$index] );
			}
		}

		Options::update( 'events', $events );
	}

	/**
	 * Sends pending events to the Actirise API.
	 *
	 * This method retrieves all pending events and sends them
	 * one by one to the API. After a successful send, the event is removed
	 * from the queue. In case of error, the event remains in the queue
	 * and an error log is generated.
	 *
	 * @return void
	 *
	 * @since 3.2.0
	 */
	private static function send_to_api() {
		if ( ! Helpers::is_user_authenticated() ) {
			return;
		}

		/** @var array<int, EventData> $events */
		$events = Options::get( 'events', array() );

		if ( ! is_array( $events ) ) {
			$events = array();
		}

		if ( count( $events ) === 0 ) {
			return;
		}

		$website = Helpers::get_website_info();

		if ( ! is_array( $website ) || empty( $website ) ) {
			return;
		}

		/** @var string $onboardingStatus */
		$onboardingStatus = $website['onboardingStatus'];

		$status_map = array(
			'suspended' => 'suspended',
			'refused'   => 'refused',
			'disabled'  => 'disabled',
		);

		if ( array_key_exists( $onboardingStatus, $status_map ) ) {
			Options::update( 'events', array() );

			return;
		}

		$dates = self::get_dates();

		foreach ( $events as $index => $event ) {
			if ( ! isset( $event['startDate'], $event['endDate'], $event['title'], $event['message'], $event['category'] ) ) {
				continue;
			}

			$body = array(
				'startDate'          => $event['startDate'],
				'endDate'            => $event['endDate'],
				'websites'           => array( $website['@id'] ),
				'title'              => $event['title'],
				'description'        => $event['message'],
				'category'           => $event['category'],
				'scope'              => 'public',
				'timezone'           => $dates['timezone'],
				'allAllowedWebsites' => false,
			);

			/** @var string $token */
			$token    = Options::get( 'settings-analytics-token' );
			$api      = new Api();
			$response = $api->post( 'api', 'event_annotations', $body, $token );

			if ( is_wp_error( $response ) ) {
				Logger::add_log( 'event annotation error: ' . $response->get_error_message(), 'include/eventannotation', 'error' );
				continue;
			}

			unset( $events[$index] );
		}

		Options::update( 'events', $events );
	}

	/**
	 * Retrieves the event annotation categories from the API.
	 *
	 * This method fetches the available event categories from the Actirise API
	 * and maps them to the appropriate category identifiers used by the plugin.
	 * It specifically looks for 'Website update' and 'Configuration Changes' categories.
	 *
	 * @return array<string, string>|null Array of category mappings or null on error.
	 *
	 * @since 3.2.0
	 */
	private static function get_categories_id() {
		if ( ! Helpers::is_user_authenticated() ) {
			return null;
		}

		/** @var string $token */
		$token    = Options::get( 'settings-analytics-token' );
		$api      = new Api();
		$response = $api->get( 'api', 'event_annotation_categories', array(), $token, true );

		if ( is_wp_error( $response ) ) {
			Logger::add_log( 'get categories id error: ' . $response->get_error_message(), 'include/eventannotation', 'error' );
			return null;
		}

		if ( ! is_array( $response ) || ! array_key_exists( 'hydra:member', $response ) ) {
			Logger::add_log( 'get categories id error: invalid response', 'include/eventannotation', 'error' );
			return null;
		}

		$categories = $response['hydra:member'];

		if ( ! is_array( $categories ) || empty( $categories ) ) {
			return null;
		}

		/** @var array<string, string> $cat_id */
		$cat_id = array();

		foreach ( $categories as $category ) {
			if ( isset( $category['name'], $category['@id'] ) && is_string( $category['@id'] ) ) {
				if ( $category['name'] === 'Website update' ) {
					$cat_id['website_update'] = $category['@id'];
				} elseif ( $category['name'] === 'Configuration Changes' ) {
					$cat_id['config_changes'] = $category['@id'];
				}
			}
		}

		if ( count( $cat_id ) === 0 ) {
			Logger::add_log( 'get categories id error: no categories found', 'include/eventannotation', 'error' );
			return null;
		}

		return $cat_id;
	}

	/**
	 * Retrieves current date information with timezone.
	 *
	 * This method generates date information including the current date,
	 * a date 5 minutes in the future, and the timezone. It uses WordPress's
	 * configured timezone if available, otherwise defaults to UTC.
	 *
	 * @return array{date_now: string, date_five: string, timezone: string} Array containing date information.
	 *
	 * @since 3.2.0
	 */
	private static function get_dates() {
		$timezone        = 'UTC';
		$timezone_string = get_option( 'timezone_string' );

		if ( is_string( $timezone_string ) && $timezone_string !== '' ) {
			$timezone = $timezone_string;
		} else {
			$gmt_offset = get_option( 'gmt_offset', 0 );
			$offset     = is_numeric( $gmt_offset ) ? (float) $gmt_offset : 0.0;
			$hours      = (int) $offset;
			$minutes    = ( $offset - $hours );
			$sign       = ( $offset < 0 ) ? '-' : '+';
			$abs_hour   = abs( $hours );
			$abs_mins   = abs( $minutes * 60 );
			$timezone   = sprintf( '%s%02d:%02d', $sign, $abs_hour, $abs_mins );
		}

		$current_date = new \DateTime( 'now' );
		$current_date->setTimezone( new \DateTimeZone( $timezone ) );

		// The API validates the "timezone" field against IANA identifiers (Symfony
		// Assert\Timezone) and rejects raw UTC offsets such as "+02:00" or "+00:00",
		// which WordPress produces when no timezone_string is configured. To stay valid
		// in every configuration, dates are emitted in RFC3339 (the UTC offset is embedded
		// in the value itself) and the "timezone" field is always the IANA id "UTC".
		$dates = array(
			'date_now'  => $current_date->format( \DateTime::RFC3339 ),
			'date_five' => $current_date->modify( '+5 minutes' )->format( \DateTime::RFC3339 ),
			'timezone'  => 'UTC',
		);

		return $dates;
	}
}
