<?php
/**
 * NotificationsDispatcher — Core notification delivery engine.
 *
 * Handles all channel-specific delivery logic:
 *   • Email (plain text & HTML, configurable From header)
 *   • Admin Notice (stored in transient, displayed in WP admin)
 *   • Webhook (JSON / Slack Block Kit, optional HMAC-SHA256 signature)
 *   • Slack (Incoming Webhook with Block Kit formatting)
 *   • Digest (batched summary email)
 *
 * All dispatch calls log to notification_log table.
 * Template rendering supports {placeholder} substitution.
 *
 * @package UltraguardPro\modules\notifications
 * @since   6.5.0
 */

namespace UltraguardPro\modules\notifications;

use UltraguardPro\Plugin;
use UltraguardPro\core\MailManager;

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

class NotificationsDispatcher {

    /** @var Plugin */
    private Plugin $plugin;

    public function __construct( Plugin $plugin ) {
        $this->plugin = $plugin;
    }

    // ── Public API ─────────────────────────────────────────────────────────────

    /**
     * Render templates and dispatch a notification via the rule's configured channel.
     *
     * @param  array  $rule     NotificationRulesTable row.
     * @param  array  $context  Event context for template substitution.
     * @param  string $prefix   Optional subject prefix (e.g. '[TEST] ').
     * @return bool             true on success.
     */
    public function dispatch( array $rule, array $context, string $prefix = '' ): bool {
        $channel  = $rule['channel'] ?? 'email';
        $subject  = $prefix . $this->render( $rule['subject_template'] ?? '', $context );
        $message  = $this->render( $rule['message_template'] ?? '', $context );
        $recipient= $rule['recipient'] ?? '';

        $success = match ( $channel ) {
            'email'        => $this->send_email( $recipient, $subject, $message, $rule, $context ),
            'admin_notice' => $this->create_admin_notice( $subject, $message ),
            'webhook'      => $this->send_webhook( $recipient, $subject, $message, $context, $rule ),
            'slack'        => $this->send_slack( $recipient, $subject, $message, $context ),
            default        => false,
        };

        // Auto-retry on failure if configured
        if ( ! $success && $this->plugin->settings->get_option( 'notifications_retry_failed', true ) ) {
            $this->schedule_retry( $rule, $context, $prefix );
        }

        $this->log( $context['event_type'] ?? 'unknown', $channel, $recipient, $subject, $message,
                    $success ? 'sent' : 'failed', $rule['id'] ?? null,
                    $success ? null : 'Dispatch failed', $context );

        return $success;
    }

    /**
     * Send a standalone test notification on a given channel.
     *
     * @param  string $channel   'email' | 'webhook' | 'slack' | 'admin_notice'
     * @param  string $recipient Email, URL, or webhook URL.
     * @return bool
     */
    public function send_test( string $channel, string $recipient ): bool {
        $context = [
            'event_type' => 'test',
            'severity'   => 'info',
            'time'       => current_time( 'mysql', true ),
            'ip'         => \UltraguardPro\core\Utils::get_client_ip(),
            'site_name'  => get_bloginfo( 'name' ),
            'site_url'   => home_url(),
        ];

        /* translators: %s: string */
        $subject = sprintf( __( '[TEST] UltraGuard Pro — Notification Test from %s', 'ultraguard-security-suite' ), get_bloginfo( 'name' ) );
        $message = __( "This is a test notification from UltraGuard Pro.\n\nIf you received this, your notification channel is working correctly.\n\nSite: {site_url}\nTime: {time}", 'ultraguard-security-suite' );
        $message = $this->render( $message, $context );

        return match ( $channel ) {
            'email'        => $this->send_email( $recipient, $subject, $message, [], $context ),
            'admin_notice' => $this->create_admin_notice( $subject, $message ),
            'webhook'      => $this->send_webhook( $recipient, $subject, $message, $context, [] ),
            'slack'        => $this->send_slack( $recipient, $subject, $message, $context ),
            default        => false,
        };
    }

