<?php
/**
 * PS_Label_Consent_Log — read/write consent log (GDPR audit) w bazie danych.
 *
 * Polityka: SPEC/11_CONSENT_AND_TERMS.md sekcja 3.1 (lokalny audit u klienta).
 * Single Responsibility: TYLKO składowanie consent log + per-order snapshots.
 * NIE waliduje zgody (to robi PS_Label_Consent_Gate). NIE renderuje UI.
 *
 * SKŁADOWANIE (v4.4.5 — refactor M4, WP.org compliance):
 *   - Główny consent log  -> opcja WP `parcselect_label_consent_log` (autoload = 'no').
 *   - Per-order snapshots -> meta zamówienia WooCommerce `_ps_consent_snapshots`
 *     (tablica numeryczna — jedno zamówienie może mieć wiele snapshotów).
 *
 * Poprzednio (do v4.4.4) dane leżały jako pliki JSON w
 * wp-content/uploads/etykietownik/_consent/ — katalog mógł być serwowany
 * publicznie (nginx ignoruje .htaccess), co ujawniało login/IP/user-agent.
 * Recenzent WP.org zablokował z tego powodu publikację. read_main() wykonuje
 * leniwą, jednorazową migrację starego pliku do opcji DB (patrz maybe_migrate_legacy()).
 *
 * @package ParcelSelectorFree
 */

defined( 'ABSPATH' ) || exit;

final class Parcselect_Label_Consent_Log {

	/**
	 * Nazwa opcji WP przechowującej główny consent log.
	 */
	const OPTION_KEY = 'parcselect_label_consent_log';

	/**
	 * Klucz meta zamówienia WooCommerce dla per-order snapshotów.
	 */
	const ORDER_META_KEY = '_ps_consent_snapshots';

	public static function init() {}

	/**
	 * Zwraca absolutną ścieżkę do (nieużywanego już) folderu consent.
	 *
	 * DEPRECATED v4.4.5: składowanie przeniesione do bazy danych. Metoda
	 * pozostawiona wyłącznie dla zgodności sygnatur — żaden konsument w
	 * obrębie wtyczki jej nie woła. Nie tworzy ani nie zapisuje plików.
	 *
	 * @deprecated 4.4.5 Consent log przeniesiony do opcji WP / meta zamówienia.
	 * @return string Absolutna ścieżka (informacyjnie — folder może nie istnieć).
	 */
	public static function dir() {
		$u = wp_upload_dir();
		return trailingslashit( $u['basedir'] ) . 'etykietownik/_consent';
	}

	/**
	 * Zwraca aktualny consent log (parsed) lub null jeśli nie ma.
	 *
	 * Przy pierwszym wywołaniu, gdy opcja DB jeszcze nie istnieje a na dysku
	 * leży stary plik consent_log.json, dane są jednorazowo migrowane do opcji.
	 *
	 * @return array|null
	 */
	public static function read_main() {
		self::maybe_migrate_legacy();

		$data = get_option( self::OPTION_KEY, null );
		if ( null === $data || false === $data ) {
			return null;
		}
		return is_array( $data ) ? $data : null;
	}

