<?php
/**
 * Template history and rollback utilities.
 *
 * @package MailDoctor
 */

namespace MailDoctor\Email;

use MailDoctor\WPMD_Email_Templates;

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

/**
 * Persists template versions via revisions (CPT storage) or options (option storage).
 */
class TemplateHistory {

	protected const OPTION_PREFIX        = 'mail_doctor_tpl_hist_';
	protected const OPTION_ACTIVE_PREFIX = 'mail_doctor_tpl_current_';
	protected const OPTION_LAST_ROLLBACK = 'mail_doctor_last_history_restore';
	protected const REVISION_META_KEY    = '_mail_doctor_history_payload';
	protected const MAX_VERSIONS         = 50;

	/**
	 * Record a new template version.
	 *
	 * @param Template             $template Template payload to capture.
	 * @param array<string, mixed> $context  Additional context (post_id, editor_mode, locale, restored_from).
	 * @return string|null
	 */
	public static function record( Template $template, array $context = array() ): ?string {
		$payload    = self::build_payload( $template, $context );
		$identifier = self::template_identifier_from_payload( $payload );

		if ( '' === $identifier ) {
			return null;
		}

		$latest = self::latest_hash( $identifier );
		if ( $latest && $latest === $payload['hash'] ) {
			return null;
		}

		if ( self::supports_revisions( $payload ) ) {
			$version_id = self::record_revision_entry( (int) $payload['post_id'], $payload );
		} else {
			$version_id = self::record_option_entry( $identifier, $payload );
		}

		if ( $version_id ) {
			/** @var mixed $payload */
			do_action( 'mail_doctor_template_history_recorded', $version_id, $payload );
		}

		return $version_id;
	}

	/**
	 * Retrieve historical versions for a template.
	 *
	 * @param int|string $template_id Post ID or option key.
	 * @param int        $limit       Maximum versions to return.
	 * @return array<int, array<string, mixed>>
	 */
	public static function list_versions( $template_id, int $limit = 20 ): array {
		$limit      = max( 1, $limit );
		$parsed_key = self::parse_template_identifier( $template_id );

		if ( 'post' === $parsed_key['type'] && $parsed_key['value'] > 0 ) {
			return self::get_revision_history( (int) $parsed_key['value'], $limit );
		}

		if ( 'option' === $parsed_key['type'] && '' !== $parsed_key['value'] ) {
			return self::get_option_history( $parsed_key['value'], $limit );
		}

		return array();
	}

	/**
	 * Fetch a single version payload by identifier.
	 *
	 * @param string $version_id Version identifier.
	 * @return array<string, mixed>|null
	 */
	public static function get( string $version_id ): ?array {
		if ( 0 === strpos( $version_id, 'rev-' ) ) {
			$revision_id = (int) substr( $version_id, 4 );
			if ( $revision_id <= 0 ) {
				return null;
			}

			return self::load_revision_payload( $revision_id );
		}

		if ( 0 === strpos( $version_id, 'opt-' ) ) {
			return self::load_option_payload( substr( $version_id, 4 ) );
		}

		return null;
	}

	/**
	 * Restore a stored version and persist the change.
	 *
	 * @param string $version_id Version identifier.
	 * @return Template|null
	 */
	public static function restore( string $version_id ): ?Template {
		$version = self::get( $version_id );
		if ( null === $version || empty( $version['template'] ) || ! is_array( $version['template'] ) ) {
			return null;
		}

		if ( 'revision' === ( $version['storage'] ?? '' ) && ! empty( $version['post_id'] ) ) {
			self::apply_payload_to_post( (int) $version['post_id'], $version );
		} elseif ( 'option' === ( $version['storage'] ?? '' ) && ! empty( $version['template_key'] ) ) {
			self::apply_payload_to_option( (string) $version['template_key'], $version );
		} else {
			return null;
		}

		self::store_last_rollback_meta(
			array(
				'version_id' => $version_id,
				'timestamp'  => self::now(),
				'email_id'   => (string) ( $version['email_id'] ?? '' ),
			)
		);

		$template = new Template(
			(string) ( $version['email_id'] ?? '' ),
			array_merge(
				$version['template'],
				array(
					'id'         => (string) ( $version['template']['id'] ?? ( $version['post_id'] ?? '' ) ),
					'post_id'    => $version['post_id'] ?? 0,
					'email_type' => $version['email_id'] ?? '',
					'source'     => 'custom',
				)
			)
		);

		self::record(
			$template,
			array(
				'post_id'       => $version['post_id'] ?? 0,
				'template_key'  => $version['template_key'] ?? '',
				'editor_mode'   => $version['editor_mode'] ?? 'code',
				'locale'        => $version['locale'] ?? '',
				'restored_from' => $version_id,
			)
		);

		return $template;
	}

