<?php
/**
 * Settings storage and sanitisation. Single option key (ssaa_settings),
 * defaults merged at read time, extension points for add-on files via
 * the ssaa_settings_get_all and ssaa_sanitise_settings filters.
 *
 * @package search-social-appearance-audit
 */

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

class SSAA_Settings {

	/** @var array|null Cached settings */
	private static $settings = null;

	public static function init() {
		// No hooks needed yet.
	}

	/**
	 * Single source of truth for the setting keys this file owns and
	 * their defaults. Add-on files contribute their own keys via the
	 * ssaa_settings_get_all filter.
	 *
	 * @return array
	 */
	public static function defaults() {
		return array(
			'log_level'          => 'info',                      // off | error | info | debug
			'search_desc_length' => 160,                         // Meta description preview cutoff (chars)
			'social_desc_length' => 110,                         // OG description preview cutoff (chars)
			'scan_post_types'    => array( 'post', 'page' ),     // Built-in types to audit
			'scan_archives'      => 1,                           // Audit archive pages (blog home, taxonomy, date)
		);
	}

	/**
	 * All settings: saved values merged over defaults, then the read-time
	 * extension filter.
	 *
	 * @return array
	 */
	public static function get_all() {
		if ( null === self::$settings ) {
			$saved          = get_option( SSAA_OPTION_KEY, array() );
			self::$settings = wp_parse_args( (array) $saved, self::defaults() );

			// Extension point: add-on files apply read-time setting overrides here.
			self::$settings = (array) apply_filters( 'ssaa_settings_get_all', self::$settings );
		}
		return self::$settings;
	}

	/**
	 * Read a single setting.
	 *
	 * @param string $key     Setting key.
	 * @param mixed  $default Fallback when the key is unknown.
	 * @return mixed
	 */
	public static function get( $key, $default = null ) {
		$all = self::get_all();
		return array_key_exists( $key, $all ) ? $all[ $key ] : $default;
	}

	/**
	 * Sanitise raw POST input. This file handles the fields it owns, then
	 * fires the extension filter for fields added by add-on files.
	 *
	 * @param array $raw Raw input.
	 * @return array
	 */
	public static function sanitise( array $raw ) {
		$saved = wp_parse_args( (array) get_option( SSAA_OPTION_KEY, array() ), self::defaults() );
		$clean = array();

		// IMPORTANT: fields absent from $raw keep their SAVED value, never
		// the default - so a form that posts only its own tab's fields (the
		// Logs tab posts only log_level) cannot reset the others. This is
		// the trap _standards calls out under "Four log levels".
		$levels = array( 'off', 'error', 'info', 'debug' );
		if ( isset( $raw['log_level'] ) ) {
			$level              = sanitize_key( $raw['log_level'] );
			$clean['log_level'] = in_array( $level, $levels, true ) ? $level : 'info';
		} else {
			$clean['log_level'] = $saved['log_level'];
		}

		$clean['search_desc_length'] = self::bounded_int( $raw, 'search_desc_length', (int) $saved['search_desc_length'], 50, 400 );
		$clean['social_desc_length'] = self::bounded_int( $raw, 'social_desc_length', (int) $saved['social_desc_length'], 50, 400 );

		if ( isset( $raw['scan_post_types'] ) ) {
			$types = array_values( array_intersect( array_map( 'sanitize_key', (array) $raw['scan_post_types'] ), array( 'post', 'page' ) ) );
			// At least one built-in type stays enabled.
			$clean['scan_post_types'] = $types ? $types : (array) $saved['scan_post_types'];
		} else {
			$clean['scan_post_types'] = (array) $saved['scan_post_types'];
		}

		// Archive pages toggle (hidden-0 + checkbox-1 in the form, so the key
		// is always posted from its own tab; absent = other tab's form).
		$clean['scan_archives'] = isset( $raw['scan_archives'] )
			? (int) (bool) absint( $raw['scan_archives'] )
			: (int) $saved['scan_archives'];

		// Extension point: add-on files hook setting sanitisation here.
		return (array) apply_filters( 'ssaa_sanitise_settings', $clean, $raw, $saved );
	}

	/**
	 * Sanitise and persist, then bust the read cache.
	 *
	 * @param array $raw Raw input.
	 */
	public static function save( array $raw ) {
		update_option( SSAA_OPTION_KEY, self::sanitise( $raw ) );
		self::$settings = null;
	}

	/**
	 * Bounded integer from raw input; fallback (the saved value) on
	 * missing/invalid input.
	 *
	 * @param array  $raw      Raw input.
	 * @param string $key      Field key.
	 * @param int    $fallback Value to keep when the field is absent/invalid.
	 * @param int    $min      Minimum.
	 * @param int    $max      Maximum.
	 * @return int
	 */
	private static function bounded_int( array $raw, $key, $fallback, $min, $max ) {
		if ( ! isset( $raw[ $key ] ) || ! is_numeric( $raw[ $key ] ) ) {
			return max( $min, min( $max, absint( $fallback ) ) );
		}
		return max( $min, min( $max, absint( $raw[ $key ] ) ) );
	}
}