    /**
     * Send a digest batch email summarising queued low-priority notifications.
     *
     * @param  array $queue  Array of [ 'rule' => ..., 'context' => ..., 'queued_at' => ... ]
     */
    public function send_digest( array $queue ): void {
        if ( empty( $queue ) ) { return; }

        $admin_email = $this->plugin->settings->get_option( 'notifications_admin_email', get_option( 'admin_email' ) );
        $site_name   = get_bloginfo( 'name' );
        $count       = count( $queue );

        $subject = sprintf(
            /* translators: %1$s: site name, %2$d: number of events in the digest. */
            __( '[%1$s] Security Alert Digest — %2$d Event(s)', 'ultraguard-security-suite' ),
            $site_name,
            $count
        );

        /* translators: %1$s: string, %2$d: integer */
        $lines = [ sprintf( __( 'UltraGuard Pro digest for %1$s — %2$d events since last digest:', 'ultraguard-security-suite' ), $site_name, $count ), '' ];

        foreach ( $queue as $i => $item ) {
            $ctx  = $item['context'] ?? [];
            $rule = $item['rule']    ?? [];
            $lines[] = sprintf(
                '%d. [%s] %s — %s',
                $i + 1,
                strtoupper( $ctx['severity'] ?? 'info' ),
                ucwords( str_replace( '_', ' ', $ctx['event_type'] ?? 'unknown' ) ),
                $ctx['ip'] ?? ''
            );
        }

        $lines[] = '';
        /* translators: %s: string */
        $lines[] = sprintf( __( 'Manage notifications: %s', 'ultraguard-security-suite' ), admin_url( 'admin.php?page=ultraguard-security-suite-dashboard&ug_page=notifications' ) );

        $this->send_email( $admin_email, $subject, implode( "\n", $lines ), [], [
            'event_type' => 'digest',
            'severity'   => 'info',
            'count'      => $count,
            'time'       => current_time( 'mysql', true ),
        ] );

        $this->log( 'digest', 'email', $admin_email, $subject, implode( "\n", $lines ), 'sent', null, null, [ 'count' => $count ] );
    }

    /**
     * Retry a failed notification once (called by WP Cron scheduled_action).
     *
     * @param array $rule
     * @param array $context
     */
    public function retry( array $rule, array $context ): void {
        $this->dispatch( $rule, $context, '[RETRY] ' );
    }

    // ── Channel implementations ────────────────────────────────────────────────

    /**
     * Send a plain-text or HTML email.
     *
     * @param  string $to
     * @param  string $subject
     * @param  string $body
     * @param  array  $rule     Rule row (may be empty for test sends).
     * @return bool
     */
    private function send_email( string $to, string $subject, string $body, array $rule, array $context ): bool {
        if ( ! is_email( $to ) ) { return false; }
        if ( ! $this->plugin->settings->get_option( 'notifications_email_enabled', true ) ) {
            return false;
        }

        return MailManager::notification( [
            'to'          => $to,
            'subject'     => $subject,
            'title'       => $subject,
            'message'     => $body,
            'tone'        => $this->map_tone( (string) ( $context['severity'] ?? 'info' ) ),
            'details'     => $this->extract_email_details( $context ),
            'cta_url'     => admin_url( 'admin.php?page=ultraguard-security-suite-dashboard&ug_page=notifications-log' ),
            'cta_label'   => __( 'Open Notification Log', 'ultraguard-security-suite' ),
            'footer_note' => __( 'This message was sent by UltraGuard Notifications.', 'ultraguard-security-suite' ),
        ] );
    }

    /**
     * Map event severity to a mail tone.
     *
     * @param string $severity
     * @return string
     */
    private function map_tone( string $severity ): string {
        return match ( $severity ) {
            'critical', 'high' => 'danger',
            'medium'           => 'warning',
            'success'          => 'success',
            default            => 'info',
        };
    }

    /**
     * Extract useful context values for branded email details.
     *
     * @param array $context
     * @return array
     */
    private function extract_email_details( array $context ): array {
        $details = [];
        $map     = [
            __( 'Event', 'ultraguard-security-suite' )      => 'event_type',
            __( 'Severity', 'ultraguard-security-suite' )   => 'severity',
            __( 'Time', 'ultraguard-security-suite' )       => 'time',
            __( 'IP Address', 'ultraguard-security-suite' ) => 'ip',
            __( 'Username', 'ultraguard-security-suite' )   => 'username',
            __( 'Count', 'ultraguard-security-suite' )      => 'count',
            __( 'Site', 'ultraguard-security-suite' )       => 'site_url',
        ];

        foreach ( $map as $label => $key ) {
            if ( isset( $context[ $key ] ) && '' !== (string) $context[ $key ] ) {
                $details[ $label ] = $context[ $key ];
            }
        }

        return $details;
    }

