<?php

namespace Fotogenic_AI;

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

/**
 * Versioned, bounded notification activity owned by WordPress user metadata.
 * Details, prompts, URLs, identifiers, and arbitrary context are never stored.
 */
final class Notification_Activity_Store {
	private const META_KEY       = '_fotogenic_ai_notification_activity';
	private const SCHEMA_VERSION = 1;
	private const MAX_ITEMS      = 30;
	private const TITLE_LIMIT    = 160;
	private const MESSAGE_LIMIT  = 240;
	private const TYPES          = [ 'success', 'info', 'warning', 'danger', 'error' ];

	private static ?Notification_Activity_Store $instance = null;

	private function __construct() {}

	public static function instance(): Notification_Activity_Store {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/** @return array{version:int,items:array<int,array<string,mixed>>,unreadCount:int} */
	public function list_for_user( int $user_id ): array {
		return $this->present_state( $this->read_state( $user_id ) );
	}

	/** @return array{version:int,items:array<int,array<string,mixed>>,unreadCount:int} */
	private function present_state( array $state ): array {
		$visible = array_values(
			array_filter(
				$state['items'],
				static fn( array $item ): bool => empty( $item['dismissed'] )
			)
		);
		return [
			'version'     => self::SCHEMA_VERSION,
			'items'       => $visible,
			'unreadCount' => count(
				array_filter(
					$visible,
					static fn( array $item ): bool => empty( $item['read'] )
				)
			),
		];
	}

	/** @return array{version:int,items:array<int,array<string,mixed>>,unreadCount:int}|\WP_Error */
	public function upsert_for_user( int $user_id, array $event ) {
		$normalized = $this->normalize_event( $event );
		if ( is_wp_error( $normalized ) ) {
			return $normalized;
		}
		return $this->mutate_state(
			$user_id,
			static function ( array $state ) use ( $normalized ): array {
				$semantic_id = $normalized['semanticKey'] . '|' . $normalized['episode'];
				$matched     = false;
				foreach ( $state['items'] as $index => $item ) {
					$item_semantic_id = (string) ( $item['semanticKey'] ?? '' ) . '|' . (string) ( $item['episode'] ?? '' );
					if ( ! hash_equals( $semantic_id, $item_semantic_id ) ) {
						continue;
					}
					$matched = true;
					$changed = (string) $item['title'] !== $normalized['title'] || (string) $item['message'] !== $normalized['message'] || (string) $item['type'] !== $normalized['type'];
					$state['items'][ $index ] = array_merge(
						$item,
						$normalized,
						[
							'id'         => (string) $item['id'],
							'createdAt'  => (int) $item['createdAt'],
							'updatedAt'  => time(),
							'read'       => $changed ? false : ! empty( $item['read'] ),
							'dismissed'  => ! empty( $item['dismissed'] ),
						]
					);
					break;
				}
				if ( ! $matched ) {
					array_unshift(
						$state['items'],
						array_merge(
							$normalized,
							[
								'id'        => wp_generate_uuid4(),
								'createdAt' => time(),
								'updatedAt' => time(),
								'read'      => false,
								'dismissed' => false,
							]
						)
					);
				}
				return $state;
			}
		);
	}

	/** @return array{version:int,items:array<int,array<string,mixed>>,unreadCount:int}|\WP_Error */
	public function mark_read_for_user( int $user_id, array $ids = [], bool $all = false ) {
		$safe_ids = array_values( array_filter( array_map( [ $this, 'normalize_id' ], $ids ) ) );
		return $this->mutate_state(
			$user_id,
			static function ( array $state ) use ( $safe_ids, $all ): array {
				foreach ( $state['items'] as $index => $item ) {
					if ( $all || in_array( (string) $item['id'], $safe_ids, true ) ) {
						$state['items'][ $index ]['read']      = true;
						$state['items'][ $index ]['updatedAt'] = time();
					}
			}
				return $state;
			}
		);
	}

	/** @return array{version:int,items:array<int,array<string,mixed>>,unreadCount:int}|\WP_Error */
	public function dismiss_for_user( int $user_id, string $id ) {
		$id = $this->normalize_id( $id );
		if ( '' === $id ) {
			return new \WP_Error( 'fotogenic_ai_notification_invalid', __( 'The notification could not be dismissed.', 'fotogenic-ai' ), [ 'status' => 400 ] );
		}
		return $this->mutate_state(
			$user_id,
			static function ( array $state ) use ( $id ) {
				foreach ( $state['items'] as $index => $item ) {
					if ( ! hash_equals( (string) $item['id'], $id ) ) {
						continue;
					}
					$state['items'][ $index ]['read']       = true;
					$state['items'][ $index ]['dismissed']  = true;
					$state['items'][ $index ]['updatedAt']  = time();
					return $state;
				}
				return new \WP_Error( 'fotogenic_ai_notification_missing', __( 'The notification is no longer available.', 'fotogenic-ai' ), [ 'status' => 404 ] );
			}
		);
	}

	/**
	 * Serialize a single user's notification mutation through the plugin lock owner.
	 *
	 * @param callable(array):array|\WP_Error $mutation Pure state mutation.
	 * @return array{version:int,items:array<int,array<string,mixed>>,unreadCount:int}|\WP_Error
	 */
	private function mutate_state( int $user_id, callable $mutation ) {
		if ( $user_id <= 0 ) {
			return new \WP_Error( 'fotogenic_ai_notification_user_invalid', __( 'Notification activity is unavailable for this user.', 'fotogenic-ai' ), [ 'status' => 403 ] );
		}
		$blog_id  = function_exists( 'get_current_blog_id' ) ? get_current_blog_id() : 1;
		$lock_key = 'notification-activity:' . $blog_id . ':' . $user_id;
		$lock     = Background_Lock::instance();
		$token    = $lock->acquire( $lock_key, 15 );
		if ( false === $token ) {
			return new \WP_Error( 'fotogenic_ai_notification_busy', __( 'Notification activity is being updated. Try again.', 'fotogenic-ai' ), [ 'status' => 409 ] );
		}
		try {
			$state = $mutation( $this->read_state( $user_id ) );
			if ( is_wp_error( $state ) ) {
				return $state;
			}
			if ( ! is_array( $state ) || ! $this->write_state( $user_id, $state ) ) {
				return new \WP_Error( 'fotogenic_ai_notification_write_failed', __( 'Notification activity could not be saved. Try again.', 'fotogenic-ai' ), [ 'status' => 500 ] );
			}
			return $this->present_state( $state );
		} finally {
			$lock->release( $lock_key, $token );
		}
	}

	/** @return array{semanticKey:string,episode:string,type:string,title:string,message:string}|\WP_Error */
	private function normalize_event( array $event ) {
		$semantic_key = $this->normalize_semantic_key( (string) ( $event['semanticKey'] ?? $event['key'] ?? '' ) );
		$episode      = $this->normalize_semantic_key( (string) ( $event['episode'] ?? 'default' ) );
		$type         = sanitize_key( (string) ( $event['type'] ?? 'info' ) );
		$title        = sanitize_text_field( (string) ( $event['title'] ?? '' ) );
		$title        = function_exists( 'mb_substr' ) ? mb_substr( $title, 0, self::TITLE_LIMIT ) : substr( $title, 0, self::TITLE_LIMIT );
		$message      = sanitize_text_field( (string) ( $event['message'] ?? '' ) );
		$message      = function_exists( 'mb_substr' ) ? mb_substr( $message, 0, self::MESSAGE_LIMIT ) : substr( $message, 0, self::MESSAGE_LIMIT );
		if ( '' === $semantic_key || '' === $episode || ! in_array( $type, self::TYPES, true ) || '' === $message ) {
			return new \WP_Error( 'fotogenic_ai_notification_invalid', __( 'The notification activity was invalid.', 'fotogenic-ai' ), [ 'status' => 400 ] );
		}
		return [
			'semanticKey' => $semantic_key,
			'episode'     => $episode,
			'type'        => $type,
			'title'       => $title,
			'message'     => $message,
		];
	}

	/** @return array{version:int,items:array<int,array<string,mixed>>} */
	private function read_state( int $user_id ): array {
		$stored = $user_id > 0 ? get_user_meta( $user_id, self::META_KEY, true ) : [];
		$items  = is_array( $stored ) && self::SCHEMA_VERSION === (int) ( $stored['version'] ?? 0 ) && is_array( $stored['items'] ?? null )
			? $stored['items']
			: [];
		$clean  = [];
		foreach ( array_slice( $items, 0, self::MAX_ITEMS ) as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$id = $this->normalize_id( (string) ( $item['id'] ?? '' ) );
			$normalized = $this->normalize_event( $item );
			if ( '' === $id || is_wp_error( $normalized ) ) {
				continue;
			}
			$clean[] = array_merge(
				$normalized,
				[
					'id'        => $id,
					'createdAt' => max( 0, (int) ( $item['createdAt'] ?? 0 ) ),
					'updatedAt' => max( 0, (int) ( $item['updatedAt'] ?? 0 ) ),
					'read'      => ! empty( $item['read'] ),
					'dismissed' => ! empty( $item['dismissed'] ),
				]
			);
		}
		usort( $clean, static fn( array $left, array $right ): int => (int) $right['updatedAt'] <=> (int) $left['updatedAt'] );
		return [
			'version' => self::SCHEMA_VERSION,
			'items'   => array_slice( $clean, 0, self::MAX_ITEMS ),
		];
	}

	private function write_state( int $user_id, array $state ): bool {
		if ( $user_id <= 0 ) {
			return false;
		}
		$state['version'] = self::SCHEMA_VERSION;
		$state['items']   = array_slice( (array) ( $state['items'] ?? [] ), 0, self::MAX_ITEMS );
		$updated = update_user_meta( $user_id, self::META_KEY, $state );
		return false !== $updated || get_user_meta( $user_id, self::META_KEY, true ) === $state;
	}

	/** @param mixed $value Candidate value. */
	private function normalize_id( $value ): string {
		$id = strtolower( trim( (string) $value ) );
		return wp_is_uuid( $id ) ? $id : '';
	}

	private function normalize_semantic_key( string $value ): string {
		$value = strtolower( trim( $value ) );
		return preg_match( '/^[a-z0-9][a-z0-9._:-]{0,79}$/', $value ) ? $value : '';
	}
}
