<?php
/**
 * Centralized configuration for DataStream.
 *
 * @package DataStream
 */

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

/**
 * Class DataStream_Config
 */
class DataStream_Config {

	/**
	 * Option key map.
	 */
	private const OPTIONS = array(
		'api_url'              => 'datastream_api_url',
		'api_method'           => 'datastream_api_method',
		'api_timeout'          => 'datastream_api_timeout',
		'api_headers'          => 'datastream_api_headers',
		'api_bearer_token'     => 'datastream_api_bearer_token',
		'api_ssl_verify'       => 'datastream_api_ssl_verify',
		'json_items_path'      => 'datastream_json_items_path',
		'id_field'             => 'datastream_id_field',
		'content_field'        => 'datastream_content_field',
		'stream_name'          => 'datastream_stream_name',
		'cron_schedule'        => 'datastream_cron_schedule',
		'cron_enabled'         => 'datastream_cron_enabled',
		'max_records_per_sync' => 'datastream_max_records_per_sync',
		'max_payload_bytes'    => 'datastream_max_payload_bytes',
		'max_response_bytes'   => 'datastream_max_response_bytes',
		'max_external_id_len'  => 'datastream_max_external_id_len',
		'stale_record_days'    => 'datastream_stale_record_days',
		'cache_ttl'            => 'datastream_cache_ttl',
		'enable_logging'       => 'datastream_enable_logging',
		'log_level'            => 'datastream_log_level',
		'sync_lock_timeout'    => 'datastream_sync_lock_timeout',
		'delete_on_uninstall'  => 'datastream_delete_on_uninstall',
		'failure_count'        => 'datastream_failure_count',
		'backoff_until'        => 'datastream_backoff_until',
		'last_sync'            => 'datastream_last_sync',
		'last_sync_result'     => 'datastream_last_sync_result',
		'sync_history'         => 'datastream_sync_history',
	);

	/**
	 * Default values.
	 *
	 * @return array<string, mixed>
	 */
	public static function defaults() {
		return array(
			'api_url'              => '',
			'api_method'           => 'GET',
			'api_timeout'          => 30,
			'api_headers'          => '',
			'api_bearer_token'     => '',
			'api_ssl_verify'       => true,
			'json_items_path'      => '',
			'id_field'             => 'id',
			'content_field'        => '',
			'stream_name'          => '',
			'cron_schedule'        => 'hourly',
			'cron_enabled'         => true,
			'max_records_per_sync' => 500,
			'max_payload_bytes'    => 1048576,
			'max_response_bytes'   => 5242880,
			'max_external_id_len'  => 100,
			'stale_record_days'    => 0,
			'cache_ttl'            => HOUR_IN_SECONDS,
			'enable_logging'       => true,
			'log_level'            => 'error',
			'sync_lock_timeout'    => 300,
			'delete_on_uninstall'  => false,
			'failure_count'        => 0,
			'backoff_until'        => 0,
		);
	}

	/**
	 * Register defaults on activation.
	 */
	public static function register_defaults() {
		foreach ( self::defaults() as $key => $value ) {
			if ( isset( self::OPTIONS[ $key ] ) ) {
				add_option( self::OPTIONS[ $key ], $value, '', false );
			}
		}

		add_option( 'datastream_last_sync', '', '', false );
		add_option( 'datastream_last_sync_result', array(), '', false );
		add_option( 'datastream_sync_history', array(), '', false );
	}

	/**
	 * Get a config value.
	 *
	 * @param string $key Config key.
	 * @return mixed
	 */
	public static function get( $key ) {
		$defaults = self::defaults();

		if ( ! isset( self::OPTIONS[ $key ] ) ) {
			return null;
		}

		$default = $defaults[ $key ] ?? null;
		$value   = get_option( self::OPTIONS[ $key ], $default );

		/**
		 * Filter a DataStream configuration value.
		 *
		 * @param mixed  $value   Option value.
		 * @param string $key     Config key.
		 */
		return apply_filters( 'datastream_config_' . $key, $value, $key );
	}

	/**
	 * Persist a config value.
	 *
	 * @param string $key   Config key.
	 * @param mixed  $value Value to store.
	 */
	public static function set( $key, $value ) {
		if ( isset( self::OPTIONS[ $key ] ) ) {
			update_option( self::OPTIONS[ $key ], $value, false );
		}
	}

