<?php
declare(strict_types=1);

namespace App\Slug;

use App\Support\TextNormalizer;

final class Slugger
{
    /** @var array<string,string> */
    private const SCRIPT_MAP = [
        "\u{0627}" => 'a', "\u{0628}" => 'b', "\u{062A}" => 't', "\u{062B}" => 'th',
        "\u{062C}" => 'j', "\u{062D}" => 'h', "\u{062E}" => 'kh', "\u{062F}" => 'd',
        "\u{0630}" => 'dh', "\u{0631}" => 'r', "\u{0632}" => 'z', "\u{0633}" => 's',
        "\u{0634}" => 'sh', "\u{0635}" => 's', "\u{0636}" => 'd', "\u{0637}" => 't',
        "\u{0638}" => 'z', "\u{0639}" => 'a', "\u{063A}" => 'gh', "\u{0641}" => 'f',
        "\u{0642}" => 'q', "\u{0643}" => 'k', "\u{0644}" => 'l', "\u{0645}" => 'm',
        "\u{0646}" => 'n', "\u{0647}" => 'h', "\u{0648}" => 'w', "\u{064A}" => 'y',
        "\u{067E}" => 'p', "\u{0686}" => 'ch', "\u{0698}" => 'zh', "\u{06AF}" => 'g',
    ];

    /**
     * Process-wide default, set once per request from the seo.slug_style setting
     * -- the same shape as the timezone, and for the same reason: every service
     * that makes a slug would otherwise have to be handed the preference.
     */
    private static bool $defaultUnicode = false;

    private readonly TextNormalizer $normalizer;
    private readonly bool $unicode;

    public function __construct(?TextNormalizer $normalizer = null, ?bool $unicode = null)
    {
        $this->normalizer = $normalizer ?? new TextNormalizer();
        $this->unicode = $unicode ?? self::$defaultUnicode;
    }

    /** Called from the front controller once the settings table has answered. */
    public static function useUnicodeByDefault(bool $unicode): void
    {
        self::$defaultUnicode = $unicode;
    }

    public function slug(string $value, string $fallback = 'question', int $maxLength = 191): string
    {
        $maxLength = max(1, $maxLength);
        $normalized = $this->normalizer->normalize(trim($value));

        // Arabic transliterated to ASCII reads as nonsense in both languages and
        // matches no search query: "ما هي أفضل المواقع" becomes "ma-hy-afdl-almwaqa".
        // Keeping the script means the words stay readable and searchable; the
        // URL is percent-encoded on the wire and decoded again by the router.
        // Built from the original text, not the normalized one: TextNormalizer
        // folds أ إ آ to ا and ة to ه so that search matches loosely, which is
        // right for matching and wrong for a URL a reader sees -- it would spell
        // مجانية as مجانيه in the address bar and in the search result.
        $slug = $this->unicode ? $this->unicodeSlug(trim($value)) : '';

        if ($slug === '') {
            $slug = $this->asciiSlug($normalized);
        }

        if ($slug === '' && $normalized !== '') {
            $slug = $this->percentEncodedSlug($normalized);
        }

        if ($slug === '') {
            $slug = $this->asciiSlug($this->normalizer->normalize(trim($fallback)));
        }

        return $this->limitSlug($slug === '' ? 'question' : $slug, $maxLength);
    }

    public function unique(string $value, callable $exists, int $maxLength = 191): string
    {
        $maxLength = max(1, $maxLength);
        $base = $this->slug($value, 'question', $maxLength);
        $candidate = $base;
        $suffix = 2;

        while ($exists($candidate)) {
            $suffixText = '-' . $suffix;
            $baseLength = max(1, $maxLength - strlen($suffixText));
            $candidate = rtrim($this->limitSlug($base, $baseLength), '-') . $suffixText;
            $suffix++;

            if ($suffix > 1000000) {
                throw new \RuntimeException('Unable to create a unique slug.');
            }
        }

        return $candidate;
    }

    private function asciiSlug(string $value): string
    {
        $value = strtr($value, self::SCRIPT_MAP);
        if (function_exists('iconv')) {
            $converted = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
            if ($converted !== false) {
                $value = $converted;
            }
        }

        $value = strtolower($value);
        $value = preg_replace('/[^a-z0-9]+/', '-', $value) ?? '';
        return trim($value, '-');
    }

    /**
     * Letters and digits of any script survive; everything else becomes a
     * separator. Stored decoded, so the column stays readable and a lookup can
     * compare it against the router's rawurldecode()d path segment directly.
     */
    private function unicodeSlug(string $value): string
    {
        if (preg_match('/[^\x00-\x7F]/', $value) !== 1) {
            // Latin text gains nothing from this path and would only risk
            // diverging from the ASCII rules the rest of the site was built on.
            return '';
        }

        if (class_exists('Normalizer')) {
            $composed = \Normalizer::normalize($value, \Normalizer::FORM_KC);
            if (is_string($composed)) {
                $value = $composed;
            }
        }

        $value = str_replace("\u{0640}", '', $value);                      // tatweel
        $value = preg_replace('/[\p{Mn}\p{Me}]+/u', '', $value) ?? $value; // harakat
        $value = function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : $value;
        $value = preg_replace('/[^\p{L}\p{N}]+/u', '-', $value) ?? '';

        return trim($value, '-');
    }

    private function percentEncodedSlug(string $value): string
    {
        return trim(rawurlencode($value), '-');
    }

    private function limitSlug(string $value, int $maxLength): string
    {
        if (strlen($value) <= $maxLength) {
            return $value;
        }

        // A decoded multi-byte slug must not be cut through a character: the
        // truncated byte would make the column value invalid UTF-8 and the URL
        // unmatchable.
        if (!str_contains($value, '%') && preg_match('/[^\x00-\x7F]/', $value) === 1) {
            while ($value !== '' && strlen($value) > $maxLength) {
                $value = quaeriva_utf8_substr($value, 0, quaeriva_utf8_length($value) - 1);
            }

            return trim($value, '-');
        }

        $value = substr($value, 0, $maxLength);
        $lastPercent = strrpos($value, '%');
        if ($lastPercent !== false && strlen($value) - $lastPercent < 3) {
            $value = substr($value, 0, $lastPercent);
        }
        return $value;
    }
}