    /**
     * Store an admin notice in a transient for display in WP admin.
     *
     * @param  string $subject
     * @param  string $message
     * @return bool
     */
    private function create_admin_notice( string $subject, string $message ): bool {
        $notices   = get_transient( 'ultraguard_admin_notices' ) ?: [];
        $notices[] = [
            'type'    => 'warning',
            'subject' => $subject,
            'message' => $message,
            'time'    => current_time( 'mysql', true ),
        ];
        // Keep last 25 notices only
        return (bool) set_transient(
            'ultraguard_admin_notices',
            array_slice( $notices, -25 ),
            2 * DAY_IN_SECONDS
        );
    }

    /**
     * POST a JSON (or Slack Block Kit) payload to a webhook URL.
     * Optionally signs the payload with HMAC-SHA256.
     *
     * @param  string $url
     * @param  string $subject
     * @param  string $message
     * @param  array  $context
     * @param  array  $rule     Rule row (used for webhook_secret).
     * @return bool
     */
    private function send_webhook( string $url, string $subject, string $message, array $context, array $rule ): bool {
        if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) { return false; }

        $format = $this->plugin->settings->get_option( 'notifications_webhook_format', 'json' );

        $body = match ( $format ) {
            'slack' => wp_json_encode( $this->build_slack_blocks( $subject, $message, $context ) ),
            default => wp_json_encode( [
                'event'     => $context['event_type'] ?? 'security_event',
                'severity'  => $context['severity']   ?? 'medium',
                'subject'   => $subject,
                'message'   => $message,
                'context'   => $context,
                'site'      => get_bloginfo( 'name' ),
                'site_url'  => home_url(),
                'timestamp' => current_time( 'U' ),
            ] ),
        };

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

        // HMAC-SHA256 signature if webhook_secret is set on the rule
        $secret = $rule['webhook_secret'] ?? $this->plugin->settings->get_option( 'notifications_webhook_secret', '' );
        if ( $secret ) {
            $headers['X-UltraGuard-Signature'] = 'sha256=' . hash_hmac( 'sha256', $body, $secret );
        }

        $response = wp_remote_post( $url, [
            'headers'     => $headers,
            'body'        => $body,
            'timeout'     => 10,
            'blocking'    => false,
            'data_format' => 'body',
        ] );

