<?php

declare(strict_types=1);

namespace SiteFort\Modules\Headers;

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

use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use SiteFort\Includes\SettingsStore;
use SiteFort\Includes\RemoteUrlValidator;

/**
 * REST API controller for the Security Headers module.
 *
 * Registers routes under the sitefort/v1 namespace for reading,
 * updating, and previewing security header settings.
 *
 */
class HeadersApi {

	private const DISCLOSURE_REMEDIATION_NONE = 'none';
	private const DISCLOSURE_REMEDIATION_PLUGIN = 'plugin';
	private const DISCLOSURE_REMEDIATION_READ_ONLY = 'read-only';

	/**
	 * REST API namespace.
	 */
	private const NAMESPACE = 'sitefort/v1';

	/**
	 * Settings table column for header settings.
	 */
	private const SETTINGS_COLUMN = SettingsStore::COL_HEADERS;

	/**
	 * Recommended security headers that should be present.
	 */
	private const RECOMMENDED_HEADERS = [
		'Content-Security-Policy',
		'Strict-Transport-Security',
		'X-Content-Type-Options',
		'X-Frame-Options',
		'Referrer-Policy',
		'Permissions-Policy',
	];

	/**
	 * Disclosure headers intentionally modeled by the WordPress headers UI.
	 *
	 * Keep this list aligned with admin/app/src/components/Headers.tsx.
	 */
	private const DISCLOSURE_HEADERS = [
		'X-Powered-By',
		'Server',
		'X-Generator',
	];

