<?php
/**
 * Settings storage and settings screen.
 *
 * @package RewindBrevoFormBridge
 */

namespace RewindBrevoFormBridge\Admin;

defined( 'ABSPATH' ) || exit;

/**
 * Reads, sanitizes and writes the rbfb_settings option.
 *
 * Storage only: the dashboard reads and writes these values over the
 * rbfb/v1 REST API, so nothing here touches the Settings API or renders
 * markup. Reads are plain get_option() calls, so the Brevo client can use
 * this class on the front end without dragging admin code in.
 *
 * @since 0.1.0
 */
class Settings {

	/**
	 * Option name.
	 *
	 * @var string
	 */
	const OPTION = 'rbfb_settings';

	/**
	 * Admin page slug (also the menu slug).
	 *
	 * @var string
	 */
	const PAGE = 'rewind-form-bridge-for-brevo';

	/**
	 * Default settings.
	 *
	 * @since 0.1.0
	 * @return array
	 */
	public function defaults(): array {
		return array(
			'api_key'                  => '',
			'api_key_source'           => 'db',
			'default_country'          => '',
			'log_retention_days'       => 30,
			'alert_email'              => '',
			'alert_failure_threshold'  => 5,
			'delete_data_on_uninstall' => false,

			// Site email delivery — the built-in replacement for a separate
			// WP SMTP plugin. Off until the owner turns it on: silently
			// rerouting a site's mail on activation would be indefensible.
			'email_enabled'            => false,
			'email_transport'          => 'api',
			'email_from_email'         => '',
			'email_from_name'          => '',
			'email_reply_to'           => '',
			'email_force_from'         => true,
			'email_fallback'           => true,
			'smtp_login'               => '',
			'smtp_key'                 => '',
			'smtp_key_source'          => 'db',
			'smtp_port'                => 587,
		);
	}

	/**
	 * All settings merged over defaults.
	 *
	 * @since 0.1.0
	 * @return array
	 */
	public function all(): array {
		$stored = get_option( self::OPTION, array() );

		if ( ! is_array( $stored ) ) {
			$stored = array();
		}

		$settings = wp_parse_args( $stored, $this->defaults() );

		$settings['api_key_source']  = $this->api_key_source();
		$settings['smtp_key_source'] = $this->smtp_key_source();

		return $settings;
	}

	/**
	 * Read one setting.
	 *
	 * @since 0.1.0
	 * @param string $key     Setting key.
	 * @param mixed  $fallback Value when the key is unknown.
	 * @return mixed
	 */
	public function get( string $key, $fallback = null ) {
		$all = $this->all();

		return array_key_exists( $key, $all ) ? $all[ $key ] : $fallback;
	}

	/**
	 * The effective API key. A RBFB_API_KEY constant in wp-config.php wins
	 * over the stored value.
	 *
	 * @since 0.1.0
	 * @return string
	 */
	public function api_key(): string {
		if ( defined( 'RBFB_API_KEY' ) && is_string( RBFB_API_KEY ) && '' !== RBFB_API_KEY ) {
			return RBFB_API_KEY;
		}

		return (string) $this->get( 'api_key', '' );
	}

	/**
	 * Where the effective key comes from: 'constant' or 'db'.
	 *
	 * @since 0.1.0
	 * @return string
	 */
	public function api_key_source(): string {
		return ( defined( 'RBFB_API_KEY' ) && is_string( RBFB_API_KEY ) && '' !== RBFB_API_KEY ) ? 'constant' : 'db';
	}

	/**
	 * The key masked for display: bullets plus the last 4 characters.
	 *
	 * @since 0.1.0
	 * @return string Empty string when no key is set.
	 */
	public function masked_key(): string {
		$key = $this->api_key();

		if ( '' === $key ) {
			return '';
		}

		return str_repeat( '•', 12 ) . substr( $key, -4 );
	}

	/**
	 * The effective SMTP key. A RBFB_SMTP_KEY constant in wp-config.php
	 * wins over the stored value.
	 *
	 * @since 0.5.0
	 * @return string
	 */
	public function smtp_key(): string {
		if ( $this->has_smtp_constant() ) {
			return RBFB_SMTP_KEY;
		}

		return (string) $this->get( 'smtp_key', '' );
	}

	/**
	 * Where the effective SMTP key comes from: 'constant' or 'db'.
	 *
	 * @since 0.5.0
	 * @return string
	 */
	public function smtp_key_source(): string {
		return $this->has_smtp_constant() ? 'constant' : 'db';
	}

	/**
	 * The SMTP key masked for display: bullets plus the last 4 characters.
	 *
	 * @since 0.5.0
	 * @return string Empty string when no key is set.
	 */
	public function masked_smtp_key(): string {
		$key = $this->smtp_key();

		if ( '' === $key ) {
			return '';
		}

		return str_repeat( '•', 12 ) . substr( $key, -4 );
	}

