<?php

declare(strict_types=1);

namespace SecurWP\Modules\Headers;

// If this file is called directly, abort.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Header generation logic for the Security Headers module.
 *
 * Provides static methods to build each security header string from settings.
 *
 */
class HeaderPolicy {

	/**
	 * CSP presets keyed by name.
	 *
	 * Each preset is an associative array of directive => value.
	 */
	private const CSP_PRESETS = [
		'wordpress' => [
			'default-src' => "'self'",
			'script-src'  => "'self' 'unsafe-inline' 'unsafe-eval'",
			'style-src'   => "'self' 'unsafe-inline'",
			'img-src'     => "'self' data: https:",
			'font-src'    => "'self' data:",
			'connect-src' => "'self'",
			'frame-ancestors' => "'self'",
		],
		'moderate' => [
			'default-src' => "'self'",
			'script-src'  => "'self' 'unsafe-inline'",
			'style-src'   => "'self' 'unsafe-inline'",
			'img-src'     => "'self' data: https:",
			'font-src'    => "'self' data:",
			'connect-src' => "'self' https:",
			'frame-ancestors' => "'self'",
		],
		'strict' => [
			'default-src'  => "'none'",
			'script-src'   => "'self'",
			'style-src'    => "'self'",
			'img-src'      => "'self'",
			'font-src'     => "'self'",
			'connect-src'  => "'self'",
			'frame-ancestors' => "'none'",
			'base-uri'     => "'self'",
			'form-action'  => "'self'",
		],
	];

	/**
	 * Valid Referrer-Policy values.
	 */
	private const VALID_REFERRER_VALUES = [
		'no-referrer',
		'no-referrer-when-downgrade',
		'origin',
		'origin-when-cross-origin',
		'same-origin',
		'strict-origin',
		'strict-origin-when-cross-origin',
		'unsafe-url',
	];

	/**
	 * Build the Content-Security-Policy header value.
	 *
	 * When custom directives are present they are used as the complete policy
	 * (no preset base), giving the UI full control over every directive.
	 * Empty custom directive values are silently omitted so the browser falls
	 * back to default-src per the CSP specification.
	 *
	 * When no custom directives exist the selected preset is used as a
	 * fallback for backward compatibility with pre-directive-editor settings.
	 *
	 * @param array $settings The full headers settings array.
	 * @return string The assembled CSP header value.
	 */
	public static function build_csp( array $settings ): string {
		$custom = $settings['csp_custom_directives'] ?? [];

		if ( is_array( $custom ) && ! empty( $custom ) ) {
			// Custom directives are the authoritative policy.
			$directives = [];

			foreach ( $custom as $directive => $value ) {
				$directive = strtolower( trim( (string) $directive ) );
				$value     = trim( (string) $value );

				if ( '' === $directive || '' === $value ) {
					continue;
				}

				$directives[ $directive ] = $value;
			}
		} else {
			// Fallback: use preset when no custom directives are configured.
			$preset_name = $settings['csp_preset'] ?? 'wordpress';

			if ( ! isset( self::CSP_PRESETS[ $preset_name ] ) ) {
				$preset_name = 'wordpress';
			}

			$directives = self::CSP_PRESETS[ $preset_name ];
		}

		// Build the header string: "directive value; directive value; ..."
		$parts = [];
		foreach ( $directives as $directive => $value ) {
			$parts[] = $directive . ' ' . $value;
		}

		return implode( '; ', $parts );
	}

	/**
	 * Build the Strict-Transport-Security header value.
	 *
	 * @param array $settings The full headers settings array.
	 * @return string The assembled HSTS header value.
	 */
	public static function build_hsts( array $settings ): string {
		$max_age = (int) ( $settings['hsts_max_age'] ?? 31536000 );

		if ( $max_age < 0 ) {
			$max_age = 0;
		}

		$value = 'max-age=' . $max_age;

		if ( ! empty( $settings['hsts_subdomains'] ) ) {
			$value .= '; includeSubDomains';
		}

		if ( ! empty( $settings['hsts_preload'] ) ) {
			$value .= '; preload';
		}

		return $value;
	}

