<?php

declare(strict_types=1);

namespace SecurWP\Modules\Notifications;

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

use SecurWP\Includes\SecretManager;

/**
 * Webhook Delivery - sends security notifications to Slack, Discord, or generic webhooks.
 *
 * Supports three providers:
 *  - slack:   Sends Block Kit formatted messages.
 *  - discord: Sends embed-formatted messages.
 *  - generic: Sends a plain JSON payload.
 *
 */
class WebhookDelivery {

	private const SLACK_URL_PATTERN = '/^https:\/\/hooks\.slack\.com\/services\/.+$/';
	private const DISCORD_URL_PATTERN = '/^https:\/\/(discord\.com|discordapp\.com)\/api\/webhooks\/.+$/';

	/**
	 * @var array<string,mixed>|NotificationSettings
	 */
	private $settings_source;

	/**
	 * @param array<string,mixed>|NotificationSettings $settings_source
	 */
	public function __construct( $settings_source ) {
		$this->settings_source = $settings_source;
	}

	/**
	 * Resolve the latest webhook settings.
	 *
	 * @return array<string,mixed>
	 */
	private function get_settings(): array {
		if ( $this->settings_source instanceof NotificationSettings ) {
			return $this->settings_source->get_all();
		}

		if ( is_array( $this->settings_source ) ) {
			return $this->settings_source;
		}

		return [];
	}

	/**
	 * Validate and normalize webhook provider.
	 */
	private function normalize_provider( string $provider ): string {
		return in_array( $provider, [ 'slack', 'discord', 'generic' ], true ) ? $provider : 'slack';
	}

	/**
	 * Validate a webhook URL against the selected provider.
	 */
	public static function validate_webhook_url( string $provider, string $url ): bool {
		$provider = in_array( $provider, [ 'slack', 'discord', 'generic' ], true ) ? $provider : 'slack';
		$url = trim( $url );
		if ( '' === $url ) {
			return false;
		}

		$is_valid_url = function_exists( 'wp_http_validate_url' )
			? wp_http_validate_url( $url )
			: (bool) filter_var( $url, FILTER_VALIDATE_URL );

		if ( ! $is_valid_url ) {
			return false;
		}

		if ( 'slack' === $provider ) {
			return 1 === preg_match( self::SLACK_URL_PATTERN, $url );
		}

		if ( 'discord' === $provider ) {
			return 1 === preg_match( self::DISCORD_URL_PATTERN, $url );
		}

		return true;
	}

	private function resolve_target( array $settings ): array {
		if ( $this->settings_source instanceof NotificationSettings ) {
			return $this->settings_source->resolve_active_webhook();
		}

		$provider = $this->normalize_provider(
			(string) ( $settings['webhook_active_provider'] ?? $settings['webhook_provider'] ?? 'slack' )
		);

		$url = '';
		if ( isset( $settings['webhook_provider_urls'] ) && is_array( $settings['webhook_provider_urls'] ) ) {
			$url = trim( (string) ( $settings['webhook_provider_urls'][ $provider ] ?? '' ) );
		}

		if ( '' === $url ) {
			$url = trim( (string) ( $settings['webhook_url'] ?? '' ) );
		}

		if ( ! self::validate_webhook_url( $provider, $url ) ) {
			$url = '';
		}

		return [
			'provider' => $provider,
			'url'      => $url,
		];
	}

	/**
	 * Whether webhook delivery is enabled and configured.
	 */
	public function is_enabled(): bool {
		$settings = $this->get_settings();
		if ( empty( $settings['webhook_enabled'] ) ) {
			return false;
		}

		$target = $this->resolve_target( $settings );
		return '' !== $target['url'];
	}

