<?php

declare(strict_types=1);

namespace SiteFort\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' => 'SiteFort Deactivated',
		'security.login_url_changed' => 'Login URL Changed',
	];

	/**
	 * 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.firewall_block'    => 'firewall-digest.php',
		'security.scan_failure'      => 'scan-failure.php',
		'security.tools_action'      => 'generic.php',
		'security.admin_login'       => 'admin-login.php',
		'security.plugin_deactivated' => 'generic.php',
		'security.login_url_changed' => 'login-url-changed.php',
	];

	/**
	 * Resolve white-label branding for notification rendering.
	 *
	 * @return array<string,string|bool>
	 */
	public static function get_branding_context(): array {
		$branding = class_exists( '\\SiteFort\\Modules\\WhiteLabel\\WhiteLabel' )
			? \SiteFort\Modules\WhiteLabel\WhiteLabel::get_active_branding()
			: [];

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

		$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( 'SiteFort', $brand_name, $label );
	}

	/**
	 * Build the canonical data payload expected by base.php.
	 *
	 * @param array<string,mixed> $data Template-specific data.
	 * @return array<string,mixed>
	 */
	public static function build_template_data( string $label, array $data = [] ): array {
		$brand      = self::get_branding_context();
		$brand_name = (string) $brand['brand_name'];
		$site_name  = get_bloginfo( 'name' ) ?: (string) wp_parse_url( home_url(), PHP_URL_HOST );

		return array_merge( $data, $brand, [
			'site_name' => $site_name,
			'site_url'  => home_url(),
			'label'     => self::brand_label( $label, $brand_name ),
		] );
	}

	/**
	 * Render an inner email template inside the global base wrapper.
	 *
	 * @param array<string,mixed> $data Template data including global base fields.
	 */
	public static function render_template_file( string $template_path, array $data ): string {
		$template_path = self::resolve_template_path( $template_path );
		if ( '' === $template_path ) {
			$template_path = __DIR__ . '/templates/generic.php';
		}

		ob_start();
		include $template_path;
		$inner_content = (string) ob_get_clean();

		return self::render_base_template( $inner_content, $data );
	}

	/**
	 * Wrap rendered inner content in the global base email template.
	 *
	 * @param array<string,mixed> $data Template data including global base fields.
	 */
	public static function render_base_template( string $inner_content, array $data ): string {
		$base_path = __DIR__ . '/templates/base.php';

		ob_start();
		$content = $inner_content;
		include $base_path;
		return (string) ob_get_clean();
	}

	/**
	 * Resolve a PHP template path inside the plugin before including it.
	 */
	private static function resolve_template_path( string $template_path ): string {
		$real_template = realpath( $template_path );
		if ( false === $real_template || ! is_file( $real_template ) || ! is_readable( $real_template ) ) {
			return '';
		}

		if ( substr( $real_template, -4 ) !== '.php' ) {
			return '';
		}

		$plugin_root = defined( 'SITEFORT_PLUGIN_DIR' ) ? realpath( SITEFORT_PLUGIN_DIR ) : realpath( dirname( __DIR__, 2 ) );
		if ( false === $plugin_root ) {
			return '';
		}

		$plugin_root = rtrim( $plugin_root, DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR;
		if ( strpos( $real_template, $plugin_root ) !== 0 ) {
			return '';
		}

		return $real_template;
	}

	/**
	 * Send global-template HTML mail with the plugin's scoped mail filters.
	 *
	 * @param mixed $recipients Recipient email address(es).
	 */
	public static function send_html_mail( $recipients, string $subject, string $html, string $from_name = '' ): bool {
		$recipients = self::normalize_mail_recipients( $recipients );
		if ( empty( $recipients ) ) {
			return false;
		}

		$from_name   = sanitize_text_field( $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' ] );
		if ( '' !== $from_name ) {
			add_filter( 'wp_mail_from_name', $from_filter );
		}

		try {
			return wp_mail( $recipients, sanitize_text_field( $subject ), $html );
		} finally {
			if ( '' !== $from_name ) {
				remove_filter( 'wp_mail_from_name', $from_filter );
			}
			remove_filter( 'wp_mail_content_type', [ self::class, 'html_content_type' ] );
		}
	}

	/**
	 * Normalize mail recipients to validated email addresses.
	 *
	 * @param mixed $recipients Raw recipient value accepted by wp_mail.
	 * @return array<int,string>
	 */
	public static function normalize_mail_recipients( $recipients ): array {
		$normalized = NotificationSettings::normalize_recipient_email_string( $recipients );
		if ( '' === $normalized ) {
			return [];
		}

		return array_values( array_filter( array_map( 'trim', explode( ',', $normalized ) ) ) );
	}

	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,
			// Merged into the email render only; never reaches webhook or
			// cloud push channels. For secrets like unlock links.
			'email_only_data'  => [],
		] );

		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_data = is_array( $options['email_only_data'] ) && ! empty( $options['email_only_data'] )
				? array_merge( $data, $options['email_only_data'] )
				: $data;
			$email_sent = $this->send_email( $event_type, $email_data );
		}

		$push_dispatched = false;
		if ( ! empty( $options['push'] ) ) {
			do_action( 'sitefort_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 = 'sitefort_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;
		}

		$data       = self::build_template_data( self::EVENT_LABELS[ $event_type ] ?? 'Security Notification', $data );
		$brand_name = (string) $data['brand_name'];
		$label      = $this->build_subject_label( $event_type, $data );
		$site_name  = (string) $data['site_name'];
		$subject   = sanitize_text_field( "[{$brand_name}] {$label} - {$site_name}" );

		// Render HTML template - pass $data explicitly (no extract).
		$html = $this->render_template( $event_type, $data );
		$sent = self::send_html_mail( $recipients, $subject, $html, (string) $data['email_from_name'] );

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

		return $sent;
	}

	/**
	 * Build an email subject label without changing the in-template event label.
	 *
	 * @param array<string,mixed> $data
	 */
	private function build_subject_label( string $event_type, array $data ): string {
		$label = isset( $data['label'] ) && is_scalar( $data['label'] )
			? (string) $data['label']
			: 'Security Notification';

		if ( 'security.firewall_block' === $event_type && isset( $data['blocked_count'], $data['digest_period'] ) ) {
			$period = is_scalar( $data['digest_period'] ) ? sanitize_text_field( (string) $data['digest_period'] ) : '';
			if ( '' !== $period ) {
				return sprintf( '%s - %d block(s) in the %s', $label, (int) $data['blocked_count'], $period );
			}
		}

		return $label;
	}

	/**
	 * 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';

		return self::render_template_file( __DIR__ . '/templates/' . $template_file, $data );
	}
}
