<?php
/**
 * Email Log Service
 *
 * Handles email logging, database operations, and settings.
 *
 * @package EmailLogs
 * @since 1.0.0
 */

namespace EmailLogs\Includes;

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

class EmailLogService {

	private const OPTION_KEY        = 'wp_powersuite_email_logs_settings';
	private const TABLE_NAME        = 'wpps_email_logs';
	public const CRON_PRUNE         = 'wpps_email_logs_prune';
	private const MAX_LOG_COUNT     = 200;
	private const MIN_LOG_COUNT     = 10;
	private const MAX_MESSAGE_BYTES = 65536;
	private const MAX_TOTAL_BYTES   = 10485760;

	/** @var int[] Pending log IDs for the current request (stack). */
	private static array $pending_log_ids = [];

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

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

		$sql = "CREATE TABLE IF NOT EXISTS $table (
			id bigint(20) NOT NULL AUTO_INCREMENT,
			to_email varchar(500) NOT NULL DEFAULT '',
			subject varchar(500) NOT NULL DEFAULT '',
			message longtext NOT NULL,
			headers text,
			status varchar(20) NOT NULL DEFAULT 'pending',
			error_message text,
			sent_date datetime DEFAULT CURRENT_TIMESTAMP,
			PRIMARY KEY (id),
			KEY sent_date (sent_date),
			KEY status (status),
			KEY to_email (to_email(191)),
			KEY subject (subject(191))
		) $charset;";

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

	public static function get_settings(): array {
		$defaults = [
			'limit'       => 100,
			'log_content' => 0,
		];
		$raw = get_option( self::OPTION_KEY, [] );
		$settings = wp_parse_args( is_array( $raw ) ? $raw : [], $defaults );
		$settings['limit'] = max( self::MIN_LOG_COUNT, min( self::MAX_LOG_COUNT, (int) $settings['limit'] ) );
		return $settings;
	}

	public static function save_settings( array $settings ): void {
		$current     = self::get_settings();
		$limit       = isset( $settings['limit'] ) ? max( self::MIN_LOG_COUNT, min( self::MAX_LOG_COUNT, (int) $settings['limit'] ) ) : $current['limit'];
		$log_content = isset( $settings['log_content'] ) ? (int) $settings['log_content'] : $current['log_content'];
		update_option( self::OPTION_KEY, [ 'limit' => $limit, 'log_content' => $log_content ], false );
	}

	public static function schedule_prune(): void {
		if ( ! wp_next_scheduled( self::CRON_PRUNE ) ) {
			wp_schedule_event( time(), 'daily', self::CRON_PRUNE );
		}
	}

	public static function clear_prune_schedule(): void {
		wp_clear_scheduled_hook( self::CRON_PRUNE );
	}

	/**
	 * Remove log rows beyond the configured count and total byte cap.
	 */
	public static function prune_logs(): void {
		global $wpdb;
		$table = self::get_table_name();
		$limit = max( self::MIN_LOG_COUNT, min( self::MAX_LOG_COUNT, (int) ( self::get_settings()['limit'] ?? 100 ) ) );

		self::prune_count_over_limit( $table, $limit );
		self::prune_by_total_bytes( $table );
	}

	/**
	 * Keep only the newest rows up to the configured count.
	 */
	private static function prune_count_over_limit( string $table, int $limit ): void {
		global $wpdb;

		$keep_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT id FROM $table ORDER BY id DESC LIMIT %d",
				$limit
			)
		);

		if ( empty( $keep_ids ) ) {
			return;
		}

		$ids_placeholder = implode( ',', array_map( 'absint', $keep_ids ) );
		$wpdb->query( "DELETE FROM $table WHERE id NOT IN ($ids_placeholder)" );
	}

	/**
	 * Delete oldest rows until stored content is under the byte cap.
	 */
	private static function prune_by_total_bytes( string $table ): void {
		global $wpdb;

		$total_bytes = (int) $wpdb->get_var(
			"SELECT COALESCE(SUM(LENGTH(message) + LENGTH(COALESCE(headers, '')) + LENGTH(COALESCE(error_message, ''))), 0) FROM $table"
		);

		if ( $total_bytes <= self::MAX_TOTAL_BYTES ) {
			return;
		}

		$batch = 25;
		while ( $total_bytes > self::MAX_TOTAL_BYTES ) {
			$oldest_ids = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT id FROM $table ORDER BY id ASC LIMIT %d",
					$batch
				)
			);

			if ( empty( $oldest_ids ) ) {
				break;
			}

			$ids_placeholder = implode( ',', array_map( 'absint', $oldest_ids ) );
			$wpdb->query( "DELETE FROM $table WHERE id IN ($ids_placeholder)" );

			$total_bytes = (int) $wpdb->get_var(
				"SELECT COALESCE(SUM(LENGTH(message) + LENGTH(COALESCE(headers, '')) + LENGTH(COALESCE(error_message, ''))), 0) FROM $table"
			);
		}
	}

	/**
	 * Log an email (called from wp_mail filter).
	 *
	 * @param array{to?:mixed,subject?:string,message?:string,headers?:string|array,attachments?:array} $args
	 * @return array The original args (unchanged)
	 */
	public static function log_email( array $args ): array {
		if ( apply_filters( 'wp_powersuite_skip_email_log', false, $args ) ) {
			return $args;
		}

		global $wpdb;
		$table = self::get_table_name();

		$to      = self::normalize_recipients( $args['to'] ?? '' );
		$subject = isset( $args['subject'] ) ? self::mask_sensitive_text( substr( sanitize_text_field( $args['subject'] ), 0, 500 ) ) : '';

		$headers = $args['headers'] ?? '';
		if ( is_array( $headers ) ) {
			$headers = implode( "\n", $headers );
		}
		$headers = self::mask_sensitive_text( sanitize_textarea_field( $headers ) );

		$settings = self::get_settings();
		if ( ! empty( $settings['log_content'] ) ) {
			$message = is_string( $args['message'] ?? '' ) ? $args['message'] : '';
			$message = self::mask_sensitive_text( $message );
			if ( strlen( $message ) > self::MAX_MESSAGE_BYTES ) {
				$message = substr( $message, 0, self::MAX_MESSAGE_BYTES ) . "\n\n" . __( '[Content truncated - exceeded size limit]', 'wp-powersuite' );
			}
		} else {
			$message = __( '(Content logging disabled)', 'wp-powersuite' );
		}

		$inserted = $wpdb->insert(
			$table,
			[
				'to_email' => $to,
				'subject'  => $subject,
				'message'  => $message,
				'headers'  => $headers,
				'status'   => 'pending',
			],
			[ '%s', '%s', '%s', '%s', '%s' ]
		);

		if ( $inserted ) {
			self::$pending_log_ids[] = (int) $wpdb->insert_id;
			$limit = max( self::MIN_LOG_COUNT, min( self::MAX_LOG_COUNT, (int) ( self::get_settings()['limit'] ?? 100 ) ) );
			$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $table" );
			if ( $count > $limit ) {
				self::prune_count_over_limit( $table, $limit );
			}
		}

		return $args;
	}

	/**
	 * Mark the current mail attempt as sent.
	 */
	public static function mark_sent(): void {
		$log_id = array_pop( self::$pending_log_ids );
		if ( ! $log_id ) {
			return;
		}

		global $wpdb;
		$table = self::get_table_name();
		$wpdb->update(
			$table,
			[ 'status' => 'sent' ],
			[ 'id' => $log_id ],
			[ '%s' ],
			[ '%d' ]
		);
	}

	/**
	 * Mark the current mail attempt as failed.
	 */
	public static function mark_failed( \WP_Error $error ): void {
		$log_id = array_pop( self::$pending_log_ids );
		if ( ! $log_id ) {
			return;
		}

		global $wpdb;
		$table = self::get_table_name();
		$wpdb->update(
			$table,
			[
				'status'        => 'failed',
				'error_message' => self::mask_sensitive_text( $error->get_error_message() ),
			],
			[ 'id' => $log_id ],
			[ '%s', '%s' ],
			[ '%d' ]
		);
	}

	/**
	 * @param mixed $to wp_mail "to" argument.
	 */
	private static function normalize_recipients( $to ): string {
		if ( is_array( $to ) ) {
			$emails = [];
			foreach ( $to as $entry ) {
				if ( is_array( $entry ) ) {
					$entry = $entry[0] ?? '';
				}
				$entry = sanitize_email( (string) $entry );
				if ( is_email( $entry ) ) {
					$emails[] = $entry;
				}
			}
			$to = implode( ', ', $emails );
		} else {
			$parts  = preg_split( '/[,;]\s*/', (string) $to ) ?: [];
			$emails = array_filter( array_map( 'sanitize_email', array_map( 'trim', $parts ) ), 'is_email' );
			$to     = implode( ', ', $emails );
		}

		return substr( $to, 0, 500 );
	}

	/**
	 * Mask tokens, reset links, OTPs, and similar sensitive values.
	 */
	public static function mask_sensitive_text( string $text ): string {
		if ( $text === '' ) {
			return $text;
		}

		$patterns = [
			'/https?:\/\/[^\s<>"\']+(?:key=|token=|otp=|code=|reset|magic|login)[^\s<>"\']*/i' => '[REDACTED URL]',
			'/(?:password|passwd|pwd|secret|token|otp|api[_-]?key|license)[\s:=]+["\']?[^\s"\']+/i' => '[REDACTED]',
			'/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\?[^\s<>"\']+/i' => '[REDACTED EMAIL LINK]',
		];

		foreach ( $patterns as $pattern => $replacement ) {
			$text = (string) preg_replace( $pattern, $replacement, $text );
		}

		return apply_filters( 'wp_powersuite_email_log_mask_text', $text );
	}

	/**
	 * Get logs with optional search and pagination.
	 *
	 * @param array{search?:string,status?:string,limit?:int,offset?:int} $args
	 * @return array{items:array,total:int}
	 */
	public static function get_logs( array $args = [] ): array {
		global $wpdb;
		$table = self::get_table_name();

		$search = isset( $args['search'] ) ? trim( sanitize_text_field( $args['search'] ) ) : '';
		$status = isset( $args['status'] ) ? sanitize_key( $args['status'] ) : '';
		$limit  = isset( $args['limit'] ) ? max( 1, min( 100, (int) $args['limit'] ) ) : 50;
		$offset = isset( $args['offset'] ) ? max( 0, (int) $args['offset'] ) : 0;

		$where  = [ '1=1' ];
		$values = [];

		if ( $search !== '' ) {
			$like = '%' . $wpdb->esc_like( $search ) . '%';
			if ( ! empty( self::get_settings()['log_content'] ) ) {
				$where[]  = '(subject LIKE %s OR to_email LIKE %s OR message LIKE %s)';
				$values[] = $like;
				$values[] = $like;
				$values[] = $like;
			} else {
				$where[]  = '(subject LIKE %s OR to_email LIKE %s)';
				$values[] = $like;
				$values[] = $like;
			}
		}

		$valid_statuses = [ 'sent', 'failed', 'pending' ];
		if ( in_array( $status, $valid_statuses, true ) ) {
			$where[]  = 'status = %s';
			$values[] = $status;
		}

		$where_sql = implode( ' AND ', $where );

		$total = (int) $wpdb->get_var(
			$values ? $wpdb->prepare( "SELECT COUNT(*) FROM $table WHERE $where_sql", $values ) : "SELECT COUNT(*) FROM $table WHERE $where_sql"
		);

		$values[] = $limit;
		$values[] = $offset;
		$items    = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM $table WHERE $where_sql ORDER BY id DESC LIMIT %d OFFSET %d",
				$values
			),
			OBJECT
		);

		return [ 'items' => $items ?: [], 'total' => $total ];
	}

	/**
	 * Export logs as CSV (formula-injection safe).
	 *
	 * @return string CSV contents.
	 */
	public static function export_csv(): string {
		$result = self::get_logs( [ 'limit' => self::MAX_LOG_COUNT, 'offset' => 0 ] );
		$lines  = [
			implode(
				',',
				array_map(
					static fn( string $h ) => self::csv_cell( $h ),
					[ 'ID', 'To', 'Subject', 'Status', 'Date', 'Error' ]
				)
			),
		];

		foreach ( $result['items'] as $log ) {
			$lines[] = implode(
				',',
				[
					self::csv_cell( (string) $log->id ),
					self::csv_cell( $log->to_email ),
					self::csv_cell( $log->subject ),
					self::csv_cell( $log->status ),
					self::csv_cell( $log->sent_date ),
					self::csv_cell( $log->error_message ?? '' ),
				]
			);
		}

		return implode( "\n", $lines );
	}

	private static function csv_cell( string $value ): string {
		$value = str_replace( [ "\r", "\n" ], ' ', $value );
		if ( preg_match( '/^[=+\-@]/', $value ) ) {
			$value = "'" . $value;
		}
		return '"' . str_replace( '"', '""', $value ) . '"';
	}

	public static function delete_log( int $id ): bool {
		global $wpdb;
		$table = self::get_table_name();
		return (bool) $wpdb->delete( $table, [ 'id' => $id ], [ '%d' ] );
	}

	public static function delete_all_logs(): int {
		global $wpdb;
		$table = self::get_table_name();
		$result = $wpdb->query( "TRUNCATE TABLE $table" );
		if ( $result === false ) {
			return (int) $wpdb->query( "DELETE FROM $table" );
		}
		return (int) $result;
	}

	public static function get_option_key(): string {
		return self::OPTION_KEY;
	}

	/**
	 * Register WordPress privacy exporter/eraser hooks.
	 */
	public static function register_privacy_hooks(): void {
		add_filter( 'wp_privacy_personal_data_exporters', [ self::class, 'register_exporter' ] );
		add_filter( 'wp_privacy_personal_data_erasers', [ self::class, 'register_eraser' ] );
	}

	/**
	 * @param array<string, array<string, mixed>> $exporters
	 * @return array<string, array<string, mixed>>
	 */
	public static function register_exporter( array $exporters ): array {
		$exporters['wp-powersuite-email-logs'] = [
			'exporter_friendly_name' => __( 'WP PowerSuite Email Logs', 'wp-powersuite' ),
			'callback'               => [ self::class, 'export_personal_data' ],
		];
		return $exporters;
	}

	/**
	 * @param array<string, array<string, mixed>> $erasers
	 * @return array<string, array<string, mixed>>
	 */
	public static function register_eraser( array $erasers ): array {
		$erasers['wp-powersuite-email-logs'] = [
			'eraser_friendly_name' => __( 'WP PowerSuite Email Logs', 'wp-powersuite' ),
			'callback'             => [ self::class, 'erase_personal_data' ],
		];
		return $erasers;
	}

	/**
	 * @param string $email_address
	 * @param int    $page
	 * @return array<string, mixed>
	 */
	public static function export_personal_data( string $email_address, int $page ): array {
		global $wpdb;
		$table    = self::get_table_name();
		$per_page = 50;
		$offset   = ( max( 1, $page ) - 1 ) * $per_page;
		$like     = '%' . $wpdb->esc_like( $email_address ) . '%';

		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, to_email, subject, status, sent_date FROM $table WHERE to_email LIKE %s ORDER BY id DESC LIMIT %d OFFSET %d",
				$like,
				$per_page,
				$offset
			)
		);

		$export_items = [];
		foreach ( $rows ?: [] as $row ) {
			$export_items[] = [
				'group_id'    => 'wp-powersuite-email-logs',
				'group_label' => __( 'Email Logs', 'wp-powersuite' ),
				'item_id'     => 'email-log-' . $row->id,
				'data'        => [
					[
						'name'  => __( 'Recipient', 'wp-powersuite' ),
						'value' => $row->to_email,
					],
					[
						'name'  => __( 'Subject', 'wp-powersuite' ),
						'value' => $row->subject,
					],
					[
						'name'  => __( 'Status', 'wp-powersuite' ),
						'value' => $row->status,
					],
					[
						'name'  => __( 'Date', 'wp-powersuite' ),
						'value' => $row->sent_date,
					],
				],
			];
		}

		return [
			'data' => $export_items,
			'done' => count( $export_items ) < $per_page,
		];
	}

	/**
	 * @param string $email_address
	 * @param int    $page
	 * @return array<string, mixed>
	 */
	public static function erase_personal_data( string $email_address, int $page ): array {
		global $wpdb;
		$table    = self::get_table_name();
		$per_page = 50;
		$like     = '%' . $wpdb->esc_like( $email_address ) . '%';

		$ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT id FROM $table WHERE to_email LIKE %s LIMIT %d",
				$like,
				$per_page
			)
		);

		$removed = 0;
		foreach ( $ids ?: [] as $id ) {
			if ( self::delete_log( (int) $id ) ) {
				++$removed;
			}
		}

		return [
			'items_removed'  => $removed > 0,
			'items_retained' => false,
			'messages'       => [],
			'done'           => count( $ids ?: [] ) < $per_page,
		];
	}
}
