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

namespace Hafenstudios\Wellenbrecher\Pro\GeoIp;

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

defined( 'ABSPATH' ) || exit;

/**
 * Opt-in-Download der DB-IP-Lite-Datenbanken (W5b, Technik A8). Holt die
 * .mmdb.gz des aktuellen Monats von download.db-ip.com (HTTPS), prueft die
 * Groesse, entpackt und legt die MMDB unter uploads/wellenbrecher/ ab. Ein
 * Monats-Cron (Plugin::boot) ruft refresh() auf.
 *
 * Datenschutz: hier verlaesst KEINE Besucher-IP die Site - es werden nur die
 * frei verfuegbaren Datenbanken geladen. Die eigentliche IP-Bewertung passiert
 * anschliessend rein lokal (GeoDatabase).
 *
 * Robustheit: "Fehlschlag = Bestand bleibt". Geschrieben wird erst nach
 * erfolgreichem Download + Groessen-/Format-Pruefung, und zwar atomar (temp
 * plus rename). Ist der aktuelle Monat noch nicht veroeffentlicht (404), wird
 * der Vormonat versucht. Jeder Fehler ist still (fail-open); eine vorhandene,
 * aeltere Datenbank bleibt unangetastet.
 *
 * Attribution "IP Geolocation by DB-IP" (CC BY 4.0): Admin\ExternaPage + readme.
 */
class GeoDownloader {

	public const CRON_HOOK = 'wellenbrecher_geoip_refresh';

	/**
	 * Eigene Monats-Wiederholung (WP kennt nur bis daily/weekly). Registriert
	 * ueber den cron_schedules-Filter in Plugin::boot.
	 */
	public const CRON_SCHEDULE = 'wellenbrecher_monthly';

	public const BASE_URL = 'https://download.db-ip.com/free/';

	public const COUNTRY_SLUG = 'dbip-country-lite';
	public const ASN_SLUG     = 'dbip-asn-lite';

	public const COUNTRY_FILE = 'dbip-country-lite.mmdb';
	public const ASN_FILE     = 'dbip-asn-lite.mmdb';

	/**
	 * Groessen-Sanity fuer den heruntergeladenen (gepackten) Body.
	 */
	public const MIN_GZ_BYTES = 10240;              // 10 KB - eine Fehlerseite ist kleiner.
	public const MAX_GZ_BYTES = 209715200;          // 200 MB - Deckel gegen Ausreisser.

	/**
	 * Groessen-Sanity fuer die entpackte MMDB.
	 */
	public const MIN_MMDB_BYTES = 51200;            // 50 KB.

	/**
	 * MaxMind/DB-IP-MMDB-Format: die Datei endet mit diesem Metadaten-Marker.
	 */
	public const MMDB_MARKER = "\xab\xcd\xefMaxMind.com";

	protected Settings $settings;

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

	/**
	 * Cron-/Button-Einstieg: laedt beide Datenbanken und merkt sich bei Erfolg
	 * den Zeitpunkt.
	 *
	 * @return array{country: array<string, mixed>, asn: array<string, mixed>}
	 */
	public function refresh(): array {
		$country = $this->download( self::COUNTRY_SLUG, self::COUNTRY_FILE );
		$asn     = $this->download( self::ASN_SLUG, self::ASN_FILE );

		if ( ! empty( $country['ok'] ) || ! empty( $asn['ok'] ) ) {
			$this->settings->set( 'pro.geoip.last_download', time() );
		}

		return array(
			'country' => $country,
			'asn'     => $asn,
		);
	}

	/**
	 * Laedt eine Datenbank (aktueller Monat, sonst Vormonat) und schreibt sie
	 * atomar. Gibt ein Ergebnis-Array {ok, error?, bytes?} zurueck.
	 *
	 * @param string $slug   Datei-Slug ohne Monat (z.B. dbip-country-lite).
	 * @param string $target Zieldateiname im Ablage-Ordner.
	 * @return array<string, mixed>
	 */
	public function download( string $slug, string $target ): array {
		$dir = Uploads::dir( true );
		if ( '' === $dir ) {
			return array( 'ok' => false, 'error' => 'no_upload_dir' );
		}

		$last_error = 'download_failed';
		foreach ( $this->months() as $month ) {
			$url  = self::BASE_URL . $slug . '-' . $month . '.mmdb.gz';
			$body = $this->fetch( $url );
			if ( ! is_string( $body ) ) {
				$last_error = 'download_failed';
				continue;
			}

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

			$mmdb = @gzdecode( $body ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- kaputtes Gzip = still verwerfen.
			if ( ! is_string( $mmdb ) || strlen( $mmdb ) < self::MIN_MMDB_BYTES ) {
				$last_error = 'bad_gzip';
				continue;
			}
			if ( false === strpos( $mmdb, self::MMDB_MARKER ) ) {
				$last_error = 'not_mmdb';
				continue;
			}

			if ( Uploads::write_atomic( $dir . $target, $mmdb ) ) {
				return array( 'ok' => true, 'bytes' => strlen( $mmdb ) );
			}
			$last_error = 'write_failed';
		}

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

	/**
	 * Aktueller Monat und Vormonat als YYYY-MM (der aktuelle kann am
	 * Monatsanfang noch nicht veroeffentlicht sein).
	 *
	 * @return array<int, string>
	 */
	protected function months(): array {
		$now = time();
		return array(
			gmdate( 'Y-m', $now ),
			gmdate( 'Y-m', $now - 31 * DAY_IN_SECONDS ),
		);
	}

	/**
	 * HTTP-GET; liefert den Body als String oder null bei Fehler/Nicht-200.
	 * Gekapselt, damit Tests es ueberschreiben oder per wp_remote_get-Stub
	 * steuern koennen.
	 *
	 * @param string $url URL.
	 * @return string|null
	 */
	protected function fetch( string $url ): ?string {
		$response = wp_remote_get(
			$url,
			array(
				'timeout'  => 60,
				'headers'  => array( 'Accept' => 'application/octet-stream' ),
			)
		);
		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;
	}
}