	/**
	 * Build the Permissions-Policy header value.
	 *
	 * @param array $directives Associative array of feature => allowlist string.
	 * @return string The assembled Permissions-Policy header value.
	 */
	public static function build_permissions_policy( array $directives ): string {
		$parts = [];

		foreach ( $directives as $feature => $allowlist ) {
			$feature   = strtolower( trim( (string) $feature ) );
			$allowlist = trim( (string) $allowlist );

			// Skip empty feature names or empty allowlists (would produce malformed "feature=").
			if ( '' === $feature || '' === $allowlist ) {
				continue;
			}

			$parts[] = $feature . '=' . $allowlist;
		}

		return implode( ', ', $parts );
	}

	/**
	 * Validate and return a Referrer-Policy value.
	 *
	 * Falls back to 'strict-origin-when-cross-origin' if the provided value
	 * is not in the list of valid values.
	 *
	 * @param string $value The referrer policy value to validate.
	 * @return string A valid Referrer-Policy value.
	 */
	public static function build_referrer_policy( string $value ): string {
		$value = strtolower( trim( $value ) );

		if ( in_array( $value, self::VALID_REFERRER_VALUES, true ) ) {
			return $value;
		}

		return 'strict-origin-when-cross-origin';
	}

	/**
	 * Get all enabled security headers as an associative array.
	 *
	 * Returns header name => header value for every header that is enabled
	 * in the provided settings. Used by both apply_headers() (to actually
	 * send them) and the test/preview REST endpoint.
	 *
	 * @param array $settings The full headers settings array.
	 * @return array<string, string> Associative array of header name => value.
	 */
	public static function get_all_headers( array $settings ): array {
		$headers = [];

		// Content-Security-Policy
		if ( ! empty( $settings['csp_enabled'] ) ) {
			$csp_value = self::build_csp( $settings );

			if ( '' !== $csp_value ) {
				$mode = $settings['csp_mode'] ?? 'enforce';
				$header_name = ( 'report-only' === $mode )
					? 'Content-Security-Policy-Report-Only'
					: 'Content-Security-Policy';

				$headers[ $header_name ] = $csp_value;
			}
		}

		// Strict-Transport-Security (only valid/safe to send over HTTPS).
		if ( ! empty( $settings['hsts_enabled'] ) && is_ssl() ) {
			$headers['Strict-Transport-Security'] = self::build_hsts( $settings );
		}

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

		// X-Frame-Options
		if ( ! empty( $settings['xfo_enabled'] ) ) {
			$xfo_value = strtoupper( trim( (string) ( $settings['xfo_value'] ?? 'SAMEORIGIN' ) ) );

			if ( ! in_array( $xfo_value, [ 'DENY', 'SAMEORIGIN' ], true ) ) {
				$xfo_value = 'SAMEORIGIN';
			}

			$headers['X-Frame-Options'] = $xfo_value;
		}

		// Referrer-Policy
		if ( ! empty( $settings['referrer_enabled'] ) ) {
			$headers['Referrer-Policy'] = self::build_referrer_policy(
				(string) ( $settings['referrer_value'] ?? 'strict-origin-when-cross-origin' )
			);
		}

		// Permissions-Policy
		if ( ! empty( $settings['permissions_enabled'] ) ) {
			$directives = $settings['permissions_directives'] ?? [];

			if ( is_array( $directives ) && ! empty( $directives ) ) {
				$pp_value = self::build_permissions_policy( $directives );

				if ( '' !== $pp_value ) {
					$headers['Permissions-Policy'] = $pp_value;
				}
			}
		}

		// Sanitize all header values to prevent CRLF injection attacks.
		foreach ( $headers as $name => $value ) {
			$headers[ $name ] = str_replace( [ "\r", "\n", "\0" ], '', $value );
		}

		return $headers;
	}

	/**
	 * Get the list of valid referrer policy values.
	 *
	 * @return string[] List of valid Referrer-Policy values.
	 */
	public static function get_valid_referrer_values(): array {
		return self::VALID_REFERRER_VALUES;
	}
}