	/**
	 * Provide last rollback metadata for diagnostics.
	 *
	 * @return array{timestamp:int,version_id:string,email_id:string}|array{}
	 */
	public static function last_rollback(): array {
		$data = get_option( self::OPTION_LAST_ROLLBACK, array() );
		return is_array( $data ) ? $data : array();
	}

	/**
	 * Build stored payload for a template snapshot.
	 *
	 * @param Template             $template Template payload.
	 * @param array<string, mixed> $context  Context (post_id, editor_mode, locale).
	 * @return array<string, mixed>
	 */
	protected static function build_payload( Template $template, array $context ): array {
		$data    = $template->to_array();
		$post_id = isset( $context['post_id'] ) ? (int) $context['post_id'] : ( isset( $data['post_id'] ) ? (int) $data['post_id'] : 0 );

		$editor_mode = isset( $context['editor_mode'] ) ? self::sanitize_editor_mode( (string) $context['editor_mode'] ) : ( isset( $data['editor_mode'] ) ? self::sanitize_editor_mode( (string) $data['editor_mode'] ) : 'code' );
		$locale      = isset( $context['locale'] ) ? (string) $context['locale'] : self::determine_locale();

		$template_payload = array(
			'subject'     => $template->subject(),
			'heading'     => $template->heading(),
			'html'        => $template->html(),
			'css'         => $template->css(),
			'is_active'   => $data['is_active'] ?? true,
			'editor_mode' => $editor_mode,
			'post_id'     => $post_id,
			'id'          => isset( $data['id'] ) ? (string) $data['id'] : ( $post_id ? (string) $post_id : '' ),
		);

		return array(
			'template'     => $template_payload,
			'email_id'     => $template->email_id(),
			'post_id'      => $post_id,
			'template_key' => isset( $context['template_key'] ) ? self::sanitize_key( (string) $context['template_key'] ) : '',
			'hash'         => md5( self::json_encode( array( $template->subject(), $template->heading(), $template->html(), $template->css() ) ) ),
			'size'         => strlen( $template->html() ) + strlen( $template->css() ),
			'timestamp'    => self::now(),
			'author_id'    => self::current_user_id(),
			'author_name'  => self::current_user_name(),
			'editor_mode'  => $editor_mode,
			'locale'       => $locale,
			'storage'      => '',
			'metadata'     => array(
				'restored_from' => isset( $context['restored_from'] ) ? (string) $context['restored_from'] : '',
			),
		);
	}

	/**
	 * Determine template identifier string for a payload.
	 *
	 * @param array<string, mixed> $payload Payload data.
	 * @return string
	 */
	protected static function template_identifier_from_payload( array $payload ): string {
		if ( ! empty( $payload['post_id'] ) ) {
			return 'post-' . (int) $payload['post_id'];
		}

		if ( ! empty( $payload['template_key'] ) ) {
			return 'opt-' . self::sanitize_key( (string) $payload['template_key'] );
		}

		if ( ! empty( $payload['email_id'] ) ) {
			return 'opt-' . self::sanitize_key( (string) $payload['email_id'] );
		}

		return '';
	}

	/**
	 * Persist a revision-backed entry.
	 *
	 * @param int                  $post_id Post identifier.
	 * @param array<string, mixed> $payload Snapshot payload.
	 * @return string|null
	 */
	protected static function record_revision_entry( int $post_id, array $payload ): ?string {
		if ( ! function_exists( 'wp_save_post_revision' ) ) {
			return null;
		}

		$revision_id = wp_save_post_revision( $post_id );

		if ( is_wp_error( $revision_id ) || ! $revision_id ) {
			return null;
		}

		$payload['storage']    = 'revision';
		$payload['version_id'] = 'rev-' . $revision_id;
		update_post_meta( $revision_id, self::REVISION_META_KEY, $payload );

		self::prune_revisions( $post_id );

		return $payload['version_id'];
	}

	/**
	 * Persist an option-backed entry.
	 *
	 * @param string               $identifier Template identifier.
	 * @param array<string, mixed> $payload    Snapshot payload.
	 * @return string
	 */
	protected static function record_option_entry( string $identifier, array $payload ): string {
		$key      = self::option_history_key( $identifier );
		$versions = get_option( $key, array() );
		$versions = is_array( $versions ) ? $versions : array();

		try {
			$random = random_int( 0, PHP_INT_MAX );
		} catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterface
			$random = wp_rand();
		}

