<?php
/**
 * Deliverability health auditor.
 *
 * @package NimblyMail\SMTP
 */

namespace NimblyMail\SMTP\Audit;

use NimblyMail\SMTP\Mailers\AbstractMailer;
use NimblyMail\SMTP\Mailers\DefaultMailer;
use NimblyMail\SMTP\Options;
use NimblyMail\SMTP\Plugin;
use NimblyMail\SMTP\Queue\Manager;

defined( 'ABSPATH' ) || exit;

/**
 * Class HealthAuditor
 *
 * Orchestrates the four deliverability pillars (SPF, DKIM, DMARC, PTR),
 * persists the latest snapshot to a single option, throttles manual recheck
 * requests via a transient, and exposes hooks for the alert dispatcher and
 * the REST controller.
 *
 * @since 1.0.0
 * @since 1.0.4 The Connectivity pillar's SMTP probe now validates the
 *              resolved IP is publicly routable before connecting, closing
 *              an internal-network-scan oracle reachable via the
 *              admin-supplied SMTP host field.
 */
final class HealthAuditor {

    /**
     * Option key for the latest audit snapshot.
     *
     * @since 1.0.0
     *
     * @var string
     */
    public const OPTION_SNAPSHOT = 'nimblymail_health_snapshot';

    /**
     * Transient key for the manual-recheck throttle.
     *
     * @since 1.0.0
     *
     * @var string
     */
    public const TRANSIENT_MANUAL_LOCK = 'nimblymail_health_manual_lock';

    /**
     * Option key tracking which pillars have already produced an alert.
     *
     * @since 1.0.0
     *
     * @var string
     */
    public const OPTION_LAST_ALERT = 'nimblymail_health_last_alert';

    /**
     * Manual recheck throttle window, in seconds.
     *
     * @since 1.0.0
     *
     * @var int
     */
    public const MANUAL_THROTTLE_SECONDS = HOUR_IN_SECONDS;

    /**
     * Pillar id for SPF.
     *
     * @since 1.0.0
     *
     * @var string
     */
    public const PILLAR_SPF = 'spf';

    /**
     * Pillar id for DKIM.
     *
     * @since 1.0.0
     *
     * @var string
     */
    public const PILLAR_DKIM = 'dkim';

    /**
     * Pillar id for DMARC.
     *
     * @since 1.0.0
     *
     * @var string
     */
    public const PILLAR_DMARC = 'dmarc';

    /**
     * Pillar id for PTR.
     *
     * @since 1.0.0
     *
     * @var string
     */
    public const PILLAR_PTR = 'ptr';
    /**
     * Pillar id for Connectivity.
     *
     * @since 1.0.0
     *
     * @var string
     */
    public const PILLAR_CONNECTIVITY = 'connectivity';

    /**
     * Pillar id for Queue.
     *
     * @since 1.0.0
     *
     * @var string
     */
    public const PILLAR_QUEUE = 'queue';

    /**
     * Public-mailbox domains we never audit (filterable).
     *
     * Sending from these is a misconfiguration regardless of DNS state, so
     * we surface a top-level warning and skip the SPF/DKIM/DMARC pillars.
     *
     * @since 1.0.0
     *
     * @var string[]
     */
    private const PUBLIC_MAILBOX_DOMAINS = array(
        'gmail.com',
        'googlemail.com',
        'outlook.com',
        'hotmail.com',
        'live.com',
        'msn.com',
        'yahoo.com',
        'ymail.com',
        'icloud.com',
        'me.com',
        'mac.com',
        'aol.com',
        'proton.me',
        'protonmail.com',
        'gmx.com',
        'gmx.net',
    );

    /**
     * DNS resolver.
     *
     * @since 1.0.0
     *
     * @var DnsResolver
     */
    private DnsResolver $resolver;

    /**
     * Constructor.
     *
     * @since 1.0.0
     *
     * @param DnsResolver|null $resolver Optional resolver injection (for tests).
     */
    public function __construct( ?DnsResolver $resolver = null ) {
        $this->resolver = $resolver ?? new DnsResolver();
    }

