<?php

namespace WPDRMS\ASP\Priorities;

use WPDRMS\ASP\Priorities\ORM\PriorityGroupsOptions;
use WPDRMS\PluginCore\Traits\SingletonTrait;

if ( ! defined( 'ABSPATH' ) ) {
	die( '-1' );
}

class PrioritiesService {
	use SingletonTrait;

	/**
	 * @var PriorityGroupsOptions
	 */
	private $storage;

	/**
	 * @var array
	 */
	private $instances;

	/**
	 * @var string
	 */
	private $order = 'priority';

	/**
	 * @var string
	 */
	private $orderBy = 'desc';

	/**
	 * Default data structure for one priority ruleset.
	 *
	 * @var array
	 */
	private $default = array(
		'priority'        => 100,
		'name'            => 'Priority group',
		'phrase'          => '',
		'phrase_logic'    => 'disabled',
		'instance'        => 0,
		'blog_id'         => 0,
		'logic'           => 'and',
		'enabled'         => true,
		'rules'           => array(),
		'condition_logic' => 'and',
		'conditions'      => array(),
		'boosts'          => array(),
	);

	/**
	 * @var array
	 */
	private $default_rule = array(
		'name'     => 'Rule name',
		'field'    => 'tax',
		'operator' => 'in',
		'values'   => array(),
		'_values'  => array(),
	);

	private function __construct() {
		$this->storage = PriorityGroupsOptions::instance();
		$this->load();
	}

	/**
	 * @param bool|int $id
	 * @param bool|int $force_refresh
	 * @return array
	 */
	public function get( $id = false, $force_refresh = false ): array {
		if ( ! isset( $this->instances ) || ! is_array( $this->instances ) || $force_refresh ) {
			$this->load();
		}

		if ( $id === false ) {
			return $this->instances;
		}

		return $this->instances[ $id ] ?? array();
	}

	/**
	 * @param string $order
	 * @param string $orderby
	 * @return array
	 */
	public function getSorted( string $order = 'priority', string $orderby = 'desc' ): array {
		$inst = $this->get();
		if ( ! empty( $inst ) ) {
			$this->order   = $order;
			$this->orderBy = $orderby;
			usort( $inst, array( $this, 'sortInstances' ) );
		}
		return $inst;
	}

	/**
	 * Returns groups with resolved display labels (_values) for taxonomy terms and post titles.
	 *
	 * @param bool|int $id
	 * @param bool|int $force_refresh
	 * @return array
	 */
	public function getForDisplay( $id = false, $force_refresh = false ): array {
		$inst = $this->get( $id, $force_refresh );
		if ( empty( $inst ) ) {
			return $inst;
		}
		if ( $id !== false ) {
			$inst = array( $inst );
		}

		foreach ( $inst as $ik => $instance ) {
			// Resolve display labels for each boost's matcher(s).
			foreach ( $instance['boosts'] ?? array() as $bk => $boost ) {
				if ( ( $boost['kind'] ?? 'rule' ) === 'ordered_posts' ) {
					$inst[ $ik ]['boosts'][ $bk ]['_values'] = array(
						'post_ids' => $this->resolvePostTitles( $boost['post_ids'] ?? array() ),
					);
					continue;
				}
				foreach ( $boost['rules'] ?? array() as $rk => $rule ) {
					$inst[ $ik ]['boosts'][ $bk ]['rules'][ $rk ]['_values'] = $this->resolveRuleDisplay( (array) $rule );
				}
			}

			// Resolve display labels for taxonomy gate conditions (term id => name), mirroring the
			// 'tax' rule resolution above so the editor and summary can show readable term names.
			foreach ( $instance['conditions'] ?? array() as $ck => $condition ) {
				if ( ( $condition['type'] ?? '' ) !== 'taxonomy' ) {
					continue;
				}
				$taxonomy                                    = (string) ( $condition['taxonomy'] ?? '' );
				$term_ids                                    = isset( $condition['term_ids'] ) && is_array( $condition['term_ids'] )
					? $condition['term_ids'] : array();
				$inst[ $ik ]['conditions'][ $ck ]['_values'] = array();
				if ( $taxonomy !== '' && ! empty( $term_ids ) ) {
					$terms = get_terms(
						array(
							'taxonomy'   => $taxonomy,
							'include'    => $term_ids,
							'fields'     => 'id=>name',
							'hide_empty' => false,
						) 
					);
					if ( ! is_wp_error( $terms ) ) {
						$inst[ $ik ]['conditions'][ $ck ]['_values']['term_labels'] = $terms;
					}
				}
			}
		}

		return $inst;
	}

