<?php

namespace WPSN\SEO;

use WPSN\Core\Settings;
use WPSN\Domain\SeoDocumentContext;
use WPSN\Domain\SeoMeta;

defined('ABSPATH') || exit;

/**
 * Resolve hreflang alternates for the current document.
 *
 * Supported sources (best-effort):
 * - Polylang post/term/home translations
 * - WPML post/term/home translations
 * - `wpsn/hreflang/alternates` filter for custom integrations
 */
final class HreflangResolver
{
    private Settings $settings;

    public function __construct(Settings $settings)
    {
        $this->settings = $settings;
    }

    /**
     * @return array<int,array{hreflang:string,href:string}>
     */
    public function resolve(SeoDocumentContext $ctx, ?SeoMeta $meta = null): array
    {
        if (!(bool) $this->settings->get('hreflang.enabled', false)) {
            return [];
        }

        if (!$this->supportsContext($ctx)) {
            return [];
        }

        $alternates = [];
        $defaultUrl = '';

        [$polyAlternates, $polyDefault] = $this->resolvePolylangAlternates($ctx);
        if (!empty($polyAlternates)) {
            $alternates = array_merge($alternates, $polyAlternates);
        }
        if ($defaultUrl === '' && $polyDefault !== '') {
            $defaultUrl = $polyDefault;
        }

        [$wpmlAlternates, $wpmlDefault] = $this->resolveWpmlAlternates($ctx);
        if (!empty($wpmlAlternates)) {
            $alternates = array_merge($alternates, $wpmlAlternates);
        }
        if ($defaultUrl === '' && $wpmlDefault !== '') {
            $defaultUrl = $wpmlDefault;
        }

        $alternates = $this->normalizeAlternates($alternates);

        /**
         * Filter resolved hreflang alternates.
         *
         * Accepts either:
         * - ['en' => 'https://example.com/en/']
         * - [['hreflang' => 'en', 'href' => 'https://example.com/en/']]
         *
         * @param array<int|string,mixed> $alternates
         */
        $alternates = $this->normalizeAlternates((array) apply_filters('wpsn/hreflang/alternates', $alternates, $ctx, $meta));

        if ((bool) $this->settings->get('hreflang.include_x_default', false)) {
            $xDefault = trim((string) $this->settings->get('hreflang.x_default_url', ''));
            if ($xDefault === '') {
                $xDefault = $defaultUrl !== '' ? $defaultUrl : $this->currentUrl($ctx, $meta);
            }
            $xDefault = $this->normalizeUrl($xDefault);
            if ($xDefault !== '') {
                $alternates['x-default'] = $xDefault;
            }
        }

        if (empty($alternates)) {
            return [];
        }

        $xDefault = isset($alternates['x-default']) ? (string) $alternates['x-default'] : '';
        unset($alternates['x-default']);
        ksort($alternates, SORT_STRING);
        if ($xDefault !== '') {
            $alternates['x-default'] = $xDefault;
        }

        $out = [];
        foreach ($alternates as $hreflang => $href) {
            $hreflang = $this->normalizeLanguageTag((string) $hreflang);
            $href = $this->normalizeUrl((string) $href);
            if ($hreflang === '' || $href === '') {
                continue;
            }
            $out[] = [
                'hreflang' => $hreflang,
                'href' => $href,
            ];
        }

        return $out;
    }

    private function supportsContext(SeoDocumentContext $ctx): bool
    {
        if ($ctx->isPaged()) {
            return false;
        }

        if (in_array($ctx->type, [SeoDocumentContext::TYPE_SEARCH, SeoDocumentContext::TYPE_404], true)) {
            return false;
        }

        return in_array($ctx->type, [
            SeoDocumentContext::TYPE_SINGULAR,
            SeoDocumentContext::TYPE_TERM,
            SeoDocumentContext::TYPE_HOME,
        ], true);
    }

