<?php
namespace AdvancedSecurityHeaders;

use WP_PowerSuite\Core\AbstractModule;

class Module extends AbstractModule {

	private const MODULE_ID = 'advanced-security-headers';
	private const MODULE_VERSION = '1.2.0';
	private const HSTS_MAX_AGE_CAP = 63072000;
	private const CSP_MAX_LENGTH = 4096;

	/** @var list<string> */
	private const REFERRER_POLICIES = [
		'no-referrer',
		'no-referrer-when-downgrade',
		'origin',
		'origin-when-cross-origin',
		'same-origin',
		'strict-origin',
		'strict-origin-when-cross-origin',
		'unsafe-url',
	];

	/** @var list<string> */
	private const BOOLEAN_KEYS = [
		'x_frame_options',
		'x_content_type_options',
		'x_xss_protection',
		'referrer_policy',
		'strict_transport_security',
		'hsts_include_subdomains',
		'content_security_policy',
		'content_security_policy_report_only',
	];

	private ?array $settings_cache = null;

	private bool $headers_emitted_this_request = false;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Security Headers', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Tell modern browsers to enforce sensible safety rules—like blocking sneaky scripts and iframe tricks—with strong defaults you can tighten further for HSTS or content policies.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		// HTTP responses only; skip ajax/cron (no HTML/REST body to protect there).
		return [ 'admin', 'frontend', 'login', 'rest' ];
	}

	/**
	 * @return array<string, mixed>
	 */
	public static function defaultSettings(): array {
		return [
			'x_frame_options'               => true,
			'x_content_type_options'        => true,
			'x_xss_protection'                      => false,
			'referrer_policy'                       => true,
			'referrer_policy_value'                 => 'strict-origin-when-cross-origin',
			'strict_transport_security'             => false,
			'hsts_max_age'                          => 31536000,
			'hsts_include_subdomains'               => false,
			'content_security_policy'               => false,
			'content_security_policy_report_only'   => true,
			'content_security_policy_value'         => "default-src 'self'",
		];
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array<string, mixed>
	 */
	public static function sanitizeSettings( array $settings ): array {
		$settings = array_merge( self::defaultSettings(), $settings );

		foreach ( self::BOOLEAN_KEYS as $key ) {
			$settings[ $key ] = ! empty( $settings[ $key ] );
		}

		$settings['referrer_policy_value'] = self::sanitizeReferrerPolicyValue(
			(string) ( $settings['referrer_policy_value'] ?? 'strict-origin-when-cross-origin' )
		);

		$settings['hsts_max_age'] = min(
			self::HSTS_MAX_AGE_CAP,
			max( 0, absint( $settings['hsts_max_age'] ?? 31536000 ) )
		);

		$settings['content_security_policy_value'] = self::sanitizeCspValue(
			(string) ( $settings['content_security_policy_value'] ?? '' )
		);

		if ( ! $settings['content_security_policy'] || $settings['content_security_policy_value'] === '' ) {
			$settings['content_security_policy'] = false;
		}

		return $settings;
	}

	protected function registerHooks(): void {
		if ( ! $this->isLicenseActive() ) {
			return;
		}

		if ( ( defined( 'WP_CLI' ) && WP_CLI ) || wp_doing_cron() ) {
			return;
		}

		$settings = $this->getSettings();
		if ( ! $this->settingsAllowAnyHeader( $settings ) ) {
			return;
		}

		add_action( 'send_headers', [ $this, 'emitSecurityHeaders' ], 999 );
		add_action( 'login_init', [ $this, 'emitSecurityHeaders' ], 1 );

		if ( is_admin() ) {
			add_action( 'admin_init', [ $this, 'emitSecurityHeaders' ], 1 );
		}

		if ( did_action( 'rest_api_init' ) ) {
			add_filter( 'rest_pre_serve_request', [ $this, 'serveRestHeaders' ], 10, 4 );
			return;
		}

		add_action( 'rest_api_init', [ $this, 'registerRestHeaders' ], 1 );
	}

	public function registerRestHeaders(): void {
		add_filter( 'rest_pre_serve_request', [ $this, 'serveRestHeaders' ], 10, 4 );
	}

	public function emitSecurityHeaders(): void {
		$this->emitOnce();
	}

	public function serveRestHeaders( $served, $result, $request, $server ): bool {
		$this->emitOnce();
		return (bool) $served;
	}

	private function emitOnce(): void {
		if ( $this->headers_emitted_this_request || headers_sent() ) {
			return;
		}

		$this->headers_emitted_this_request = true;

		$settings = $this->getSettings();
		$headers  = [];

		if ( ! empty( $settings['x_frame_options'] ) ) {
			$headers['X-Frame-Options'] = 'SAMEORIGIN';
		}

		if ( ! empty( $settings['x_content_type_options'] ) ) {
			$headers['X-Content-Type-Options'] = 'nosniff';
		}

		if ( ! empty( $settings['x_xss_protection'] ) ) {
			$headers['X-XSS-Protection'] = '1; mode=block';
		}

		if ( ! empty( $settings['referrer_policy'] ) ) {
			$headers['Referrer-Policy'] = self::sanitizeReferrerPolicyValue( (string) $settings['referrer_policy_value'] );
		}

		if ( ! empty( $settings['strict_transport_security'] ) && is_ssl() ) {
			$max_age = absint( $settings['hsts_max_age'] ?? 31536000 );
			$max_age = min( self::HSTS_MAX_AGE_CAP, max( 0, $max_age ) );
			$include_subdomains = ! empty( $settings['hsts_include_subdomains'] ) ? '; includeSubDomains' : '';
			$headers['Strict-Transport-Security'] = 'max-age=' . $max_age . $include_subdomains;
		}

		if ( ! empty( $settings['content_security_policy'] ) ) {
			$csp = self::sanitizeCspValue( (string) ( $settings['content_security_policy_value'] ?? '' ) );
			if ( $csp !== '' ) {
				$csp_header = ! empty( $settings['content_security_policy_report_only'] )
					? 'Content-Security-Policy-Report-Only'
					: 'Content-Security-Policy';
				$headers[ $csp_header ] = $csp;
			}
		}

		/**
		 * Filters security headers before they are sent.
		 *
		 * @param array<string, string> $headers  Header names mapped to values.
		 * @param array<string, mixed>  $settings Module settings.
		 */
		$headers = apply_filters( 'wpps_security_headers', $headers, $settings );
		if ( ! is_array( $headers ) ) {
			return;
		}

		foreach ( $headers as $name => $value ) {
			$name = trim( (string) $name );
			if ( $name === '' || ( ! is_string( $value ) && ! is_numeric( $value ) ) ) {
				continue;
			}

			$value = trim( preg_replace( '/[^\x20-\x7E]/', '', (string) $value ) ?? '' );
			if ( $value === '' ) {
				continue;
			}

			header( $name . ': ' . $value, true );
		}
	}

	/**
	 * @param array<string, mixed> $settings
	 */
	private function settingsAllowAnyHeader( array $settings ): bool {
		foreach ( [ 'x_frame_options', 'x_content_type_options', 'x_xss_protection', 'referrer_policy', 'strict_transport_security' ] as $key ) {
			if ( ! empty( $settings[ $key ] ) ) {
				return true;
			}
		}

		if ( ! empty( $settings['content_security_policy'] ) ) {
			$csp = self::sanitizeCspValue(
				isset( $settings['content_security_policy_value'] ) ? (string) $settings['content_security_policy_value'] : ''
			);
			if ( $csp !== '' ) {
				return true;
			}
		}

		return false;
	}

	public static function sanitizeCspValue( string $csp ): string {
		$csp = trim( preg_replace( '/[^\x20-\x7E]/', '', $csp ) ?? '' );
		if ( $csp === '' ) {
			return '';
		}

		if ( strlen( $csp ) > self::CSP_MAX_LENGTH ) {
			$csp = substr( $csp, 0, self::CSP_MAX_LENGTH );
		}

		return trim( $csp );
	}

	private static function sanitizeReferrerPolicyValue( string $policy ): string {
		$policy = trim( preg_replace( '/[^\x20-\x7E]/', '', $policy ) ?? '' );
		if ( $policy === '' ) {
			return 'strict-origin-when-cross-origin';
		}

		return in_array( $policy, self::REFERRER_POLICIES, true )
			? $policy
			: 'strict-origin-when-cross-origin';
	}

	private function isLicenseActive(): bool {
		return function_exists( 'wp_powersuite_is_license_valid' ) && wp_powersuite_is_license_valid();
	}

	/**
	 * @return array<string, mixed>
	 */
	private function getSettings(): array {
		if ( $this->settings_cache !== null ) {
			return $this->settings_cache;
		}

		$stored = $this->moduleSettings()->all( [] );
		if ( ! is_array( $stored ) ) {
			$stored = [];
		}

		$this->settings_cache = self::sanitizeSettings( $stored );
		return $this->settings_cache;
	}
}
