<?php
declare( strict_types=1 );

namespace FiboFilters\SeoRules;

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

/**
 * Represents a single dynamic SEO rule stored in the `seo_rules` table.
 *
 * Responsibility:
 * 1. Encapsulate state for one row (rule) – no persistence here.
 * 2. Validate its own data when mutating via setter methods.
 * 3. Offer accessor (getter) methods for read‑only usage.
 *
 * Persistence is delegated to {@see SeoRuleRepository}.
 */
final class SeoRule {

	/**
	 * Primary key, or null when not persisted.
	 *
	 * @var int|null
	 */
	private ?int $id = null;

	/**
	 * Human-readable label for the rule.
	 *
	 * @var string
	 */
	private string $label = '';

	/**
	 * Identifier for the base view where the rule applies.
	 *
	 * @var string
	 */
	private string $base_view = '';

	/**
	 * JSON-decoded associative array defining the rule logic.
	 *
	 * @var array<string, mixed>
	 */
	private array $rules = [];

	/**
	 * Templates keyed by template slot name.
	 *
	 * @var array<string, string>
	 */
	private array $templates = [];

	/**
	 * Whether this rule is indexable by robots.
	 *
	 * @var bool
	 */
	private bool $indexable = true;

	/**
	 * Whether this rule should be taken into account (true) or skipped (false).
	 *
	 * @var bool
	 */
	private bool $in_sitemap = true;

	/**
	 * Priority for sitemaps (0.0 to 1.0).
	 *
	 * @var float
	 */
	private float $priority = 0.5;

	/**
	 * Ordering weight for processing/display.
	 *
	 * @var int
	 */
	private int $order = 1;

	/**
	 * Language code for the rule (e.g. 'en', 'pl').
	 *
	 * @var string
	 */
	private string $lang = '';

	/**
	 * Hydrates the object from a WPDB row (array or stdClass).
	 *
	 * @param array|object $data Row returned by {@see wpdb::get_row()} /
	 *                           {@see wpdb::get_results()}.
	 * @throws \InvalidArgumentException When mandatory columns are missing.
	 */
	public function populate( $data ): void {

		// Required fields.
		$required = [ 'id', 'label', 'base_view', 'rules' ];
		$missing  = array_diff( $required, array_keys( (array) $data ) );
		if ( ! empty( $missing ) ) {
			/* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped */
			throw new \InvalidArgumentException( sprintf( 'Missing required data: %s', implode( ', ', $missing ) ) );
		}

		// From the blocks options, the identifier comes in the format seo-rule-(\d+), but in the database we store it as an integer.
		if ( is_string( $data['id'] ) && preg_match( '/^seo-rule-(\d+)$/', $data['id'], $m ) ) {
			$data['id'] = $m[1];
		}

		$this->id = absint( $data['id'] );
		$this->set_label( isset( $data['label'] ) ? (string) mb_substr( $data['label'], 0, 255 ) : '' );
		$this->set_base_view( ! empty( $data['base_view'] ) ? (string) $data['base_view'] : '' );
		$this->set_rules( $data['rules'] ?? '[]' );

		$this->set_template( 'title', (string) ( mb_substr( $data['title_tpl'] ?? '', 0, 255 ) ) );

		foreach ( [ 'title', 'meta_desc', 'headline', 'top_desc', 'bottom_desc' ] as $name ) {
			$this->set_template( $name, (string) ( $data[ $name . '_tpl' ] ?? '' ) );
		}

		$this->set_indexable( (bool) ( $data['indexable'] ?? 1 ) );
		$this->set_in_sitemap( (bool) ( $data['sitemap'] ?? 1 ) );

		$this->set_priority( (float) ( $data['priority'] ?? 0.5 ) );
		$this->set_order( (int) ( $data['order'] ?? 1 ) );
		$this->set_lang( (string) ( $data['lang'] ?? '' ) );
	}

	public function set_label( string $label ): void {
		$this->label = $label;
	}

	public function set_base_view( string $base_view ): void {
		$this->base_view = mb_substr( $base_view, 0, 255 );
	}

	/**
	 * Accepts either raw JSON string or already-decoded array of rules.
	 *
	 * @param string|array $input Rules definition.
	 */
	private function set_rules( $input ): void {

		if ( is_string( $input ) ) {
			$decoded     = json_decode( $input, true );
			$this->rules = ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) )
				? $decoded
				: [];
			return;
		}

		$this->rules = is_array( $input ) ? $input : [];
	}

	/**
	 * Stores a template if the name is whitelisted.
	 *
	 * @param string $name    Template key (title|meta_desc|headline|top_desc|bottom_desc).
	 * @param string $content Template content.
	 */
	public function set_template( string $name, string $content ): void {

		$allowed = [ 'title', 'meta_desc', 'headline', 'top_desc', 'bottom_desc' ];

		if ( ! in_array( $name, $allowed, true ) || '' === $content ) {
			return;
		}

		if ( 'title' === $name ) {
			$content = mb_substr( $content, 0, 255 );
		}

		$this->templates[ $name ] = $content;
	}

	public function set_indexable( bool $indexable ): void {
		$this->indexable = $indexable;
	}

	public function set_in_sitemap( bool $in_sitemap ): void {
		$this->in_sitemap = $in_sitemap;
	}

	public function set_priority( float $priority ): void {
		$this->priority = round(
			max( 0.0, min( 1.0, $priority ) ),
			2
		);
	}

	public function set_order( int $order ): void {
		$this->order = $order;
	}

	public function set_lang( string $lang ): void {
		$this->lang = $lang;
	}

	public function get_id(): ?int {
		return $this->id;
	}

	public function get_label(): string {
		return $this->label;
	}

	public function get_base_view(): string {
		return $this->base_view;
	}

	public function get_rules(): array {
		return $this->rules;
	}

	public function get_template( $template ): ?string {

		return $this->templates[ $template ] ?? '';
	}

	public function is_indexable(): bool {
		return $this->indexable;
	}

	public function is_in_sitemap(): bool {
		return $this->in_sitemap;
	}

	public function get_priority(): float {
		return $this->priority;
	}

	public function get_order(): int {
		return $this->order;
	}

	public function get_lang(): string {
		return $this->lang;
	}

	public function get_block_data(): array {
		return [
			'id'              => 'seo-rule-' . $this->get_id(),
			'label'           => $this->get_label(),
			'base_view'       => $this->get_base_view(),
			'rules'           => $this->get_rules(),
			'title_tpl'       => $this->get_template( 'title' ),
			'meta_desc_tpl'   => $this->get_template( 'meta_desc' ),
			'headline_tpl'    => $this->get_template( 'headline' ),
			'top_desc_tpl'    => $this->get_template( 'top_desc' ),
			'bottom_desc_tpl' => $this->get_template( 'bottom_desc' ),
			'indexable'       => $this->is_indexable(),
			'sitemap'         => $this->is_in_sitemap(),
			'priority'        => $this->get_priority(),
			'order'           => $this->get_order(),
		];
	}
}