    /**
     * @return array{0:array<int,array{hreflang:string,href:string}>,1:string}
     */
    private function resolvePolylangAlternates(SeoDocumentContext $ctx): array
    {
        if (!$this->looksLikePolylangAvailable()) {
            return [[], ''];
        }

        $alternates = [];
        $defaultUrl = '';
        $defaultLang = function_exists('pll_default_language') ? (string) pll_default_language('slug') : '';

        if ($ctx->isSingular() && function_exists('pll_get_post_translations')) {
            $translations = pll_get_post_translations((int) $ctx->objectId);
            if (is_array($translations)) {
                foreach ($translations as $lang => $translatedId) {
                    $url = $this->normalizeUrl(get_permalink((int) $translatedId));
                    if ($url === '') {
                        continue;
                    }
                    $alternates[] = [
                        'hreflang' => (string) $lang,
                        'href' => $url,
                    ];
                    if ($defaultUrl === '' && $defaultLang !== '' && strtolower((string) $lang) === strtolower($defaultLang)) {
                        $defaultUrl = $url;
                    }
                }
            }
        } elseif ($ctx->isTerm() && function_exists('pll_get_term_translations')) {
            $translations = pll_get_term_translations((int) $ctx->objectId);
            if (is_array($translations)) {
                foreach ($translations as $lang => $translatedId) {
                    $url = $this->normalizeTermUrl((int) $translatedId, (string) ($ctx->objectSubType ?? ''));
                    if ($url === '') {
                        continue;
                    }
                    $alternates[] = [
                        'hreflang' => (string) $lang,
                        'href' => $url,
                    ];
                    if ($defaultUrl === '' && $defaultLang !== '' && strtolower((string) $lang) === strtolower($defaultLang)) {
                        $defaultUrl = $url;
                    }
                }
            }
        } elseif ($ctx->type === SeoDocumentContext::TYPE_HOME && function_exists('pll_languages_list')) {
            $languages = pll_languages_list(['fields' => 'slug']);
            if (is_array($languages)) {
                foreach ($languages as $lang) {
                    $lang = (string) $lang;
                    if ($lang === '' || !function_exists('pll_home_url')) {
                        continue;
                    }
                    $url = $this->normalizeUrl((string) pll_home_url($lang));
                    if ($url === '') {
                        continue;
                    }
                    $alternates[] = [
                        'hreflang' => $lang,
                        'href' => $url,
                    ];
                    if ($defaultUrl === '' && $defaultLang !== '' && strtolower($lang) === strtolower($defaultLang)) {
                        $defaultUrl = $url;
                    }
                }
            }
        }

        return [$alternates, $defaultUrl];
    }

    /**
     * @return array{0:array<int,array{hreflang:string,href:string}>,1:string}
     */
    private function resolveWpmlAlternates(SeoDocumentContext $ctx): array
    {
        $languages = apply_filters('wpml_active_languages', null, ['skip_missing' => 0, 'orderby' => 'code']);
        if (!is_array($languages) || empty($languages)) {
            return [[], ''];
        }

        $alternates = [];
        $defaultUrl = '';
        $defaultLang = apply_filters('wpml_default_language', null);
        $defaultLang = is_string($defaultLang) ? trim($defaultLang) : '';

        if ($ctx->isSingular()) {
            $elementType = (string) ($ctx->objectSubType ?? 'post');

            foreach ($languages as $langKey => $row) {
                $langCode = $this->wpmlLanguageCode($langKey, $row);
                if ($langCode === '') {
                    continue;
                }

                $translatedId = apply_filters('wpml_object_id', (int) $ctx->objectId, $elementType, false, $langCode);
                $translatedId = (int) $translatedId;
                if ($translatedId < 1) {
                    continue;
                }

                $url = $this->normalizeUrl(get_permalink($translatedId));
                if ($url === '') {
                    continue;
                }

                $alternates[] = [
                    'hreflang' => $langCode,
                    'href' => $url,
                ];
                if ($defaultUrl === '' && $defaultLang !== '' && strtolower($langCode) === strtolower($defaultLang)) {
                    $defaultUrl = $url;
                }
            }
        } elseif ($ctx->isTerm()) {
            $taxonomy = (string) ($ctx->objectSubType ?? 'category');

            foreach ($languages as $langKey => $row) {
                $langCode = $this->wpmlLanguageCode($langKey, $row);
                if ($langCode === '') {
                    continue;
                }

                $translatedId = apply_filters('wpml_object_id', (int) $ctx->objectId, $taxonomy, false, $langCode);
                $translatedId = (int) $translatedId;
                if ($translatedId < 1) {
                    continue;
                }

                $url = $this->normalizeTermUrl($translatedId, $taxonomy);
                if ($url === '') {
                    continue;
                }

                $alternates[] = [
                    'hreflang' => $langCode,
                    'href' => $url,
                ];
                if ($defaultUrl === '' && $defaultLang !== '' && strtolower($langCode) === strtolower($defaultLang)) {
                    $defaultUrl = $url;
                }
            }
        } elseif ($ctx->type === SeoDocumentContext::TYPE_HOME) {
            foreach ($languages as $langKey => $row) {
                $langCode = $this->wpmlLanguageCode($langKey, $row);
                if ($langCode === '') {
                    continue;
                }

                $url = '';
                if (is_array($row) && isset($row['url']) && is_string($row['url'])) {
                    $url = $this->normalizeUrl((string) $row['url']);
                }
                if ($url === '') {
                    $maybe = apply_filters('wpml_permalink', home_url('/'), $langCode);
                    $url = is_string($maybe) ? $this->normalizeUrl($maybe) : '';
                }
                if ($url === '') {
                    continue;
                }

                $alternates[] = [
                    'hreflang' => $langCode,
                    'href' => $url,
                ];
                if ($defaultUrl === '' && $defaultLang !== '' && strtolower($langCode) === strtolower($defaultLang)) {
                    $defaultUrl = $url;
                }
            }
        }

        return [$alternates, $defaultUrl];
    }

