<?php
/**
 * affilun Cloud settings value object.
 *
 * @package Affilun\Application\Settings
 */

declare(strict_types=1);

namespace Affilun\Application\Settings;

use InvalidArgumentException;

/**
 * Immutable affilun Cloud connection settings.
 */
final class CloudSettings {

	/**
	 * Default affilun Cloud API endpoint.
	 */
	public const DEFAULT_ENDPOINT = 'https://api.affilun.st/api/v1';

	/**
	 * Default endpoint aliases that should transparently follow production.
	 *
	 * @var list<string>
	 */
	private const DEFAULT_ENDPOINT_ALIASES = array(
		'https://api.affilun.st',
		'https://cbl.rio.st',
		'https://cbl.rio.st/api/v1',
	);

	/**
	 * Whether cloud features are enabled.
	 */
	private bool $enabled;

	/**
	 * API endpoint.
	 */
	private string $endpoint;

	/**
	 * Site identifier issued by affilun Cloud.
	 */
	private string $siteId;

	/**
	 * Site API key.
	 */
	private string $apiKey;

	/**
	 * Cached plan label.
	 */
	private string $plan;

	/**
	 * Default article-draft tone.
	 */
	private string $articleDraftTone;

	/**
	 * Default article-draft target length.
	 */
	private string $articleDraftTargetLength;

	/**
	 * Default article-draft review-source policy.
	 */
	private string $articleDraftReviewSourcePolicy;

	/**
	 * @param bool   $enabled  Whether cloud is enabled.
	 * @param string $endpoint API endpoint.
	 * @param string $siteId   Site id.
	 * @param string $apiKey   API key.
	 * @param string $plan                           Cached plan label.
	 * @param string $articleDraftTone               Article-draft tone.
	 * @param string $articleDraftTargetLength       Article-draft length.
	 * @param string $articleDraftReviewSourcePolicy Review source policy.
	 * @param bool   $allowCustomEndpoint           Explicit test/dev opt-in for non-default endpoints.
	 *
	 * @throws InvalidArgumentException When endpoint is not HTTP(S) or is not allowed.
	 */
	public function __construct(
		bool $enabled,
		string $endpoint,
		string $siteId,
		string $apiKey,
		string $plan,
		string $articleDraftTone = 'professional_blog',
		string $articleDraftTargetLength = 'medium',
		string $articleDraftReviewSourcePolicy = 'product_url',
		bool $allowCustomEndpoint = false
	) {
		$endpoint           = trim( $endpoint );
		$normalizedEndpoint = rtrim( $endpoint, '/' );
		if ( preg_match( '#\Ahttps?://#i', $endpoint ) !== 1 && $endpoint !== '' ) {
			throw new InvalidArgumentException( 'affilun Cloud endpoint must be an HTTP(S) URL.' );
		}
		if ( $endpoint === '' || in_array( $normalizedEndpoint, self::DEFAULT_ENDPOINT_ALIASES, true ) ) {
			$endpoint           = self::DEFAULT_ENDPOINT;
			$normalizedEndpoint = self::DEFAULT_ENDPOINT;
		}
		if (
			$normalizedEndpoint !== self::DEFAULT_ENDPOINT
			&& ! $allowCustomEndpoint
			&& ! self::customEndpointsAllowed()
		) {
			throw new InvalidArgumentException( 'Custom affilun Cloud endpoints are disabled.' );
		}

		$this->enabled                        = $enabled;
		$this->endpoint                       = rtrim( $endpoint, '/' );
		$this->siteId                         = trim( $siteId );
		$this->apiKey                         = trim( $apiKey );
		$this->plan                           = trim( $plan );
		$this->articleDraftTone               = self::normalizeArticleDraftTone( $articleDraftTone );
		$this->articleDraftTargetLength       = self::normalizeArticleDraftTargetLength( $articleDraftTargetLength );
		$this->articleDraftReviewSourcePolicy = self::normalizeArticleDraftReviewSourcePolicy(
			$articleDraftReviewSourcePolicy
		);
	}

	/**
	 * Default settings.
	 */
	public static function defaults(): self {
		return new self( false, self::DEFAULT_ENDPOINT, '', '', '' );
	}

