<?php

declare(strict_types=1);

namespace SecurWP\Modules\Notifications;

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

/**
 * Notification Service - handles email delivery and event forwarding to Console.
 *
 * Instance-based; receives NotificationSettings via constructor.
 */
class NotificationService {

	private NotificationSettings $settings;

	/**
	 * Severity rank for threshold comparison (higher = more severe).
	 */
	private const SEVERITY_RANK = [
		'low'      => 0,
		'medium'   => 1,
		'high'     => 2,
		'critical' => 3,
	];

	/**
	 * Events that support severity-based filtering.
	 */
	private const SEVERITY_FILTERABLE_EVENTS = [
		'security.scan_completed',
		'security.vuln_detected',
	];

	/**
	 * Human-readable labels for event types.
	 */
	private const EVENT_LABELS = [
		'security.scan_completed'    => 'Scan Results',
		'security.vuln_detected'     => 'Vulnerability Detection',
		'security.scan_failure'      => 'Scan Could Not Complete',
		'security.firewall_block'    => 'Firewall Block Digest',
		'security.login_lockout'     => 'Login Lockout Activated',
		'security.2fa_change'        => 'Two-Factor Authentication Change',
		'security.tools_action'      => 'Administrative Tool Usage',
		'security.admin_login'       => 'Administrator Login',
		'security.plugin_deactivated' => 'SecurWP Deactivated',
	];

	/**
	 * Template file map for event types.
	 */
	private const TEMPLATE_MAP = [
		'security.scan_completed'    => 'scan-complete.php',
		'security.vuln_detected'     => 'vuln-alert.php',
		'security.login_lockout'     => 'login-lockout.php',
		'security.2fa_change'        => 'generic.php',
		'security.scan_failure'      => 'scan-failure.php',
		'security.tools_action'      => 'generic.php',
		'security.admin_login'       => 'admin-login.php',
		'security.plugin_deactivated' => 'generic.php',
	];

	/**
	 * Resolve white-label branding for notification rendering.
	 *
	 * @return array<string,string|bool>
	 */
	private static function get_branding_context(): array {
		$branding = \SecurWP\Includes\SettingsStore::get( \SecurWP\Includes\SettingsStore::COL_BRANDING );
		$branding = is_array( $branding ) ? $branding : [];
		if ( class_exists( '\\SecurWP\\Modules\\WhiteLabel\\WhiteLabel' ) ) {
			$branding = \SecurWP\Modules\WhiteLabel\WhiteLabel::normalize_branding( $branding );
		}

		$plugin_name = sanitize_text_field( (string) ( $branding['plugin_name'] ?? '' ) );
		if ( '' === $plugin_name ) {
			$plugin_name = 'SecurWP';
		}

		$company_name = sanitize_text_field( (string) ( $branding['company_name'] ?? '' ) );
		if ( '' === $company_name ) {
			$company_name = $plugin_name;
		}

		$email_from_name = sanitize_text_field( (string) ( $branding['email_from_name'] ?? '' ) );

		return [
			'brand_name'       => $plugin_name,
			'company_name'     => $company_name,
			'email_from_name'  => '' !== $email_from_name ? $email_from_name : $plugin_name,
			'logo_url'         => esc_url( $branding['logo_url'] ?? '', [ 'https' ] ),
			'support_url'      => esc_url( $branding['support_url'] ?? '', [ 'https' ] ),
			'hide_attribution' => ! empty( $branding['hide_attribution'] ),
		];
	}

	/**
	 * Replace hard-coded product labels in event titles with the active brand.
	 */
	private static function brand_label( string $label, string $brand_name ): string {
		return str_ireplace( 'securwp', $brand_name, $label );
	}

	public function __construct( NotificationSettings $settings ) {
		$this->settings = $settings;
	}

	/**
	 * Send a notification for an event.
	 *
	 * @param string $event_type One of the known event keys.
	 * @param array  $data       Event-specific data for the template.
	 * @param array  $options    Optional delivery overrides:
	 *                           - email (bool): whether to send email
	 *                           - push (bool): whether to push webhook events
	 *                           - respect_cooldown (bool): whether cooldowns apply
	 * @return bool Whether any delivery channel was triggered.
	 */
	public function send( string $event_type, array $data, array $options = [] ): bool {
		$options = wp_parse_args( $options, [
			'email'            => true,
			'push'             => true,
			'respect_cooldown' => true,
		] );

		if ( ! $this->passes_delivery_guards( $event_type, $data, ! empty( $options['respect_cooldown'] ) ) ) {
			return false;
		}

		$email_sent = false;
		if ( ! empty( $options['email'] ) && $this->settings->is_email_enabled() ) {
			$email_sent = $this->send_email( $event_type, $data );
		}

		$push_dispatched = false;
		if ( ! empty( $options['push'] ) ) {
			do_action( 'securwp_push_notification_event', $event_type, $data );
			$push_dispatched = true;
		}

		return $email_sent || $push_dispatched;
	}