	/**
	 * Whether a usable RBFB_SMTP_KEY constant is defined.
	 *
	 * @since 0.5.0
	 * @return bool
	 */
	private function has_smtp_constant(): bool {
		return defined( 'RBFB_SMTP_KEY' ) && is_string( RBFB_SMTP_KEY ) && '' !== RBFB_SMTP_KEY;
	}

	/**
	 * Sanitize the whole settings array on save.
	 *
	 * @since 0.1.0
	 * @param mixed $input Raw submitted value.
	 * @return array Clean settings to store.
	 */
	public function sanitize( $input ): array {
		if ( ! is_array( $input ) ) {
			$input = array();
		}

		$current = $this->all();
		$clean   = array();

		// API key: an empty submission means "keep the existing key" — the
		// field is only rendered (empty) when the user clicks Replace key.
		// When the key comes from the RBFB_API_KEY constant the stored value
		// is left untouched.
		$submitted_key = isset( $input['api_key'] ) ? trim( sanitize_text_field( $input['api_key'] ) ) : '';

		if ( 'constant' === $this->api_key_source() || '' === $submitted_key ) {
			$clean['api_key'] = (string) $current['api_key'];
		} else {
			$clean['api_key'] = $submitted_key;
		}

		$country = isset( $input['default_country'] ) ? strtoupper( sanitize_text_field( $input['default_country'] ) ) : '';

		$clean['default_country'] = preg_match( '/^[A-Z]{2}$/', $country ) ? $country : '';

		$clean['log_retention_days'] = isset( $input['log_retention_days'] ) ? max( 1, absint( $input['log_retention_days'] ) ) : $current['log_retention_days'];

		$alert_email          = isset( $input['alert_email'] ) ? sanitize_email( $input['alert_email'] ) : '';
		$clean['alert_email'] = is_email( $alert_email ) ? $alert_email : '';

		$clean['alert_failure_threshold'] = isset( $input['alert_failure_threshold'] ) ? max( 1, absint( $input['alert_failure_threshold'] ) ) : $current['alert_failure_threshold'];

		$clean['delete_data_on_uninstall'] = ! empty( $input['delete_data_on_uninstall'] );

		$clean = array_merge( $clean, $this->sanitize_email( $input, $current ) );

		// The *_source keys are derived at read time, never stored as truth.
		$clean['api_key_source']  = 'db';
		$clean['smtp_key_source'] = 'db';

		return $clean;
	}

	/**
	 * Sanitize the site-email settings.
	 *
	 * Split out because these carry the second credential in the plugin and
	 * deserve the same "empty means keep" handling as the API key.
	 *
	 * @since 0.5.0
	 * @param array $input   Raw submitted values.
	 * @param array $current The settings as they stand.
	 * @return array Clean email settings.
	 */
	private function sanitize_email( array $input, array $current ): array {
		$clean = array();

		$clean['email_enabled'] = ! empty( $input['email_enabled'] );

		$transport                = isset( $input['email_transport'] ) ? sanitize_key( $input['email_transport'] ) : '';
		$clean['email_transport'] = in_array( $transport, array( 'api', 'smtp' ), true ) ? $transport : 'api';

		$from                      = isset( $input['email_from_email'] ) ? sanitize_email( $input['email_from_email'] ) : '';
		$clean['email_from_email'] = is_email( $from ) ? $from : '';

		$clean['email_from_name'] = isset( $input['email_from_name'] ) ? sanitize_text_field( $input['email_from_name'] ) : '';

		$reply_to                = isset( $input['email_reply_to'] ) ? sanitize_email( $input['email_reply_to'] ) : '';
		$clean['email_reply_to'] = is_email( $reply_to ) ? $reply_to : '';

		$clean['email_force_from'] = ! empty( $input['email_force_from'] );
		$clean['email_fallback']   = ! empty( $input['email_fallback'] );

		$clean['smtp_login'] = isset( $input['smtp_login'] ) ? sanitize_text_field( $input['smtp_login'] ) : '';

		$port               = isset( $input['smtp_port'] ) ? absint( $input['smtp_port'] ) : 0;
		$clean['smtp_port'] = in_array( $port, array( 587, 465, 2525 ), true ) ? $port : 587;

		// Same contract as the API key: an empty submission keeps the stored
		// credential, because the field is rendered empty by design.
		$submitted = isset( $input['smtp_key'] ) ? trim( sanitize_text_field( $input['smtp_key'] ) ) : '';

		$clean['smtp_key'] = ( 'constant' === $this->smtp_key_source() || '' === $submitted )
			? (string) $current['smtp_key']
			: $submitted;

		return $clean;
	}
}
