<?php
/**
 * Product link-health status store.
 *
 * @package Affilun\Application\LinkHealth
 */

declare(strict_types=1);

namespace Affilun\Application\LinkHealth;

use Affilun\Application\Cloud\CloudLinkHealthResult;
use Affilun\Infrastructure\Persistence\ProductMetaRecord;
use Affilun\Infrastructure\Persistence\ProductMetaRepository;
use DateTimeImmutable;
use Exception;

/**
 * Persists latest Cloud link-health status rows to product meta.
 */
final class ProductLinkHealthStatusStore {

	/**
	 * Product meta key for the latest saved-link health payload.
	 */
	public const META_KEY_LINK_HEALTH_STATUS = 'link_health_status';

	/**
	 * @param ProductMetaRepository $metaRepository Product-meta repository.
	 */
	public function __construct( private ProductMetaRepository $metaRepository ) {}

	/**
	 * Read the latest status payload for a product.
	 *
	 * @param int $productId Product id.
	 *
	 * @return array<string,mixed>|null
	 */
	public function readForProduct( int $productId ): ?array {
		if ( $productId <= 0 ) {
			return null;
		}

		$meta = $this->metaRepository->findByProductAndKey(
			$productId,
			self::META_KEY_LINK_HEALTH_STATUS
		);
		if ( $meta === null || $meta->metaValue() === null ) {
			return null;
		}

		$data = json_decode( $meta->metaValue(), true );
		return is_array( $data ) ? $data : null;
	}

	/**
	 * Persist one Cloud result for a product.
	 *
	 * @param int                   $productId  Product id.
	 * @param CloudLinkHealthResult $result     Cloud result.
	 * @param DateTimeImmutable     $recordedAt Local recorded-at instant.
	 */
	public function persistResult(
		int $productId,
		CloudLinkHealthResult $result,
		DateTimeImmutable $recordedAt
	): bool {
		if ( $productId <= 0 ) {
			return false;
		}

		$payload = array(
			'url'         => $result->url(),
			'status'      => $result->status(),
			'http_status' => $result->httpStatus(),
			'message'     => $result->message(),
			'checked_at'  => $result->checkedAtIso(),
			'recorded_at' => $recordedAt->format( DATE_ATOM ),
		);
		$value   = self::jsonEncode( $payload );
		if ( $value === null ) {
			return false;
		}

		$existing = $this->metaRepository->findByProductAndKey(
			$productId,
			self::META_KEY_LINK_HEALTH_STATUS
		);
		if ( $existing === null ) {
			$this->metaRepository->save(
				ProductMetaRecord::createNew(
					$productId,
					self::META_KEY_LINK_HEALTH_STATUS,
					$recordedAt,
					array( 'metaValue' => $value )
				)
			);
			return true;
		}
		if ( self::isOlderThanExisting( $result, $existing ) ) {
			return false;
		}

		$this->metaRepository->save( $existing->withMetaValue( $value )->withUpdatedAt( $recordedAt ) );
		return true;
	}

	/**
	 * Persist all result rows that can be attributed to product ids.
	 *
	 * @param list<CloudLinkHealthResult>       $results    Cloud results.
	 * @param array<string,array<string,mixed>> $contexts   URL contexts.
	 * @param DateTimeImmutable                 $recordedAt Local recorded-at instant.
	 */
	public function persistResults( array $results, array $contexts, DateTimeImmutable $recordedAt ): int {
		$persisted = 0;
		foreach ( $results as $result ) {
			$context   = $contexts[ $result->url() ] ?? array();
			$productId = $context['product_id'] ?? null;
			if ( is_string( $productId ) && ctype_digit( $productId ) ) {
				$productId = (int) $productId;
			}
			if ( ! is_int( $productId ) || $productId <= 0 ) {
				continue;
			}
			if ( $this->persistResult( $productId, $result, $recordedAt ) ) {
				++$persisted;
			}
		}

		return $persisted;
	}

	/**
	 * Whether a product has a status payload fresher than the TTL.
	 *
	 * @param int               $productId  Product id.
	 * @param DateTimeImmutable $now        Current instant.
	 * @param int               $ttlSeconds Freshness TTL in seconds.
	 */
	public function isFresh( int $productId, DateTimeImmutable $now, int $ttlSeconds ): bool {
		if ( $ttlSeconds <= 0 ) {
			return false;
		}

		$data = $this->readForProduct( $productId );
		if ( $data === null ) {
			return false;
		}

		$stamp = is_scalar( $data['recorded_at'] ?? null ) ? (string) $data['recorded_at'] : '';
		if ( $stamp === '' ) {
			$stamp = is_scalar( $data['checked_at'] ?? null ) ? (string) $data['checked_at'] : '';
		}
		if ( $stamp === '' ) {
			return false;
		}

		try {
			$recordedAt = new DateTimeImmutable( $stamp );
		} catch ( Exception $unused ) {
			unset( $unused );
			return false;
		}

		return $recordedAt->getTimestamp() >= ( $now->getTimestamp() - $ttlSeconds );
	}

	/**
	 * JSON-encode a product-meta payload.
	 *
	 * @param array<string,mixed> $payload Payload.
	 */
	private static function jsonEncode( array $payload ): ?string {
		if ( function_exists( 'wp_json_encode' ) ) {
			$value = wp_json_encode( $payload );
		} else {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- Pure-unit fallback when WordPress is unavailable.
			$value = json_encode( $payload );
		}

		return is_string( $value ) ? $value : null;
	}

	/**
	 * Whether an incoming result is older than an existing persisted status.
	 *
	 * @param CloudLinkHealthResult $result   Incoming result.
	 * @param ProductMetaRecord     $existing Existing product meta.
	 */
	private static function isOlderThanExisting( CloudLinkHealthResult $result, ProductMetaRecord $existing ): bool {
		$incoming = self::timestampFromString( $result->checkedAtIso() );
		if ( $incoming === null || $existing->metaValue() === null ) {
			return false;
		}

		$payload = json_decode( $existing->metaValue(), true );
		if ( ! is_array( $payload ) ) {
			return false;
		}
		$existingCheckedAt = is_scalar( $payload['checked_at'] ?? null ) ? (string) $payload['checked_at'] : '';
		$existingStamp     = self::timestampFromString( $existingCheckedAt );
		return $existingStamp !== null && $incoming < $existingStamp;
	}

	/**
	 * Parse an ISO-ish timestamp.
	 *
	 * @param string $value Timestamp string.
	 */
	private static function timestampFromString( string $value ): ?int {
		$value = trim( $value );
		if ( $value === '' ) {
			return null;
		}
		try {
			return ( new DateTimeImmutable( $value ) )->getTimestamp();
		} catch ( Exception $unused ) {
			unset( $unused );
			return null;
		}
	}
}
