<?php
/**
 * CORS allowlist for the `seovaultai/v1` REST namespace (Program E).
 *
 * Replaces the previous echo-any-Origin behavior with an explicit
 * allowlist of production web-app origins, the site's own home URL,
 * and (only when WP_DEBUG) local Next.js dev origins.
 *
 * Filters:
 * - {@see ORIGINS_FILTER} (`seovaultai_cors_allowed_origins`) — replace
 *   or extend the full allowlist array.
 * - {@see DEV_ORIGINS_FILTER} (`seovaultai_cors_allow_dev_origins`) —
 *   bool; default is `WP_DEBUG`. Return false to disable localhost
 *   origins even when debugging.
 *
 * @package SEOvaultSEO\Api
 */

namespace SEOvaultSEO\Api;

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

class CorsPolicy {

	/**
	 * Filter for the full CORS allowlist array.
	 */
	const ORIGINS_FILTER = 'seovaultai_cors_allowed_origins';

	/**
	 * Filter to enable/disable WP_DEBUG local-dev origins.
	 * Callbacks receive the default (`WP_DEBUG`) and must return bool.
	 */
	const DEV_ORIGINS_FILTER = 'seovaultai_cors_allow_dev_origins';

	/**
	 * Production / staging web-app origins always included by default.
	 *
	 * @return string[]
	 */
	public static function get_static_default_origins() {
		return array(
			'https://seovaultai.com',
			'https://app.seovaultai.com',
			'https://seovaultai-dev.web.app',
		);
	}

	/**
	 * Local Next.js origins allowed only when {@see should_allow_dev_origins()}.
	 *
	 * @return string[]
	 */
	public static function get_dev_origins() {
		return array(
			'http://localhost:3000',
			'http://127.0.0.1:3000',
		);
	}

	/**
	 * Whether local-dev origins should be merged into the allowlist.
	 * Defaults to `WP_DEBUG`; filterable via {@see DEV_ORIGINS_FILTER}.
	 *
	 * @return bool
	 */
	public static function should_allow_dev_origins() {
		$default = defined( 'WP_DEBUG' ) && WP_DEBUG;
		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Constant value is 'seovaultai_cors_allow_dev_origins'.
		return (bool) apply_filters( self::DEV_ORIGINS_FILTER, $default );
	}

	/**
	 * Built-in defaults before the origins filter runs:
	 * static production origins + home_url() + optional dev origins.
	 *
	 * @return string[]
	 */
	public static function get_default_origins() {
		$origins   = self::get_static_default_origins();
		$origins[] = untrailingslashit( home_url() );

		if ( self::should_allow_dev_origins() ) {
			$origins = array_merge( $origins, self::get_dev_origins() );
		}

		return self::normalize_origin_list( $origins );
	}

	/**
	 * Allowlist after {@see ORIGINS_FILTER}. Non-array filter returns fall
	 * back to defaults.
	 *
	 * @return string[]
	 */
	public static function get_allowed_origins() {
		$defaults = self::get_default_origins();
		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Constant value is 'seovaultai_cors_allowed_origins'.
		$filtered = apply_filters( self::ORIGINS_FILTER, $defaults );

		if ( ! is_array( $filtered ) ) {
			return $defaults;
		}

		return self::normalize_origin_list( $filtered );
	}

	/**
	 * Exact-match check against the allowlist. Empty origin is never
	 * allowed (do not reflect / wildcard for CORS).
	 *
	 * @param string $origin Raw Origin header value.
	 * @return bool
	 */
	public static function is_allowed_origin( $origin ) {
		$normalized = self::normalize_origin( (string) $origin );
		if ( '' === $normalized ) {
			return false;
		}

		$allowed = self::get_allowed_origins();
		return in_array( $normalized, $allowed, true );
	}

	/**
	 * Normalize an origin for exact comparison: trim, require http(s),
	 * strip trailing slash. Invalid / empty → `''`.
	 *
	 * @param string $origin Raw origin.
	 * @return string
	 */
	public static function normalize_origin( $origin ) {
		$origin = trim( (string) $origin );
		if ( '' === $origin || '*' === $origin ) {
			return '';
		}

		$sanitized = esc_url_raw( $origin );
		if ( '' === $sanitized ) {
			return '';
		}

		$parts = wp_parse_url( $sanitized );
		if ( ! is_array( $parts ) ) {
			return '';
		}

		$scheme = isset( $parts['scheme'] ) ? strtolower( (string) $parts['scheme'] ) : '';
		if ( 'http' !== $scheme && 'https' !== $scheme ) {
			return '';
		}

		$host = isset( $parts['host'] ) ? (string) $parts['host'] : '';
		if ( '' === $host ) {
			return '';
		}

		$normalized = $scheme . '://' . $host;
		if ( isset( $parts['port'] ) ) {
			$normalized .= ':' . (int) $parts['port'];
		}

		return untrailingslashit( $normalized );
	}

	/**
	 * @param array<int|string, mixed> $origins Raw origin list.
	 * @return string[] Unique normalized origins (empty entries dropped).
	 */
	private static function normalize_origin_list( $origins ) {
		$out = array();
		foreach ( $origins as $origin ) {
			if ( ! is_string( $origin ) && ! is_numeric( $origin ) ) {
				continue;
			}
			$normalized = self::normalize_origin( (string) $origin );
			if ( '' === $normalized ) {
				continue;
			}
			$out[ $normalized ] = $normalized;
		}
		return array_values( $out );
	}
}
