<?php
/**
 * Redirect Value Object
 *
 * Represents a single redirect entry with origin, target, type, and format.
 *
 * @package SEOvaultSEO\RedirectManager
 */

namespace SEOvaultSEO\RedirectManager;

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

/**
 * Redirect value object.
 *
 * Holds a single redirect definition and provides sanitization for
 * origin and target URLs.
 */
class Redirect {

	const FORMAT_PLAIN = 'plain';
	const FORMAT_REGEX = 'regex';

	const TYPE_PERMANENT   = 301;
	const TYPE_FOUND       = 302;
	const TYPE_TEMPORARY   = 307;
	const TYPE_DELETED     = 410;
	const TYPE_UNAVAILABLE = 451;

	/**
	 * All valid redirect types.
	 *
	 * @var int[]
	 */
	public static $valid_types = array(
		self::TYPE_PERMANENT,
		self::TYPE_FOUND,
		self::TYPE_TEMPORARY,
		self::TYPE_DELETED,
		self::TYPE_UNAVAILABLE,
	);

	/**
	 * All valid formats.
	 *
	 * @var string[]
	 */
	public static $valid_formats = array(
		self::FORMAT_PLAIN,
		self::FORMAT_REGEX,
	);

	/**
	 * Redirect origin (source URL or pattern).
	 *
	 * @var string
	 */
	protected $origin;

	/**
	 * Redirect target (destination URL).
	 *
	 * @var string
	 */
	protected $target = '';

	/**
	 * HTTP status code.
	 *
	 * @var int
	 */
	protected $type;

	/**
	 * Format: plain or regex.
	 *
	 * @var string
	 */
	protected $format;

	/**
	 * Optional database ID.
	 *
	 * @var int|null
	 */
	protected $id;

	/**
	 * Constructor.
	 *
	 * @param string   $origin Source URL or pattern.
	 * @param string   $target Destination URL.
	 * @param int      $type   HTTP status code.
	 * @param string   $format plain|regex.
	 * @param int|null $id     Database ID (when loading from DB).
	 */
	public function __construct( $origin, $target = '', $type = self::TYPE_PERMANENT, $format = self::FORMAT_PLAIN, $id = null ) {
		$this->format = in_array( $format, self::$valid_formats, true ) ? $format : self::FORMAT_PLAIN;
		$this->type   = in_array( (int) $type, self::$valid_types, true ) ? (int) $type : self::TYPE_PERMANENT;
		$this->origin = $this->sanitize_origin( (string) $origin );
		$this->target = $this->sanitize_target( (string) $target );
		$this->id     = null !== $id ? (int) $id : null;
	}

	/**
	 * Get the origin.
	 *
	 * @return string
	 */
	public function get_origin() {
		return $this->origin;
	}

	/**
	 * Get the target.
	 *
	 * @return string
	 */
	public function get_target() {
		return $this->target;
	}

	/**
	 * Get the redirect type (HTTP status code).
	 *
	 * @return int
	 */
	public function get_type() {
		return $this->type;
	}

	/**
	 * Get the format.
	 *
	 * @return string
	 */
	public function get_format() {
		return $this->format;
	}

	/**
	 * Get the database ID.
	 *
	 * @return int|null
	 */
	public function get_id() {
		return $this->id;
	}

	/**
	 * Set the database ID.
	 *
	 * @param int $id Database ID.
	 * @return void
	 */
	public function set_id( $id ) {
		$this->id = (int) $id;
	}

	/**
	 * Check whether the given URL matches this redirect's origin.
	 *
	 * For plain redirects, the URL is normalized and compared.
	 * For regex redirects, the pattern is matched against the URL.
	 *
	 * @param string $url The URL to check.
	 * @return bool
	 */
	public function origin_matches( $url ) {
		$url = (string) $url;

		if ( self::FORMAT_REGEX === $this->format ) {
			if ( $this->regex_matches_url( $url ) ) {
				return true;
			}

			// Match both slashless paths and leading-slash paths so patterns
			// like "^/old-.*$" work regardless of which form the caller passes.
			if ( '' !== $url && '/' !== $url[0] ) {
				return $this->regex_matches_url( '/' . $url );
			}

			if ( '' !== $url && '/' === $url[0] ) {
				return $this->regex_matches_url( ltrim( $url, '/' ) );
			}

			return false;
		}

		return $this->origin === $this->sanitize_origin( $url );
	}

	/**
	 * Run the stored regex pattern against a URL.
	 *
	 * @param string $url URL to test.
	 * @return bool
	 */
	protected function regex_matches_url( $url ) {
		return 1 === @preg_match( '#' . $this->origin . '#i', $url );
	}

	/**
	 * Convert to an associative array (for DB storage / JSON).
	 *
	 * @return array
	 */
	public function to_array() {
		return array(
			'id'     => $this->id,
			'origin' => $this->origin,
			'target' => $this->target,
			'type'   => $this->type,
			'format' => $this->format,
		);
	}