    /**
     * Run an audit, optionally bypassing the cached snapshot.
     *
     * When `$force` is false, returns the cached snapshot if present. When
     * true (or no cache exists), runs all applicable pillars, persists a new
     * snapshot, and returns it. Manual throttling is applied by the caller —
     * background scheduler runs invoke `audit( true )` directly.
     *
     * @since 1.0.0
     *
     * @param bool $force Bypass the cache and run a fresh audit.
     *
     * @return AuditResult
     */
    public function audit( bool $force = false ): AuditResult {
        if ( ! $force ) {
            $cached = $this->get_cached_result();
            if ( null !== $cached ) {
                return $cached;
            }
        }

        $domain       = $this->discover_domain();
        $mailer       = $this->get_active_mailer();
        $is_public    = '' !== $domain && $this->is_public_mailbox_domain( $domain );
        $requirements = $mailer->get_audit_requirements();
        $now          = time();

        $this->resolver->reset_resolver_tracker();

        $connectivity = $this->check_connectivity( $mailer );
        $queue        = $this->check_queue_health();

        if ( '' === $domain ) {
            $pillars = array(
                self::PILLAR_SPF          => $this->skipped_pillar( __( 'No sender domain configured.', 'nimblymail-smtp' ) ),
                self::PILLAR_DKIM         => $this->skipped_pillar( __( 'No sender domain configured.', 'nimblymail-smtp' ) ),
                self::PILLAR_DMARC        => $this->skipped_pillar( __( 'No sender domain configured.', 'nimblymail-smtp' ) ),
                self::PILLAR_PTR          => $this->skipped_pillar( __( 'PTR check not applicable.', 'nimblymail-smtp' ) ),
                self::PILLAR_CONNECTIVITY => $connectivity,
                self::PILLAR_QUEUE        => $queue,
            );

            $result = new AuditResult( $now, '', $mailer->get_slug(), $pillars, AuditResult::RESOLVER_FAILED, false );
            $this->persist_snapshot( $result );
            return $result;
        }

        if ( $is_public ) {
            $message = __( 'Sending from a public mailbox domain is not recommended.', 'nimblymail-smtp' );
            $pillars = array(
                self::PILLAR_SPF          => $this->skipped_pillar( $message ),
                self::PILLAR_DKIM         => $this->skipped_pillar( $message ),
                self::PILLAR_DMARC        => $this->skipped_pillar( $message ),
                self::PILLAR_PTR          => $this->skipped_pillar( __( 'PTR check not applicable.', 'nimblymail-smtp' ) ),
                self::PILLAR_CONNECTIVITY => $connectivity,
                self::PILLAR_QUEUE        => $queue,
            );

            $result = new AuditResult( $now, $domain, $mailer->get_slug(), $pillars, $this->resolver->get_resolver_used(), true );
            $this->persist_snapshot( $result );
            return $result;
        }

        $pillars = array(
            self::PILLAR_SPF          => $this->check_spf( $domain, $requirements['spf_includes'] ),
            self::PILLAR_DKIM         => $this->check_dkim( $domain, $requirements['dkim_selectors'] ),
            self::PILLAR_DMARC        => $this->check_dmarc( $domain ),
            self::PILLAR_PTR          => $this->check_ptr( $mailer ),
            self::PILLAR_CONNECTIVITY => $connectivity,
            self::PILLAR_QUEUE        => $queue,
        );

        $result = new AuditResult( $now, $domain, $mailer->get_slug(), $pillars, $this->resolver->get_resolver_used(), false );
        $this->persist_snapshot( $result );

        return $result;
    }

    /**
     * Get the cached snapshot from option storage, or null when absent.
     *
     * @since 1.0.0
     *
     * @return AuditResult|null
     */
    public function get_cached_result(): ?AuditResult {
        $raw = get_option( self::OPTION_SNAPSHOT, null );
        if ( ! is_array( $raw ) ) {
            return null;
        }

        return AuditResult::from_array( $raw );
    }

    /**
     * Whether manual rechecks are currently throttled.
     *
     * @since 1.0.0
     *
     * @return bool
     */
    public function is_manual_throttled(): bool {
        return false !== get_transient( self::TRANSIENT_MANUAL_LOCK );
    }

    /**
     * Engage the manual-recheck throttle.
     *
     * Call this immediately after a successful manual audit run so subsequent
     * forced runs from the UI return 429 until the window expires.
     *
     * @since 1.0.0
     *
     * @return void
     */
    public function lock_manual_throttle(): void {
        set_transient( self::TRANSIENT_MANUAL_LOCK, time() + self::MANUAL_THROTTLE_SECONDS, self::MANUAL_THROTTLE_SECONDS );
    }