        return ! is_wp_error( $response );
    }

    /**
     * Send a Slack Incoming Webhook notification with Block Kit formatting.
     *
     * @param  string $webhook_url  Slack incoming webhook URL.
     * @param  string $subject
     * @param  string $message
     * @param  array  $context
     * @return bool
     */
    private function send_slack( string $webhook_url, string $subject, string $message, array $context ): bool {
        if ( ! filter_var( $webhook_url, FILTER_VALIDATE_URL ) ) { return false; }

        $payload = $this->build_slack_blocks( $subject, $message, $context );

        $response = wp_remote_post( $webhook_url, [
            'headers'     => [ 'Content-Type' => 'application/json' ],
            'body'        => wp_json_encode( $payload ),
            'timeout'     => 10,
            'blocking'    => false,
            'data_format' => 'body',
        ] );

        return ! is_wp_error( $response );
    }

    // ── Slack Block Kit builder ────────────────────────────────────────────────

    /**
     * Build a Slack Block Kit message payload.
     *
     * @param  string $subject
     * @param  string $message
     * @param  array  $context
     * @return array
     */
    private function build_slack_blocks( string $subject, string $message, array $context ): array {
        $severity = strtolower( $context['severity'] ?? 'medium' );
        $emoji    = match ( $severity ) {
            'critical' => ':rotating_light:',
            'high'     => ':warning:',
            'medium'   => ':large_yellow_circle:',
            'low'      => ':large_blue_circle:',
            default    => ':information_source:',
        };

        $fields = [];
        if ( ! empty( $context['ip'] ) ) {
            $fields[] = [ 'type' => 'mrkdwn', 'text' => "*IP:*\n`" . $context['ip'] . '`' ];
        }
        if ( ! empty( $context['username'] ) ) {
            $fields[] = [ 'type' => 'mrkdwn', 'text' => "*Username:*\n" . $context['username'] ];
        }
        if ( ! empty( $context['count'] ) ) {
            $fields[] = [ 'type' => 'mrkdwn', 'text' => "*Count:*\n" . $context['count'] ];
        }
        $fields[] = [ 'type' => 'mrkdwn', 'text' => "*Time:*\n" . ( $context['time'] ?? current_time( 'mysql', true ) ) ];
        $fields[] = [ 'type' => 'mrkdwn', 'text' => "*Site:*\n<" . home_url() . '|' . get_bloginfo( 'name' ) . '>' ];

        $blocks = [
            [
                'type' => 'header',
                'text' => [ 'type' => 'plain_text', 'text' => $emoji . ' ' . $subject, 'emoji' => true ],
            ],
            [
                'type'   => 'section',
                'text'   => [ 'type' => 'mrkdwn', 'text' => $message ],
            ],
        ];

        if ( ! empty( $fields ) ) {
            $blocks[] = [ 'type' => 'section', 'fields' => array_slice( $fields, 0, 10 ) ];
        }

        $blocks[] = [
            'type'     => 'actions',
            'elements' => [
                [
                    'type'  => 'button',
                    'text'  => [ 'type' => 'plain_text', 'text' => 'View Dashboard', 'emoji' => true ],
                    'url'   => admin_url( 'admin.php?page=ultraguard-security-suite-dashboard&ug_page=notifications' ),
                    'style' => $severity === 'critical' ? 'danger' : 'primary',
                ],
            ],
        ];

        return [
            'text'   => $subject, // Fallback text for notifications
            'blocks' => $blocks,
        ];
    }

    // ── Template rendering ────────────────────────────────────────────────────

    /**
     * Substitute {placeholders} in a template string with context values.
     *
     * @param  string $template
     * @param  array  $context
     * @return string
     */
    public function render( string $template, array $context ): string {
        $replacements = [];
        foreach ( $context as $key => $value ) {
            if ( is_scalar( $value ) ) {
                $replacements[ '{' . $key . '}' ] = (string) $value;
            }
        }
        // Always available site tokens
        $replacements['{site_name}']    = get_bloginfo( 'name' );
        $replacements['{site_url}']     = home_url();
        $replacements['{admin_url}']    = admin_url( 'admin.php?page=ultraguard-security-suite-dashboard&ug_page=notifications' );
        $replacements['{current_time}'] = current_time( 'mysql', true );

        return strtr( $template, $replacements );
    }

    // ── Retry scheduling ──────────────────────────────────────────────────────

    /**
     * Schedule a one-time retry for a failed notification (10 minutes later).
     *
     * @param array  $rule
     * @param array  $context
     * @param string $prefix
     */
    private function schedule_retry( array $rule, array $context, string $prefix ): void {
        // Store retry data in a short transient to avoid WP cron data bloat
        $key   = 'ultraguard_notif_retry_' . ( $rule['id'] ?? 0 ) . '_' . time();
        $data  = [ 'rule' => $rule, 'context' => $context, 'prefix' => $prefix ];
        set_transient( $key, $data, 30 * MINUTE_IN_SECONDS );
        wp_schedule_single_event( time() + ( 10 * MINUTE_IN_SECONDS ), 'ultraguard_notification_retry', [ $key ] );
    }

    // ── Logging ───────────────────────────────────────────────────────────────

    /**
     * Persist a dispatch result to the notification_log table.
     *
     * @param string      $event_type
     * @param string      $channel
     * @param string      $recipient
     * @param string      $subject
     * @param string      $message
     * @param string      $status      'sent' | 'failed'
     * @param int|null    $rule_id
     * @param string|null $error
     * @param array       $context
     */
    public function log(
        string  $event_type,
        string  $channel,
        string  $recipient,
        string  $subject,
        string  $message,
        string  $status    = 'sent',
        ?int    $rule_id   = null,
        ?string $error     = null,
        array   $context   = []
    ): void {
        $log_t = $this->table( 'notification_log' );
        if ( ! $log_t ) { return; }

        $log_t->record( $event_type, $channel, $recipient, $subject, $message, $status, $rule_id, $error, $context );
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    /**
     * @param string $key
     * @return \UltraguardPro\db\BaseTable|null
     */
    private function table( string $key ) {
        $db = $this->plugin->modules->get_db_tables();
        return $db ? $db->get_table( $key ) : null;
    }
}