	/**
	 * Build from persisted array.
	 *
	 * @param array<string,mixed> $data Persisted data.
	 */
	public static function fromArray( array $data ): self {
		$articleDraft = is_array( $data['article_draft'] ?? null )
			? $data['article_draft']
			: array();

		return new self(
			(bool) ( $data['enabled'] ?? false ),
			is_scalar( $data['endpoint'] ?? null ) ? (string) $data['endpoint'] : self::DEFAULT_ENDPOINT,
			is_scalar( $data['site_id'] ?? null ) ? (string) $data['site_id'] : '',
			is_scalar( $data['api_key'] ?? null ) ? (string) $data['api_key'] : '',
			is_scalar( $data['plan'] ?? null ) ? (string) $data['plan'] : '',
			is_scalar( $articleDraft['tone'] ?? null ) ? (string) $articleDraft['tone'] : 'professional_blog',
			is_scalar( $articleDraft['target_length'] ?? null ) ? (string) $articleDraft['target_length'] : 'medium',
			is_scalar( $articleDraft['review_source_policy'] ?? null )
				? (string) $articleDraft['review_source_policy']
				: 'product_url'
		);
	}

	/**
	 * Whether custom Cloud endpoints are explicitly enabled for development.
	 */
	private static function customEndpointsAllowed(): bool {
		$allowed = defined( 'AFFILUN_CLOUD_ALLOW_CUSTOM_ENDPOINTS' ) && (bool) AFFILUN_CLOUD_ALLOW_CUSTOM_ENDPOINTS;
		if ( function_exists( 'apply_filters' ) ) {
			$allowed = (bool) apply_filters( 'affilun_cloud_allow_custom_endpoints', $allowed );
		}
		return $allowed;
	}

	/**
	 * Serialize for persistence.
	 *
	 * @return array<string,mixed>
	 */
	public function toArray(): array {
		return array(
			'enabled'       => $this->enabled,
			'endpoint'      => $this->endpoint,
			'site_id'       => $this->siteId,
			'api_key'       => $this->apiKey,
			'plan'          => $this->plan,
			'article_draft' => array(
				'tone'                 => $this->articleDraftTone,
				'target_length'        => $this->articleDraftTargetLength,
				'review_source_policy' => $this->articleDraftReviewSourcePolicy,
			),
		);
	}

	/**
	 * Whether cloud features are enabled.
	 */
	public function enabled(): bool {
		return $this->enabled;
	}

	/**
	 * API endpoint.
	 */
	public function endpoint(): string {
		return $this->endpoint;
	}

	/**
	 * Site id.
	 */
	public function siteId(): string {
		return $this->siteId;
	}

	/**
	 * API key.
	 */
	public function apiKey(): string {
		return $this->apiKey;
	}

	/**
	 * Cached plan.
	 */
	public function plan(): string {
		return $this->plan;
	}

	/**
	 * Default article-draft tone.
	 */
	public function articleDraftTone(): string {
		return $this->articleDraftTone;
	}

	/**
	 * Default article-draft target length.
	 */
	public function articleDraftTargetLength(): string {
		return $this->articleDraftTargetLength;
	}

	/**
	 * Default article-draft review-source policy.
	 */
	public function articleDraftReviewSourcePolicy(): string {
		return $this->articleDraftReviewSourcePolicy;
	}

	/**
	 * Whether settings can authenticate to affilun Cloud.
	 */
	public function isConfigured(): bool {
		return $this->enabled && $this->siteId !== '' && $this->apiKey !== '';
	}

	/**
	 * Masked API key for admin display.
	 */
	public function maskedApiKey(): string {
		return SettingsCredentialMask::mask( $this->apiKey );
	}

	/**
	 * Copy with API key.
	 *
	 * @param string $apiKey API key.
	 */
	public function withApiKey( string $apiKey ): self {
		return new self(
			$this->enabled,
			$this->endpoint,
			$this->siteId,
			$apiKey,
			$this->plan,
			$this->articleDraftTone,
			$this->articleDraftTargetLength,
			$this->articleDraftReviewSourcePolicy
		);
	}

	/**
	 * Normalize article-draft tone.
	 *
	 * @param string $value Raw value.
	 */
	private static function normalizeArticleDraftTone( string $value ): string {
		$value = trim( $value );
		return in_array(
			$value,
			array( 'professional_blog', 'friendly', 'review_comparison', 'concise' ),
			true
		) ? $value : 'professional_blog';
	}

	/**
	 * Normalize article-draft target length.
	 *
	 * @param string $value Raw value.
	 */
	private static function normalizeArticleDraftTargetLength( string $value ): string {
		$value = trim( $value );
		return in_array( $value, array( 'short', 'medium', 'long' ), true ) ? $value : 'medium';
	}

	/**
	 * Normalize article-draft review-source policy.
	 *
	 * @param string $value Raw value.
	 */
	private static function normalizeArticleDraftReviewSourcePolicy( string $value ): string {
		$value = trim( $value );
		return in_array( $value, array( 'product_url', 'none' ), true ) ? $value : 'product_url';
	}

	/**
	 * Compare equality.
	 *
	 * @param self $other Other settings.
	 */
	public function equals( self $other ): bool {
		return $this->toArray() === $other->toArray();
	}
}
