<?php

declare(strict_types=1);

namespace Signocore\Multilanguage\Features;

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

use Signocore\Multilanguage\Application\Constants;
use Signocore\Multilanguage\Infrastructure\Database\TranslationRepository;
use Signocore\Multilanguage\Infrastructure\Traits\Singleton;
use Signocore\Multilanguage\Support\PostTypeHelper;
use Signocore\Multilanguage\Support\TaxonomyHelper;
use Signocore\Multilanguage\Support\TranslationHelper;
use WP_Post;
use WP_Query;

final class SharedSlugs
{
	use Singleton;

	/** @var array{term_id: int, taxonomy: string}|null */
	private ?array $termSlugDuplicateContext = null;

	protected function init(): void
	{
		// Post/page/CPT slug uniqueness (fires in admin and frontend)
		add_filter('wp_unique_post_slug', [$this, 'allowSharedPostSlug'], 10, 6);

		// Taxonomy term slug uniqueness (WP 6.0+)
		add_filter('wp_unique_term_slug_is_bad_slug', [$this, 'allowSharedTermSlug'], 10, 3);

		// Language-aware duplicate slug check for wp_update_term()
		add_filter('wp_update_term_parent', [$this, 'captureTermUpdateContext'], 10, 5);
		add_filter('terms_clauses', [$this, 'filterDuplicateSlugCheck'], 10, 3);

		// Frontend-only hooks
		if (!Constants::$isAdmin) {
			add_action('parse_query', [$this, 'resolveSharedSlug'], 0);
		}
	}

	/**
	 * Allow posts in different languages to share the same slug.
	 *
	 * @param string $slug The sanitized (possibly suffixed) slug.
	 * @param int $postId Post ID.
	 * @param string $postStatus Post status.
	 * @param string $postType Post type.
	 * @param int $postParent Post parent ID.
	 * @param string $originalSlug The original slug before WP added a suffix.
	 */
	public function allowSharedPostSlug(
		string $slug,
		int $postId,
		string $postStatus,
		string $postType,
		int $postParent,
		string $originalSlug
	): string {
		if ($slug === $originalSlug) {
			return $slug;
		}

		if (!PostTypeHelper::isTranslatable($postType)) {
			return $slug;
		}

		$lang = $this->getPostLanguageForSave($postId, $postType);
		if ($lang === '') {
			return $slug;
		}

		$collisionExists = $this->postSlugExistsInLanguage(
			$originalSlug,
			$postId,
			$postType,
			$postParent,
			$lang
		);

		return $collisionExists ? $slug : $originalSlug;
	}

	/**
	 * Allow terms in different languages to share the same slug.
	 *
	 * @param bool $isBadSlug Whether WP considers the slug a collision.
	 * @param string $slug The term slug being checked.
	 * @param object $term The term object.
	 */
	public function allowSharedTermSlug(bool $isBadSlug, string $slug, object $term): bool
	{
		if (!$isBadSlug) {
			return false;
		}

		$taxonomy = $term->taxonomy ?? '';
		if ($taxonomy === '' || !TaxonomyHelper::isTranslatable($taxonomy)) {
			return $isBadSlug;
		}

		$termId = (int) ($term->term_id ?? 0);
		$lang = $this->getTermLanguageForSave($termId, $taxonomy);
		if ($lang === '') {
			return $isBadSlug;
		}

		$collisionExists = $this->termSlugExistsInLanguage($slug, $termId, $taxonomy, $lang);

		return $collisionExists;
	}

	/**
	 * Capture context before wp_update_term() runs its duplicate slug check.
	 *
	 * @param int $parent Parent term ID.
	 * @param int $termId Term ID being updated.
	 * @param string $taxonomy Taxonomy slug.
	 * @param array<string, mixed> $parsedArgs Parsed term arguments.
	 * @param array<string, mixed> $args Original arguments.
	 */
	public function captureTermUpdateContext(int $parent, int $termId, string $taxonomy, array $parsedArgs, array $args): int
	{
		if (!TaxonomyHelper::isTranslatable($taxonomy)) {
			return $parent;
		}

		$this->termSlugDuplicateContext = [
			'term_id' => $termId,
			'taxonomy' => $taxonomy,
		];

		return $parent;
	}

	/**
	 * Make the duplicate slug check in wp_update_term() language-aware.
	 *
	 * @param array{fields: string, join: string, where: string, distinct: string, orderby: string, order: string, limits: string} $clauses
	 * @param string[] $taxonomies
	 * @param array<string, mixed> $args
	 * @return array{fields: string, join: string, where: string, distinct: string, orderby: string, order: string, limits: string}
	 */
	public function filterDuplicateSlugCheck(array $clauses, array $taxonomies, array $args): array
	{
		if ($this->termSlugDuplicateContext === null) {
			return $clauses;
		}

		$context = $this->termSlugDuplicateContext;
		$this->termSlugDuplicateContext = null;

		$lang = $this->getTermLanguageForSave($context['term_id'], $context['taxonomy']);
		if ($lang === '') {
			return $clauses;
		}

		global $wpdb;

		$table = $wpdb->prefix . Constants::TABLE_TRANSLATIONS;
		$elementType = 'tax_' . $context['taxonomy'];

		// Only match terms in the same language
		$clauses['join'] .= $wpdb->prepare(
			" INNER JOIN `{$table}` AS sml_dup ON (t.term_id = sml_dup.element_id AND sml_dup.element_type = %s)",
			$elementType
		);
		$clauses['where'] .= $wpdb->prepare(
			' AND sml_dup.language_code = %s',
			$lang
		);

		return $clauses;
	}

