<?php
/**
 * @package Hafenstudios\Kurato
 */

namespace Hafenstudios\Kurato\Mail;

defined( 'ABSPATH' ) || exit;

/**
 * Prueft SPF/DKIM/DMARC der Absender-Domain (K4-Konzept Abschnitt 5).
 *
 * Standard: lokale Aufloesung ueber `dns_get_record` (kein externer Call,
 * Markenversprechen). Ist das blockiert, liefert der Service den Status
 * "nicht pruefbar" - ausser der Betreiber hat DNS-over-HTTPS ausdruecklich
 * freigeschaltet, dann wird ueber Cloudflare aufgeloest (Inhaber-Entscheidung).
 *
 * Die Analyse-Methoden sind rein (Records rein, Status raus) und damit ohne
 * Netzwerk testbar. Die Klasse ist nicht final, damit Tests die Fetch-Methoden
 * per Test-Double ersetzen koennen.
 */
class DnsCheckService {

	public const PASS    = 'pass';
	public const WARN    = 'warn';
	public const FAIL    = 'fail';
	public const UNKNOWN = 'unknown';

	private bool $doh_enabled;

	/**
	 * @param bool $doh_enabled DNS-over-HTTPS erlaubt?
	 */
	public function __construct( bool $doh_enabled = false ) {
		$this->doh_enabled = $doh_enabled;
	}

	/**
	 * Fuehrt die vollstaendige Pruefung durch.
	 *
	 * @param string $domain        Absender-Domain.
	 * @param string $transport     Aktiver Versand-Transport.
	 * @param string $dkim_selector Optionaler DKIM-Selektor.
	 * @return array<string, mixed>
	 */
	public function check( string $domain, string $transport, string $dkim_selector = '' ): array {
		$domain = strtolower( trim( $domain ) );

		$spf   = $this->analyze_spf( $this->fetch_txt( $domain ), $transport );
		$dmarc = $this->analyze_dmarc( $this->fetch_txt( '_dmarc.' . $domain ) );
		$dkim  = $this->analyze_dkim( $domain, $transport, $dkim_selector );

		// Kampagnenversand blockieren, wenn SPF ODER DKIM eindeutig fehlt (rot).
		// "Nicht pruefbar" blockiert nicht (sonst waeren Hoster ohne DNS-Zugriff
		// dauerhaft gesperrt) - der Wizard weist dann auf die manuelle Pruefung hin.
		$can_send = self::FAIL !== $spf['status'] && self::FAIL !== $dkim['status'];

		return array(
			'domain'    => $domain,
			'checkable' => self::UNKNOWN !== $spf['status'],
			'spf'       => $spf,
			'dkim'      => $dkim,
			'dmarc'     => $dmarc,
			'can_send'  => $can_send,
		);
	}

	/**
	 * Wertet SPF aus.
	 *
	 * @param array<int, string>|null $txts      TXT-Records (null = nicht pruefbar).
	 * @param string                  $transport Transport (fuer die Include-Heuristik).
	 * @return array{status:string,message:string}
	 */
	public function analyze_spf( ?array $txts, string $transport ): array {
		if ( null === $txts ) {
			return $this->result( self::UNKNOWN, __( 'SPF konnte nicht geprueft werden.', 'kurato' ) );
		}
		$spf = '';
		foreach ( $txts as $txt ) {
			if ( false !== stripos( $txt, 'v=spf1' ) ) {
				$spf = $txt;
				break;
			}
		}
		if ( '' === $spf ) {
			return $this->result( self::FAIL, __( 'Kein SPF-Eintrag gefunden. Ohne SPF landen Ihre Mails im Spam oder werden abgewiesen.', 'kurato' ) );
		}
		$include = $this->spf_include( $transport );
		if ( '' !== $include && false === stripos( $spf, $include ) ) {
			return $this->result( self::WARN, __( 'SPF ist vorhanden, aber der Eintrag Ihres Versanddienstes fehlt darin. Bitte ergaenzen Sie ihn.', 'kurato' ) );
		}
		return $this->result( self::PASS, __( 'SPF-Eintrag vorhanden.', 'kurato' ) );
	}

	/**
	 * Wertet DMARC aus (fehlend = Warnung, kein Block).
	 *
	 * @param array<int, string>|null $txts TXT-Records von _dmarc.domain.
	 * @return array{status:string,message:string}
	 */
	public function analyze_dmarc( ?array $txts ): array {
		if ( null === $txts ) {
			return $this->result( self::UNKNOWN, __( 'DMARC konnte nicht geprueft werden.', 'kurato' ) );
		}
		foreach ( $txts as $txt ) {
			if ( false !== stripos( $txt, 'v=DMARC1' ) ) {
				return $this->result( self::PASS, __( 'DMARC-Eintrag vorhanden.', 'kurato' ) );
			}
		}
		return $this->result( self::WARN, __( 'Kein DMARC-Eintrag. Google und Yahoo verlangen ihn ab 5.000 Mails pro Tag.', 'kurato' ) );
	}

