<?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\TranslationHelper;
use WP_Post;
use WP_Query;

final class PostTranslation
{
	use Singleton;

	/** @var array<string, int>|null */
	private ?array $frontPageTranslations = null;

	/** @var array<string, int>|null */
	private ?array $blogPageTranslations = null;

	protected function init(): void
	{
		add_action('save_post', [$this, 'onSavePost'], 10, 2);
		add_action('before_delete_post', [$this, 'onDeletePost']);
		add_action('pre_get_posts', [$this, 'filterMainQuery']);
		add_filter('posts_join', [$this, 'joinTranslationTable'], 10, 2);
		add_filter('posts_where', [$this, 'whereTranslationLanguage'], 10, 2);

		if (!Constants::$isAdmin) {
			add_filter('page_link', [$this, 'filterFrontPageLink'], 9, 2);
			add_action('pre_get_posts', [$this, 'resolveTranslatedFrontPage'], 1);
			add_filter('redirect_canonical', [$this, 'preventFrontPageRedirect'], 10, 2);
			add_action('wp', [$this, 'fixFrontPageConditional']);
			add_action('wp', [$this, 'fixBlogPageConditional']);
		}
	}

	/**
	 * Auto-assign language to new posts on save.
	 *
	 * @param int $postId
	 * @param \WP_Post $post
	 */
	public function onSavePost(int $postId, \WP_Post $post): void
	{
		if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
			return;
		}

		if (wp_is_post_revision($postId) || wp_is_post_autosave($postId)) {
			return;
		}

		if (!PostTypeHelper::isTranslatable($post->post_type)) {
			return;
		}

		$elementType = TranslationHelper::resolveElementType($postId);
		$existing = TranslationRepository::findByElement($postId, $elementType);

		if ($existing !== null) {
			return;
		}