	/**
	 * Apply the shared event delivery guards before any channel is triggered.
	 */
	private function passes_delivery_guards( string $event_type, array $data, bool $respect_cooldown = true ): bool {
		if ( ! $this->settings->is_event_enabled( $event_type ) ) {
			return false;
		}

		if ( ! $this->passes_severity_threshold( $event_type, $data ) ) {
			return false;
		}

        if ( $respect_cooldown && ! $this->check_cooldown( $event_type, $data ) ) {
            return false;
        }

		return true;
	}

	/**
	 * Check if the event's severity meets the configured threshold.
	 *
	 * Non-filterable events always pass. If data has no severity key, pass through.
	 * Threshold 'all' means no filtering.
	 */
	private function passes_severity_threshold( string $event_type, array $data ): bool {
		if ( ! in_array( $event_type, self::SEVERITY_FILTERABLE_EVENTS, true ) ) {
			return true;
		}

		$threshold = $this->settings->get_severity_threshold( $event_type );
		if ( 'all' === $threshold ) {
			return true;
		}

		$severity = $data['severity'] ?? null;
		if ( null === $severity || ! isset( self::SEVERITY_RANK[ $severity ] ) ) {
			return true; // No severity data - pass through.
		}

		$threshold_rank = self::SEVERITY_RANK[ $threshold ] ?? 0;
		$event_rank     = self::SEVERITY_RANK[ $severity ];

		return $event_rank >= $threshold_rank;
	}

	/**
	 * Check cooldown for an event. Returns true if NOT in cooldown (ok to send).
	 */
    private function check_cooldown( string $event_type, array $data ): bool {
        $cooldown = $this->settings->get_cooldown( $event_type );
        if ( $cooldown <= 0 ) {
            return true;
        }

        $key = 'securwp_notif_cd_' . md5( $this->build_cooldown_scope( $event_type, $data ) );
        if ( get_transient( $key ) ) {
            return false; // In cooldown.
        }

        set_transient( $key, '1', $cooldown );
        return true;
    }

    /**
     * Build a cooldown scope that matches the event's actual deduplication unit.
     */
    private function build_cooldown_scope( string $event_type, array $data ): string {
        if ( $event_type === 'security.login_lockout' ) {
            $type       = isset( $data['type'] ) && is_scalar( $data['type'] ) ? sanitize_key( (string) $data['type'] ) : 'unknown';
            $identifier = isset( $data['identifier'] ) && is_scalar( $data['identifier'] ) ? sanitize_text_field( (string) $data['identifier'] ) : '';
            if ( $identifier !== '' ) {
                return $event_type . '|' . $type . '|' . $identifier;
            }
        }

		return $event_type;
	}

	/**
	 * Send the actual email.
	 */
	private function send_email( string $event_type, array $data ): bool {
		$recipients = $this->settings->get_recipients();
		if ( empty( $recipients ) ) {
			return false;
		}

		$brand     = self::get_branding_context();
		$brand_name = (string) $brand['brand_name'];
		$label     = self::brand_label( self::EVENT_LABELS[ $event_type ] ?? 'Security Notification', $brand_name );
		$site_name = get_bloginfo( 'name' ) ?: (string) wp_parse_url( home_url(), PHP_URL_HOST );
		$subject   = sanitize_text_field( "[{$brand_name}] {$label} - {$site_name}" );

		// Render HTML template - pass $data explicitly (no extract).
		$html = $this->render_template( $event_type, array_merge( $data, $brand, [
			'site_name' => $site_name,
			'site_url'  => home_url(),
			'label'     => $label,
		] ) );

		$from_name = sanitize_text_field( (string) $brand['email_from_name'] );
		$from_filter = static function ( string $name ) use ( $from_name ): string {
			return '' !== $from_name ? $from_name : $name;
		};

		add_filter( 'wp_mail_content_type', [ self::class, 'html_content_type' ] );
		add_filter( 'wp_mail_from_name', $from_filter );
		try {
			$sent = wp_mail( $recipients, $subject, $html );
		} finally {
			remove_filter( 'wp_mail_from_name', $from_filter );
			remove_filter( 'wp_mail_content_type', [ self::class, 'html_content_type' ] );
		}

		if ( ! $sent && function_exists( 'securwp_log' ) ) {
			securwp_log( sprintf( '[%s Notifications] Email delivery failed for event %s to %s.', $brand_name, $event_type, implode( ', ', $recipients ) ) );
		}

		return $sent;
	}

	/**
	 * HTML content type filter for wp_mail.
	 */
	public static function html_content_type(): string {
		return 'text/html';
	}

	/**
	 * Render an email template with data.
	 *
	 * Templates access data via the $data array (no extract()).
	 */
	private function render_template( string $event_type, array $data ): string {
		$template_file = self::TEMPLATE_MAP[ $event_type ] ?? 'generic.php';
		$template_path = __DIR__ . '/templates/' . $template_file;

		if ( ! file_exists( $template_path ) ) {
			$template_path = __DIR__ . '/templates/generic.php';
		}

		$base_path = __DIR__ . '/templates/base.php';

		// Render inner content - $data is available in template scope.
		ob_start();
		include $template_path;
		$inner_content = ob_get_clean();

		// Render base wrapper - $content and $data available in template scope.
		ob_start();
		$content = $inner_content;
		include $base_path;
		return (string) ob_get_clean();
	}
}