	/**
	 * Resolve the correct post when multiple posts share the same slug.
	 */
	public function resolveSharedSlug(WP_Query $query): void
	{
		if (is_admin() || !$query->is_main_query()) {
			return;
		}

		$currentLang = $query->get('sml_lang') ?: LanguageDetection::load()->getCurrentLanguage();

		$pagename = $query->get('pagename');
		if ($pagename !== '') {
			$post = $this->getPageByPath($pagename, $currentLang);
			if ($post !== null) {
				$query->set('page_id', $post->ID);
				$query->set('pagename', '');
				$query->is_page = true;
				$query->is_singular = true;
			}
			return;
		}

		$name = $query->get('name');
		if ($name !== '') {
			$post = $this->getPostBySlug($name, $currentLang);
			if ($post !== null) {
				$query->set('p', $post->ID);
				$query->set('name', '');
				$query->is_single = true;
				$query->is_singular = true;
			}
		}
	}

	/**
	 * Language-aware page-by-path resolution for hierarchical post types.
	 */
	private function getPageByPath(string $pagePath, string $lang): ?WP_Post
	{
		global $wpdb;

		$table = $wpdb->prefix . Constants::TABLE_TRANSLATIONS;
		$parts = explode('/', trim($pagePath, '/'));
		$leafSlug = end($parts);

		// Find pages with the leaf slug in the requested language
		$results = $wpdb->get_results($wpdb->prepare(
			"SELECT p.ID, p.post_parent, p.post_name, p.post_type
			FROM {$wpdb->posts} p
			INNER JOIN `{$table}` t ON p.ID = t.element_id AND t.element_type = CONCAT('post_', p.post_type)
			WHERE p.post_name = %s
			AND p.post_status = 'publish'
			AND p.post_type IN ('page', 'attachment')
			AND t.language_code = %s",
			$leafSlug,
			$lang
		));

		if (empty($results)) {
			// Also try all hierarchical public post types
			$hierarchicalTypes = get_post_types(['public' => true, 'hierarchical' => true], 'names');
			$typePlaceholders = implode(', ', array_fill(0, count($hierarchicalTypes), '%s'));

			$args = [$leafSlug];
			$args = array_merge($args, array_values($hierarchicalTypes));
			$args[] = $lang;

			$results = $wpdb->get_results($wpdb->prepare(
				"SELECT p.ID, p.post_parent, p.post_name, p.post_type
				FROM {$wpdb->posts} p
				INNER JOIN `{$table}` t ON p.ID = t.element_id AND t.element_type = CONCAT('post_', p.post_type)
				WHERE p.post_name = %s
				AND p.post_status = 'publish'
				AND p.post_type IN ({$typePlaceholders})
				AND t.language_code = %s",
				...$args
			));
		}

		if (empty($results)) {
			return null;
		}

		// For simple (non-hierarchical) paths, return the first match
		if (count($parts) === 1) {
			return get_post($results[0]->ID);
		}

		// For hierarchical paths, verify the full parent chain
		foreach ($results as $row) {
			if ($this->verifyParentChain((int) $row->ID, array_slice($parts, 0, -1), $lang)) {
				return get_post($row->ID);
			}
		}

		return null;
	}

	/**
	 * Verify that a page's parent chain matches the expected slug path.
	 *
	 * @param int $postId The leaf page ID.
	 * @param string[] $parentSlugs The expected parent slugs, from root to immediate parent.
	 * @param string $lang The language code.
	 */
	private function verifyParentChain(int $postId, array $parentSlugs, string $lang): bool
	{
		$current = get_post($postId);
		$reversedSlugs = array_reverse($parentSlugs);

		foreach ($reversedSlugs as $expectedSlug) {
			if ($current === null || (int) $current->post_parent === 0) {
				return false;
			}

			$parent = get_post($current->post_parent);
			if ($parent === null || $parent->post_name !== $expectedSlug) {
				return false;
			}

			$current = $parent;
		}

		// After matching all parent slugs, the current post should have no parent
		return $current !== null && (int) $current->post_parent === 0;
	}