	/**
	 * Allowed cron schedules for the current tier.
	 *
	 * @return array<string, string>
	 */
	public static function cron_schedules() {
		if ( class_exists( 'DataStream_Feature_Gate' ) ) {
			return DataStream_Feature_Gate::allowed_schedules();
		}

		return array(
			'datastream_15min' => __( 'Every 15 Minutes', 'datastream-api-sync' ),
			'hourly'           => __( 'Hourly', 'datastream-api-sync' ),
			'twicedaily'       => __( 'Twice Daily', 'datastream-api-sync' ),
			'daily'            => __( 'Daily', 'datastream-api-sync' ),
		);
	}

	/**
	 * Validate cron schedule slug.
	 *
	 * @param string $schedule Schedule slug.
	 * @return string
	 */
	public static function validate_schedule( $schedule ) {
		$schedule = sanitize_key( $schedule );
		$allowed  = array_keys( self::cron_schedules() );

		return in_array( $schedule, $allowed, true ) ? $schedule : 'hourly';
	}

	/**
	 * Sanitize API URL — allows empty to disable remote fetch.
	 *
	 * @param string $url Raw URL.
	 * @return string
	 */
	public static function sanitize_api_url( $url ) {
		$url = trim( (string) $url );

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

		$url = esc_url_raw( $url );

		if ( '' === $url || ! preg_match( '#^https?://#i', $url ) ) {
			add_settings_error(
				'datastream_settings',
				'invalid_api_url',
				__( 'API URL must be a valid HTTP or HTTPS address.', 'datastream-api-sync' ),
				'error'
			);
			return (string) get_option( 'datastream_api_url', '' );
		}

		if ( self::reject_unsafe_urls() && function_exists( 'wp_http_validate_url' ) && false === wp_http_validate_url( $url ) ) {
			add_settings_error(
				'datastream_settings',
				'unsafe_api_url',
				__( 'This URL points at a private or local address, which is blocked by default. To allow intranet APIs, define DATASTREAM_ALLOW_PRIVATE_URLS as true in wp-config.php.', 'datastream-api-sync' ),
				'error'
			);
			return (string) get_option( 'datastream_api_url', '' );
		}

		return $url;
	}

	/**
	 * Whether requests to private/local addresses are rejected (default yes).
	 *
	 * @return bool
	 */
	public static function reject_unsafe_urls() {
		$default = ! ( defined( 'DATASTREAM_ALLOW_PRIVATE_URLS' ) && DATASTREAM_ALLOW_PRIVATE_URLS );

		/**
		 * Filter whether DataStream rejects URLs on private/local addresses.
		 *
		 * @param bool $reject True to block private/loopback/link-local targets.
		 */
		return (bool) apply_filters( 'datastream_reject_unsafe_urls', $default );
	}

	/**
	 * Maximum accepted HTTP response size in bytes.
	 *
	 * @return int
	 */
	public static function max_response_bytes() {
		return self::sanitize_bounded_int( self::get( 'max_response_bytes' ), 65536, 52428800, 5242880 );
	}

	/**
	 * Sanitize HTTP method.
	 *
	 * @param string $method Raw method.
	 * @return string
	 */
	public static function sanitize_api_method( $method ) {
		$method = strtoupper( sanitize_text_field( $method ) );
		return in_array( $method, array( 'GET', 'POST' ), true ) ? $method : 'GET';
	}

	/**
	 * Sanitize timeout within safe bounds.
	 *
	 * @param mixed $timeout Raw timeout.
	 * @return int
	 */
	public static function sanitize_timeout( $timeout ) {
		$timeout = absint( $timeout );
		return max( 5, min( 120, $timeout ) );
	}

	/**
	 * Sanitize custom headers JSON.
	 *
	 * @param string $json Raw JSON string.
	 * @return string JSON string of sanitized headers.
	 */
	public static function sanitize_headers_json( $json ) {
		$json = trim( (string) $json );

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

		$decoded = json_decode( $json, true );

		if ( ! is_array( $decoded ) ) {
			add_settings_error(
				'datastream_settings',
				'invalid_headers',
				__( 'Custom headers must be valid JSON object.', 'datastream-api-sync' ),
				'error'
			);
			return (string) get_option( 'datastream_api_headers', '' );
		}

		$safe = array();

		foreach ( $decoded as $key => $value ) {
			if ( ! is_string( $key ) || ! is_scalar( $value ) ) {
				continue;
			}

			$header_key = sanitize_text_field( $key );

			if ( '' === $header_key ) {
				continue;
			}

			$safe[ $header_key ] = sanitize_text_field( (string) $value );
		}

		$encoded = wp_json_encode( $safe );

		return false !== $encoded ? $encoded : '';
	}