    /**
     * Get the unix timestamp at which the manual throttle expires.
     *
     * Returns null when no throttle is active. The stored value is the
     * expiry timestamp itself, written by lock_manual_throttle().
     *
     * @since 1.0.0
     *
     * @return int|null
     */
    public function get_next_manual_check_at(): ?int {
        $value = get_transient( self::TRANSIENT_MANUAL_LOCK );
        if ( false === $value ) {
            return null;
        }
        return (int) $value;
    }

    /**
     * Clear the cached snapshot and the manual throttle.
     *
     * @since 1.0.0
     *
     * @return void
     */
    public function clear_cache(): void {
        delete_option( self::OPTION_SNAPSHOT );
        delete_transient( self::TRANSIENT_MANUAL_LOCK );
    }

    /**
     * Determine the domain to audit.
     *
     * Resolution order:
     *   1. Host of mail.from_email when set and parseable.
     *   2. Host of home_url() (per-site on multisite).
     *   3. Empty string when neither yields a host.
     *
     * @since 1.0.0
     *
     * @return string Lower-cased domain name, or empty string.
     */
    public function discover_domain(): string {
        $from_email = (string) Options::init()->get( 'mail', 'from_email' );
        if ( '' !== $from_email && false !== strpos( $from_email, '@' ) ) {
            $parts = explode( '@', $from_email );
            $host  = strtolower( trim( end( $parts ) ) );
            if ( '' !== $host ) {
                return $host;
            }
        }

        $home_host = (string) wp_parse_url( home_url(), PHP_URL_HOST );
        if ( '' !== $home_host ) {
            return strtolower( $home_host );
        }

        return '';
    }

    /**
     * Check whether a domain is on the public-mailbox blocklist.
     *
     * @since 1.0.0
     *
     * @param string $domain Domain to check.
     *
     * @return bool
     */
    public function is_public_mailbox_domain( string $domain ): bool {
        /**
         * Filters the list of public-mailbox domains the auditor refuses to
         * audit. Sites sending from any of these should configure a domain
         * they actually own.
         *
         * @since 1.0.0
         *
         * @param string[] $domains Lower-cased domain list.
         */
        $domains = (array) apply_filters( 'nimblymail_health_public_domains', self::PUBLIC_MAILBOX_DOMAINS );

        return in_array( strtolower( $domain ), array_map( 'strtolower', $domains ), true );
    }

    /**
     * Get the active mailer.
     *
     * @since 1.0.0
     *
     * @return AbstractMailer
     */
    private function get_active_mailer(): AbstractMailer {
        return Plugin::instance()->get_processor()->get_active_mailer();
    }

    /**
     * Persist a snapshot to the option store.
     *
     * @since 1.0.0
     *
     * @param AuditResult $result Snapshot to persist.
     *
     * @return void
     */
    private function persist_snapshot( AuditResult $result ): void {
        update_option( self::OPTION_SNAPSHOT, $result->to_array(), false );
    }

    /**
     * Build a Skipped pillar with a given message.
     *
     * @since 1.0.0
     *
     * @param string $message Reason for skipping.
     *
     * @return PillarResult
     */
    private function skipped_pillar( string $message ): PillarResult {
        return new PillarResult( PillarResult::STATUS_SKIPPED, $message );
    }