	/**
	 * Wertet DKIM aus. Ohne bekannten Selektor bleibt der Status "nicht pruefbar"
	 * (Selektoren sind kontospezifisch und nicht ratbar).
	 *
	 * @param string $domain    Domain.
	 * @param string $transport Transport.
	 * @param string $selector  Selektor (leer = unbekannt).
	 * @return array{status:string,message:string}
	 */
	public function analyze_dkim( string $domain, string $transport, string $selector ): array {
		$selector = '' !== $selector ? $selector : $this->dkim_selector( $transport );
		if ( '' === $selector ) {
			return $this->result( self::UNKNOWN, __( 'DKIM konnte nicht automatisch geprueft werden. Bitte stellen Sie sicher, dass Ihr Versanddienst DKIM eingerichtet hat.', 'kurato' ) );
		}

		$name  = $selector . '._domainkey.' . $domain;
		$txts  = $this->fetch_txt( $name );
		$cname = $this->fetch_cname( $name );

		if ( null === $txts && null === $cname ) {
			return $this->result( self::UNKNOWN, __( 'DKIM konnte nicht geprueft werden.', 'kurato' ) );
		}
		foreach ( (array) $txts as $txt ) {
			if ( false !== stripos( $txt, 'v=DKIM1' ) || false !== stripos( $txt, 'k=rsa' ) ) {
				return $this->result( self::PASS, __( 'DKIM-Eintrag vorhanden.', 'kurato' ) );
			}
		}
		if ( ! empty( $cname ) ) {
			return $this->result( self::PASS, __( 'DKIM ist per CNAME zu Ihrem Versanddienst eingerichtet.', 'kurato' ) );
		}
		return $this->result( self::FAIL, __( 'Kein DKIM-Eintrag am erwarteten Selektor gefunden.', 'kurato' ) );
	}

	/**
	 * TXT-Records aufloesen. null = nicht pruefbar, [] = geprueft ohne Eintrag.
	 *
	 * @param string $name Hostname.
	 * @return array<int, string>|null
	 */
	protected function fetch_txt( string $name ): ?array {
		if ( function_exists( 'dns_get_record' ) ) {
			$records = @dns_get_record( $name, DNS_TXT ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			if ( false !== $records ) {
				$out = array();
				foreach ( $records as $record ) {
					if ( isset( $record['txt'] ) ) {
						$out[] = (string) $record['txt'];
					}
				}
				return $out;
			}
		}
		return $this->doh_enabled ? $this->fetch_doh( $name, 'TXT' ) : null;
	}

	/**
	 * CNAME aufloesen.
	 *
	 * @param string $name Hostname.
	 * @return array<int, string>|null
	 */
	protected function fetch_cname( string $name ): ?array {
		if ( function_exists( 'dns_get_record' ) ) {
			$records = @dns_get_record( $name, DNS_CNAME ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			if ( false !== $records ) {
				$out = array();
				foreach ( $records as $record ) {
					if ( isset( $record['target'] ) ) {
						$out[] = (string) $record['target'];
					}
				}
				return $out;
			}
		}
		return $this->doh_enabled ? $this->fetch_doh( $name, 'CNAME' ) : null;
	}

	/**
	 * Aufloesung ueber Cloudflare DNS-over-HTTPS (nur nach Freischaltung).
	 *
	 * @param string $name Hostname.
	 * @param string $type Record-Typ.
	 * @return array<int, string>|null
	 */
	protected function fetch_doh( string $name, string $type ): ?array {
		$response = wp_remote_get(
			add_query_arg(
				array( 'name' => rawurlencode( $name ), 'type' => $type ),
				'https://cloudflare-dns.com/dns-query'
			),
			array(
				'timeout' => 8,
				'headers' => array( 'accept' => 'application/dns-json' ),
			)
		);
		if ( is_wp_error( $response ) ) {
			return null;
		}
		$data = json_decode( (string) wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $data ) ) {
			return null;
		}
		$out = array();
		foreach ( (array) ( $data['Answer'] ?? array() ) as $answer ) {
			if ( isset( $answer['data'] ) ) {
				$out[] = trim( (string) $answer['data'], '"' );
			}
		}
		return $out;
	}

	/**
	 * SPF-Include-Fragment je Transport (grobe Heuristik).
	 *
	 * @param string $transport Transport.
	 * @return string
	 */
	private function spf_include( string $transport ): string {
		$map = array(
			'brevo'   => 'brevo',
			'ses'     => 'amazonses.com',
			'mailgun' => 'mailgun.org',
			'smtp2go' => 'smtp2go.com',
		);
		return $map[ $transport ] ?? '';
	}

	/**
	 * Bekannter DKIM-Selektor je Transport (leer = kontospezifisch).
	 *
	 * @param string $transport Transport.
	 * @return string
	 */
	private function dkim_selector( string $transport ): string {
		// Nur SMTP2GO nutzt einen festen, oeffentlich bekannten Selektor.
		return 'smtp2go' === $transport ? 's1' : '';
	}

	/**
	 * Baut ein Statusergebnis.
	 *
	 * @param string $status  Status.
	 * @param string $message Erklaerung.
	 * @return array{status:string,message:string}
	 */
	private function result( string $status, string $message ): array {
		return array( 'status' => $status, 'message' => $message );
	}
}