	/**
	 * Create a Redirect instance from a DB row array.
	 *
	 * @param array $row Row data with keys: id, origin, target, type, format.
	 * @return Redirect
	 */
	public static function from_array( array $row ) {
		return new self(
			$row['origin'] ?? '',
			$row['target'] ?? '',
			(int) ( $row['type'] ?? self::TYPE_PERMANENT ),
			$row['format'] ?? self::FORMAT_PLAIN,
			isset( $row['id'] ) ? (int) $row['id'] : null
		);
	}

	/**
	 * Sanitize the origin URL.
	 *
	 * For plain redirects: strip home URL prefix, trim slashes on relative URLs.
	 * For regex redirects: keep as-is (only strip control chars).
	 *
	 * @param string $url Origin URL.
	 * @return string
	 */
	protected function sanitize_origin( $url ) {
		$url = self::strip_control_chars( $url );

		if ( self::FORMAT_REGEX === $this->format ) {
			return $url;
		}

		$home        = home_url();
		$parsed_home = wp_parse_url( $home );
		$parsed_url  = wp_parse_url( $url );

		$stripped_home = false;

		// If the URL contains the home URL, strip it to make a relative path.
		if ( isset( $parsed_url['scheme'] ) && isset( $parsed_home['host'] )
			&& isset( $parsed_url['host'] )
			&& $parsed_url['host'] === $parsed_home['host'] ) {

			$home_path = $parsed_home['path'] ?? '';
			$url_path  = $parsed_url['path'] ?? '';

			if ( '' !== $home_path && 0 === strpos( $url_path, $home_path ) ) {
				$url_path = substr( $url_path, strlen( $home_path ) );
			}

			$url           = $url_path;
			$stripped_home = true;
		}

		// Trim leading/trailing slashes for relative paths (but keep root "/").
		// Relative paths have no scheme; absolute home URLs become relative once stripped.
		if ( '/' !== $url && ( $stripped_home || ! isset( $parsed_url['scheme'] ) ) ) {
			$url = trim( $url, '/' );
		}

		return $url;
	}

	/**
	 * Sanitize the target URL.
	 *
	 * Strips control chars. If the URL is on the same domain as the home URL,
	 * strips the scheme and host to store a relative path.
	 *
	 * @param string $url Target URL.
	 * @return string
	 */
	protected function sanitize_target( $url ) {
		$url = self::strip_control_chars( $url );

		$parsed_home = wp_parse_url( home_url() );
		$parsed_url  = wp_parse_url( $url );

		if ( isset( $parsed_url['scheme'] ) && isset( $parsed_url['host'] )
			&& isset( $parsed_home['host'] )
			&& $parsed_url['host'] === $parsed_home['host'] ) {

			$home_path = $parsed_home['path'] ?? '';
			$url_path  = $parsed_url['path'] ?? '';

			if ( '' !== $home_path && 0 === strpos( $url_path, $home_path ) ) {
				$url_path = substr( $url_path, strlen( $home_path ) );
			}

			// Ensure leading slash for relative target paths.
			$url = '/' . ltrim( $url_path, '/' );
		}

		return $url;
	}

	/**
	 * Strip C0 control characters and DEL from a string.
	 *
	 * @param string|null $value The value to strip.
	 * @return string
	 */
	public static function strip_control_chars( $value ) {
		return preg_replace( '/[\x00-\x1F\x7F]/', '', (string) $value );
	}

	/**
	 * Check whether a string contains C0 control characters or DEL.
	 *
	 * @param string|null $value The value to check.
	 * @return bool
	 */
	public static function contains_control_chars( $value ) {
		return 1 === preg_match( '/[\x00-\x1F\x7F]/', (string) $value );
	}

	/**
	 * Check if a type value is valid.
	 *
	 * @param mixed $type Type to check.
	 * @return bool
	 */
	public static function is_valid_type( $type ) {
		return in_array( (int) $type, self::$valid_types, true );
	}

	/**
	 * Check if a format value is valid.
	 *
	 * @param mixed $format Format to check.
	 * @return bool
	 */
	public static function is_valid_format( $format ) {
		return in_array( (string) $format, self::$valid_formats, true );
	}

	/**
	 * Get human-readable labels for all redirect types.
	 *
	 * @return array<int,string>
	 */
	public static function get_type_labels() {
		return array(
			self::TYPE_PERMANENT   => __( '301 Moved Permanently', 'seovault-ai' ),
			self::TYPE_FOUND       => __( '302 Found', 'seovault-ai' ),
			self::TYPE_TEMPORARY   => __( '307 Temporary Redirect', 'seovault-ai' ),
			self::TYPE_DELETED     => __( '410 Content Deleted', 'seovault-ai' ),
			self::TYPE_UNAVAILABLE => __( '451 Unavailable For Legal Reasons', 'seovault-ai' ),
		);
	}
}