    /**
     * Run the SPF pillar.
     *
     * Looks up TXT records on the apex domain, finds those starting with
     * `v=spf1`, and scores per the table:
     *
     *   - Multiple SPF records → Warn (RFC 7208 §3.2 forbids this).
     *   - Required include missing → Fail with the expected include surfaced.
     *   - Required include found, single record → Pass.
     *   - No requirement set, single record → Pass.
     *   - No record at all → Fail.
     *
     * @since 1.0.0
     *
     * @param string   $domain           Apex domain to query.
     * @param string[] $required_includes Mailer-declared SPF includes.
     *
     * @return PillarResult
     */
    private function check_spf( string $domain, array $required_includes ): PillarResult {
        $records   = $this->resolver->query( $domain, 'TXT' );
        $spf_lines = array();

        foreach ( $records as $record ) {
            $trimmed = trim( $record );
            if ( 0 === stripos( $trimmed, 'v=spf1' ) ) {
                $spf_lines[] = $trimmed;
            }
        }

        if ( empty( $spf_lines ) ) {
            $fix = ! empty( $required_includes )
                ? sprintf( 'v=spf1 include:%s ~all', $required_includes[0] )
                : 'v=spf1 ~all';

            return new PillarResult(
                PillarResult::STATUS_FAIL,
                __( 'No SPF record found for this domain.', 'nimblymail-smtp' ),
                array(),
                $fix
            );
        }

        if ( count( $spf_lines ) > 1 ) {
            return new PillarResult(
                PillarResult::STATUS_WARN,
                __( 'Multiple SPF records found. RFC 7208 requires exactly one — merge them into a single TXT record.', 'nimblymail-smtp' ),
                $spf_lines,
                ! empty( $required_includes )
                    ? sprintf( 'v=spf1 include:%s ~all', $required_includes[0] )
                    : null
            );
        }

        $spf = $spf_lines[0];

        if ( empty( $required_includes ) ) {
            return new PillarResult(
                PillarResult::STATUS_PASS,
                __( 'A valid SPF record is published.', 'nimblymail-smtp' ),
                $spf_lines
            );
        }

        foreach ( $required_includes as $needed ) {
            if ( '' === $needed ) {
                continue;
            }
            if ( false !== stripos( $spf, 'include:' . $needed ) ) {
                return new PillarResult(
                    PillarResult::STATUS_PASS,
                    sprintf(
                        /* translators: %s: SPF include domain (e.g. spf.brevo.com). */
                        __( 'SPF includes %s.', 'nimblymail-smtp' ),
                        $needed
                    ),
                    $spf_lines
                );
            }
        }

        return new PillarResult(
            PillarResult::STATUS_FAIL,
            sprintf(
                /* translators: %s: SPF include domain. */
                __( 'SPF record is published but does not include %s.', 'nimblymail-smtp' ),
                $required_includes[0]
            ),
            $spf_lines,
            sprintf( 'include:%s', $required_includes[0] )
        );
    }

    /**
     * Run the DKIM pillar.
     *
     * Queries `{selector}._domainkey.{domain}` for each declared selector and
     * scores:
     *   - All selectors resolve → Pass.
     *   - Some resolve, some missing → Warn.
     *   - None resolve → Fail.
     *   - No selectors declared → Skipped (informational).
     *
     * @since 1.0.0
     *
     * @param string   $domain    Apex domain.
     * @param string[] $selectors Mailer-declared DKIM selectors.
     *
     * @return PillarResult
     */
    private function check_dkim( string $domain, array $selectors ): PillarResult {
        $selectors = array_values( array_filter( array_map( 'strval', $selectors ) ) );

        if ( empty( $selectors ) ) {
            return new PillarResult(
                PillarResult::STATUS_SKIPPED,
                __( 'DKIM selector is not known for this mailer. Configure DKIM at your provider and follow their instructions.', 'nimblymail-smtp' )
            );
        }

        $found_records = array();
        $found_count   = 0;

        foreach ( $selectors as $selector ) {
            $name    = $selector . '._domainkey.' . $domain;
            $records = $this->resolver->query( $name, 'TXT' );

            $matched = false;
            foreach ( $records as $record ) {
                // RFC 6376 §3.6.1 requires every valid DKIM key record to
                // begin with the v=DKIM1 tag; anything else at this name is
                // not a DKIM record and must not be counted.
                if ( 0 === stripos( ltrim( $record ), 'v=dkim1' ) ) {
                    $found_records[] = $name . ' → ' . $record;
                    $matched         = true;
                    break;
                }
            }

            if ( $matched ) {
                ++$found_count;
            }
        }

        $expected_count = count( $selectors );

        if ( 0 === $found_count ) {
            return new PillarResult(
                PillarResult::STATUS_FAIL,
                __( 'DKIM record(s) not found at the expected selector.', 'nimblymail-smtp' ),
                array(),
                sprintf( '%s._domainkey.%s', $selectors[0], $domain )
            );
        }

        if ( $found_count < $expected_count ) {
            return new PillarResult(
                PillarResult::STATUS_WARN,
                __( 'Some DKIM selectors resolve, others are missing.', 'nimblymail-smtp' ),
                $found_records
            );
        }

        return new PillarResult(
            PillarResult::STATUS_PASS,
            __( 'DKIM selectors are published and resolve.', 'nimblymail-smtp' ),
            $found_records
        );
    }