    private function looksLikePolylangAvailable(): bool
    {
        if (defined('POLYLANG_VERSION')) {
            return true;
        }

        return function_exists('pll_current_language')
            || function_exists('pll_languages_list')
            || function_exists('pll_get_post_translations')
            || function_exists('pll_get_term_translations');
    }

    /**
     * @param array<int|string,mixed> $alternates
     * @return array<string,string>
     */
    private function normalizeAlternates(array $alternates): array
    {
        $out = [];

        foreach ($alternates as $key => $row) {
            $hreflang = '';
            $href = '';

            if (is_array($row)) {
                if (isset($row['hreflang']) && is_string($row['hreflang'])) {
                    $hreflang = (string) $row['hreflang'];
                } elseif (is_string($key)) {
                    $hreflang = $key;
                }

                if (isset($row['href']) && is_string($row['href'])) {
                    $href = (string) $row['href'];
                } elseif (isset($row['url']) && is_string($row['url'])) {
                    $href = (string) $row['url'];
                }
            } elseif (is_string($key) && (is_string($row) || is_numeric($row))) {
                $hreflang = $key;
                $href = (string) $row;
            }

            $hreflang = $this->normalizeLanguageTag($hreflang);
            $href = $this->normalizeUrl($href);

            if ($hreflang === '' || $href === '') {
                continue;
            }

            $out[$hreflang] = $href;
        }

        return $out;
    }

    private function currentUrl(SeoDocumentContext $ctx, ?SeoMeta $meta = null): string
    {
        $url = '';
        if ($meta instanceof SeoMeta && is_string($meta->canonical) && trim($meta->canonical) !== '') {
            $url = trim($meta->canonical);
        }
        if ($url === '') {
            $url = $ctx->url;
        }

        return $this->normalizeUrl($url);
    }

    private function normalizeTermUrl(int $termId, string $taxonomy): string
    {
        $link = get_term_link($termId, $taxonomy);
        if (is_wp_error($link) || !is_string($link)) {
            return '';
        }

        return $this->normalizeUrl($link);
    }

    private function normalizeUrl(string $url): string
    {
        $url = trim($url);
        if ($url === '') {
            return '';
        }

        if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) {
            $url = home_url($url);
        }

        return esc_url_raw($url);
    }

    private function normalizeLanguageTag(string $tag): string
    {
        $tag = trim(str_replace('_', '-', $tag));
        if ($tag === '') {
            return '';
        }

        if (strtolower($tag) === 'x-default') {
            return 'x-default';
        }

        $parts = preg_split('/-+/', $tag) ?: [];
        $parts = array_values(array_filter(array_map('trim', $parts), static function (string $part): bool {
            return $part !== '';
        }));

        if (empty($parts)) {
            return '';
        }

        foreach ($parts as $i => $part) {
            $len = strlen($part);
            if ($i === 0) {
                $parts[$i] = strtolower($part);
                continue;
            }

            if ($len === 2 || $len === 3) {
                $parts[$i] = strtoupper($part);
                continue;
            }

            $parts[$i] = ucfirst(strtolower($part));
        }

        return implode('-', $parts);
    }

    /**
     * @param mixed $row
     */
    private function wpmlLanguageCode($langKey, $row): string
    {
        if (is_array($row)) {
            if (isset($row['language_code']) && is_string($row['language_code']) && trim($row['language_code']) !== '') {
                return trim((string) $row['language_code']);
            }
            if (isset($row['code']) && is_string($row['code']) && trim($row['code']) !== '') {
                return trim((string) $row['code']);
            }
        }

        return is_string($langKey) ? trim($langKey) : '';
    }
}