	/**
	 * Zapisuje pierwsze przyjęcie zgody (pierwszy raz).
	 *
	 * @param array $context [wp_user_id, wp_user_login, remote_ip, user_agent, url_at_time]
	 * @return bool
	 */
	public static function write_first_acceptance( array $context ) {
		$installation_id = class_exists( 'Parcselect_Label_Installation' )
			? (string) Parcselect_Label_Installation::current()
			: (string) get_option( 'parcselect_installation_id', '' );

		$now   = gmdate( 'c' );
		$entry = array(
			'schema_version'   => '1.0',
			'installation_id'  => $installation_id,
			'first_acceptance' => array(
				'ts'              => $now,
				'wp_user_id'      => isset( $context['wp_user_id'] ) ? (int) $context['wp_user_id'] : 0,
				'wp_user_login'   => isset( $context['wp_user_login'] ) ? (string) $context['wp_user_login'] : '',
				'remote_ip'       => isset( $context['remote_ip'] ) ? (string) $context['remote_ip'] : '',
				'user_agent'      => isset( $context['user_agent'] ) ? (string) $context['user_agent'] : '',
				'terms_version'   => Parcselect_Label_Terms::version(),
				'terms_md5'       => Parcselect_Label_Terms::canonical_md5(),
				'checkbox_action' => 'manual_click',
				'url_at_time'     => isset( $context['url_at_time'] ) ? (string) $context['url_at_time'] : '',
			),
			'history'          => array(
				array(
					'ts'      => $now,
					'event'   => 'first_acceptance',
					'version' => Parcselect_Label_Terms::version(),
					'md5'     => Parcselect_Label_Terms::canonical_md5(),
				),
			),
		);

		// autoload 'no' — log audytowy czytany rzadko, nie ładujemy go na każdym żądaniu.
		$ok = update_option( self::OPTION_KEY, $entry, 'no' );
		// update_option() zwraca false gdy nowa wartość == starej; tu zapis pierwszego
		// przyjęcia, więc traktujemy istnienie opcji jako potwierdzenie sukcesu.
		if ( false === $ok ) {
			$ok = ( get_option( self::OPTION_KEY, null ) === $entry );
		}
		if ( $ok ) {
			// v4.2 A.B3 — future-proof hook for Oracle sync (deploy Oracle endpoint to enable)
			do_action( 'parcselect_consent_log_remote_sync', 'first_acceptance', $entry );
		}
		return (bool) $ok;
	}

	/**
	 * Aktualizuje consent log — dodaje history entry (np. renew po terms change).
	 *
	 * @param string $event   np. 'renewed_after_terms_change'
	 * @param array  $details
	 * @return bool
	 */
	public static function append_history( $event, array $details = array() ) {
		$current = self::read_main();
		if ( null === $current ) {
			return false;
		}
		if ( ! isset( $current['history'] ) || ! is_array( $current['history'] ) ) {
			$current['history'] = array();
		}
		$current['history'][] = array_merge(
			array(
				'ts'      => gmdate( 'c' ),
				'event'   => (string) $event,
				'version' => Parcselect_Label_Terms::version(),
				'md5'     => Parcselect_Label_Terms::canonical_md5(),
			),
			$details
		);
		$ok = update_option( self::OPTION_KEY, $current, 'no' );
		if ( false === $ok ) {
			$ok = ( get_option( self::OPTION_KEY, null ) === $current );
		}
		return (bool) $ok;
	}

	/**
	 * Zapisuje per-order snapshot zgody (każde użycie konkretnego ordera).
	 *
	 * Snapshot trafia do meta zamówienia WooCommerce pod kluczem
	 * `_ps_consent_snapshots` jako kolejny element tablicy numerycznej.
	 *
	 * @param int    $order_id
	 * @param string $pi_id
	 * @param array  $context
	 * @return bool
	 */
	public static function write_per_order( $order_id, $pi_id, array $context = array() ) {
		$order_id = (int) $order_id;
		if ( $order_id <= 0 || ! function_exists( 'wc_get_order' ) ) {
			return false;
		}
		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return false;
		}

		$entry = array(
			'order_id'           => $order_id,
			'pi_id'              => (string) $pi_id,
			'ts'                 => gmdate( 'c' ),
			'consent_referenced' => 'first_acceptance',
			'terms_version'      => Parcselect_Label_Terms::version(),
			'terms_md5'          => Parcselect_Label_Terms::canonical_md5(),
			'wp_user_id'         => isset( $context['wp_user_id'] ) ? (int) $context['wp_user_id'] : 0,
			'remote_ip'          => isset( $context['remote_ip'] ) ? (string) $context['remote_ip'] : '',
			'user_agent'         => isset( $context['user_agent'] ) ? (string) $context['user_agent'] : '',
		);

		$snapshots = $order->get_meta( self::ORDER_META_KEY, true );
		if ( ! is_array( $snapshots ) ) {
			$snapshots = array();
		}
		$snapshots[] = $entry;

		$order->update_meta_data( self::ORDER_META_KEY, $snapshots );
		$saved = $order->save();