    /**
     * Run the DMARC pillar.
     *
     * Looks up `_dmarc.{domain}` TXT, parses the `p=` policy, and scores:
     *   - p=reject or p=quarantine → Pass.
     *   - p=none → Warn (monitoring-only, no enforcement).
     *   - missing → Fail.
     *
     * @since 1.0.0
     *
     * @param string $domain Apex domain.
     *
     * @return PillarResult
     */
    private function check_dmarc( string $domain ): PillarResult {
        $records = $this->resolver->query( '_dmarc.' . $domain, 'TXT' );
        $dmarc   = '';

        foreach ( $records as $record ) {
            $trimmed = trim( $record );
            if ( 0 === stripos( $trimmed, 'v=dmarc1' ) ) {
                $dmarc = $trimmed;
                break;
            }
        }

        if ( '' === $dmarc ) {
            return new PillarResult(
                PillarResult::STATUS_FAIL,
                __( 'No DMARC record found for this domain.', 'nimblymail-smtp' ),
                array(),
                'v=DMARC1; p=quarantine; rua=mailto:dmarc@' . $domain
            );
        }

        $policy = $this->extract_dmarc_policy( $dmarc );

        if ( 'reject' === $policy || 'quarantine' === $policy ) {
            return new PillarResult(
                PillarResult::STATUS_PASS,
                sprintf(
                    /* translators: %s: DMARC policy (reject or quarantine). */
                    __( 'DMARC policy is %s.', 'nimblymail-smtp' ),
                    $policy
                ),
                array( $dmarc )
            );
        }

        if ( 'none' === $policy ) {
            return new PillarResult(
                PillarResult::STATUS_WARN,
                __( 'DMARC policy is p=none — receivers will not enforce alignment. Consider tightening to p=quarantine once monitoring confirms legitimate mail aligns.', 'nimblymail-smtp' ),
                array( $dmarc ),
                'v=DMARC1; p=quarantine; rua=mailto:dmarc@' . $domain
            );
        }

        return new PillarResult(
            PillarResult::STATUS_FAIL,
            __( 'DMARC record found but policy could not be determined.', 'nimblymail-smtp' ),
            array( $dmarc ),
            'v=DMARC1; p=quarantine; rua=mailto:dmarc@' . $domain
        );
    }

    /**
     * Parse the DMARC `p=` tag value.
     *
     * @since 1.0.0
     *
     * @param string $record Raw DMARC TXT record.
     *
     * @return string Policy token (`reject`, `quarantine`, `none`, or empty).
     */
    private function extract_dmarc_policy( string $record ): string {
        if ( preg_match( '/\bp\s*=\s*(reject|quarantine|none)\b/i', $record, $m ) ) {
            return strtolower( $m[1] );
        }
        return '';
    }

    /**
     * Run the PTR/Reverse DNS pillar.
     *
     * Only meaningful when the active mailer is the PHP `mail()` transport,
     * because for SMTP/API mailers the sending IP is the provider's, not the
     * webserver's. Anything else is reported as Skipped.
     *
     * @since 1.0.0
     *
     * @param AbstractMailer $mailer The active mailer.
     *
     * @return PillarResult
     */
    private function check_ptr( AbstractMailer $mailer ): PillarResult {
        if ( ! $mailer instanceof DefaultMailer ) {
            return new PillarResult(
                PillarResult::STATUS_SKIPPED,
                __( 'PTR check applies only to the PHP mail() transport.', 'nimblymail-smtp' )
            );
        }

        $ip = $this->detect_outbound_ip();
        if ( '' === $ip ) {
            return new PillarResult(
                PillarResult::STATUS_SKIPPED,
                __( 'Outbound server IP could not be determined.', 'nimblymail-smtp' )
            );
        }

        $ptr_name  = $this->ip_to_ptr_name( $ip );
        $ptr_hosts = $this->resolver->query( $ptr_name, 'PTR' );

        if ( empty( $ptr_hosts ) ) {
            return new PillarResult(
                PillarResult::STATUS_FAIL,
                sprintf(
                    /* translators: %s: server outbound IP. */
                    __( 'No reverse DNS (PTR) record for %s.', 'nimblymail-smtp' ),
                    $ip
                ),
                array( $ip )
            );
        }

        $hostname     = rtrim( (string) $ptr_hosts[0], '.' );
        $forward_ips  = $this->resolver->query( $hostname, 'A' );
        $forward_hits = array_values( array_filter( $forward_ips, static fn( $candidate ) => $candidate === $ip ) );

        if ( empty( $forward_hits ) ) {
            return new PillarResult(
                PillarResult::STATUS_WARN,
                sprintf(
                    /* translators: 1: server IP, 2: PTR hostname. */
                    __( 'PTR resolves %1$s to %2$s but the forward A record does not match.', 'nimblymail-smtp' ),
                    $ip,
                    $hostname
                ),
                array( $ip, $hostname )
            );
        }

        return new PillarResult(
            PillarResult::STATUS_PASS,
            sprintf(
                /* translators: 1: server IP, 2: PTR hostname. */
                __( 'PTR for %1$s resolves to %2$s and matches the forward record.', 'nimblymail-smtp' ),
                $ip,
                $hostname
            ),
            array( $ip, $hostname )
        );
    }

