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

namespace Hafenstudios\Wellenbrecher\Pro\Disposable;

use Hafenstudios\Core\Settings\Settings;
use Hafenstudios\Wellenbrecher\Support\Uploads;

defined( 'ABSPATH' ) || exit;

/**
 * Disposable-Auto-Update (Pro, W5b, Technik A7). Ein Opt-in-Wochen-Cron laedt
 * die strict-Liste von github.com/disposable/disposable (domains_strict.txt,
 * HTTPS), parst sie und legt sie als PHP-Hash-Set unter uploads/wellenbrecher/
 * ab. Data\Lists::disposable_domains() mischt diese Datei mit der gebundelten
 * CC0-Liste. "Fehlschlag = Bestand bleibt": geschrieben wird erst nach
 * erfolgreichem Download + Groessen-/Inhalts-Pruefung, atomar.
 *
 * Datenschutz: hier verlaesst KEINE Besucher-Daten die Site - es wird nur eine
 * oeffentliche Domain-Liste geladen.
 *
 * Sicherheit: die geschriebene Datei wird spaeter per require eingebunden. Jede
 * Domain wird deshalb hart gegen ein Domain-Muster geprueft (nur [a-z0-9.-]);
 * nichts anderes gelangt in die Datei - eine Code-Injektion ueber die Liste ist
 * damit ausgeschlossen.
 */
class DisposableUpdater {

	public const CRON_HOOK = 'wellenbrecher_disposable_refresh';

	public const TARGET = 'disposable-extra.php';

	/**
	 * Quellen (branch-Fallback master -> main).
	 */
	public const SOURCES = array(
		'https://raw.githubusercontent.com/disposable/disposable/master/domains_strict.txt',
		'https://raw.githubusercontent.com/disposable/disposable/main/domains_strict.txt',
	);

	/**
	 * Groessen-Sanity des Downloads (0,5 - 10 MB laut Bauauftrag).
	 */
	public const MIN_BYTES = 524288;      // 0,5 MB.
	public const MAX_BYTES = 10485760;    // 10 MB.

	/**
	 * Mindestzahl geparster Domains (eine echte Liste, keine Fehlerseite).
	 */
	public const MIN_DOMAINS = 100;

	/**
	 * Domain-Whitelist (Kleinbuchstaben, Ziffern, Punkt, Bindestrich; mind. ein Punkt).
	 */
	public const DOMAIN_PATTERN = '/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/';

	protected Settings $settings;

	/**
	 * @param Settings $settings Wellenbrecher-Settings (fuer last_update).
	 */
	public function __construct( Settings $settings ) {
		$this->settings = $settings;
	}

	/**
	 * Cron-/Button-Einstieg: laedt, parst, schreibt. Gibt ein Ergebnis-Array
	 * {ok, error?, count?} zurueck. Fehlschlag laesst den Bestand unberuehrt.
	 *
	 * @return array<string, mixed>
	 */
	public function refresh(): array {
		$dir = Uploads::dir( true );
		if ( '' === $dir ) {
			return array( 'ok' => false, 'error' => 'no_upload_dir' );
		}

		$last_error = 'download_failed';
		foreach ( self::SOURCES as $url ) {
			$body = $this->fetch( $url );
			if ( ! is_string( $body ) ) {
				$last_error = 'download_failed';
				continue;
			}

			$len = strlen( $body );
			if ( $len < self::MIN_BYTES || $len > self::MAX_BYTES ) {
				$last_error = 'bad_size';
				continue;
			}

			$domains = $this->parse( $body );
			if ( count( $domains ) < self::MIN_DOMAINS ) {
				$last_error = 'too_few';
				continue;
			}

			if ( Uploads::write_atomic( $dir . self::TARGET, $this->render_php( $domains ) ) ) {
				$this->settings->set( 'pro.disposable.last_update', time() );
				return array( 'ok' => true, 'count' => count( $domains ) );
			}
			$last_error = 'write_failed';
		}

		return array( 'ok' => false, 'error' => $last_error );
	}

	/**
	 * Parst die Textliste zu einem eindeutigen Domain-Array. Ignoriert
	 * Kommentar-/Leerzeilen und alles, was nicht dem Domain-Muster entspricht
	 * (reine, testbare Logik).
	 *
	 * @param string $body Rohtext (eine Domain pro Zeile).
	 * @return array<int, string>
	 */
	public function parse( string $body ): array {
		$domains = array();
		foreach ( preg_split( '/\r\n|\r|\n/', $body ) as $line ) {
			$domain = strtolower( trim( (string) $line ) );
			if ( '' === $domain || '#' === $domain[0] ) {
				continue;
			}
			if ( strlen( $domain ) > 253 || ! preg_match( self::DOMAIN_PATTERN, $domain ) ) {
				continue;
			}
			$domains[ $domain ] = true;
		}
		return array_keys( $domains );
	}

	/**
	 * Baut den PHP-Quelltext des Hash-Sets. Domains sind bereits gegen das
	 * Muster geprueft (nur [a-z0-9.-]); sie koennen keine Anfuehrungszeichen
	 * oder Zeilenumbrueche enthalten.
	 *
	 * @param array<int, string> $domains Geprueftes Domain-Array.
	 * @return string
	 */
	protected function render_php( array $domains ): string {
		$lines = array(
			'<?php',
			'/**',
			' * Disposable-E-Mail-Domains (strict), automatisch geladen von',
			' * github.com/disposable/disposable (MIT). Generiert ' . gmdate( 'Y-m-d H:i:s' ) . ' UTC.',
			' * Wird mit der gebundelten CC0-Liste gemischt (Data\Lists). Nicht von Hand pflegen.',
			' *',
			' * @package Hafenstudios\Wellenbrecher',
			' */',
			'',
			"defined( 'ABSPATH' ) || exit;",
			'',
			'return array(',
		);
		foreach ( $domains as $domain ) {
			$lines[] = "\t'" . $domain . "' => true,";
		}
		$lines[] = ');';
		return implode( "\n", $lines ) . "\n";
	}

	/**
	 * HTTP-GET; liefert den Body als String oder null bei Fehler/Nicht-200.
	 * Gekapselt fuer die Test-Steuerung.
	 *
	 * @param string $url URL.
	 * @return string|null
	 */
	protected function fetch( string $url ): ?string {
		$response = wp_remote_get(
			$url,
			array(
				'timeout' => 30,
				'headers' => array( 'Accept' => 'text/plain' ),
			)
		);
		if ( is_wp_error( $response ) ) {
			return null;
		}
		if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
			return null;
		}
		$body = wp_remote_retrieve_body( $response );
		return is_string( $body ) && '' !== $body ? $body : null;
	}
}
