<?php
/**
 * Base class for all schema types
 *
 * Provides common methods for schema generation, validation, and output.
 *
 * @package BW_Schema
 * @since 3.0.0-babel
 */

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

abstract class BW_Schema_Schema_Base {

	/**
	 * Schema context URL
	 *
	 * @var string
	 */
	const CONTEXT = 'https://schema.org';

	/**
	 * Schema data
	 *
	 * @var array
	 */
	protected $schema = array();

	/**
	 * Schema type (must be overridden by subclasses)
	 *
	 * @var string
	 */
	protected $type = '';

	/**
	 * Constructor
	 */
	public function __construct() {
		// Initialize schema with context and type
		$this->schema = array(
			'@context' => self::CONTEXT,
			'@type'    => $this->type,
		);

		// Build the schema
		$this->build();
	}

	/**
	 * Build the schema (implemented by subclasses)
	 *
	 * Subclasses should populate $this->schema with relevant data.
	 *
	 * @return void
	 */
	abstract protected function build();

	/**
	 * Add a property to the schema
	 *
	 * @param string $key Property name (without @ prefix)
	 * @param mixed  $value Property value
	 * @return void
	 */
	protected function set( $key, $value ) {
		if ( null !== $value && '' !== $value ) {
			$this->schema[ $key ] = $value;
		}
	}

	/**
	 * Add nested properties to schema
	 *
	 * Example:
	 *   $this->set_nested( 'address', array(
	 *       '@type'            => 'PostalAddress',
	 *       'streetAddress'    => '123 Main St',
	 *       'addressLocality'  => 'City',
	 *   ));
	 *
	 * @param string $key Property name
	 * @param array  $properties Nested properties (will add @type if not present)
	 * @return void
	 */
	protected function set_nested( $key, $properties ) {
		if ( empty( $properties ) ) {
			return;
		}

		// Ensure nested objects have @type
		if ( ! isset( $properties['@type'] ) && isset( $properties[0] ) ) {
			// If array of items, wrap as array
			$this->schema[ $key ] = $properties;
		} else {
			$this->schema[ $key ] = $properties;
		}
	}

	/**
	 * Get a schema property
	 *
	 * @param string $key Property name
	 * @param mixed  $default Default value if not found
	 * @return mixed Property value or default
	 */
	protected function get( $key, $default = null ) {
		return $this->schema[ $key ] ?? $default;
	}

	/**
	 * Check if schema has required properties
	 *
	 * @param array $required Required property names
	 * @return bool True if all required properties are present
	 */
	protected function has_required( $required = array() ) {
		if ( empty( $required ) ) {
			return true;
		}

		foreach ( $required as $key ) {
			if ( ! isset( $this->schema[ $key ] ) || '' === $this->schema[ $key ] ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Get the complete schema as array
	 *
	 * @return array Schema data
	 */
	public function to_array() {
		return $this->schema;
	}

	/**
	 * Get schema as JSON string
	 *
	 * @return string JSON-LD output
	 */
	public function to_json() {
		return wp_json_encode( $this->schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	}

	/**
	 * Output schema as HTML script tag
	 *
	 * @return string HTML script tag
	 */
	public function to_html() {
		return sprintf(
			'<script type="application/ld+json">%s</script>',
			$this->to_json()
		);
	}

	/**
	 * Validate schema against basic requirements
	 *
	 * Override in subclasses for specific validation.
	 *
	 * @return array Validation errors (empty if valid)
	 */
	public function validate() {
		$errors = array();

		// Check required properties
		if ( ! isset( $this->schema['@type'] ) || ! $this->schema['@type'] ) {
			$errors[] = 'Schema is missing @type';
		}

		return $errors;
	}

	/**
	 * Check if schema is valid
	 *
	 * @return bool True if schema passes validation
	 */
	public function is_valid() {
		return empty( $this->validate() );
	}

	/**
	 * Get validation errors
	 *
	 * @return array List of error messages
	 */
	public function get_errors() {
		return $this->validate();
	}

	/**
	 * Get schema type
	 *
	 * @return string Schema.org type
	 */
	public function get_type() {
		return $this->type;
	}

	/**
	 * Sanitize and escape text for schema
	 *
	 * @param string $text Text to sanitize
	 * @return string Sanitized text
	 */
	protected function sanitize_text( $text ) {
		return sanitize_text_field( $text );
	}

	/**
	 * Validate and sanitize URL
	 *
	 * @param string $url URL to validate
	 * @return string|null Sanitized URL or null if invalid
	 */
	protected function sanitize_url( $url ) {
		$url = esc_url_raw( $url );
		return ! empty( $url ) ? $url : null;
	}

	/**
	 * Convert date to ISO 8601 format
	 *
	 * @param string|int $date Date string or timestamp
	 * @return string ISO 8601 date (YYYY-MM-DD)
	 */
	protected function format_date( $date ) {
		if ( is_numeric( $date ) ) {
			return gmdate( 'Y-m-d', $date );
		}

		// Parse date string
		$timestamp = strtotime( $date );
		if ( $timestamp ) {
			return gmdate( 'Y-m-d', $timestamp );
		}

		return $date;
	}

	/**
	 * Convert datetime to ISO 8601 format
	 *
	 * @param string|int $datetime DateTime string or timestamp
	 * @return string ISO 8601 datetime (YYYY-MM-DDTHH:MM:SS)
	 */
	protected function format_datetime( $datetime ) {
		if ( is_numeric( $datetime ) ) {
			return gmdate( 'Y-m-d\TH:i:s', $datetime );
		}

		// Parse datetime string
		$timestamp = strtotime( $datetime );
		if ( $timestamp ) {
			return gmdate( 'Y-m-d\TH:i:s', $timestamp );
		}

		return $datetime;
	}

	/**
	 * Get WordPress site URL
	 *
	 * @return string Site URL
	 */
	protected function get_site_url() {
		return trailingslashit( home_url() );
	}

	/**
	 * Get WordPress home URL
	 *
	 * @return string Home URL
	 */
	protected function get_home_url() {
		return home_url();
	}

	/**
	 * Get current page URL
	 *
	 * @return string Current page URL
	 */
	protected function get_current_url() {
		global $wp;
		return home_url( $wp->request );
	}
}
