<?php
/**
 * Cookie consent journal (audit log).
 *
 * @package BlaminhorEssentials
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Stores user consent choices in a dedicated SQL table to satisfy RGPD Art.7(1)
 * and equivalent obligations (LGPD, Quebec Loi 25, KVKK, POPIA, etc.).
 */
class Blaminhor_Essentials_Cookies_Consent_Log {

	const TABLE_SUFFIX     = 'blaminhor_essentials_consent_log';
	const DB_VERSION       = '1.0.0';
	const DB_VERSION_OPT   = 'blaminhor_essentials_cookies_db_version';
	const RETENTION_MONTHS = 13;

	/**
	 * @var string Full table name with WP prefix.
	 */
	protected $table;

	public function __construct() {
		global $wpdb;
		$this->table = $wpdb->prefix . self::TABLE_SUFFIX;
	}

	/**
	 * Create / upgrade the table if needed.
	 */
	public function maybe_install_schema() {
		if ( get_option( self::DB_VERSION_OPT ) === self::DB_VERSION ) {
			return;
		}

		global $wpdb;
		$charset_collate = $wpdb->get_charset_collate();
		$sql             = "CREATE TABLE {$this->table} (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			created_at DATETIME NOT NULL,
			visitor_hash CHAR(64) NOT NULL,
			ua_hash CHAR(64) NOT NULL,
			choices LONGTEXT NOT NULL,
			policy_version VARCHAR(32) NOT NULL,
			lang VARCHAR(10) NOT NULL,
			geo_country CHAR(2) NULL,
			source VARCHAR(16) NOT NULL,
			tc_string VARCHAR(1024) NULL,
			PRIMARY KEY  (id),
			KEY idx_created (created_at),
			KEY idx_visitor (visitor_hash)
		) $charset_collate;";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );
		update_option( self::DB_VERSION_OPT, self::DB_VERSION, false );
	}

	/**
	 * Drop the table (called from uninstall.php only).
	 */
	public function uninstall_schema() {
		global $wpdb;
		$wpdb->query( "DROP TABLE IF EXISTS {$this->table}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		delete_option( self::DB_VERSION_OPT );
	}

	/**
	 * Insert a consent record.
	 *
	 * @param array $row Sanitized data.
	 * @return int|false Inserted ID or false on failure.
	 */
	public function insert( array $row ) {
		global $wpdb;
		$ok = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$this->table,
			array(
				'created_at'     => $row['created_at'],
				'visitor_hash'   => $row['visitor_hash'],
				'ua_hash'        => $row['ua_hash'],
				'choices'        => wp_json_encode( $row['choices'] ),
				'policy_version' => $row['policy_version'],
				'lang'           => $row['lang'],
				'geo_country'    => $row['geo_country'] ?? null,
				'source'         => $row['source'],
				'tc_string'      => $row['tc_string'] ?? null,
			),
			array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
		);

		return $ok ? (int) $wpdb->insert_id : false;
	}

	/**
	 * Fetch recent entries.
	 *
	 * @param int $limit  How many to fetch.
	 * @param int $offset Pagination offset.
	 * @return array
	 */
	public function get_recent( $limit = 50, $offset = 0 ) {
		global $wpdb;
		$rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$wpdb->prepare(
				"SELECT * FROM {$this->table} ORDER BY id DESC LIMIT %d OFFSET %d",
				absint( $limit ),
				absint( $offset )
			),
			ARRAY_A
		);
		return is_array( $rows ) ? $rows : array();
	}

	/**
	 * Total row count.
	 *
	 * @return int
	 */
	public function count() {
		global $wpdb;
		return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$this->table}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}

	/**
	 * Daily aggregate (last N days).
	 *
	 * @param int $days Number of trailing days.
	 * @return array Date (Y-m-d) => array of counts.
	 */
	public function get_daily_stats( $days = 30 ) {
		global $wpdb;
		$days = max( 1, min( 365, absint( $days ) ) );
		$rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$wpdb->prepare(
				"SELECT DATE(created_at) AS day, source, COUNT(*) AS n FROM {$this->table}
				WHERE created_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
				GROUP BY day, source ORDER BY day ASC",
				$days
			),
			ARRAY_A
		);

		$out = array();
		foreach ( (array) $rows as $r ) {
			$day = $r['day'];
			if ( ! isset( $out[ $day ] ) ) {
				$out[ $day ] = array( 'banner' => 0, 'revoke' => 0, 'gpc' => 0, 'reconsent' => 0 );
			}
			if ( isset( $out[ $day ][ $r['source'] ] ) ) {
				$out[ $day ][ $r['source'] ] = (int) $r['n'];
			}
		}
		return $out;
	}

	/**
	 * Purge entries older than retention window.
	 *
	 * @return int Rows deleted.
	 */
	public function cleanup_expired() {
		global $wpdb;
		$months  = (int) apply_filters( 'blaminhor_cookies_log_retention_months', self::RETENTION_MONTHS );
		$deleted = $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$wpdb->prepare(
				"DELETE FROM {$this->table} WHERE created_at < DATE_SUB(NOW(), INTERVAL %d MONTH)",
				$months
			)
		);
		return (int) $deleted;
	}

	/**
	 * Stream a CSV export of the log.
	 *
	 * @param string|null $from ISO date or null for unlimited.
	 * @param string|null $to   ISO date or null for unlimited.
	 */
	public function stream_csv( $from = null, $to = null ) {
		global $wpdb;

		$where  = array();
		$params = array();
		if ( $from ) {
			$where[]  = 'created_at >= %s';
			$params[] = $from;
		}
		if ( $to ) {
			$where[]  = 'created_at <= %s';
			$params[] = $to;
		}
		$where_sql = $where ? 'WHERE ' . implode( ' AND ', $where ) : '';

		$sql = "SELECT id, created_at, visitor_hash, ua_hash, choices, policy_version, lang, geo_country, source, tc_string
		        FROM {$this->table} $where_sql ORDER BY id ASC";

		if ( $params ) {
			$sql = $wpdb->prepare( $sql, ...$params ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		}

		nocache_headers();
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename="blaminhor-essentials-consent-log-' . gmdate( 'Y-m-d' ) . '.csv"' );

		$out = fopen( 'php://output', 'w' );
		// PHP 8.4 deprecates the implicit $escape parameter — pass it explicitly.
		fputcsv( $out, array( 'id', 'created_at', 'visitor_hash', 'ua_hash', 'choices', 'policy_version', 'lang', 'geo_country', 'source', 'tc_string' ), ',', '"', '\\' );

		$results = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared
		foreach ( (array) $results as $row ) {
			fputcsv( $out, $row, ',', '"', '\\' );
		}
		fclose( $out );
	}

	/**
	 * @return string The full table name.
	 */
	public function get_table() {
		return $this->table;
	}
}