	/**
	 * Send notification to Slack/Discord/generic webhook.
	 *
	 * @param string $event_type Event identifier (e.g. 'security.scan_completed').
	 * @param array  $data       Event-specific data.
	 * @param bool   $is_retry   Whether this is a retry attempt (prevents infinite retry chains).
	 * @return bool Whether the webhook responded successfully.
	 */
	public function send( string $event_type, array $data, bool $is_retry = false ): bool {
		$settings = $this->get_settings();
		if ( empty( $settings['webhook_enabled'] ) ) {
			return false;
		}

		if ( 'security.firewall_block' === $event_type && ! $this->is_firewall_digest_payload( $data ) ) {
			return true;
		}

		$target = $this->resolve_target( $settings );
		$url = $target['url'];
		if ( '' === $url ) {
			return false;
		}

		$provider = $target['provider'];

		$payload  = $this->format_payload( $provider, $event_type, $data );
		$body_json = wp_json_encode( $payload );
		if ( false === $body_json ) {
			return false;
		}

		$headers = [ 'Content-Type' => 'application/json' ];

		// Send the webhook signature header for generic receivers.
		if ( 'generic' === $provider ) {
			$secret = $this->get_webhook_secret();
			if ( $secret ) {
				$signature = 'sha256=' . hash_hmac( 'sha256', $body_json, $secret );
				$headers['X-SecurWP-Signature'] = $signature;
			}
		}

		$response = wp_remote_post( $url, [
			'body'    => $body_json,
			'headers' => $headers,
			'timeout' => 5,
		] );

		$success = ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) < 300;

		// Schedule a single retry if delivery failed and this is not already a retry.
		if ( ! $success && ! $is_retry ) {
			$args = [ $event_type, $data ];
			if ( ! wp_next_scheduled( 'securwp_webhook_retry', $args ) ) {
				wp_schedule_single_event(
					time() + 300, // 5 minutes later
					'securwp_webhook_retry',
					$args
				);
			}
		}

