<?php
/**
 * Per-user Email Log views and display preferences.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\MailLog;

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

/**
 * Keeps personal Email Log presentation state out of the shared event history.
 */
final class EmailLogPreferences {
	const PREFERENCES_META_KEY = 'subin_email_log_preferences';
	const SAVED_VIEWS_META_KEY = 'subin_email_log_saved_views';
	const MAX_SAVED_VIEWS      = 8;

	/**
	 * Read normalized table preferences for one user.
	 *
	 * @param int $user_id User id, defaults to the current user.
	 * @return array{density:string,columns:array<int,string>,per_page:int}
	 */
	public function get_preferences( $user_id = 0 ) {
		$user_id = $this->user_id( $user_id );
		$stored  = $user_id > 0 ? $this->read_site_meta( $user_id, self::PREFERENCES_META_KEY ) : array();

		return $this->sanitize_preferences( is_array( $stored ) ? $stored : array() );
	}

	/**
	 * Save normalized table preferences.
	 *
	 * @param array<string,mixed> $preferences Preferences.
	 * @param int                 $user_id User id, defaults to the current user.
	 * @return bool
	 */
	public function save_preferences( array $preferences, $user_id = 0 ) {
		$user_id = $this->user_id( $user_id );
		if ( $user_id <= 0 ) {
			return false;
		}

		$meta_key   = $this->site_meta_key( self::PREFERENCES_META_KEY );
		$normalized = $this->sanitize_preferences( $preferences );
		if ( false !== update_user_meta( $user_id, $meta_key, $normalized ) ) {
			return true;
		}

		return metadata_exists( 'user', $user_id, $meta_key )
			&& $normalized === $this->sanitize_preferences( (array) get_user_meta( $user_id, $meta_key, true ) );
	}

	/**
	 * Get bounded normalized saved views.
	 *
	 * @param int $user_id User id, defaults to current user.
	 * @return array<int,array{id:string,name:string,filters:array<string,mixed>,created_at:string}>
	 */
	public function get_saved_views( $user_id = 0 ) {
		$user_id = $this->user_id( $user_id );
		$stored  = $user_id > 0 ? $this->read_site_meta( $user_id, self::SAVED_VIEWS_META_KEY ) : array();
		$views   = array();

		foreach ( is_array( $stored ) ? $stored : array() as $view ) {
			if ( ! is_array( $view ) ) {
				continue;
			}
			$id   = substr( sanitize_key( (string) ( $view['id'] ?? '' ) ), 0, 40 );
			$name = substr( sanitize_text_field( (string) ( $view['name'] ?? '' ) ), 0, 80 );
			if ( '' === $id || '' === $name ) {
				continue;
			}
			$views[] = array(
				'id'         => $id,
				'name'       => $name,
				'filters'    => $this->sanitize_filters( is_array( $view['filters'] ?? null ) ? $view['filters'] : array() ),
				'created_at' => sanitize_text_field( (string) ( $view['created_at'] ?? '' ) ),
			);
			if ( count( $views ) >= self::MAX_SAVED_VIEWS ) {
				break;
			}
		}

		return $views;
	}

	/**
	 * Save a personal view without accepting executable or arbitrary query args.
	 *
	 * @param string              $name View name.
	 * @param array<string,mixed> $filters Email Log filters.
	 * @param int                 $user_id User id, defaults to current user.
	 * @return string|\WP_Error New view id or error.
	 */
	public function save_view( $name, array $filters, $user_id = 0 ) {
		$user_id = $this->user_id( $user_id );
		$name    = substr( sanitize_text_field( (string) $name ), 0, 80 );
		if ( $user_id <= 0 || '' === $name ) {
			return new \WP_Error( 'subin_email_log_view_invalid', __( 'Enter a name for this saved view.', 'submission-inbox' ) );
		}

		$views = $this->get_saved_views( $user_id );
		if ( count( $views ) >= self::MAX_SAVED_VIEWS ) {
			return new \WP_Error(
				'subin_email_log_view_limit',
				sprintf(
					/* translators: %d: maximum saved views. */
					__( 'You can save up to %d Email Log views. Delete one before saving another.', 'submission-inbox' ),
					self::MAX_SAVED_VIEWS
				)
			);
		}

		$id      = substr( str_replace( '-', '', wp_generate_uuid4() ), 0, 24 );
		$views[] = array(
			'id'         => $id,
			'name'       => $name,
			'filters'    => $this->sanitize_filters( $filters ),
			'created_at' => current_time( 'mysql' ),
		);
		if ( false === update_user_meta( $user_id, $this->site_meta_key( self::SAVED_VIEWS_META_KEY ), $views ) ) {
			return new \WP_Error( 'subin_email_log_view_not_saved', __( 'The Email Log view could not be saved.', 'submission-inbox' ) );
		}

		return $id;
	}

	/**
	 * Delete one personal view.
	 *
	 * @param string $view_id View id.
	 * @param int    $user_id User id, defaults to current user.
	 * @return bool
	 */
	public function delete_view( $view_id, $user_id = 0 ) {
		$user_id = $this->user_id( $user_id );
		$view_id = substr( sanitize_key( (string) $view_id ), 0, 40 );
		if ( $user_id <= 0 || '' === $view_id ) {
			return false;
		}

		$views = array_values(
			array_filter(
				$this->get_saved_views( $user_id ),
				static function ( $view ) use ( $view_id ) {
					return $view_id !== (string) ( $view['id'] ?? '' );
				}
			)
		);

		return false !== update_user_meta( $user_id, $this->site_meta_key( self::SAVED_VIEWS_META_KEY ), $views );
	}

