<?php
/**
 * Generation logger for AI Alt Text.
 *
 * @package AIAltTextGenerator
 * @since 1.4.0
 */

namespace AIAltTextGenerator;

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

/**
 * Persists generation events to a custom table.
 *
 * @since 1.4.0
 */
class Logger {

	private const TABLE = 'wpps_ai_alt_log';

	public const CRON_HOOK       = 'wpps_ai_alt_purge_logs';
	public const PURGE_TRANSIENT = 'wpps_ai_alt_log_purge_lock';

	public static function tableName(): string {
		global $wpdb;
		return $wpdb->prefix . self::TABLE;
	}

	public static function maybeCreateTable(): void {
		$version = (string) get_option( 'wpps_ai_alt_db_version', '' );
		if ( $version === '1' ) {
			return;
		}

		self::createTable();
		update_option( 'wpps_ai_alt_db_version', '1', false );
	}

	public static function createTable(): void {
		global $wpdb;
		$table   = self::tableName();
		$charset = $wpdb->get_charset_collate();

		$sql = "CREATE TABLE {$table} (
			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
			attachment_id bigint(20) unsigned NOT NULL DEFAULT 0,
			filename varchar(255) NOT NULL DEFAULT '',
			old_alt text NULL,
			new_alt text NULL,
			provider varchar(50) NOT NULL DEFAULT 'openai',
			model varchar(100) NOT NULL DEFAULT '',
			user_id bigint(20) unsigned NOT NULL DEFAULT 0,
			status varchar(30) NOT NULL DEFAULT 'success',
			error_message text NULL,
			tokens_used int unsigned NOT NULL DEFAULT 0,
			created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
			PRIMARY KEY (id),
			KEY attachment_id (attachment_id),
			KEY status (status),
			KEY created_at (created_at)
		) {$charset};";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );
	}

	/**
	 * @param array<string, mixed> $data
	 */
	public static function log( array $data ): void {
		global $wpdb;

		$wpdb->insert(
			self::tableName(),
			[
				'attachment_id' => (int) ( $data['attachment_id'] ?? 0 ),
				'filename'      => substr( sanitize_text_field( (string) ( $data['filename'] ?? '' ) ), 0, 255 ),
				'old_alt'       => (string) ( $data['old_alt'] ?? '' ),
				'new_alt'       => (string) ( $data['new_alt'] ?? '' ),
				'provider'      => sanitize_key( (string) ( $data['provider'] ?? 'openai' ) ),
				'model'         => sanitize_text_field( (string) ( $data['model'] ?? '' ) ),
				'user_id'       => (int) ( $data['user_id'] ?? get_current_user_id() ),
				'status'        => sanitize_key( (string) ( $data['status'] ?? 'success' ) ),
				'error_message' => (string) ( $data['error_message'] ?? '' ),
				'tokens_used'   => max( 0, (int) ( $data['tokens_used'] ?? 0 ) ),
				'created_at'    => current_time( 'mysql', true ),
			],
			[ '%d', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%d', '%s' ]
		);

		self::maybePurge();
	}

	public static function getRecent( int $limit = 20 ): array {
		return self::getPage( $limit, 0 );
	}

	/**
	 * @return list<array<string, mixed>>
	 */
	public static function getPage( int $limit = 20, int $offset = 0, string $search = '' ): array {
		global $wpdb;

		$table  = self::tableName();
		$limit  = max( 1, min( 100, $limit ) );
		$offset = max( 0, $offset );
		$search = trim( $search );

		if ( $search !== '' ) {
			$like = '%' . $wpdb->esc_like( $search ) . '%';
			$rows = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT * FROM {$table}
					WHERE CAST(attachment_id AS CHAR) LIKE %s
					OR filename LIKE %s
					OR new_alt LIKE %s
					OR old_alt LIKE %s
					OR status LIKE %s
					ORDER BY id DESC
					LIMIT %d OFFSET %d",
					$like,
					$like,
					$like,
					$like,
					$like,
					$limit,
					$offset
				),
				ARRAY_A
			);
		} else {
			$rows = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT * FROM {$table} ORDER BY id DESC LIMIT %d OFFSET %d",
					$limit,
					$offset
				),
				ARRAY_A
			);
		}

		return is_array( $rows ) ? $rows : [];
	}

	public static function countEntries( string $search = '' ): int {
		global $wpdb;

		$table  = self::tableName();
		$search = trim( $search );

		if ( $search === '' ) {
			return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
		}

		$like = '%' . $wpdb->esc_like( $search ) . '%';

		return (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$table}
				WHERE CAST(attachment_id AS CHAR) LIKE %s
				OR filename LIKE %s
				OR new_alt LIKE %s
				OR old_alt LIKE %s
				OR status LIKE %s",
				$like,
				$like,
				$like,
				$like,
				$like
			)
		);
	}

	public static function countByStatus( string $status ): int {
		global $wpdb;

		$table = self::tableName();
		// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- table is prefix + plugin constant.
		return (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM `{$table}` WHERE status = %s",
				sanitize_key( $status )
			)
		);
	}

	/**
	 * @param list<int> $ids
	 * @return list<array<string, mixed>>
	 */
	public static function getByIds( array $ids ): array {
		global $wpdb;

		$ids = array_values( array_filter( array_map( 'intval', $ids ) ) );
		if ( $ids === [] ) {
			return [];
		}

		$table        = self::tableName();
		$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
		// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- table is prefix + plugin constant; IN list is %d placeholders.
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM `{$table}` WHERE id IN ($placeholders)",
				...$ids
			),
			ARRAY_A
		);

		return is_array( $rows ) ? $rows : [];
	}

	/**
	 * @param list<int> $ids
	 */
	public static function deleteByIds( array $ids ): int {
		global $wpdb;

		$ids = array_values( array_filter( array_map( 'intval', $ids ) ) );
		if ( $ids === [] ) {
			return 0;
		}

		$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );

		$table = self::tableName();
		// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- table is prefix + plugin constant; IN list is %d placeholders.
		return (int) $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM `{$table}` WHERE id IN ($placeholders)",
				...$ids
			)
		);
	}

	public static function retentionDays(): int {
		return Settings::clampLogRetentionDays( (int) ( Settings::get()['log_retention_days'] ?? Settings::LOG_RETENTION_DEFAULT ) );
	}

	public static function purgeExpired( ?int $days = null ): int {
		$days = $days ?? self::retentionDays();
		if ( $days <= 0 ) {
			return 0;
		}

		global $wpdb;

		$cutoff = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) );

		$table = self::tableName();
		// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- table is prefix + plugin constant.
		return (int) $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM `{$table}` WHERE created_at < %s",
				$cutoff
			)
		);
	}

	public static function maybePurge(): void {
		if ( self::retentionDays() <= 0 ) {
			return;
		}

		$last = (int) get_transient( self::PURGE_TRANSIENT );
		if ( $last > 0 && ( time() - $last ) < 12 * HOUR_IN_SECONDS ) {
			return;
		}

		set_transient( self::PURGE_TRANSIENT, time(), 12 * HOUR_IN_SECONDS );
		self::purgeExpired();
	}

	public static function maybeSchedulePurge(): void {
		if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
			wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK );
		}
	}

	public static function unschedulePurge(): void {
		wp_clear_scheduled_hook( self::CRON_HOOK );
	}
}