	/**
	 * Resolves the display `_values` for a single boost rule (taxonomy term labels, post titles).
	 *
	 * @param array<string, mixed> $rule
	 * @return array<string, mixed>
	 */
	private function resolveRuleDisplay( array $rule ): array {
		$out   = array();
		$field = $rule['field'] ?? '';
		if ( $field === 'tax' ) {
			$taxonomy = (string) ( $rule['values']['taxonomy'] ?? '' );
			$term_ids = isset( $rule['values']['term_ids'] ) && is_array( $rule['values']['term_ids'] )
				? $rule['values']['term_ids'] : array();
			if ( $taxonomy !== '' && ! empty( $term_ids ) ) {
				$terms = get_terms(
					array(
						'taxonomy'   => $taxonomy,
						'include'    => $term_ids,
						'fields'     => 'id=>name',
						'hide_empty' => false,
					)
				);
				if ( ! is_wp_error( $terms ) ) {
					$out['term_labels'] = $terms;
				}
			}
		} elseif ( $field === 'post' ) {
			$out['post_ids'] = $this->resolvePostTitles( $rule['values']['post_ids'] ?? array() );
		}
		return $out;
	}

	/**
	 * Maps a list of post ids to a `id => title` array, skipping posts that no longer exist.
	 *
	 * @param array<int, int|string> $ids
	 * @return array<int, string>
	 */
	private function resolvePostTitles( array $ids ): array {
		$titles = array();
		foreach ( $ids as $post_id ) {
			if ( get_post( $post_id ) !== null ) {
				$title                    = get_the_title( $post_id );
				$titles[ (int) $post_id ] = $title !== '' ? $title : __( '(no title)', 'ajax-search-pro' );
			}
		}
		return $titles;
	}

	/**
	 * Drops deleted term ids (taxonomy rule) / post ids (post rule) from a single rule in place.
	 *
	 * @param array<string, mixed> $rule
	 * @return bool Whether the rule was modified.
	 */
	private function pruneRuleValues( array &$rule ): bool {
		$changed = false;
		$field   = $rule['field'] ?? '';
		if ( $field === 'tax' ) {
			$taxonomy = (string) ( $rule['values']['taxonomy'] ?? '' );
			$term_ids = isset( $rule['values']['term_ids'] ) && is_array( $rule['values']['term_ids'] )
				? $rule['values']['term_ids'] : array();
			if ( $taxonomy !== '' && ! empty( $term_ids ) ) {
				$terms = get_terms(
					array(
						'taxonomy'   => $taxonomy,
						'include'    => $term_ids,
						'fields'     => 'ids',
						'hide_empty' => false,
					)
				);
				// If WP_Error (unknown taxonomy), leave values intact.
				if ( ! is_wp_error( $terms ) ) {
					if ( count( $terms ) === 0 ) {
						$rule['values']['term_ids'] = array();
						$changed                    = true;
					} elseif ( count( $terms ) < count( $term_ids ) ) {
						$rule['values']['term_ids'] = array_values( $terms );
						$changed                    = true;
					}
				}
			}
		} elseif ( $field === 'post' ) {
			$valid_ids = array();
			foreach ( $rule['values']['post_ids'] ?? array() as $post_id ) {
				if ( get_post( $post_id ) !== null ) {
					$valid_ids[] = $post_id;
				}
			}
			if ( count( $valid_ids ) < count( $rule['values']['post_ids'] ?? array() ) ) {
				$rule['values']['post_ids'] = $valid_ids;
				$changed                    = true;
			}
		}
		return $changed;
	}

	/**
	 * @param bool|int $id
	 * @param bool|int $force_refresh
	 * @return string
	 */
	public function getEncoded( $id = false, $force_refresh = false ): string {
		return base64_encode( json_encode( $this->get( $id, $force_refresh ) ) );
	}

	/**
	 * @param bool|int $id
	 * @param bool|int $force_refresh
	 * @return string
	 */
	public function getForDisplayEncoded( $id = false, $force_refresh = false ): string {
		return base64_encode( json_encode( $this->getForDisplay( $id, $force_refresh ) ) );
	}

	/**
	 * @param bool $force_refresh
	 * @return int
	 */
	public function getCount( bool $force_refresh = false ): int {
		if ( ! isset( $this->instances ) || ! is_array( $this->instances ) || $force_refresh ) {
			$this->load();
		}
		return count( $this->instances );
	}

	/**
	 * @param array    $data
	 * @param bool|int $save
	 */
	public function set( $data, $save = true ): void {
		$this->instances = $data;
		if ( $save ) {
			$this->save();
		}
	}

