<?php

namespace Fotogenic_AI;

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

final class Credits_Service {
	public const TTL_SECONDS = 600;

	private const STALE_TTL_SECONDS = 86400;
	private const PROVENANCE_FRESH  = 'fresh';
	private const PROVENANCE_STALE  = 'stale';

	private static ?Credits_Service $instance = null;

	private function __construct() {}

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

		return self::$instance;
	}

	/** @return array|\WP_Error */
	public function get_status( int $user_id ) {
		$cached = $this->get_cached();
		if ( null === $cached ) {
			if ( Logger::instance()->enabled() ) {
				Logger::instance()->warn( 'credits_cache_miss', [ 'user_id' => $user_id ] );
			}

			return new \WP_Error(
				'fotogenic_ai_no_cached_credits',
				__( 'No cached credits data available.', 'fotogenic-ai' ),
				[ 'status' => 404 ]
			);
		}

		return $this->format_cached_response( $cached, 'cache' );
	}

	/** @return array|\WP_Error */
	public function refresh( int $user_id, array $options = [] ) {
		$log_event   = isset( $options['log_event'] ) ? sanitize_key( (string) $options['log_event'] ) : '';
		$log_context = isset( $options['log_context'] ) && is_array( $options['log_context'] )
			? $this->sanitize_log_context( $options['log_context'] )
			: [];
		$should_log  = '' !== $log_event && Logger::instance()->enabled();

		$response = Api_Client::instance()->request( 'GET', 'credits_balance', [] );
		if ( is_wp_error( $response ) ) {
			if ( $should_log ) {
				Logger::instance()->error(
					'credits_refresh_failed',
					array_merge(
						$log_context,
						[
							'user_id'    => $user_id,
							'error_code' => sanitize_key( (string) $response->get_error_code() ),
							'status'     => (int) ( $response->get_error_data()['status'] ?? 0 ),
						]
					)
				);
			}

			return $response;
		}

		$payload = $this->normalize_api_response( $response );
		if ( is_wp_error( $payload ) ) {
			if ( $should_log ) {
				Logger::instance()->error(
					'credits_refresh_failed',
					array_merge(
						$log_context,
						[
							'user_id'    => $user_id,
							'error_code' => sanitize_key( (string) $payload->get_error_code() ),
							'status'     => (int) ( $payload->get_error_data()['status'] ?? 0 ),
						]
					)
				);
			}

			return $payload;
		}
		$payload = $this->merge_available_free_trial( $payload );

		$this->set_cached( $payload );
		if ( $should_log ) {
			Logger::instance()->info(
				$log_event,
				array_merge(
					$log_context,
					[
						'user_id' => $user_id,
						'balance' => $payload['balance'],
					]
				)
			);
		}

		return $this->format_cached_response(
			[
				'data'        => $payload,
				'force_stale' => false,
			],
			'network'
		);
	}

	/**
	 * Extract the non-sensitive free-trial grant fields used by plugin UI.
	 */
	public function get_safe_free_trial_data( array $payload ): array {
		$free_trial = isset( $payload['freeTrial'] ) && is_array( $payload['freeTrial'] )
			? $payload['freeTrial']
			: [];

		if ( empty( $free_trial ) ) {
			return [];
		}

		$safe             = [];
		$granted_credits  = $free_trial['grantedCredits'] ?? null;
		$normalized_grant = $this->normalize_non_negative_number( $granted_credits );
		if ( null !== $normalized_grant ) {
			$safe['grantedCredits'] = $normalized_grant;
		}

		$notice = isset( $free_trial['grantNotice'] ) && is_array( $free_trial['grantNotice'] )
			? $free_trial['grantNotice']
			: [];

		$eligible  = $this->normalize_optional_bool( $notice['eligible'] ?? null );
		$notice_id = $this->sanitize_identifier( $notice['id'] ?? '' );

		if ( null !== $eligible || '' !== $notice_id ) {
			$safe['grantNotice'] = [];
			if ( null !== $eligible ) {
				$safe['grantNotice']['eligible'] = $eligible;
			}
			if ( '' !== $notice_id ) {
				$safe['grantNotice']['id'] = $notice_id;
			}
		}

		$content_version = $this->sanitize_identifier( $free_trial['contentVersion'] ?? '' );
		if ( '' !== $content_version ) {
			$safe['contentVersion'] = $content_version;
		}

		return $safe;
	}

	public function clear_user_cache( ?string $identity = null, ?string $env = null ): void {
		$env_value     = null !== $env ? $env : (string) ( Settings::instance()->get_options()['api_environment'] ?? 'production' );
		$cache_identity = null !== $identity ? $identity : Auth_Session::instance()->get_cache_identity();
		$cache_key      = $this->build_cache_key( $env_value, $cache_identity );

		delete_transient( $cache_key );
		delete_transient( $this->get_stale_cache_key( $cache_key ) );
	}

	/** @return array|\WP_Error */
	private function normalize_api_response( $response ) {
		if ( ! is_array( $response ) ) {
			return $this->invalid_balance_response();
		}

		$payload = $response;

		$balance = $this->normalize_non_negative_number( $payload['balance'] ?? null );
		if ( null === $balance ) {
			return $this->invalid_balance_response();
		}

		$billing_status = sanitize_text_field( (string) ( $payload['billingSubscriptionStatus'] ?? '' ) );
		$active_plan    = isset( $payload['activePlan'] ) && is_array( $payload['activePlan'] )
			? $payload['activePlan']
			: [];
		$explicit_plan  = $this->normalize_optional_bool( $payload['hasPlan'] ?? null );
		$has_plan       = null !== $explicit_plan
			? $explicit_plan
			: $this->derive_has_plan( $active_plan, $billing_status );

		$normalized = [
			'balance'                   => $balance,
			'topupBalance'              => $this->normalize_non_negative_number( $payload['topupBalance'] ?? null ),
			'subscriptionBalance'       => $this->normalize_non_negative_number( $payload['subscriptionBalance'] ?? null ),
			'billingSubscriptionStatus' => $billing_status,
			'activePlan'                => $this->sanitize_active_plan( $active_plan ),
			'hasPlan'                   => $has_plan,
			'updated_at'                => time(),
		];

		$free_trial = $this->get_safe_free_trial_data( $payload );
		if ( ! empty( $free_trial ) ) {
			$normalized['freeTrial'] = $free_trial;
		}

		return $normalized;
	}

	private function invalid_balance_response(): \WP_Error {
		return new \WP_Error(
			'fotogenic_ai_invalid_credits_response',
			__( 'Fotogenic credits are temporarily unavailable.', 'fotogenic-ai' ),
			[ 'status' => 502 ]
		);
	}

	private function merge_available_free_trial( array $payload ): array {
		if ( ! class_exists( Config_Provider::class ) ) {
			return $payload;
		}

		$config_result = Config_Provider::get_config();
		$config        = isset( $config_result['data'] ) && is_array( $config_result['data'] ) ? $config_result['data'] : [];
		$from_config   = $this->get_safe_free_trial_data( $config );
		if ( empty( $from_config ) ) {
			return $payload;
		}

		$from_credits        = isset( $payload['freeTrial'] ) && is_array( $payload['freeTrial'] ) ? $payload['freeTrial'] : [];
		$payload['freeTrial'] = array_replace_recursive( $from_config, $from_credits );

		return $payload;
	}

	private function get_cache_key( ?string $identity = null ): string {
		$settings = Settings::instance();
		$env      = (string) ( $settings->get_options()['api_environment'] ?? 'production' );
		$identity = null !== $identity ? $identity : Auth_Session::instance()->get_cache_identity();

		return $this->build_cache_key( $env, $identity );
	}

	private function get_cached(): ?array {
		$cache_key = $this->get_cache_key();
		$cached    = get_transient( $cache_key );
		if ( $this->is_valid_cached_payload( $cached ) ) {
			return [
				'data'        => $cached,
				'force_stale' => false,
			];
		}

		$stale = get_transient( $this->get_stale_cache_key( $cache_key ) );
		if ( $this->is_valid_cached_payload( $stale ) ) {
			return [
				'data'        => $stale,
				'force_stale' => true,
			];
		}

		return null;
	}

	private function set_cached( array $payload ): void {
		$cache_key = $this->get_cache_key();

		set_transient( $cache_key, $payload, self::TTL_SECONDS );
		set_transient( $this->get_stale_cache_key( $cache_key ), $payload, self::STALE_TTL_SECONDS );
	}

	private function format_cached_response( array $cached, string $source ): array {
		$payload      = isset( $cached['data'] ) && is_array( $cached['data'] ) ? $cached['data'] : [];
		$fetched_at   = (int) ( $payload['updated_at'] ?? 0 );
		$age_seconds  = max( 0, time() - $fetched_at );
		$force_stale  = ! empty( $cached['force_stale'] );
		$is_fresh     = ! $force_stale && $fetched_at > 0 && $age_seconds < self::TTL_SECONDS;
		$provenance   = $is_fresh ? self::PROVENANCE_FRESH : self::PROVENANCE_STALE;
		$has_plan     = (bool) ( $payload['hasPlan'] ?? false );
		$active_plan  = isset( $payload['activePlan'] ) && is_array( $payload['activePlan'] ) ? $payload['activePlan'] : [];
		$free_trial   = isset( $payload['freeTrial'] ) && is_array( $payload['freeTrial'] ) ? $payload['freeTrial'] : [];

		$response = [
			'balance'                   => (float) $payload['balance'],
			'topupBalance'              => $this->normalize_non_negative_number( $payload['topupBalance'] ?? null ),
			'subscriptionBalance'       => $this->normalize_non_negative_number( $payload['subscriptionBalance'] ?? null ),
			'billingSubscriptionStatus' => sanitize_text_field( (string) ( $payload['billingSubscriptionStatus'] ?? '' ) ),
			'activePlan'                => $this->sanitize_active_plan( $active_plan ),
			'hasPlan'                   => $has_plan,
			'provenance'                => $provenance,
			'available'                 => true,
			'fresh'                     => $is_fresh,
			'stale'                     => ! $is_fresh,
			'cached'                    => 'cache' === $source,
			'fetchedAt'                 => $fetched_at,
			'ttlSeconds'                => self::TTL_SECONDS,
			'expiresAt'                 => $fetched_at + self::TTL_SECONDS,
		];

		if ( ! empty( $free_trial ) ) {
			$response['freeTrial'] = $this->get_safe_free_trial_data( [ 'freeTrial' => $free_trial ] );
		}

		return $response;
	}

	private function build_cache_key( string $env, string $identity ): string {
		$env         = sanitize_key( $env );
		$env         = '' !== $env ? $env : 'production';
		$identity    = trim( $identity );
		$fingerprint = 'guest';

		if ( '' !== $identity && 'guest' !== $identity ) {
			$fingerprint = substr( hash_hmac( 'sha256', $identity, wp_salt( 'auth' ) ), 0, 32 );
		}

		return sprintf(
			'fotogenic_ai_credits_%d_%s_%s',
			max( 1, get_current_blog_id() ),
			$env,
			$fingerprint
		);
	}

	private function get_stale_cache_key( string $cache_key ): string {
		return $cache_key . '_stale';
	}

	private function is_valid_cached_payload( $payload ): bool {
		return is_array( $payload )
			&& ! empty( $payload['updated_at'] )
			&& null !== $this->normalize_non_negative_number( $payload['balance'] ?? null );
	}

	private function normalize_non_negative_number( $value ): ?float {
		if ( ! is_int( $value ) && ! is_float( $value ) && ! is_string( $value ) ) {
			return null;
		}
		if ( ! is_numeric( $value ) ) {
			return null;
		}

		$number = (float) $value;
		if ( ! is_finite( $number ) || $number < 0 ) {
			return null;
		}

		return $number;
	}

	private function normalize_optional_bool( $value ): ?bool {
		if ( is_bool( $value ) ) {
			return $value;
		}
		if ( ! is_scalar( $value ) ) {
			return null;
		}
		if ( 1 === $value || '1' === $value || 'true' === strtolower( trim( (string) $value ) ) ) {
			return true;
		}
		if ( 0 === $value || '0' === $value || 'false' === strtolower( trim( (string) $value ) ) ) {
			return false;
		}

		return null;
	}

	private function derive_has_plan( array $active_plan, string $billing_status ): bool {
		if ( ! empty( $active_plan ) ) {
			return true;
		}

		$status = strtolower( trim( $billing_status ) );

		return in_array( $status, [ 'active', 'trialing' ], true );
	}

	private function sanitize_active_plan( array $active_plan ): array {
		$allowed = [ 'name', 'slug', 'status', 'tier', 'interval', 'billingInterval' ];
		$safe    = [];

		foreach ( $allowed as $key ) {
			if ( ! isset( $active_plan[ $key ] ) || ! is_scalar( $active_plan[ $key ] ) ) {
				continue;
			}
			$safe[ $key ] = sanitize_text_field( (string) $active_plan[ $key ] );
		}

		return $safe;
	}

	private function sanitize_identifier( $value ): string {
		if ( ! is_scalar( $value ) ) {
			return '';
		}

		$value = sanitize_text_field( (string) $value );
		$value = trim( $value );
		if ( strlen( $value ) > 191 ) {
			$value = substr( $value, 0, 191 );
		}

		return $value;
	}

	private function sanitize_log_context( array $context ): array {
		$safe = [];
		foreach ( $context as $key => $value ) {
			$key = sanitize_key( (string) $key );
			if ( '' === $key || preg_match( '/token|authorization|account|email|user_uid|credential|secret/i', $key ) ) {
				continue;
			}
			if ( is_bool( $value ) || is_int( $value ) || is_float( $value ) ) {
				$safe[ $key ] = $value;
			} elseif ( is_string( $value ) ) {
				$safe[ $key ] = sanitize_text_field( $value );
			}
		}

		return $safe;
	}
}