    /**
     * Detect the webserver outbound IP for the PTR check.
     *
     * Uses `SERVER_ADDR` when available and routable; falls back to empty
     * string so the PTR pillar reports Skipped rather than fabricating
     * results from a private/loopback address.
     *
     * @since 1.0.0
     *
     * @return string IPv4 address, or empty string.
     */
    private function detect_outbound_ip(): string {
        $candidate = isset( $_SERVER['SERVER_ADDR'] )
            ? sanitize_text_field( wp_unslash( (string) $_SERVER['SERVER_ADDR'] ) )
            : '';

        if ( '' === $candidate ) {
            return '';
        }

        $is_routable = (bool) filter_var(
            $candidate,
            FILTER_VALIDATE_IP,
            FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
        );

        if ( ! $is_routable ) {
            return '';
        }

        return $candidate;
    }

    /**
     * Convert an IPv4 address to its in-addr.arpa PTR query name.
     *
     * @since 1.0.0
     *
     * @param string $ip IPv4 address.
     *
     * @return string PTR query name.
     */
    private function ip_to_ptr_name( string $ip ): string {
        $octets = array_reverse( explode( '.', $ip ) );
        return implode( '.', $octets ) . '.in-addr.arpa';
    }

    /**
     * Check outbound connectivity to mailer endpoints.
     *
     * Iterates through the endpoints provided by the active mailer and performs
     * lightweight probes (sockets for SMTP, HEAD requests for APIs) to ensure
     * the server can reach the provider.
     *
     * @since 1.0.0
     * @since 1.0.4 SMTP probes now resolve the host once, validate the
     *              resolved IP is publicly routable, and connect straight to
     *              that validated IP instead of letting fsockopen() resolve
     *              the hostname a second time — an admin-supplied SMTP host
     *              is untrusted input, and without this guard the probe was
     *              an authenticated oracle any manage_options user (any
     *              sub-site admin on multisite) could use to fingerprint the
     *              server's internal network via PASS/FAIL and timing. The
     *              raw fsockopen() error string is no longer surfaced in the
     *              persisted/REST-exposed failure message for the same
     *              reason. The API branch now sets reject_unsafe_urls so WP
     *              core applies the same private/reserved-range protection
     *              DnsResolver::query_doh() already relies on.
     *
     * @param AbstractMailer $mailer The active mailer instance.
     *
     * @return PillarResult The connectivity pillar result.
     */
    private function check_connectivity( AbstractMailer $mailer ): PillarResult {
        $endpoints = $mailer->get_connectivity_endpoints();
        if ( empty( $endpoints ) ) {
            return $this->skipped_pillar( __( 'No connectivity endpoints defined for this mailer.', 'nimblymail-smtp' ) );
        }

        $records = array();
        $failed  = array();

        foreach ( $endpoints as $endpoint ) {
            $type = $endpoint['type'] ?? 'smtp';
            $host = $endpoint['host'] ?? '';
            $url  = $endpoint['url'] ?? '';
            $port = $endpoint['port'] ?? 443;
            $name = $endpoint['name'] ?? ( 'api' === $type ? $url : $host );

            if ( 'smtp' === $type ) {
                $safe_ip = $this->resolve_safe_connect_ip( $host );

                if ( null === $safe_ip ) {
                    $failed[]  = $name;
                    $records[] = sprintf(
                        /* translators: 1: endpoint name, 2: host, 3: port. */
                        __( '[FAIL] %1$s (%2$s:%3$d) - the configured host could not be safely probed.', 'nimblymail-smtp' ),
                        $name,
                        $host,
                        $port
                    );
                    continue;
                }

                $connect_target = false !== strpos( $safe_ip, ':' ) ? '[' . $safe_ip . ']' : $safe_ip;

                // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_fsockopen -- TCP socket probe; WP_Filesystem cannot open sockets.
                $conn = @fsockopen( $connect_target, $port, $errno, $errstr, 5 );
                if ( is_resource( $conn ) ) {
                    $records[] = sprintf( '[PASS] %s (%s:%d)', $name, $host, $port );
                    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing a TCP socket; WP_Filesystem does not apply.
                    fclose( $conn );
                } else {
                    $failed[]  = $name;
                    $records[] = sprintf( '[FAIL] %s (%s:%d)', $name, $host, $port );
                }
            } else {
                // API check (HEAD request).
                if ( empty( $url ) ) {
                    $failed[]  = $name;
                    $records[] = sprintf( '[FAIL] %s - A valid URL was not provided.', $name );
                    continue;
                }

                $response = wp_remote_head(
                    $url,
                    array(
                        'timeout'            => 5,
                        'redirection'        => 0,
                        'reject_unsafe_urls' => true,
                    )
                );

                if ( ! is_wp_error( $response ) ) {
                    $records[] = sprintf( '[PASS] %s (%s)', $name, $url );
                } else {
                    $failed[]  = $name;
                    $records[] = sprintf( '[FAIL] %s (%s) - %s', $name, $url, $response->get_error_message() );
                }
            }
        }

        if ( empty( $failed ) ) {
            return new PillarResult(
                PillarResult::STATUS_PASS,
                __( 'Outbound connectivity is healthy.', 'nimblymail-smtp' ),
                $records
            );
        }

        return new PillarResult(
            PillarResult::STATUS_FAIL,
            sprintf(
                /* translators: %s: Comma-separated list of failed endpoints. */
                __( 'Connection failed to: %s', 'nimblymail-smtp' ),
                implode( ', ', $failed )
            ),
            $records
        );
    }