	/**
	 * Register all REST API routes for the headers module.
	 *
	 * @return void
	 */
	public function register_routes(): void {
		register_rest_route( self::NAMESPACE, '/headers/settings', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_settings' ],
				'permission_callback' => [ $this, 'check_permission' ],
			],
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'update_settings' ],
				'permission_callback' => [ $this, 'check_permission' ],
			],
		] );

		register_rest_route( self::NAMESPACE, '/headers/test', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'test_headers' ],
				'permission_callback' => [ $this, 'check_permission' ],
			],
		] );

		register_rest_route( self::NAMESPACE, '/headers/scan', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'scan_headers' ],
				'permission_callback' => [ $this, 'check_permission' ],
			],
		] );
	}

	/**
	 * Permission check for all endpoints.
	 *
	 * @return bool Whether the current user can manage the plugin control plane.
	 */
	public function check_permission(): bool {
		return \SiteFort\Includes\Capabilities::can_manage_plugin();
	}

	/**
	 * GET /headers/settings - Return current settings merged with defaults.
	 *
	 * @param WP_REST_Request $request The incoming request.
	 * @return WP_REST_Response
	 */
	public function get_settings( WP_REST_Request $request ): WP_REST_Response {
		$settings = $this->load_settings();

		return new WP_REST_Response( $settings, 200 );
	}

	/**
	 * POST /headers/settings - Validate, sanitize, and save settings.
	 *
	 * @param WP_REST_Request $request The incoming request.
	 * @return WP_REST_Response
	 */
	public function update_settings( WP_REST_Request $request ): WP_REST_Response {
		$params = $request->get_json_params();

		if ( ! is_array( $params ) ) {
			return new WP_REST_Response(
				[ 'error' => __( 'Invalid JSON body.', 'sitefort' ) ],
				400
			);
		}

		$current   = $this->load_settings();
		$sanitized = Headers::sanitize_settings_patch( $current, $params );

		$updated = SettingsStore::save( self::SETTINGS_COLUMN, $sanitized );

		return new WP_REST_Response( [
			'success'  => true,
			'message'  => $updated
				? __( 'Security header settings saved successfully.', 'sitefort' )
				: __( 'Settings are already up to date.', 'sitefort' ),
			'settings' => $sanitized,
		], 200 );
	}

	/**
	 * GET /headers/test - Preview the headers that would be sent.
	 *
	 * Returns the computed headers based on current settings without
	 * actually sending them.
	 *
	 * @param WP_REST_Request $request The incoming request.
	 * @return WP_REST_Response
	 */
	public function test_headers( WP_REST_Request $request ): WP_REST_Response {
		$settings = $this->load_settings();
		$headers  = HeaderPolicy::get_all_headers( $settings );

		return new WP_REST_Response( [
			'success' => true,
			'headers' => $headers,
		], 200 );
	}

	/**
	 * GET /headers/scan - Perform a loopback HTTP request to the site's
	 * homepage and analyse the response headers for advisory coverage.
	 *
	 * @param WP_REST_Request $request The incoming request.
	 * @return WP_REST_Response
	 */
	public function scan_headers( WP_REST_Request $request ): WP_REST_Response {
		$url = home_url( '/' );

		$args = [
			'timeout'     => 15,
			'sslverify'   => true,
			'user-agent'  => 'SiteFort-HeaderScanner/1.0',
			'redirection' => 5,
		];

		$response = wp_remote_get( $url, $args );

		if ( is_wp_error( $response ) && RemoteUrlValidator::is_tls_error( $response ) ) {
			$args['sslverify'] = false;
			$response          = wp_remote_get( $url, $args );
		}

		if ( is_wp_error( $response ) ) {
			return new WP_REST_Response( [
				'error' => sprintf(
					/* translators: %s: error message */
					__( 'Could not reach the site homepage: %s', 'sitefort' ),
					$response->get_error_message()
				),
			], 502 );
		}

		$raw_headers = wp_remote_retrieve_headers( $response );

		// Normalise to a plain key => value array (case-insensitive lookup).
		$headers_lower = [];
		if ( is_object( $raw_headers ) && method_exists( $raw_headers, 'getAll' ) ) {
			foreach ( $raw_headers->getAll() as $name => $value ) {
				$headers_lower[ strtolower( $name ) ] = is_array( $value ) ? implode( ', ', $value ) : (string) $value;
			}
		} elseif ( is_array( $raw_headers ) ) {
			foreach ( $raw_headers as $name => $value ) {
				$headers_lower[ strtolower( (string) $name ) ] = is_array( $value ) ? implode( ', ', $value ) : (string) $value;
			}
		}

		// Analyse security headers.
		$security = [];
		foreach ( self::RECOMMENDED_HEADERS as $header ) {
			$key   = strtolower( $header );
			$found = isset( $headers_lower[ $key ] );

			// CSP-Report-Only is an acceptable alternative for CSP.
			if ( ! $found && 'content-security-policy' === $key ) {
				$alt_key = 'content-security-policy-report-only';
				if ( isset( $headers_lower[ $alt_key ] ) ) {
					$found = true;
					$key   = $alt_key;
					$header = 'Content-Security-Policy-Report-Only';
				}
			}

			$security[ $header ] = [
				'present' => $found,
				'value'   => $found ? $headers_lower[ $key ] : '',
			];
		}

		// Analyse disclosure headers.
		$disclosure = [];
		foreach ( self::DISCLOSURE_HEADERS as $header ) {
			$disclosure[ $header ] = $this->build_disclosure_finding( $header, $headers_lower );
		}

		// Build missing list.
		$missing = [];
		foreach ( $security as $name => $info ) {
			if ( ! $info['present'] ) {
				$missing[] = $name;
			}
		}

		return new WP_REST_Response( [
			'security'   => $security,
			'disclosure' => $disclosure,
			'missing'    => $missing,
		], 200 );
	}

	/**
	 * Load settings from the database, merged with defaults.
	 *
	 * @return array The complete settings array.
	 */
	private function load_settings(): array {
		$defaults   = Headers::get_default_settings();
		$raw        = SettingsStore::get( self::SETTINGS_COLUMN, $defaults );
		$normalized = Headers::normalize_settings( $raw );

		if ( $normalized !== $raw ) {
			SettingsStore::save( self::SETTINGS_COLUMN, $normalized );
		}

		return $normalized;
	}

	/**
	 * Build a disclosure finding with backend-owned remediation classification.
	 *
	 * The frontend should render the remediation contract returned here rather
	 * than infer actionability from a static header list.
	 *
	 * @param string               $header        Header name.
	 * @param array<string,string> $headers_lower Normalized response headers.
	 * @return array<string,mixed>
	 */
	private function build_disclosure_finding( string $header, array $headers_lower ): array {
		$key   = strtolower( $header );
		$found = isset( $headers_lower[ $key ] );
		$value = $found ? (string) $headers_lower[ $key ] : '';

		$finding = [
			'present'          => $found,
			'value'            => $value,
			'remediation_type' => self::DISCLOSURE_REMEDIATION_NONE,
		];

		if ( ! $found ) {
			return $finding;
		}

		switch ( $header ) {
			case 'X-Powered-By':
			case 'X-Generator':
				$finding['remediation_type'] = self::DISCLOSURE_REMEDIATION_PLUGIN;
				break;

			case 'Server':
				if ( self::header_value_exposes_version( $value ) ) {
					$finding['remediation_type'] = self::DISCLOSURE_REMEDIATION_READ_ONLY;
				}
				break;
		}

		return $finding;
	}

	/**
	 * Determine whether a disclosure header exposes concrete version details.
	 *
	 * @param string $value Raw header value.
	 * @return bool
	 */
	private static function header_value_exposes_version( string $value ): bool {
		$value = trim( $value );

		if ( $value === '' ) {
			return false;
		}

		return preg_match( '/\/\d/', $value ) === 1;
	}

}