	/**
	 * Sanitize the bounded filter contract persisted in user meta.
	 *
	 * @param array<string,mixed> $filters Raw filters.
	 * @return array<string,mixed>
	 */
	public function sanitize_filters( array $filters ) {
		$outcomes = array( 'accepted', 'handled', 'failed', 'not_generated', 'awaiting', 'processing', 'stalled', 'needs_attention', 'queued', 'delivered', 'bounced', 'deferred', 'dropped', 'complained', 'opened' );
		$statuses = array( 'sent', 'failed', 'pending', 'unknown', 'no_email_generated' );
		$ranges   = array( 'today', 'yesterday', '7', '30', '90' );
		$sorts    = array( 'newest', 'oldest' );
		$clean    = array(
			'outcome'    => in_array( sanitize_key( (string) ( $filters['outcome'] ?? '' ) ), $outcomes, true ) ? sanitize_key( (string) $filters['outcome'] ) : '',
			'status'     => in_array( sanitize_key( (string) ( $filters['status'] ?? '' ) ), $statuses, true ) ? sanitize_key( (string) $filters['status'] ) : '',
			'source'     => substr( sanitize_key( (string) ( $filters['source'] ?? '' ) ), 0, 100 ),
			'provider'   => substr( sanitize_key( (string) ( $filters['provider'] ?? '' ) ), 0, 64 ),
			'form_id'    => substr( sanitize_text_field( (string) ( $filters['form_id'] ?? '' ) ), 0, 190 ),
			'email_type' => substr( sanitize_key( (string) ( $filters['email_type'] ?? '' ) ), 0, 50 ),
			'event_kind' => substr( sanitize_key( (string) ( $filters['event_kind'] ?? '' ) ), 0, 50 ),
			'rule_id'    => absint( $filters['rule_id'] ?? 0 ),
			'date_range' => in_array( (string) ( $filters['date_range'] ?? '' ), $ranges, true ) ? (string) $filters['date_range'] : '',
			'date_from'  => $this->sanitize_date( $filters['date_from'] ?? '' ),
			'date_to'    => $this->sanitize_date( $filters['date_to'] ?? '' ),
			'search'     => substr( sanitize_text_field( (string) ( $filters['search'] ?? '' ) ), 0, 200 ),
			'sort'       => in_array( sanitize_key( (string) ( $filters['sort'] ?? '' ) ), $sorts, true ) ? sanitize_key( (string) $filters['sort'] ) : 'newest',
		);

		return array_filter(
			$clean,
			static function ( $value, $key ) {
				return 'sort' === $key || ( is_int( $value ) ? $value > 0 : '' !== (string) $value );
			},
			ARRAY_FILTER_USE_BOTH
		);
	}

	/** Normalize display preferences. */
	private function sanitize_preferences( array $preferences ) {
		$allowed_columns = array( 'type_origin', 'outcome', 'related' );
		$columns         = is_array( $preferences['columns'] ?? null ) ? $preferences['columns'] : $allowed_columns;
		$columns         = array_values( array_intersect( $allowed_columns, array_map( 'sanitize_key', $columns ) ) );
		$density         = sanitize_key( (string) ( $preferences['density'] ?? 'comfortable' ) );
		$per_page        = absint( $preferences['per_page'] ?? 20 );

		return array(
			'density'  => in_array( $density, array( 'comfortable', 'compact' ), true ) ? $density : 'comfortable',
			'columns'  => $columns,
			'per_page' => in_array( $per_page, array( 20, 50, 100 ), true ) ? $per_page : 20,
		);
	}

	/** Sanitize one YYYY-MM-DD value. */
	private function sanitize_date( $value ) {
		$value = sanitize_text_field( (string) $value );
		if ( 1 !== preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) {
			return '';
		}
		$parts = array_map( 'absint', explode( '-', $value ) );

		return 3 === count( $parts ) && checkdate( $parts[1], $parts[2], $parts[0] ) ? $value : '';
	}

	/** Resolve a safe user id. */
	private function user_id( $user_id ) {
		$user_id = absint( $user_id );

		return $user_id > 0 ? $user_id : absint( get_current_user_id() );
	}

	/**
	 * Read this site's value and copy the legacy network-global value once.
	 *
	 * The legacy key is deliberately retained: deleting it during the first site
	 * visit would make preferences disappear on every other site in a network.
	 * An existing scoped value, including an intentionally empty array, always
	 * wins and prevents the fallback from being resurrected.
	 *
	 * @param int    $user_id User id.
	 * @param string $legacy_key Legacy unscoped meta key.
	 * @return mixed
	 */
	private function read_site_meta( $user_id, $legacy_key ) {
		$site_key = $this->site_meta_key( $legacy_key );
		if ( metadata_exists( 'user', $user_id, $site_key ) ) {
			return get_user_meta( $user_id, $site_key, true );
		}

		$legacy = get_user_meta( $user_id, $legacy_key, true );
		if ( is_array( $legacy ) && ! empty( $legacy ) ) {
			update_user_meta( $user_id, $site_key, $legacy );
			return $legacy;
		}

		return array();
	}

	/** @return string */
	private function site_meta_key( $legacy_key ) {
		return sanitize_key( (string) $legacy_key ) . '_' . absint( get_current_blog_id() );
	}
}