    /**
     * Resolve an SMTP probe host to a single IP address that is safe to connect to.
     *
     * The SMTP host is admin-supplied free text with no IP-range restriction,
     * so it cannot be trusted the way a hardcoded API endpoint can. Resolution
     * happens exactly once here, and the caller connects fsockopen() directly
     * to the IP this method returns — never to the original hostname — so a
     * second, independent DNS lookup can never resolve to a different address
     * than the one that was validated (a classic DNS-rebinding SSRF bypass).
     *
     * Fails closed: an IP literal or hostname that cannot be confirmed safe
     * (including one that simply fails to resolve, e.g. AAAA-only with no A
     * record) returns null rather than being treated as safe by default.
     *
     * @since 1.0.4
     *
     * @param string $host Admin-supplied SMTP host (IP literal or hostname).
     *
     * @return string|null The validated, publicly routable IP to connect to, or null when unsafe/unresolvable.
     */
    private function resolve_safe_connect_ip( string $host ): ?string {
        $literal_ip = filter_var( $host, FILTER_VALIDATE_IP );
        if ( false !== $literal_ip ) {
            return $this->is_publicly_routable_ip( $literal_ip ) ? $literal_ip : null;
        }

        $resolved_ips = gethostbynamel( $host );
        if ( empty( $resolved_ips ) ) {
            return null;
        }

        foreach ( $resolved_ips as $resolved_ip ) {
            if ( ! $this->is_publicly_routable_ip( $resolved_ip ) ) {
                return null;
            }
        }

        return $resolved_ips[0];
    }

