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

namespace EmailLogs\Includes;

use WP_PowerSuite\Email\MailHeaders;

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'] ) ? substr( sanitize_text_field( $args['subject'] ), 0, 500 ) : '';

		$headers = $args['headers'] ?? '';
		if ( is_array( $headers ) ) {
			$headers = implode( "\n", $headers );
		}
		$headers = MailHeaders::sanitizeForStorage( (string) $headers );

		$settings = self::get_settings();
		if ( ! empty( $settings['log_content'] ) ) {
			$message = is_string( $args['message'] ?? '' ) ? $args['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' => substr( sanitize_textarea_field( $error->get_error_message() ), 0, 2000 ),
			],
			[ 'id' => $log_id ],
			[ '%s', '%s' ],
			[ '%d' ]
		);
	}

	/**
	 * Parse wp_mail-style recipients into validated email addresses.
	 *
	 * @param mixed $to String, list, or associative array from wp_mail().
	 * @return list<string>
	 */
	public static function parse_recipient_addresses( $to ): array {
		return MailHeaders::parseAddresses( $to );
	}

	/**
	 * Extract a single email address from a plain or "Name <email>" string.
	 */
	private static function extract_email_address( string $address ): string {
		return MailHeaders::extractAddress( $address );
	}

	/**
	 * @param object $log Log row from the database.
	 * @return list<string>
	 */
	private static function resolve_log_recipients( object $log ): array {
		$recipients = self::parse_recipient_addresses( $log->to_email ?? '' );
		if ( $recipients !== [] ) {
			return $recipients;
		}

		if ( ! empty( $log->headers ) ) {
			return self::parse_recipients_from_headers( (string) $log->headers );
		}

		return [];
	}

	/**
	 * @return list<string>
	 */
	private static function parse_recipients_from_headers( string $headers ): array {
		$emails = [];
		$lines  = preg_split( '/\r\n|\r|\n/', $headers ) ?: [];

		foreach ( $lines as $line ) {
			$line = trim( (string) $line );
			if ( $line === '' || ! str_contains( $line, ':' ) ) {
				continue;
			}

			[ $name, $value ] = array_pad( explode( ':', $line, 2 ), 2, '' );
			if ( strtolower( trim( $name ) ) !== 'to' ) {
				continue;
			}

			$emails = array_merge( $emails, self::parse_recipient_addresses( trim( $value ) ) );
		}

		return array_values( array_unique( $emails ) );
	}

	public static function can_resend_log( object $log ): bool {
		$disabled_message = __( '(Content logging disabled)', 'wp-powersuite' );
		$message          = is_string( $log->message ?? '' ) ? $log->message : '';
		if ( $message === '' || $message === $disabled_message ) {
			return false;
		}

		return self::resolve_log_recipients( $log ) !== [];
	}

	/**
	 * @param mixed $to wp_mail "to" argument.
	 */
	private static function normalize_recipients( $to ): string {
		$emails = self::parse_recipient_addresses( $to );

		return substr( implode( ', ', $emails ), 0, 500 );
	}

	/**
	 * Narrow masking for privacy exports only (not applied when storing admin logs).
	 */
	public static function mask_sensitive_text( string $text ): string {
		if ( $text === '' ) {
			return $text;
		}

		$patterns = [
			'/https?:\/\/[^\s<>"\']+[?&](?:key|token|otp|code|reset_key|login_token)=[^\s<>"\'&]+/i' => '[REDACTED URL]',
			'/(?:password|passwd|pwd|secret|api[_-]?key)[\s:=]+["\']?[^\s"\']+/i' => '[REDACTED]',
		];

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

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

	public static function get_log_by_id( int $id ): ?object {
		if ( $id < 1 ) {
			return null;
		}

		global $wpdb;
		$table = self::get_table_name();
		$row   = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table WHERE id = %d", $id ) );

		return $row ?: null;
	}

	/**
	 * Resend a logged email using stored recipients, subject, body, and headers.
	 *
	 * @return true|\WP_Error
	 */
	public static function resend_log( int $id ) {
		$log = self::get_log_by_id( $id );
		if ( ! $log ) {
			return new \WP_Error( 'not_found', __( 'Log entry not found.', 'wp-powersuite' ) );
		}

		$disabled_message = __( '(Content logging disabled)', 'wp-powersuite' );
		$message          = is_string( $log->message ?? '' ) ? $log->message : '';
		if ( $message === '' || $message === $disabled_message ) {
			return new \WP_Error(
				'no_content',
				__( 'This log has no stored message body. Enable “Store email body content in logs” and send a new test email to resend from logs.', 'wp-powersuite' )
			);
		}

		$recipients = self::resolve_log_recipients( $log );
		if ( $recipients === [] ) {
			return new \WP_Error( 'invalid_recipient', __( 'No valid recipient address is stored for this log.', 'wp-powersuite' ) );
		}

		$headers = MailHeaders::applyIdentityHeaders(
			MailHeaders::normalizeLines( (string) ( $log->headers ?? '' ) )
		);

		$sent = wp_mail( $recipients, (string) $log->subject, $message, $headers );

		if ( ! $sent ) {
			return new \WP_Error( 'send_failed', __( 'Failed to resend email.', 'wp-powersuite' ) );
		}

		return true;
	}

	/**
	 * 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,
		];
	}
}