		return $success;
	}

	/**
	 * Firewall webhook delivery is digest-only. Legacy single-block payloads are ignored.
	 */
	private function is_firewall_digest_payload( array $data ): bool {
		return isset( $data['blocked_count'] )
			|| isset( $data['digest_period'] )
			|| ( isset( $data['by_type'] ) && is_array( $data['by_type'] ) );
	}

	/**
	 * Retry handler for failed webhook deliveries.
	 *
	 * Hooked to 'securwp_webhook_retry' via the Notifications module.
	 *
	 * @param string $event_type Event identifier.
	 * @param array  $data       Event-specific data.
	 */
	public function retry_send( string $event_type, array $data ): void {
		$this->send( $event_type, $data, true );
	}

	/**
	 * Get or auto-generate the webhook signing secret.
	 *
	 * @return string|false The HMAC secret, or false if encryption is unavailable.
	 */
	private function get_webhook_secret() {
		$secret = SecretManager::get( 'webhook_hmac_secret' );
		if ( false === $secret || '' === $secret ) {
			// Auto-generate a 32-byte hex secret on first use.
			try {
				$secret = bin2hex( random_bytes( 32 ) );
				SecretManager::set( 'webhook_hmac_secret', $secret );
			} catch ( \Exception $e ) {
				return false;
			}
		}
		return $secret;
	}

	/**
	 * Normalize a scalar value for message output.
	 */
	private function normalize_text( $value, int $max_length = 160 ): string {
		if ( ! is_scalar( $value ) ) {
			return '';
		}

		$text = trim( wp_strip_all_tags( (string) $value ) );
		if ( '' === $text ) {
			return '';
		}

		if ( strlen( $text ) > $max_length ) {
			return substr( $text, 0, $max_length - 3 ) . '...';
		}

		return $text;
	}

	/**
	 * Add a fact row when the value is non-empty.
	 *
	 * @param array<int,array{label:string,value:string}> $facts
	 * @param mixed                                       $value
	 */
	private function add_fact( array &$facts, string $label, $value ): void {
		$text = $this->normalize_text( $value );
		if ( '' === $text ) {
			return;
		}

		$facts[] = [
			'label' => $label,
			'value' => $text,
		];
	}

	/**
	 * Normalize keyed counters from event payloads.
	 *
	 * @return array<string,int>
	 */
	private function normalize_counts( array $counts ): array {
		$normalized = [];
		foreach ( $counts as $key => $value ) {
			if ( ! is_scalar( $key ) ) {
				continue;
			}

			$normalized[ sanitize_key( (string) $key ) ] = max( 0, (int) $value );
		}

		return $normalized;
	}

	/**
	 * Resolve the SecurWP admin destination for the current event.
	 */
	private function get_action_url( string $event_type ): string {
		if ( in_array( $event_type, [ 'security.firewall_block', 'security.login_lockout' ], true ) ) {
			return admin_url( 'admin.php?page=securwp-firewall' );
		}

		if ( in_array( $event_type, [ 'security.scan_completed', 'security.vuln_detected', 'security.scan_failure' ], true ) ) {
			return admin_url( 'admin.php?page=securwp' );
		}

		return admin_url( 'admin.php?page=securwp-settings&tab=notifications' );
	}

	/**
	 * Resolve the primary action label for the current event.
	 */
	private function get_action_label( string $event_type ): string {
		if ( in_array( $event_type, [ 'security.scan_completed', 'security.vuln_detected', 'security.scan_failure' ], true ) ) {
			return 'Review Findings';
		}

		if ( in_array( $event_type, [ 'security.firewall_block', 'security.login_lockout' ], true ) ) {
			return 'Review Firewall';
		}

		return 'Open SecurWP';
	}

	/**
	 * Resolve the notification accent color.
	 */
	private function get_event_color( string $event_type, array $data, string $severity ): int {
		if ( 'critical' === $severity ) {
			return 16711680;
		}

		if ( 'high' === $severity ) {
			return 16098851;
		}

		if ( 'security.scan_completed' === $event_type ) {
			$counts = $this->normalize_counts( (array) ( $data['by_type'] ?? [] ) );
			if ( ( $counts['malware'] ?? 0 ) > 0 ) {
				return 16711680;
			}
		}

		if ( in_array( $event_type, [ 'security.scan_failure', 'security.firewall_block', 'security.plugin_deactivated' ], true ) ) {
			return 16098851;
		}

		return 3447003;
	}

	/**
	 * Build a normalized event presentation for webhook providers.
	 *
	 * @return array{
	 *   label:string,
	 *   title:string,
	 *   summary:string,
	 *   facts:array<int,array{label:string,value:string}>,
	 *   action_label:string,
	 *   action_url:string,
	 *   color:int,
	 *   severity:string,
	 *   site_name:string,
	 *   site_url:string,
	 *   site_display:string,
	 *   timestamp:string
	 * }
	 */
	private function build_presentation( string $event_type, array $data ): array {
		$labels = [
			'security.scan_completed'     => 'Scan Results',
			'security.vuln_detected'      => 'Vulnerability Detection',
			'security.scan_failure'       => 'Scan Error',
			'security.firewall_block'     => 'Firewall Block Digest',
			'security.login_lockout'      => 'Login Lockout',
			'security.2fa_change'         => 'Two-Factor Authentication Change',
			'security.tools_action'       => 'Administrative Tool Usage',
			'security.admin_login'        => 'Administrator Login',
			'security.plugin_deactivated' => 'SecurWP Deactivated',
		];

		$site_url     = home_url();
		$site_name    = $this->normalize_text( get_bloginfo( 'name' ), 120 );
		$site_host    = $this->normalize_text( wp_parse_url( $site_url, PHP_URL_HOST ), 120 );
		$site_display = $site_name;
		if ( '' !== $site_host && '' !== $site_name && strtolower( $site_host ) !== strtolower( $site_name ) ) {
			$site_display = $site_name . ' (' . $site_host . ')';
		} elseif ( '' === $site_display ) {
			$site_display = $site_host;
		}

		$label     = $labels[ $event_type ] ?? $this->normalize_text( $event_type );
		$severity  = strtolower( $this->normalize_text( $data['severity'] ?? '', 20 ) );
		$timestamp = $this->normalize_text( $data['detected_at'] ?? $data['login_time'] ?? $data['details']['Time'] ?? '', 40 );
		if ( '' === $timestamp ) {
			$timestamp = gmdate( 'Y-m-d H:i:s' ) . ' UTC';
		}

		$title   = $label;
		$summary = $this->normalize_text( $data['message'] ?? '', 280 );
		$facts   = [];
		$this->add_fact( $facts, 'Site', $site_display );

		switch ( $event_type ) {
			case 'security.scan_completed':
				$issues_count    = max( 0, (int) ( $data['issues_count'] ?? 0 ) );
				$counts          = $this->normalize_counts( (array) ( $data['by_type'] ?? [] ) );
				$malware_count   = $counts['malware'] ?? 0;
				$vuln_count      = $counts['vulnerability'] ?? 0;
				$other_count     = max( 0, $issues_count - $malware_count - $vuln_count );
				$severity_label  = '' !== $severity ? ucfirst( $severity ) : 'Unknown';
				$title           = ( 'none' !== $severity && '' !== $severity ) ? $severity_label . ' scan findings detected' : 'Security scan findings detected';
				$summary         = sprintf( 'Scan completed with %d finding(s). Highest severity: %s.', $issues_count, $severity_label );
				$this->add_fact( $facts, 'Severity', $severity_label );
				$this->add_fact( $facts, 'Findings', (string) $issues_count );
				if ( $malware_count > 0 ) {
					$this->add_fact( $facts, 'Malware', (string) $malware_count );
				}
				if ( $vuln_count > 0 ) {
					$this->add_fact( $facts, 'Vulnerabilities', (string) $vuln_count );
				}
				if ( $other_count > 0 ) {
					$this->add_fact( $facts, 'Other Findings', (string) $other_count );
				}
				$this->add_fact( $facts, 'Scan ID', $data['scan_id'] ?? '' );
				break;

			case 'security.vuln_detected':
				$vulns = isset( $data['vulnerabilities'] ) && is_array( $data['vulnerabilities'] ) ? $data['vulnerabilities'] : [];
				$count = isset( $data['count'] ) ? (int) $data['count'] : count( $vulns );
				$severity_label = '' !== $severity ? ucfirst( $severity ) : 'Unknown';
				if ( $count > 1 ) {
					$title   = sprintf( '%d vulnerabilities detected', $count );
					$summary = sprintf( '%d known vulnerabilities detected. Highest severity: %s.', $count, $severity_label );
					$this->add_fact( $facts, 'New Vulnerabilities', (string) $count );
					$this->add_fact( $facts, 'Highest Severity', $severity_label );
					$components = [];
					foreach ( array_slice( $vulns, 0, 5 ) as $vuln ) {
						$components[] = $this->normalize_text( (string) ( $vuln['plugin_name'] ?? 'Unknown' ) );
					}
					if ( ! empty( $components ) ) {
						$this->add_fact( $facts, 'Components', implode( ', ', $components ) . ( $count > 5 ? ', ...' : '' ) );
					}
				} else {
					$first = ! empty( $vulns ) && is_array( $vulns[0] ) ? $vulns[0] : [];
					$component = $this->normalize_text( (string) ( $first['plugin_name'] ?? 'Unknown component' ) );
					$title   = ( 'none' !== $severity && '' !== $severity ) ? $severity_label . ' vulnerability detected' : 'Vulnerability detected';
					$summary = 'Known vulnerability detected in ' . $component . '.';
					$this->add_fact( $facts, 'Component', $component );
					$this->add_fact( $facts, 'Severity', $severity_label );
					$this->add_fact( $facts, 'CVE', (string) ( $first['cve_id'] ?? '' ) );
				}
				break;

			case 'security.scan_failure':
				$title   = 'Security scan could not complete';
				$summary = '' !== $summary ? $summary : 'SecurWP could not finish the latest scan.';
				$this->add_fact( $facts, 'Failure Type', $data['type'] ?? '' );
				$this->add_fact( $facts, 'Scan ID', $data['scan_id'] ?? '' );
				break;

			case 'security.firewall_block':
				$blocked_count = max( 0, (int) ( $data['blocked_count'] ?? 0 ) );
				$digest_period = $this->normalize_text( $data['digest_period'] ?? '', 40 );
				$title   = 'Firewall block digest';
				$summary = '' !== $summary ? $summary : sprintf(
					'SecurWP blocked %d request(s)%s.',
					$blocked_count,
					'' !== $digest_period ? ' during the ' . $digest_period : ''
				);
				$this->add_fact( $facts, 'Digest Period', $digest_period );
				$this->add_fact( $facts, 'Total Blocks', $blocked_count > 0 ? (string) $blocked_count : '' );
				break;

			case 'security.login_lockout':
				$title   = 'Login lockout activated';
				$summary = '' !== $summary ? $summary : 'SecurWP locked out repeated failed login attempts.';
				$this->add_fact( $facts, 'Scope', 'user' === $this->normalize_text( $data['type'] ?? '', 20 ) ? 'Account' : 'IP Address' );
				$this->add_fact( $facts, 'Identifier', $data['identifier'] ?? '' );
				$this->add_fact( $facts, 'Threshold', $data['threshold'] ?? '' );
				$this->add_fact( $facts, 'Window', $data['detection_window'] ?? '' );
				$this->add_fact( $facts, 'Lockout', $data['lockout_duration'] ?? '' );
				break;

			case 'security.2fa_change':
				$title   = 'Two-factor authentication updated';
				$summary = '' !== $summary ? $summary : 'SecurWP updated two-factor authentication settings.';
				$this->add_fact( $facts, 'User', $data['username'] ?? '' );
				$this->add_fact( $facts, 'Action', ucfirst( $this->normalize_text( $data['action'] ?? '', 30 ) ) );
				break;

			case 'security.tools_action':
				$title   = 'Security tool action completed';
				$summary = '' !== $summary ? $summary : 'A SecurWP administrative tool finished successfully.';
				$this->add_fact( $facts, 'Action', $data['details']['Action'] ?? '' );
				break;

			case 'security.admin_login':
				$title   = 'Administrator login detected';
				$summary = '' !== $summary ? $summary : 'An administrator authenticated successfully.';
				$this->add_fact( $facts, 'User', $data['username'] ?? '' );
				$this->add_fact( $facts, 'IP Address', $data['ip_address'] ?? '' );
				$this->add_fact( $facts, 'Browser', $data['browser'] ?? '' );
				break;

			case 'security.plugin_deactivated':
				$title   = 'SecurWP plugin deactivated';
				$summary = '' !== $summary ? $summary : 'SecurWP was deactivated on this site.';
				$this->add_fact( $facts, 'User', $data['username'] ?? '' );
				$this->add_fact( $facts, 'IP Address', $data['details']['IP Address'] ?? '' );
				break;
		}

		$this->add_fact( $facts, 'Time', $timestamp );

		if ( isset( $data['details'] ) && is_array( $data['details'] ) ) {
			foreach ( $data['details'] as $detail_label => $detail_value ) {
				if ( ! is_scalar( $detail_label ) ) {
					continue;
				}

				$label_text = $this->normalize_text( $detail_label, 40 );
				if ( '' === $label_text ) {
					continue;
				}

				$already_present = false;
				foreach ( $facts as $fact ) {
					if ( strtolower( $fact['label'] ) === strtolower( $label_text ) ) {
						$already_present = true;
						break;
					}
				}
				if ( $already_present ) {
					continue;
				}

				$this->add_fact( $facts, $label_text, $detail_value );
			}
		}

		return [
			'label'        => $label,
			'title'        => $this->normalize_text( $title, 120 ),
			'summary'      => $this->normalize_text( $summary, 280 ),
			'facts'        => $facts,
			'action_label' => $this->get_action_label( $event_type ),
			'action_url'   => $this->get_action_url( $event_type ),
			'color'        => $this->get_event_color( $event_type, $data, $severity ),
			'severity'     => $severity,
			'site_name'    => $site_name,
			'site_url'     => $site_url,
			'site_display' => $site_display,
			'timestamp'    => $timestamp,
		];
	}

	/**
	 * Format the webhook payload for the given provider.
	 *
	 * @param string $provider   One of 'slack', 'discord', 'generic'.
	 * @param string $event_type Event identifier.
	 * @param array  $data       Event-specific data.
	 * @return array Payload to send as JSON.
	 */
	private function format_payload( string $provider, string $event_type, array $data ): array {
		$presentation = $this->build_presentation( $event_type, $data );
		$message      = '[SecurWP] ' . $presentation['title'] . ' on ' . $presentation['site_display'];

		if ( $provider === 'slack' ) {
			$blocks = [
				[
					'type' => 'header',
					'text' => [
						'type' => 'plain_text',
						'text' => "\xF0\x9F\x9B\xA1\xEF\xB8\x8F " . $presentation['title'],
					],
				],
				[
					'type' => 'section',
					'text' => [
						'type' => 'mrkdwn',
						'text' => $presentation['summary'],
					],
				],
			];

			$fields = [];
			foreach ( array_slice( $presentation['facts'], 0, 10 ) as $fact ) {
				$fields[] = [
					'type' => 'mrkdwn',
					'text' => '*' . $fact['label'] . "*\n" . $fact['value'],
				];
			}
			if ( ! empty( $fields ) ) {
				$blocks[] = [
					'type'   => 'section',
					'fields' => $fields,
				];
			}

			$blocks[] = [
				'type' => 'actions',
				'elements' => [
					[
						'type' => 'button',
						'text' => [
							'type' => 'plain_text',
							'text' => $presentation['action_label'],
						],
						'url' => $presentation['action_url'],
					],
				],
			];

			$blocks[] = [
				'type' => 'context',
				'elements' => [
					[
						'type' => 'mrkdwn',
						'text' => 'SecurWP | ' . $presentation['label'] . ' | ' . $presentation['site_display'],
					],
				],
			];

			return [
				'text'   => $message,
				'blocks' => $blocks,
			];
		}

		if ( $provider === 'discord' ) {
			$fields = [];
			foreach ( array_slice( $presentation['facts'], 0, 12 ) as $fact ) {
				$fields[] = [
					'name'   => $fact['label'],
					'value'  => $fact['value'],
					'inline' => true,
				];
			}

			return [
				'content' => $message,
				'embeds'  => [
					[
						'title'       => "\xF0\x9F\x9B\xA1\xEF\xB8\x8F " . $presentation['title'],
						'description' => $presentation['summary'],
						'url'         => $presentation['action_url'],
						'color'       => $presentation['color'],
						'fields'      => $fields,
						'footer'      => [ 'text' => 'SecurWP | ' . $presentation['label'] ],
						'timestamp'   => gmdate( 'c' ),
					],
				],
			];
		}

		return [
			'event'        => $event_type,
			'label'        => $presentation['label'],
			'title'        => $presentation['title'],
			'summary'      => $presentation['summary'],
			'site'         => $presentation['site_name'],
			'site_url'     => $presentation['site_url'],
			'severity'     => $presentation['severity'],
			'facts'        => $presentation['facts'],
			'action_label' => $presentation['action_label'],
			'action_url'   => $presentation['action_url'],
			'timestamp'    => time(),
			'data'         => $data,
		];
	}
}