		$template_key_base = 0 === strpos( $identifier, 'opt-' ) ? substr( $identifier, 4 ) : $identifier;
		$version_id        = 'opt-' . self::sanitize_key( (string) $template_key_base ) . '-' . substr( md5( microtime( true ) . $random ), 0, 12 );

		$payload['storage']      = 'option';
		$payload['version_id']   = $version_id;
		$payload['template_key'] = self::sanitize_key( (string) $template_key_base );

		array_unshift( $versions, $payload );

		$max = self::max_versions();
		if ( count( $versions ) > $max ) {
			$versions = array_slice( $versions, 0, $max );
		}

		update_option( $key, $versions, false );

		return $version_id;
	}

	/**
	 * Fetch option history entries.
	 *
	 * @param string $identifier Template identifier (without prefix).
	 * @param int    $limit      Maximum results.
	 * @return array<int, array<string, mixed>>
	 */
	protected static function get_option_history( string $identifier, int $limit ): array {
		$key      = self::option_history_key( 'opt-' . $identifier );
		$versions = get_option( $key, array() );

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

		return array_slice( array_map( array( __CLASS__, 'normalise_payload' ), $versions ), 0, $limit );
	}

	/**
	 * Load payload for option version.
	 *
	 * @param string $raw Raw identifier after 'opt-'.
	 * @return array<string, mixed>|null
	 */
	protected static function load_option_payload( string $raw ): ?array {
		$last_dash = strrpos( $raw, '-' );
		if ( false === $last_dash ) {
			return null;
		}

		$template_key = substr( $raw, 0, $last_dash );
		$key          = self::option_history_key( 'opt-' . $template_key );
		$versions     = get_option( $key, array() );

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

		foreach ( $versions as $payload ) {
			if ( isset( $payload['version_id'] ) && $payload['version_id'] === 'opt-' . $raw ) {
				return self::normalise_payload( $payload );
			}
		}

		return null;
	}

	/**
	 * Fetch revision-backed history.
	 *
	 * @param int $post_id Post identifier.
	 * @param int $limit   Entry limit.
	 * @return array<int, array<string, mixed>>
	 */
	protected static function get_revision_history( int $post_id, int $limit ): array {
		if ( ! function_exists( 'wp_get_post_revisions' ) ) {
			return array();
		}

		$args = array(
			'numberposts' => $limit,
			'order'       => 'DESC',
			'orderby'     => 'date ID',
		);

		$revisions = wp_get_post_revisions( $post_id, $args );
		if ( empty( $revisions ) ) {
			return array();
		}

		$history = array();

		foreach ( $revisions as $revision ) {
			$payload = get_post_meta( $revision->ID, self::REVISION_META_KEY, true );
			if ( ! is_array( $payload ) ) {
				continue;
			}

			$payload['version_id'] = $payload['version_id'] ?? ( 'rev-' . $revision->ID );
			$history[]             = self::normalise_payload( $payload );
		}

		return $history;
	}

	/**
	 * Load payload for a revision entry.
	 *
	 * @param int $revision_id Revision identifier.
	 * @return array<string, mixed>|null
	 */
	protected static function load_revision_payload( int $revision_id ): ?array {
		if ( ! function_exists( 'get_post_meta' ) ) {
			return null;
		}

		$payload = get_post_meta( $revision_id, self::REVISION_META_KEY, true );
		if ( ! is_array( $payload ) ) {
			return null;
		}

		$payload['version_id'] = $payload['version_id'] ?? ( 'rev-' . $revision_id );

		return self::normalise_payload( $payload );
	}

	/**
	 * Apply a payload to a CPT template.
	 *
	 * @param int                  $post_id Post identifier.
	 * @param array<string, mixed> $payload Snapshot payload.
	 * @return void
	 */
	protected static function apply_payload_to_post( int $post_id, array $payload ): void {
		if ( ! class_exists( WPMD_Email_Templates::class ) ) {
			return;
		}

		$template = $payload['template'] ?? array();

		update_post_meta( $post_id, WPMD_Email_Templates::META_SUBJECT, $template['subject'] ?? '' );
		update_post_meta( $post_id, WPMD_Email_Templates::META_HEADING, $template['heading'] ?? '' );
		update_post_meta( $post_id, WPMD_Email_Templates::META_BODY, $template['html'] ?? '' );
		update_post_meta( $post_id, WPMD_Email_Templates::META_CSS, $template['css'] ?? '' );
		update_post_meta( $post_id, WPMD_Email_Templates::META_ACTIVE, ! empty( $template['is_active'] ) ? 1 : 0 );
		update_post_meta( $post_id, '_mail_doctor_editor_mode', $payload['editor_mode'] ?? 'code' );

		WPMD_Email_Templates::clear_template_cache( $payload['email_id'] ?? '' );
	}

	/**
	 * Apply a payload to option storage.
	 *
	 * @param string               $template_key Template identifier.
	 * @param array<string, mixed> $payload      Snapshot payload.
	 * @return void
	 */
	protected static function apply_payload_to_option( string $template_key, array $payload ): void {
		$key = self::option_active_key( $template_key );
		update_option( $key, $payload['template'], false );

		if ( class_exists( WPMD_Email_Templates::class ) && ! empty( $payload['email_id'] ) ) {
			WPMD_Email_Templates::clear_template_cache( (string) $payload['email_id'] );
		}

		if ( class_exists( CacheService::class ) ) {
			CacheService::invalidate_template( $template_key );
		}
	}

	/**
	 * Remove excess revisions to cap history size.
	 *
	 * @param int $post_id Post identifier.
	 * @return void
	 */
	protected static function prune_revisions( int $post_id ): void {
		if ( ! function_exists( 'wp_get_post_revisions' ) || ! function_exists( 'wp_delete_post_revision' ) ) {
			return;
		}

		$revisions = wp_get_post_revisions(
			$post_id,
			array(
				'numberposts' => -1,
				'order'       => 'ASC',
				'orderby'     => 'date ID',
			)
		);

		$total = count( $revisions );
		$max   = self::max_versions();

		if ( $total <= $max ) {
			return;
		}

		$to_remove = $total - $max;

		foreach ( $revisions as $revision ) {
			if ( $to_remove <= 0 ) {
				break;
			}

			wp_delete_post_revision( $revision->ID );
			--$to_remove;
		}
	}

	/**
	 * Check if CPT revisions are available for a payload.
	 *
	 * @param array<string, mixed> $payload Payload data.
	 * @return bool
	 */
	protected static function supports_revisions( array $payload ): bool {
		if ( empty( $payload['post_id'] ) ) {
			return false;
		}

		if ( ! function_exists( 'post_type_supports' ) ) {
			return false;
		}

		$post_type = function_exists( 'get_post_type' ) ? get_post_type( (int) $payload['post_id'] ) : WPMD_Email_Templates::CPT;

		return post_type_supports( $post_type ?: WPMD_Email_Templates::CPT, 'revisions' );
	}

	/**
	 * Fetch latest stored hash for duplicate detection.
	 *
	 * @param string $identifier Template identifier.
	 * @return string|null
	 */
	protected static function latest_hash( string $identifier ): ?string {
		$list = self::list_versions( $identifier, 1 );
		if ( empty( $list ) || empty( $list[0]['hash'] ) ) {
			return null;
		}

		return (string) $list[0]['hash'];
	}

	/**
	 * Ensure payload contains consistent defaults.
	 *
	 * @param array<string, mixed> $payload Payload data.
	 * @return array<string, mixed>
	 */
	protected static function normalise_payload( array $payload ): array {
		$template = isset( $payload['template'] ) && is_array( $payload['template'] ) ? $payload['template'] : array();

		return array(
			'version_id'   => (string) ( $payload['version_id'] ?? '' ),
			'timestamp'    => (int) ( $payload['timestamp'] ?? self::now() ),
			'author_id'    => (int) ( $payload['author_id'] ?? 0 ),
			'author_name'  => (string) ( $payload['author_name'] ?? '' ),
			'email_id'     => (string) ( $payload['email_id'] ?? '' ),
			'locale'       => (string) ( $payload['locale'] ?? '' ),
			'size'         => (int) ( $payload['size'] ?? 0 ),
			'hash'         => (string) ( $payload['hash'] ?? '' ),
			'storage'      => (string) ( $payload['storage'] ?? '' ),
			'post_id'      => isset( $payload['post_id'] ) ? (int) $payload['post_id'] : ( isset( $template['post_id'] ) ? (int) $template['post_id'] : 0 ),
			'template_key' => (string) ( $payload['template_key'] ?? '' ),
			'editor_mode'  => (string) ( $payload['editor_mode'] ?? 'code' ),
			'template'     => array(
				'subject'   => $template['subject'] ?? '',
				'heading'   => $template['heading'] ?? '',
				'html'      => $template['html'] ?? '',
				'css'       => $template['css'] ?? '',
				'is_active' => ! empty( $template['is_active'] ),
				'id'        => $template['id'] ?? '',
				'post_id'   => $template['post_id'] ?? ( $payload['post_id'] ?? 0 ),
			),
			'metadata'     => isset( $payload['metadata'] ) && is_array( $payload['metadata'] ) ? $payload['metadata'] : array(),
		);
	}

	/**
	 * Parse an incoming template identifier argument.
	 *
	 * @param mixed $identifier Raw identifier.
	 * @return array{type:string,value:int|string}
	 */
	protected static function parse_template_identifier( $identifier ): array {
		if ( is_numeric( $identifier ) ) {
			return array(
				'type'  => 'post',
				'value' => (int) $identifier,
			);
		}

		$identifier = (string) $identifier;

		if ( 0 === strpos( $identifier, 'post-' ) ) {
			return array(
				'type'  => 'post',
				'value' => (int) substr( $identifier, 5 ),
			);
		}

		if ( 0 === strpos( $identifier, 'opt-' ) ) {
			return array(
				'type'  => 'option',
				'value' => substr( $identifier, 4 ),
			);
		}

		return array(
			'type'  => 'option',
			'value' => self::sanitize_key( $identifier ),
		);
	}

	/**
	 * Resolve template history option key.
	 *
	 * @param string $identifier Identifier (with opt- prefix).
	 * @return string
	 */
	protected static function option_history_key( string $identifier ): string {
		return self::OPTION_PREFIX . self::sanitize_key( $identifier );
	}

	/**
	 * Resolve key for storing active template when using option storage.
	 *
	 * @param string $identifier Template key without prefix.
	 * @return string
	 */
	protected static function option_active_key( string $identifier ): string {
		return self::OPTION_ACTIVE_PREFIX . self::sanitize_key( $identifier );
	}

	/**
	 * Store metadata about the last rollback event.
	 *
	 * @param array<string, mixed> $data Meta payload.
	 * @return void
	 */
	protected static function store_last_rollback_meta( array $data ): void {
		update_option(
			self::OPTION_LAST_ROLLBACK,
			array(
				'timestamp'  => (int) ( $data['timestamp'] ?? self::now() ),
				'version_id' => (string) ( $data['version_id'] ?? '' ),
				'email_id'   => (string) ( $data['email_id'] ?? '' ),
			),
			false
		);
	}

	/**
	 * Determine current locale safely.
	 *
	 * @return string
	 */
	protected static function determine_locale(): string {
		if ( function_exists( 'determine_locale' ) ) {
			return (string) determine_locale();
		}

		if ( function_exists( 'get_locale' ) ) {
			return (string) get_locale();
		}

		return 'en_US';
	}

	/**
	 * Fetch current user identifier.
	 *
	 * @return int
	 */
	protected static function current_user_id(): int {
		return function_exists( 'get_current_user_id' ) ? (int) get_current_user_id() : 0;
	}

	/**
	 * Fetch current user name for auditing.
	 *
	 * @return string
	 */
	protected static function current_user_name(): string {
		if ( function_exists( 'wp_get_current_user' ) ) {
			$user = wp_get_current_user();
			if ( $user && $user->exists() ) {
				$name = $user->display_name ?: $user->user_login;
				if ( $name ) {
					return (string) $name;
				}
			}
		}

		return function_exists( '__' ) ? __( 'System', 'mail-doctor' ) : 'System';
	}

	/**
	 * Provide current timestamp.
	 *
	 * @return int
	 */
	protected static function now(): int {
		if ( function_exists( 'current_time' ) ) {
			return (int) current_time( 'timestamp' );
		}

		return time();
	}

	/**
	 * Cap editor mode to expected values.
	 *
	 * @param string $mode Editor mode.
	 * @return string
	 */
	protected static function sanitize_editor_mode( string $mode ): string {
		return in_array( $mode, array( 'visual', 'code' ), true ) ? $mode : 'code';
	}

	/**
	 * Sanitize key for identifiers.
	 *
	 * @param string $key Raw key.
	 * @return string
	 */
	protected static function sanitize_key( string $key ): string {
		if ( function_exists( 'sanitize_key' ) ) {
			return sanitize_key( $key );
		}

		return strtolower( preg_replace( '/[^a-z0-9_\-]/', '', $key ) );
	}

	/**
	 * Determine maximum versions per template.
	 *
	 * @return int
	 */
	protected static function max_versions(): int {
		$max = self::MAX_VERSIONS;

		if ( function_exists( 'apply_filters' ) ) {
			$max = (int) apply_filters( 'mail_doctor_template_history_max_versions', self::MAX_VERSIONS );
		}

		return max( 1, $max );
	}

	/**
	 * Safe JSON encoding helper.
	 *
	 * @param mixed $value Value to encode.
	 * @return string
	 */
	protected static function json_encode( $value ): string {
		if ( function_exists( 'wp_json_encode' ) ) {
			return (string) wp_json_encode( $value );
		}

		return (string) json_encode( $value );
	}
}