		$ok = ( $saved && $saved > 0 );
		if ( $ok ) {
			// v4.2 A.B3 — future-proof hook for Oracle sync (per_order_use)
			do_action( 'parcselect_consent_log_remote_sync', 'per_order_use', $entry );
		}
		return (bool) $ok;
	}

	/**
	 * Zwraca wszystkie per-order snapshoty zgody dla danego zamówienia.
	 *
	 * Pomocnicza metoda odczytowa (nowa w v4.4.5) — zastępuje listowanie
	 * plików z folderu per-order. Nie zmienia istniejącego kontraktu.
	 *
	 * @param int $order_id
	 * @return array Lista snapshotów (pusta gdy brak).
	 */
	public static function read_per_order( $order_id ) {
		$order_id = (int) $order_id;
		if ( $order_id <= 0 || ! function_exists( 'wc_get_order' ) ) {
			return array();
		}
		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return array();
		}
		$snapshots = $order->get_meta( self::ORDER_META_KEY, true );
		return is_array( $snapshots ) ? $snapshots : array();
	}

	/**
	 * Leniwa, jednorazowa migracja starego pliku consent_log.json do opcji DB.
	 *
	 * Wykonuje się tylko gdy opcja DB jeszcze nie istnieje a stary plik jest
	 * czytelny i zawiera poprawny JSON. Po udanej migracji stary plik (oraz
	 * folder _consent z per-order snapshotami) jest usuwany, by nie zostawiać
	 * danych osobowych w potencjalnie publicznym katalogu uploads.
	 *
	 * Stare per-order snapshoty plikowe NIE są migrowane do meta zamówień —
	 * to dane historyczne (audyt już zarejestrowany w głównym logu jako
	 * first_acceptance + history); ich przeniesienie jest poza zakresem i
	 * ryzykowne (mapowanie pliku -> zamówienie). Pliki są jedynie kasowane.
	 *
	 * @return void
	 */
	private static function maybe_migrate_legacy() {
		// Opcja już istnieje — nie ma czego migrować.
		if ( null !== get_option( self::OPTION_KEY, null ) ) {
			return;
		}

		$u            = wp_upload_dir();
		$legacy_dir   = trailingslashit( $u['basedir'] ) . 'etykietownik/_consent';
		$legacy_main  = $legacy_dir . '/consent_log.json';
		$legacy_porder = $legacy_dir . '/per-order';

		if ( ! is_readable( $legacy_main ) ) {
			return;
		}

		$raw = file_get_contents( $legacy_main ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents -- one-time read of legacy plugin-managed consent log for migration to DB.
		if ( false === $raw ) {
			return;
		}
		$data = json_decode( $raw, true );
		if ( ! is_array( $data ) || empty( $data ) ) {
			return;
		}

		// Przenieś dane do opcji DB (autoload 'no').
		update_option( self::OPTION_KEY, $data, 'no' );

		// X-CHECK M4: kasowanie starych plików DOPIERO po potwierdzeniu, że dane
		// faktycznie są w DB. update_option() może zwrócić false (błąd DB lub
		// wartość identyczna) — nie ufamy wynikowi, czytamy opcję na nowo.
		// Brak potwierdzenia => zostawiamy pliki nietknięte (jedyna kopia audytu RODO).
		if ( ! is_array( get_option( self::OPTION_KEY, null ) ) ) {
			return;
		}

		// Po udanej migracji usuń stare pliki — nie zostawiamy danych osobowych
		// w katalogu uploads, który mógł być serwowany publicznie.
		wp_delete_file( $legacy_main );
		wp_delete_file( $legacy_dir . '/.htaccess' );
		wp_delete_file( $legacy_dir . '/index.php' );

		if ( is_dir( $legacy_porder ) ) {
			foreach ( (array) glob( $legacy_porder . '/*' ) as $legacy_file ) {
				if ( is_file( $legacy_file ) ) {
					wp_delete_file( $legacy_file );
				}
			}
			@rmdir( $legacy_porder ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- best-effort cleanup of emptied legacy consent dir.
		}
		@rmdir( $legacy_dir ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- best-effort cleanup of emptied legacy consent dir.
	}
}