	/**
	 * @param string   $data
	 * @param bool|int $save
	 */
	public function setEncoded( $data, $save = true ): void {
		$this->set( json_decode( base64_decode( $data ), true ), $save );
	}

	/**
	 * @param array    $data
	 * @param bool|int $save
	 */
	public function add( $data, $save = true ): void {
		$this->instances[] = $data;
		if ( $save ) {
			$this->save();
		}
	}

	/**
	 * @return array
	 */
	public function load(): array {
		$this->instances = $this->storage->load()->getGroups();
		if ( $this->cleanInstances() ) {
			$this->save( false );
		}
		return $this->instances;
	}

	/**
	 * @param bool|int $clean
	 */
	public function save( $clean = true ): void {
		if ( $clean ) {
			$this->cleanInstances();
		}
		$this->storage->saveGroups( $this->instances );
	}

	/**
	 * @param array $a
	 * @param array $b
	 * @return int
	 */
	public function sortInstances( $a, $b ): int {
		if ( isset( $a[ $this->order ], $b[ $this->order ] ) ) {
			if ( $a[ $this->order ] == $b[ $this->order ] ) {
				return 0;
			}
			if ( $this->orderBy === 'asc' ) {
				return ( $a[ $this->order ] < $b[ $this->order ] ) ? -1 : 1;
			} else {
				return ( $a[ $this->order ] < $b[ $this->order ] ) ? 1 : -1;
			}
		}
		return 0;
	}

	/**
	 * @return bool
	 */
	private function cleanInstances(): bool {
		$changed = false;

		foreach ( $this->instances as $k => $v ) {
			$v             = wp_parse_args( $v, $this->default );
			$instance_keys = array_keys( $this->default );
			foreach ( array_keys( $v ) as $kk ) {
				if ( ! in_array( $kk, $instance_keys ) ) {
					unset( $v[ $kk ] );
				}
			}
			$rule_keys = array_keys( $this->default_rule );
			foreach ( $v['rules'] as $rk => $rule ) {
				foreach ( array_keys( $rule ) as $kk ) {
					if ( ! in_array( $kk, $rule_keys ) ) {
						unset( $v['rules'][ $rk ][ $kk ] );
					}
				}
			}

			if ( ! wp_doing_ajax() && is_admin() ) {
				// Prune deleted terms/posts from legacy top-level rules and from each boost.
				foreach ( $v['rules'] as $rk => $rule ) {
					$r = (array) $rule;
					if ( $this->pruneRuleValues( $r ) ) {
						$v['rules'][ $rk ] = $r;
						$changed           = true;
					}
				}
				foreach ( $v['boosts'] as $bk => $boost ) {
					if ( ( $boost['kind'] ?? 'rule' ) === 'ordered_posts' ) {
						$valid = array();
						foreach ( $boost['post_ids'] ?? array() as $post_id ) {
							if ( get_post( $post_id ) !== null ) {
								$valid[] = $post_id;
							}
						}
						if ( count( $valid ) < count( $boost['post_ids'] ?? array() ) ) {
							$v['boosts'][ $bk ]['post_ids'] = $valid;
							$changed                        = true;
						}
						continue;
					}
					foreach ( $boost['rules'] ?? array() as $rk => $rule ) {
						$r = (array) $rule;
						if ( $this->pruneRuleValues( $r ) ) {
							$v['boosts'][ $bk ]['rules'][ $rk ] = $r;
							$changed                            = true;
						}
					}
				}

				// Drop deleted term ids referenced by taxonomy gate conditions.
				foreach ( $v['conditions'] as $ck => $condition ) {
					if ( ( $condition['type'] ?? '' ) !== 'taxonomy' ) {
						continue;
					}
					$taxonomy = (string) ( $condition['taxonomy'] ?? '' );
					$term_ids = isset( $condition['term_ids'] ) && is_array( $condition['term_ids'] )
						? $condition['term_ids'] : array();
					if ( $taxonomy === '' || empty( $term_ids ) ) {
						continue;
					}
					$terms = get_terms(
						array(
							'taxonomy'   => $taxonomy,
							'include'    => $term_ids,
							'fields'     => 'ids',
							'hide_empty' => false,
						) 
					);
					if ( ! is_wp_error( $terms ) ) {
						if ( count( $terms ) === 0 ) {
							$v['conditions'][ $ck ]['term_ids'] = array();
							$changed                            = true;
						} elseif ( count( $terms ) < count( $term_ids ) ) {
							$v['conditions'][ $ck ]['term_ids'] = array_values( $terms );
							$changed                            = true;
						}
					}
				}
			}

			$this->instances[ $k ] = $v;
		}

		return $changed;
	}
}