	/**
	 * Parse stored headers into wp_remote_* format.
	 *
	 * @return array<string, string>
	 */
	public static function get_request_headers() {
		$json = (string) self::get( 'api_headers' );

		if ( '' === $json ) {
			$headers = array();
		} else {
			$decoded = json_decode( $json, true );
			$headers = is_array( $decoded ) ? $decoded : array();
		}

		// A token defined as a constant in wp-config.php wins over the stored
		// option, so credentials can be kept out of the database entirely.
		if ( defined( 'DATASTREAM_API_BEARER_TOKEN' ) && '' !== (string) DATASTREAM_API_BEARER_TOKEN ) {
			$token = (string) DATASTREAM_API_BEARER_TOKEN;
		} else {
			$token = (string) self::get( 'api_bearer_token' );
		}

		if ( '' !== $token ) {
			$headers['Authorization'] = 'Bearer ' . sanitize_text_field( $token );
		}

		return $headers;
	}

	/**
	 * Sanitize positive integer within bounds.
	 *
	 * @param mixed $value    Raw value.
	 * @param int   $min      Minimum.
	 * @param int   $max      Maximum.
	 * @param int   $fallback Fallback.
	 * @return int
	 */
	public static function sanitize_bounded_int( $value, $min, $max, $fallback ) {
		$value = absint( $value );

		if ( $value < $min || $value > $max ) {
			return $fallback;
		}

		return $value;
	}

	/**
	 * Record a sync result in history.
	 *
	 * @param array<string, mixed> $result Sync outcome.
	 */
	public static function record_sync_result( $result ) {
		$result['recorded_at'] = current_time( 'mysql' );
		update_option( 'datastream_last_sync_result', $result, false );

		if ( ! empty( $result['success'] ) ) {
			update_option( 'datastream_last_sync', current_time( 'mysql' ), false );
		}

		$history   = get_option( 'datastream_sync_history', array() );
		$history   = is_array( $history ) ? $history : array();
		$history[] = $result;

		if ( count( $history ) > 20 ) {
			$history = array_slice( $history, -20 );
		}

		update_option( 'datastream_sync_history', $history, false );

		// Human-readable activity ring buffer (Tools tab). One line per
		// stream per run, capped at 30 entries.
		$activity = get_option( 'datastream_activity', array() );
		$activity = is_array( $activity ) ? $activity : array();

		$per_stream = isset( $result['streams'] ) && is_array( $result['streams'] )
			? $result['streams']
			: array( '' => $result );

		foreach ( $per_stream as $stream_id => $detail ) {
			$activity[] = array(
				'at'      => $result['recorded_at'],
				'stream'  => (string) $stream_id,
				'success' => ! empty( $detail['success'] ),
				'message' => (string) ( $detail['message'] ?? '' ),
			);
		}

		if ( count( $activity ) > 30 ) {
			$activity = array_slice( $activity, -30 );
		}

		update_option( 'datastream_activity', $activity, false );
	}

	/**
	 * Register custom cron intervals.
	 *
	 * @param array<string, array<string, int|string>> $schedules WP schedules.
	 * @return array<string, array<string, int|string>>
	 */
	public static function register_cron_intervals( $schedules ) {
		$schedules['datastream_15min'] = array(
			'interval' => 15 * MINUTE_IN_SECONDS,
			'display'  => __( 'Every 15 Minutes', 'datastream-api-sync' ),
		);

		// Always registered so an existing 5-min schedule keeps firing even
		// if a license lapses; selecting it anew is gated in the UI.
		$schedules['datastream_5min'] = array(
			'interval' => 5 * MINUTE_IN_SECONDS,
			'display'  => __( 'Every 5 Minutes', 'datastream-api-sync' ),
		);

		return $schedules;
	}
}

add_filter( 'cron_schedules', array( 'DataStream_Config', 'register_cron_intervals' ) );
