<?php
/**
 * SEO class - hreflang tags, canonical URLs, Open Graph, sitemap
 *
 * @package LangForge
 */

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

class LF_SEO {

    private static $instance = null;

    public static function instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        $this->init_hooks();
    }

    private function init_hooks() {
        // Hreflang and canonical tags
        add_action('wp_head', [$this, 'output_hreflang_tags'], 1);
        add_action('wp_head', [$this, 'output_canonical_tag'], 2);

        // Open Graph locale tags
        add_filter('language_attributes', [$this, 'filter_language_attributes']);
        add_action('wp_head', [$this, 'output_og_locale_tags'], 3);

        // Remove WP's default canonical ONLY when LangForge is the one
        // emitting it. When we defer to WPML (is_wpml_active), our own
        // output_canonical_tag/output_hreflang_tags bail out — so if we
        // ALSO stripped rel_canonical here the page would end up with no
        // canonical at all. Decide on `wp` (after the main query is known)
        // so should_defer_to_wpml can run its is_admin/conditional checks.
        add_action('wp', [$this, 'maybe_remove_default_canonical'], 20);

        // WordPress native sitemap - add hreflang links
        add_filter('wp_sitemaps_posts_entry', [$this, 'add_sitemap_hreflang'], 10, 3);
        add_filter('wp_sitemaps_posts_query_args', [$this, 'filter_sitemap_query'], 10, 2);

        // Yoast SEO compatibility
        add_filter('wpseo_sitemap_entry', [$this, 'yoast_sitemap_entry'], 10, 3);

        // Copy Yoast SEO meta on translation creation
        add_action('lf_translation_created', [$this, 'copy_yoast'], 15, 3);
    }

    private function should_defer_to_wpml() {
        return LF_Core::instance()->is_wpml_active();
    }

    /**
 * Strip WordPress' built-in canonical link ONLY when LangForge is going
 * to output its own. If WPML is active we defer the canonical to it (and
 * our output_canonical_tag returns early), so removing rel_canonical in
 * that case would leave the page with no canonical tag whatsoever — the
 * exact regression this guard fixes. Hooked on `wp` so the conditional
 * tags inside should_defer_to_wpml are available.
 */
    public function maybe_remove_default_canonical() {
        if (is_admin()) {
            return;
        }
        if ($this->should_defer_to_wpml()) {
            return; // WPML / WP core own the canonical here.
        }
        remove_action('wp_head', 'rel_canonical');
    }

    /**
 * Output hreflang link tags for all translations of the current page
 */
    public function output_hreflang_tags() {
        if (is_admin() || $this->should_defer_to_wpml()) {
            return;
        }

        $languages = LF_Core::instance()->get_active_languages();
        $default_lang = LF_Core::instance()->get_default_language();

        if (count($languages) < 2) {
            return;
        }

        $hreflang_urls = $this->get_hreflang_urls();

        if (empty($hreflang_urls)) {
            return;
        }

        echo "\n<!-- LangForge hreflang tags -->\n";

        foreach ($hreflang_urls as $lang_code => $url) {
            $locale = $this->get_hreflang_code($lang_code);
            printf(
                '<link rel="alternate" hreflang="%s" href="%s" />' . "\n",
                esc_attr($locale),
                esc_url($url)
            );
        }

        // x-default points to default language
        if (isset($hreflang_urls[$default_lang])) {
            printf(
                '<link rel="alternate" hreflang="x-default" href="%s" />' . "\n",
                esc_url($hreflang_urls[$default_lang])
            );
        }

        echo "<!-- /LangForge hreflang tags -->\n\n";
    }

    /**
 * Output canonical tag with language awareness
 */
    public function output_canonical_tag() {
        if (is_admin() || is_search() || is_404() || $this->should_defer_to_wpml()) {
            return;
        }

        $canonical = $this->get_canonical_url();

        if ($canonical) {
            printf(
                '<link rel="canonical" href="%s" />' . "\n",
                esc_url($canonical)
            );
        }
    }

    /**
 * Filter html lang attribute
 */
    public function filter_language_attributes($output) {
        if ($this->should_defer_to_wpml()) {
            return $output;
        }
        $current_lang = LF_Core::instance()->get_current_language();
        $languages = LF_Core::instance()->get_active_languages();

        if (isset($languages[$current_lang]['locale'])) {
            $locale = str_replace('_', '-', $languages[$current_lang]['locale']);
            $output = preg_replace('/lang="[^"]*"/', 'lang="' . esc_attr($locale) . '"', $output);
        }

        return $output;
    }

    /**
 * Output Open Graph locale tags
 */
    public function output_og_locale_tags() {
        if (is_admin() || $this->should_defer_to_wpml()) {
            return;
        }

        $current_lang = LF_Core::instance()->get_current_language();
        $languages = LF_Core::instance()->get_active_languages();

        if (!isset($languages[$current_lang])) {
            return;
        }

        // Current locale — fall back to language code when admin saved the
        // language without an explicit locale (e.g. legacy fixtures from
        // earlier releases that omitted the field). Avoids the PHP 8.0+
        // warning "Undefined array key 'locale'".
        $current_locale = $languages[$current_lang]['locale'] ?? $current_lang;
        printf(
            '<meta property="og:locale" content="%s" />' . "\n",
            esc_attr($current_locale)
        );

        // Alternate locales — same fallback.
        foreach ($languages as $code => $lang) {
            if ($code === $current_lang) {
                continue;
            }
            $alt_locale = isset($lang['locale']) ? $lang['locale'] : $code;
            printf(
                '<meta property="og:locale:alternate" content="%s" />' . "\n",
                esc_attr($alt_locale)
            );
        }
    }

    /**
 * Get hreflang URLs for the current page
 */
    private function get_hreflang_urls() {
        $languages = LF_Core::instance()->get_active_languages();
        $urls = [];

        if (is_singular()) {
            $post_id = get_queried_object_id();
            $translations = LF_Post_Translation::instance()->get_translations($post_id);

            foreach ($languages as $code => $lang) {
                if (isset($translations[$code])) {
                    $trans_post = get_post($translations[$code]);
                    // Only include published translations in hreflang
                    if ($trans_post && $trans_post->post_status === 'publish') {
                        $urls[$code] = get_permalink($translations[$code]);
                    }
                } elseif (get_post_meta($post_id, '_lf_language', true) === $code) {
                    $urls[$code] = get_permalink($post_id);
                }
            }
        } elseif (is_front_page() || is_home()) {
            // Homepage - generate language URLs
            foreach ($languages as $code => $lang) {
                $urls[$code] = LF_Core::instance()->get_language_url($code);
            }
        } elseif (is_archive() || is_tax() || is_category() || is_tag()) {
            // Archives - generate language URLs
            foreach ($languages as $code => $lang) {
                $urls[$code] = LF_Core::instance()->get_language_url($code);
            }
        }

        return $urls;
    }

    /**
 * Get canonical URL for the current page
 */
    private function get_canonical_url() {
        if (is_singular()) {
            $post_id = get_queried_object_id();
            return get_permalink($post_id);
        }

        if (is_front_page() || is_home()) {
            $current_lang = LF_Core::instance()->get_current_language();
            return LF_Core::instance()->get_language_url($current_lang);
        }

        // Default: current URL without pagination params
        global $wp;
        return home_url(add_query_arg([], $wp->request));
    }

    /**
 * Convert language code to hreflang format
 * E.g., 'en' -> 'en', 'zh' -> 'zh-CN'
 */
    private function get_hreflang_code($lang_code) {
        $languages = LF_Core::instance()->get_active_languages();

        if (!isset($languages[$lang_code])) {
            return $lang_code;
        }

        // Defensive: legacy language fixtures may omit 'locale'; fall back to
        // the WordPress-canonical form (e.g. `en` → `en_US`).
        $locale = isset($languages[$lang_code]['locale'])
            ? $languages[$lang_code]['locale']
            : $lang_code;

        // If locale has country code (e.g., pt_BR, zh_CN), use full hreflang
        if (strpos($locale, '_') !== false) {
            $parts = explode('_', $locale);
            $language = strtolower($parts[0]);
            $country = strtoupper($parts[1]);

            // For common languages where country matters (pt-BR vs pt-PT, zh-CN vs zh-TW)
            $needs_country = ['zh', 'pt', 'en'];
            if (in_array($language, $needs_country)) {
                return $language . '-' . $country;
            }
        }

        return strtolower($lang_code);
    }

    /**
 * Add hreflang links to WordPress native sitemap entries
 */
    public function add_sitemap_hreflang($sitemap_entry, $post_type, $post) {
        $languages = LF_Core::instance()->get_active_languages();

        if (count($languages) < 2) {
            return $sitemap_entry;
        }

        $translations = LF_Post_Translation::instance()->get_translations($post->ID);
        $post_lang = get_post_meta($post->ID, '_lf_language', true);

        if (empty($translations) && empty($post_lang)) {
            return $sitemap_entry;
        }

        // Build xhtml:link entries for all translations
        $links = [];
        foreach ($languages as $code => $lang) {
            $url = null;
            if (isset($translations[$code])) {
                $url = get_permalink($translations[$code]);
            } elseif ($post_lang === $code) {
                $url = get_permalink($post->ID);
            }

            if ($url) {
                $links[] = [
                    'rel' => 'alternate',
                    'hreflang' => $this->get_hreflang_code($code),
                    'href' => $url,
                ];
            }
        }

        // Add x-default
        $default_lang = LF_Core::instance()->get_default_language();
        if (isset($translations[$default_lang])) {
            $links[] = [
                'rel' => 'alternate',
                'hreflang' => 'x-default',
                'href' => get_permalink($translations[$default_lang]),
            ];
        }

        if (!empty($links)) {
            $sitemap_entry['links'] = $links;
        }

        return $sitemap_entry;
    }

    /**
 * Filter sitemap queries to only include posts in their primary language
 * (prevent duplicate entries for translations)
 */
    public function filter_sitemap_query($args, $post_type) {
        $translatable = LF_Core::get_translatable_post_types();

        if (!in_array($post_type, $translatable)) {
            return $args;
        }

        // Include all posts that have a language set - the hreflang links
        // will point to translations. Don't filter by language so all
        // language versions appear in the sitemap.
        return $args;
    }

    /**
 * Copy Yoast SEO meta fields when creating a translation
 */
    public function copy_yoast($source_id, $new_id, $target_lang) {
        if (!defined('WPSEO_VERSION')) {
            return;
        }

        $meta_keys = [
            '_yoast_wpseo_title',
            '_yoast_wpseo_metadesc',
            '_yoast_wpseo_focuskw',
            '_yoast_wpseo_canonical',
            '_yoast_wpseo_meta-robots-noindex',
            '_yoast_wpseo_meta-robots-nofollow',
            '_yoast_wpseo_opengraph-title',
            '_yoast_wpseo_opengraph-description',
            '_yoast_wpseo_opengraph-image',
            '_yoast_wpseo_twitter-title',
            '_yoast_wpseo_twitter-description',
            '_yoast_wpseo_twitter-image',
            '_yoast_wpseo_schema_article_type',
            '_yoast_wpseo_schema_page_type',
            '_yoast_wpseo_primary_category',
        ];

        foreach ($meta_keys as $key) {
            $val = get_post_meta($source_id, $key, true);
            if ($val !== '' && $val !== false) {
                update_post_meta($new_id, $key, $val);
            }
        }
    }

    /**
 * Yoast SEO sitemap compatibility - add hreflang to entries
 */
    public function yoast_sitemap_entry($url, $type, $post) {
        if (!is_object($post) || !isset($post->ID)) {
            return $url;
        }

        $languages = LF_Core::instance()->get_active_languages();
        if (count($languages) < 2) {
            return $url;
        }

        $translations = LF_Post_Translation::instance()->get_translations($post->ID);

        if (empty($translations)) {
            return $url;
        }

        // Yoast uses 'languages' key for hreflang
        if (!isset($url['languages'])) {
            $url['languages'] = [];
        }

        foreach ($languages as $code => $lang) {
            $trans_url = null;
            if (isset($translations[$code])) {
                $trans_url = get_permalink($translations[$code]);
            } elseif (get_post_meta($post->ID, '_lf_language', true) === $code) {
                $trans_url = get_permalink($post->ID);
            }

            if ($trans_url) {
                $url['languages'][$this->get_hreflang_code($code)] = $trans_url;
            }
        }

        return $url;
    }
}