		$defaultCode = LanguageManager::load()->getDefaultLanguageCode();
		$this->assignLanguage($postId, $defaultCode);
	}

	/**
	 * Clean up translation records when a post is deleted.
	 */
	public function onDeletePost(int $postId): void
	{
		$postType = get_post_type($postId);
		if ($postType === false || !PostTypeHelper::isTranslatable($postType)) {
			return;
		}

		$elementType = TranslationHelper::resolveElementType($postId);
		TranslationRepository::deleteByElement($postId, $elementType);

		/**
		 * Fires after translation records are removed for a deleted post.
		 *
		 * @param int $postId
		 * @param string $elementType
		 */
		do_action('signocore_ml_translation_deleted', $postId, $elementType);
	}

	/**
	 * Create a new empty post as a translation of the source post.
	 */
	public function createTranslation(int $sourcePostId, string $langCode): int|false
	{
		$sourcePost = get_post($sourcePostId);
		if ($sourcePost === null) {
			return false;
		}

		if (!PostTypeHelper::isTranslatable($sourcePost->post_type)) {
			return false;
		}

		$languages = LanguageManager::load()->getActiveLanguages();
		if (!isset($languages[$langCode])) {
			return false;
		}

		// Attachments need special handling (file reference, metadata, inherit status)
		if ($sourcePost->post_type === 'attachment') {
			return $this->createAttachmentTranslation($sourcePost, $langCode);
		}

		$langName = $languages[$langCode]->name;

		$newPostId = wp_insert_post([
			'post_title' => sprintf('[%s] %s: %s', strtoupper($langCode), __('Translation of', 'signocore-multilanguage'), $sourcePost->post_title),
			'post_status' => 'draft',
			'post_type' => $sourcePost->post_type,
			'post_author' => get_current_user_id(),
		]);

		if (is_wp_error($newPostId) || $newPostId === 0) {
			return false;
		}

		$elementType = TranslationHelper::resolveElementType($sourcePostId);
		$sourceLanguage = TranslationHelper::getElementLanguage($sourcePostId, $elementType);

		// Ensure the source has a group
		$groupId = TranslationRepository::getGroupId($sourcePostId, $elementType);
		if ($groupId === null) {
			$groupId = TranslationRepository::getNextGroupId();

			// Assign source to this group if it has no record
			$sourceRecord = TranslationRepository::findByElement($sourcePostId, $elementType);
			if ($sourceRecord === null) {
				TranslationRepository::insert([
					'group_id' => $groupId,
					'element_id' => $sourcePostId,
					'element_type' => $elementType,
					'language_code' => $sourceLanguage,
					'source_language_code' => null,
					'status' => Constants::STATUS_PUBLISHED,
				]);
			}
		}

		// Remove any orphan auto-assigned record created by onSavePost hook
		$newElementType = TranslationHelper::resolveElementType($newPostId);
		TranslationRepository::deleteByElement($newPostId, $newElementType);

		// Insert translation record for the new post
		TranslationRepository::insert([
			'group_id' => $groupId,
			'element_id' => $newPostId,
			'element_type' => $newElementType,
			'language_code' => $langCode,
			'source_language_code' => $sourceLanguage,
			'status' => Constants::STATUS_DRAFT,
		]);

		/**
		 * Fires after a translation post is created.
		 *
		 * @param int $newPostId
		 * @param int $sourcePostId
		 * @param string $langCode
		 * @param int $groupId
		 */
		do_action('signocore_ml_translation_created', $newPostId, $sourcePostId, $langCode, $groupId);

		return $newPostId;
	}

	/**
	 * Create a translated copy of an attachment with shared file reference.
	 */
	private function createAttachmentTranslation(\WP_Post $sourcePost, string $langCode): int|false
	{
		$filePath = get_post_meta($sourcePost->ID, '_wp_attached_file', true);
		$uploadDir = wp_get_upload_dir();

		$newPostId = wp_insert_attachment([
			'post_title' => $sourcePost->post_title,
			'post_content' => $sourcePost->post_content,
			'post_excerpt' => $sourcePost->post_excerpt,
			'post_mime_type' => $sourcePost->post_mime_type,
			'post_status' => 'inherit',
		], $uploadDir['basedir'] . '/' . $filePath);

		if (is_wp_error($newPostId) || $newPostId === 0) {
			return false;
		}

		// Copy attachment metadata (image sizes, dimensions, etc.)
		$metadata = get_post_meta($sourcePost->ID, '_wp_attachment_metadata', true);
		if (!empty($metadata)) {
			update_post_meta($newPostId, '_wp_attachment_metadata', $metadata);
		}

		// Copy alt text
		$altText = get_post_meta($sourcePost->ID, '_wp_attachment_image_alt', true);
		if ($altText !== '' && $altText !== false) {
			update_post_meta($newPostId, '_wp_attachment_image_alt', $altText);
		}

		// Link as translation
		$elementType = TranslationHelper::resolveElementType($sourcePost->ID);
		$sourceLanguage = TranslationHelper::getElementLanguage($sourcePost->ID, $elementType);

		$groupId = TranslationRepository::getGroupId($sourcePost->ID, $elementType);
		if ($groupId === null) {
			$groupId = TranslationRepository::getNextGroupId();

			TranslationRepository::insert([
				'group_id' => $groupId,
				'element_id' => $sourcePost->ID,
				'element_type' => $elementType,
				'language_code' => $sourceLanguage,
				'source_language_code' => null,
				'status' => Constants::STATUS_PUBLISHED,
			]);
		}

		// Remove any orphan record from onSavePost
		$newElementType = TranslationHelper::resolveElementType($newPostId);
		TranslationRepository::deleteByElement($newPostId, $newElementType);

		TranslationRepository::insert([
			'group_id' => $groupId,
			'element_id' => $newPostId,
			'element_type' => $newElementType,
			'language_code' => $langCode,
			'source_language_code' => $sourceLanguage,
			'status' => Constants::STATUS_PUBLISHED,
		]);

		/** @see createTranslation() for action documentation */
		do_action('signocore_ml_translation_created', $newPostId, $sourcePost->ID, $langCode, $groupId);

		return $newPostId;
	}

	/**
	 * Link an existing post as a translation of the source post.
	 */
	public function linkTranslation(int $sourcePostId, int $targetPostId, string $langCode): bool
	{
		$sourceType = TranslationHelper::resolveElementType($sourcePostId);
		$targetType = TranslationHelper::resolveElementType($targetPostId);

		// Verify target is not already in a different translation group
		$targetGroupId = TranslationRepository::getGroupId($targetPostId, $targetType);
		$sourceGroupId = TranslationRepository::getGroupId($sourcePostId, $sourceType);

		if ($targetGroupId !== null && $sourceGroupId !== null && $targetGroupId !== $sourceGroupId) {
			return false;
		}

		// Get or create group for source
		$groupId = $sourceGroupId;
		if ($groupId === null) {
			$groupId = TranslationRepository::getNextGroupId();

			$sourceLanguage = TranslationHelper::getElementLanguage($sourcePostId, $sourceType);

			TranslationRepository::insert([
				'group_id' => $groupId,
				'element_id' => $sourcePostId,
				'element_type' => $sourceType,
				'language_code' => $sourceLanguage,
				'source_language_code' => null,
				'status' => Constants::STATUS_PUBLISHED,
			]);
		}

		// Check if the target already has a record in this group
		$existingRecord = TranslationRepository::findByElementAndLanguage($targetPostId, $targetType, $langCode);

		if ($existingRecord !== null) {
			TranslationRepository::update((int) $existingRecord->id, [
				'group_id' => $groupId,
				'source_language_code' => TranslationHelper::getElementLanguage($sourcePostId, $sourceType),
			]);
		} else {
			TranslationRepository::insert([
				'group_id' => $groupId,
				'element_id' => $targetPostId,
				'element_type' => $targetType,
				'language_code' => $langCode,
				'source_language_code' => TranslationHelper::getElementLanguage($sourcePostId, $sourceType),
				'status' => Constants::STATUS_DRAFT,
			]);
		}

		/**
		 * Fires after a post is linked as a translation.
		 *
		 * @param int $targetPostId
		 * @param int $sourcePostId
		 * @param string $langCode
		 * @param int $groupId
		 */
		do_action('signocore_ml_translation_linked', $targetPostId, $sourcePostId, $langCode, $groupId);

		return true;
	}

	/**
	 * Remove a translation link without deleting the post.
	 */
	public function unlinkTranslation(int $postId, string $langCode): bool
	{
		$elementType = TranslationHelper::resolveElementType($postId);
		$record = TranslationRepository::findByElementAndLanguage($postId, $elementType, $langCode);

		if ($record === null) {
			return false;
		}

		$result = TranslationRepository::delete((int) $record->id);

		if ($result) {
			/**
			 * Fires after a translation link is removed.
			 *
			 * @param int $postId
			 * @param string $langCode
			 */
			do_action('signocore_ml_translation_unlinked', $postId, $langCode);
		}

		return $result;
	}

	/**
	 * Assign a language to a post that doesn't have one yet.
	 */
	public function assignLanguage(int $postId, string $langCode, ?string $sourceLanguage = null): bool
	{
		$elementType = TranslationHelper::resolveElementType($postId);
		$existing = TranslationRepository::findByElement($postId, $elementType);

		if ($existing !== null) {
			return TranslationRepository::update((int) $existing->id, [
				'language_code' => $langCode,
			]);
		}

		$groupId = TranslationRepository::getNextGroupId();

		$post = get_post($postId);
		$status = ($post !== null && $post->post_status === 'publish')
			? Constants::STATUS_PUBLISHED
			: Constants::STATUS_DRAFT;

		$result = TranslationRepository::insert([
			'group_id' => $groupId,
			'element_id' => $postId,
			'element_type' => $elementType,
			'language_code' => $langCode,
			'source_language_code' => $sourceLanguage,
			'status' => $status,
		]);

		return $result !== false;
	}

	/**
	 * Get all linked translations for a post, keyed by language code.
	 *
	 * @return array<string, int>
	 */
	public function getTranslationGroup(int $postId): array
	{
		return TranslationRepository::getTranslationsForPost($postId);
	}

	/**
	 * Return home URL for translated front pages so they show as /en/ instead of /en/slug/.
	 *
	 * @param string $link The page permalink.
	 * @param int|WP_Post $post Post ID or object.
	 */
	public function filterFrontPageLink(string $link, $post): string
	{
		if (get_option('show_on_front') !== 'page') {
			return $link;
		}

		$postId = $post instanceof WP_Post ? (int) $post->ID : (int) $post;
		$translations = $this->getFrontPageTranslations();

		if (in_array($postId, $translations, true)) {
			return trailingslashit(get_option('home'));
		}

		return $link;
	}

	/**
	 * Load translated front page when visiting a language root URL like /en/.
	 *
	 * Handles two scenarios:
	 * 1. Rewrite rules set sml_lang (Apache / correct nginx config)
	 * 2. Rewrite rules fail to match (some nginx configs set PHP_SELF to the
	 *    request URI, causing WordPress to skip permalink matching). In this case
	 *    WordPress auto-sets page_id to the front page, but the language filter
	 *    removes it because it's in the wrong language.
	 */
	public function resolveTranslatedFrontPage(WP_Query $query): void
	{
		if (is_admin() || !$query->is_main_query()) {
			return;
		}

		if (get_option('show_on_front') !== 'page') {
			return;
		}

		$frontPageId = (int) get_option('page_on_front');
		if ($frontPageId === 0) {
			return;
		}

		// Determine language: prefer rewrite rule query var, fall back to detected language
		$smlLang = $query->get('sml_lang') ?: LanguageDetection::load()->getCurrentLanguage();
		if ($smlLang === '') {
			return;
		}

		$defaultLang = LanguageManager::load()->getDefaultLanguageCode();
		if ($smlLang === $defaultLang) {
			return;
		}

		// Only act when no specific content was requested (language root URL).
		// Allow page_id equal to front page — WordPress auto-sets this when
		// parse_request does not match any rewrite rule.
		$pagename = $query->get('pagename');
		$name = $query->get('name');
		$pageId = (int) $query->get('page_id');

		if ($pagename !== '' || $name !== '') {
			return;
		}

		if ($pageId !== 0 && $pageId !== $frontPageId) {
			return;
		}

		// Don't override if WordPress has already identified this as an archive query.
		// This method should only act on language root URLs (e.g. /da/), not on
		// category, tag, date, author, or other archive pages.
		if ($query->is_category || $query->is_tag || $query->is_tax ||
			$query->is_archive || $query->is_author || $query->is_date ||
			$query->is_search || $query->is_post_type_archive) {
			return;
		}

		$translations = $this->getFrontPageTranslations();
		$translatedId = $translations[$smlLang] ?? null;

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

		$query->set('page_id', $translatedId);
		$query->set('post_type', 'page');
		$query->is_page = true;
		$query->is_singular = true;
		$query->is_home = false;
	}

	/**
	 * Prevent WordPress from redirecting translated front pages to their slug URL.
	 *
	 * @param string|false $redirectUrl The redirect URL.
	 * @param string $requestedUrl The original request URL.
	 * @return string|false
	 */
	public function preventFrontPageRedirect($redirectUrl, string $requestedUrl)
	{
		if ($redirectUrl === false) {
			return false;
		}

		// Prevent infinite redirect loops for missing translations.
		// When a non-default language URL points to a page that doesn't exist
		// (e.g., /sv/terms-of-service/ without a Swedish translation),
		// WordPress's redirect_canonical tries to redirect, causing a loop.
		$currentLang = LanguageDetection::load()->getCurrentLanguage();
		$defaultCode = LanguageManager::load()->getDefaultLanguageCode();

		if ($currentLang !== $defaultCode && is_404()) {
			return false;
		}

		$queriedObject = get_queried_object();
		if (!$queriedObject instanceof WP_Post) {
			return $redirectUrl;
		}

		$translations = $this->getFrontPageTranslations();

		if (in_array((int) $queriedObject->ID, $translations, true)) {
			return false;
		}

		return $redirectUrl;
	}

	/**
	 * Make is_front_page() return true for translated front pages.
	 *
	 * WordPress checks get_queried_object_id() === page_on_front.
	 * Translated pages have different IDs, so we filter the option
	 * to match the translated page ID when viewing one.
	 */
	public function fixFrontPageConditional(): void
	{
		if (get_option('show_on_front') !== 'page') {
			return;
		}

		$queriedId = get_queried_object_id();
		if ($queriedId <= 0) {
			return;
		}

		$translations = $this->getFrontPageTranslations();

		if (in_array($queriedId, $translations, true)) {
			add_filter('option_page_on_front', fn () => (string) $queriedId);
		}
	}

	/**
	 * Get cached front page translation group.
	 *
	 * @return array<string, int>
	 */
	private function getFrontPageTranslations(): array
	{
		if ($this->frontPageTranslations !== null) {
			return $this->frontPageTranslations;
		}

		$frontPageId = (int) get_option('page_on_front');
		if ($frontPageId === 0) {
			$this->frontPageTranslations = [];
			return $this->frontPageTranslations;
		}

		$this->frontPageTranslations = TranslationHelper::getTranslationGroup($frontPageId, 'post_page');

		// Ensure the front page itself is included
		if (!in_array($frontPageId, $this->frontPageTranslations, true)) {
			$lang = TranslationHelper::getElementLanguage($frontPageId, 'post_page');
			$this->frontPageTranslations[$lang] = $frontPageId;
		}

		return $this->frontPageTranslations;
	}

	/**
	 * Make is_home() return true for translated blog pages.
	 *
	 * WordPress checks get_queried_object_id() === page_for_posts.
	 * Translated pages have different IDs, so we filter the option
	 * to match the translated page ID when viewing one.
	 */
	public function fixBlogPageConditional(): void
	{
		if (get_option('show_on_front') !== 'page') {
			return;
		}

		$queriedId = get_queried_object_id();
		if ($queriedId <= 0) {
			return;
		}

		$translations = $this->getBlogPageTranslations();

		if (in_array($queriedId, $translations, true)) {
			add_filter('option_page_for_posts', fn () => (string) $queriedId);
		}
	}

	/**
	 * Get cached blog page translation group.
	 *
	 * @return array<string, int>
	 */
	private function getBlogPageTranslations(): array
	{
		if ($this->blogPageTranslations !== null) {
			return $this->blogPageTranslations;
		}

		$blogPageId = (int) get_option('page_for_posts');
		if ($blogPageId === 0) {
			$this->blogPageTranslations = [];
			return $this->blogPageTranslations;
		}

		$this->blogPageTranslations = TranslationHelper::getTranslationGroup($blogPageId, 'post_page');

		// Ensure the blog page itself is included
		if (!in_array($blogPageId, $this->blogPageTranslations, true)) {
			$lang = TranslationHelper::getElementLanguage($blogPageId, 'post_page');
			$this->blogPageTranslations[$lang] = $blogPageId;
		}

		return $this->blogPageTranslations;
	}

	/**
	 * Filter the main query on the frontend to only show posts in the current language.
	 */
	public function filterMainQuery(WP_Query $query): void
	{
		if (is_admin()) {
			return;
		}

		if (!$query->is_main_query()) {
			return;
		}

		// Fix blog listing page (page_for_posts): WordPress's parse_query() resolves
		// the blog page slug to a page_id, which re-sets is_page=true after the
		// page_for_posts detection cleared it. This causes SQL to use post_type='page'
		// instead of 'post'. Correct the flags so WordPress generates the right query.
		if ($query->is_home && !empty($query->is_posts_page)) {
			$query->is_page = false;
			$query->is_singular = false;
			$query->set('post_type', 'post');
		}

		$postType = $query->get('post_type');
		if ($postType === '') {
			$postType = 'post';
		}

		if (is_array($postType)) {
			$hasTranslatable = false;
			foreach ($postType as $type) {
				if (PostTypeHelper::isTranslatable($type)) {
					$hasTranslatable = true;
					break;
				}
			}
			if (!$hasTranslatable) {
				return;
			}
		} elseif (!PostTypeHelper::isTranslatable($postType)) {
			return;
		}

		$query->set('sml_filter_language', true);
	}

	/**
	 * Add JOIN clause for language filtering.
	 */
	public function joinTranslationTable(string $join, WP_Query $query): string
	{
		if (!$query->get('sml_filter_language')) {
			return $join;
		}

		global $wpdb;

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

		// Narrow the JOIN to the specific queried post type(s) to avoid
		// cross-type matches (e.g. post_page records matching a post query).
		$postType = $query->get('post_type');
		if ($postType === '' || $postType === 'any') {
			$typeCondition = "sml_t.element_type LIKE 'post_%'";
		} elseif (is_array($postType)) {
			$types = array_map(fn (string $t): string => "'" . esc_sql('post_' . $t) . "'", $postType);
			$typeCondition = 'sml_t.element_type IN (' . implode(',', $types) . ')';
		} else {
			$typeCondition = "sml_t.element_type = '" . esc_sql('post_' . $postType) . "'";
		}

		$join .= " LEFT JOIN `{$table}` AS sml_t ON ({$wpdb->posts}.ID = sml_t.element_id AND {$typeCondition})";

		return $join;
	}

	/**
	 * Add WHERE clause for language filtering.
	 *
	 * Posts without a translation record are treated as default-language content.
	 * They only appear on the default language frontend, not on other languages.
	 */
	public function whereTranslationLanguage(string $where, WP_Query $query): string
	{
		if (!$query->get('sml_filter_language')) {
			return $where;
		}

		$currentLang = LanguageDetection::load()->getCurrentLanguage();
		$defaultLang = LanguageManager::load()->getDefaultLanguageCode();

		if ($currentLang === $defaultLang) {
			$where .= " AND (sml_t.language_code = '" . esc_sql($currentLang) . "' OR sml_t.id IS NULL)";
		} else {
			$where .= " AND sml_t.language_code = '" . esc_sql($currentLang) . "'";
		}

		return $where;
	}
}