	/**
	 * Find a non-hierarchical post by slug in the given language.
	 */
	private function getPostBySlug(string $slug, string $lang): ?WP_Post
	{
		global $wpdb;

		$table = $wpdb->prefix . Constants::TABLE_TRANSLATIONS;

		$postId = $wpdb->get_var($wpdb->prepare(
			"SELECT p.ID
			FROM {$wpdb->posts} p
			INNER JOIN `{$table}` t ON p.ID = t.element_id AND t.element_type = CONCAT('post_', p.post_type)
			WHERE p.post_name = %s
			AND p.post_status = 'publish'
			AND t.language_code = %s
			LIMIT 1",
			$slug,
			$lang
		));

		if ($postId === null) {
			return null;
		}

		return get_post((int) $postId);
	}

	/**
	 * Determine the language of a post being saved.
	 */
	private function getPostLanguageForSave(int $postId, string $postType): string
	{
		$elementType = 'post_' . $postType;

		// Try existing translation record (use findByElement directly
		// because getElementLanguage() falls back to default language,
		// which would prevent the POST data fallback from ever running)
		$record = TranslationRepository::findByElement($postId, $elementType);
		if ($record !== null) {
			return $record->language_code;
		}

		// Try POST data (when creating/editing in admin)
		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		if (isset($_POST['sml_post_language']) && is_string($_POST['sml_post_language'])) {
			// phpcs:ignore WordPress.Security.NonceVerification.Missing
			$postLang = sanitize_text_field(wp_unslash($_POST['sml_post_language']));
			if ($postLang !== '') {
				return $postLang;
			}
		}

		return LanguageDetection::load()->getCurrentLanguage();
	}

	/**
	 * Determine the language of a term being saved.
	 */
	private function getTermLanguageForSave(int $termId, string $taxonomy): string
	{
		$elementType = 'tax_' . $taxonomy;

		// Try existing translation record (use findByElement directly
		// because getElementLanguage() falls back to default language,
		// which would prevent the POST data fallback from ever running)
		$record = TranslationRepository::findByElement($termId, $elementType);
		if ($record !== null) {
			return $record->language_code;
		}

		// Try POST data (taxonomy edit form uses sml_term_language)
		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		if (isset($_POST['sml_term_language']) && is_string($_POST['sml_term_language'])) {
			// phpcs:ignore WordPress.Security.NonceVerification.Missing
			$postLang = sanitize_text_field(wp_unslash($_POST['sml_term_language']));
			if ($postLang !== '') {
				return $postLang;
			}
		}

		return LanguageDetection::load()->getCurrentLanguage();
	}

	/**
	 * Check if a post slug collision exists within the same language.
	 */
	private function postSlugExistsInLanguage(
		string $slug,
		int $postId,
		string $postType,
		int $postParent,
		string $lang
	): bool {
		global $wpdb;

		$table = $wpdb->prefix . Constants::TABLE_TRANSLATIONS;
		$elementType = 'post_' . $postType;

		if (is_post_type_hierarchical($postType)) {
			// Hierarchical: scope to same parent + same post type + same language
			$result = $wpdb->get_var($wpdb->prepare(
				"SELECT p.ID
				FROM {$wpdb->posts} p
				INNER JOIN `{$table}` t ON p.ID = t.element_id AND t.element_type = %s
				WHERE p.post_name = %s
				AND p.post_type = %s
				AND p.post_parent = %d
				AND p.ID != %d
				AND p.post_status != 'trash'
				AND t.language_code = %s
				LIMIT 1",
				$elementType,
				$slug,
				$postType,
				$postParent,
				$postId,
				$lang
			));
		} else {
			// Flat: scope to same post type + same language
			$result = $wpdb->get_var($wpdb->prepare(
				"SELECT p.ID
				FROM {$wpdb->posts} p
				INNER JOIN `{$table}` t ON p.ID = t.element_id AND t.element_type = %s
				WHERE p.post_name = %s
				AND p.post_type = %s
				AND p.ID != %d
				AND p.post_status != 'trash'
				AND t.language_code = %s
				LIMIT 1",
				$elementType,
				$slug,
				$postType,
				$postId,
				$lang
			));
		}

		return $result !== null;
	}

	/**
	 * Check if a term slug collision exists within the same language.
	 */
	private function termSlugExistsInLanguage(
		string $slug,
		int $termId,
		string $taxonomy,
		string $lang
	): bool {
		global $wpdb;

		$table = $wpdb->prefix . Constants::TABLE_TRANSLATIONS;
		$elementType = 'tax_' . $taxonomy;

		$result = $wpdb->get_var($wpdb->prepare(
			"SELECT tt.term_id
			FROM {$wpdb->terms} AS t
			INNER JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id
			INNER JOIN `{$table}` AS tr ON tt.term_id = tr.element_id AND tr.element_type = %s
			WHERE t.slug = %s
			AND tt.taxonomy = %s
			AND tt.term_id != %d
			AND tr.language_code = %s
			LIMIT 1",
			$elementType,
			$slug,
			$taxonomy,
			$termId,
			$lang
		));

		return $result !== null;
	}
}