    /**
     * Whether an IP address is publicly routable (not private/loopback/link-local/reserved).
     *
     * Mirrors the flag combination detect_outbound_ip() already uses for the
     * PTR pillar, minus the IPv4-only restriction: an attacker can supply an
     * IPv6 literal directly as the SMTP host, so IPv6 private/reserved ranges
     * (e.g. ::1, fc00::/7, fe80::/10) must be rejected too, not just IPv4
     * RFC1918/link-local addresses like 169.254.169.254.
     *
     * Dropping the IPv4-only restriction is what makes the embedded-IPv4
     * check below mandatory rather than defensive: PHP's range flags only
     * consult their IPv6 table and never unwrap an embedded IPv4, so
     * ::ffff:169.254.169.254 is classified publicly routable while
     * fsockopen() still connects to the private IPv4 behind it.
     *
     * @since 1.0.4
     *
     * @param string $ip IP address to classify.
     *
     * @return bool True when the IP is publicly routable.
     */
    private function is_publicly_routable_ip( string $ip ): bool {
        if ( $this->embeds_ipv4_address( $ip ) ) {
            return false;
        }

        return false !== filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE );
    }

    /**
     * Whether an IPv6 address carries an IPv4 address inside it.
     *
     * Such addresses are rejected outright instead of being unwrapped and
     * re-validated, because a legitimate SMTP host is never configured in one
     * of these notations, and because each notation has several spellings of
     * the same address (::ffff:10.0.0.1 and ::ffff:a00:1 are the same 16
     * bytes) — comparing the packed form avoids having to enumerate them.
     *
     * @since 1.0.4
     *
     * @param string $ip IP address to inspect.
     *
     * @return bool True when the address is IPv6 embedding an IPv4 address.
     */
    private function embeds_ipv4_address( string $ip ): bool {
        if ( false === filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
            return false;
        }

        $packed = inet_pton( $ip );
        if ( false === $packed || 16 !== strlen( $packed ) ) {
            return false;
        }

        $embedding_prefixes = array(
            // ::ffff:0:0/96 — IPv4-mapped (RFC 4291 §2.5.5.2).
            "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff",
            // ::/96 — IPv4-compatible, deprecated (RFC 4291 §2.5.5.1). Also
            // covers :: and ::1, which are not routable under any reading.
            "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
            // 64:ff9b::/96 — NAT64 well-known prefix (RFC 6052 §2.1), which a
            // NAT64 gateway translates straight back to the embedded IPv4.
            "\x00\x64\xff\x9b\x00\x00\x00\x00\x00\x00\x00\x00",
        );

        foreach ( $embedding_prefixes as $prefix ) {
            if ( 0 === strncmp( $packed, $prefix, 12 ) ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check background queue health.
     *
     * Queries the Queue Manager for aggregate statistics and flags failures
     * or stale pending items that might indicate a stuck background runner.
     *
     * @since 1.0.0
     *
     * @return PillarResult The queue health pillar result.
     */
    private function check_queue_health(): PillarResult {
        if ( ! Options::init()->get( 'queue', 'enabled' ) ) {
            return $this->skipped_pillar( __( 'Background queue is disabled.', 'nimblymail-smtp' ) );
        }

        $manager = new Manager();
        $stats   = $manager->get_stats();
        $records = array(
            sprintf( 'Pending: %d', $stats['pending'] ),
            sprintf( 'Failed: %d', $stats['failed'] ),
            sprintf( 'Processing: %d', $stats['processing'] ),
        );

        if ( $stats['failed'] > 0 ) {
            return new PillarResult(
                PillarResult::STATUS_FAIL,
                sprintf(
                    /* translators: %d: Number of failed queue items. */
                    _n( '%d item has failed to deliver.', '%d items have failed to deliver.', $stats['failed'], 'nimblymail-smtp' ),
                    $stats['failed']
                ),
                $records
            );
        }

        // Check for stuck pending items (> 1 hour old).
        if ( $stats['pending'] > 0 && $stats['oldest_pending_at'] ) {
            $oldest_ts = strtotime( $stats['oldest_pending_at'] . ' UTC' );
            if ( $oldest_ts < ( time() - HOUR_IN_SECONDS ) ) {
                return new PillarResult(
                    PillarResult::STATUS_WARN,
                    __( 'Some items have been pending for over an hour. Check your background runner.', 'nimblymail-smtp' ),
                    $records
                );
            }
        }

        return new PillarResult(
            PillarResult::STATUS_PASS,
            __( 'Background queue is processing normally.', 'nimblymail-smtp' ),
            $records
        );
    }
}
