<?php

declare(strict_types=1);

namespace App\Traits;

/**
 * Trait for detecting hashed, obfuscated, and gibberish release names.
 *
 * Provides granular detection methods that can be used independently
 * (for distinct matchedBy tags) or combined via isHashedOrGibberish().
 *
 * Used by both MiscCategorizer (categorization pipeline) and
 * FileNameCleaner (name-fixing) to keep detection logic in sync.
 */
trait DetectsHashedNames
{
    /**
     * Combined check: returns true if the name appears hashed or gibberish.
     */
    public function isHashedOrGibberish(string $name): bool
    {
        $cleaned = $this->stripExtensionsForAnalysis($name);
        $coreName = $this->getCoreNameWithoutSeparators($cleaned);

        return $this->isUuidPattern($coreName)
            || $this->isPureHexString($coreName)
            || $this->isBoundedMd5Hash($name)
            || $this->isBoundedSha1Hash($name)
            || $this->isBoundedSha256Hash($name)
            || $this->isBoundedGenericHash($name)
            || $this->isBase64LikeToken($name)
            || $this->isObfuscatedUppercaseString($name)
            || $this->isObfuscatedMixedAlphanumeric($name)
            || $this->isObfuscatedUsenetFilename($name)
            || $this->isRandomByCharacterAnalysis($coreName, $name)
            || $this->hasInsufficientWordStructure($coreName)
            || $this->isZeroVowelLongToken($coreName)
            || $this->isRandomDigitPattern($coreName);
    }

    // ---------------------------------------------------------------
    // Hex / hash boundary checks (operate on the raw release name)
    // ---------------------------------------------------------------

    /**
     * Detect MD5 hash (32 hex characters) with word boundaries.
     */
    protected function isBoundedMd5Hash(string $name): bool
    {
        return (bool) preg_match('/(?:^|["\'\s\[\]\/\-])([a-f0-9]{32})(?:["\'\s\[\]\/.\-]|$)/i', $name);
    }

    /**
     * Detect SHA-1 hash (40 hex characters) with word boundaries.
     */
    protected function isBoundedSha1Hash(string $name): bool
    {
        return (bool) preg_match('/(?:^|["\'\s\[\]\/\-])([a-f0-9]{40})(?:["\'\s\[\]\/.\-]|$)/i', $name);
    }

    /**
     * Detect SHA-256 hash (64 hex characters) with word boundaries.
     */
    protected function isBoundedSha256Hash(string $name): bool
    {
        return (bool) preg_match('/(?:^|["\'\s\[\]\/\-])([a-f0-9]{64})(?:["\'\s\[\]\/.\-]|$)/i', $name);
    }

    /**
     * Detect generic long hex hash (32-128 chars) with word boundaries.
     */
    protected function isBoundedGenericHash(string $name): bool
    {
        if (! preg_match('/(?:^|["\'\s\[\]\/\-])([a-z0-9]{24,128})(?:["\'\s\[\]\/.\-]|$)/i', $name, $matches)) {
            return false;
        }

        $token = $matches[1];

        if (preg_match('/\b(19|20)\d{2}\b/', $token)) {
            return false;
        }

        return preg_match('/\d/', $token) === 1
            && $this->looksLikeRandomString($token);
    }

    /**
     * Detect base64/base64url-like tokens that commonly appear in obfuscated subjects.
     */
    protected function isBase64LikeToken(string $name): bool
    {
        if (! preg_match('/(?:^|["\'\s\[\]\/\-])([A-Za-z0-9+\/_=-]{24,})(?:["\'\s\[\]\/.\-]|$)/', $name, $matches)) {
            return false;
        }

        $token = rtrim($matches[1], '=');

        if (strlen($token) < 24 || preg_match('/\b(19|20)\d{2}\b/', $token)) {
            return false;
        }

        if (! preg_match('/[A-Z]/', $token) || ! preg_match('/[a-z]/', $token) || ! preg_match('/\d/', $token)) {
            return false;
        }

        return $this->looksLikeRandomString($token);
    }

    // ---------------------------------------------------------------
    // Core-name checks (operate on stripped / separator-free names)
    // ---------------------------------------------------------------

    /**
     * Detect UUID patterns (with or without dashes stripped).
     */
    protected function isUuidPattern(string $coreName): bool
    {
        return (bool) preg_match('/^[a-f0-9]{8}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{12}$/i', $coreName);
    }

    /**
     * Detect pure hex strings of 16+ characters.
     */
    protected function isPureHexString(string $coreName, int $minLength = 16): bool
    {
        return (bool) preg_match('/^[a-f0-9]{'.$minLength.',}$/i', $coreName);
    }

    /**
     * Detect all-uppercase alphanumeric strings of 15+ characters.
     */
    protected function isObfuscatedUppercaseString(string $name): bool
    {
        return (bool) preg_match('/^[A-Z0-9]{15,}$/', $name);
    }

    /**
     * Detect mixed-case alphanumeric strings of 15+ characters
     * that are not CamelCase words and contain no year pattern.
     */
    protected function isObfuscatedMixedAlphanumeric(string $name): bool
    {
        return (bool) preg_match('/^[a-zA-Z0-9]{15,}$/', $name)
            && ! preg_match('/\b(19|20)\d{2}\b/', $name)
            && ! preg_match('/^[A-Z][a-z]+([A-Z][a-z]+)+$/', $name);
    }

    /**
     * Detect obfuscated filenames embedded in usenet subject lines.
     *
     * Matches patterns like: [XX/XX] - "RANDOMSTRING.partXX.rar"
     */
    protected function isObfuscatedUsenetFilename(string $name): bool
    {
        if (preg_match('/\[\d+\/\d+]\s*-\s*"([a-zA-Z0-9]{12,})\.(part\d+\.rar|7z\.\d{3}|rar|zip|vol\d+\+\d+\.par2|par2)"/i', $name, $matches)) {
            $filename = $matches[1];

            return ! preg_match('/[._ -]/', $filename)
                && ! preg_match('/\b(19|20)\d{2}\b/', $filename)
                && $this->looksLikeRandomString($filename);
        }

        return false;
    }

    /**
     * Detect names consisting of only punctuation and numbers with no clear structure.
     */
    protected function isObfuscatedPunctuation(string $name): bool
    {
        return (bool) preg_match('/^[^a-zA-Z]*[A-Z0-9._\-]{5,}[^a-zA-Z]*$/', $name)
            && ! preg_match('/\.(mkv|avi|mp4|mp3|flac|pdf|epub|exe|iso)$/i', $name);
    }

    // ---------------------------------------------------------------
    // Character-analysis heuristics
    // ---------------------------------------------------------------

    /**
     * Analyse character transitions to detect random strings.
     *
     * A high transition rate (upper↔lower, letter↔digit) with no
     * media keywords in the original name suggests randomness.
     *
     * @param  string  $coreName  Name with extensions and separators stripped.
     * @param  string  $originalName  The unmodified release name (for keyword check).
     */
    protected function isRandomByCharacterAnalysis(string $coreName, string $originalName = ''): bool
    {
        $coreLen = strlen($coreName);

        if ($coreLen < 16 || ! preg_match('/^[a-zA-Z0-9]+$/', $coreName)) {
            return false;
        }

        $transitions = $this->countCharacterTransitions($coreName);
        $transitionRate = $transitions / ($coreLen - 1);

        if ($transitionRate > 0.35) {
            $nameToCheck = $originalName !== '' ? $originalName : $coreName;

            return ! preg_match(
                '/\b(movie|film|series|episode|season|show|video|audio|music|album|dvd|bluray|hdtv|webrip|xvid|x264|x265|hevc|aac|mp3|flac|720p|1080p|2160p|4k|complete|proper|repack|dubbed|subbed|english|french|german|spanish|italian|rip|web|hdr|remux|disc|internal|retail)\b/i',
                $nameToCheck
            );
        }

        return false;
    }

    /**
     * Detect names that are long alphanumeric but lack word-like letter sequences.
     *
     * @param  string  $coreName  Name with extensions and separators stripped.
     */
    protected function hasInsufficientWordStructure(string $coreName): bool
    {
        $coreLen = strlen($coreName);

        if ($coreLen < 20 || ! preg_match('/^[a-zA-Z0-9]+$/', $coreName)) {
            return false;
        }

        $maxConsecutiveLetters = $this->getMaxConsecutiveLetters($coreName);

        return $maxConsecutiveLetters < 5
            && ! preg_match('/^[a-zA-Z]+\d{1,4}$/', $coreName);
    }

    /**
     * Detect random-looking patterns dominated by digits.
     *
     * @param  string  $coreName  Name with extensions and separators stripped.
     */
    protected function isRandomDigitPattern(string $coreName): bool
    {
        return (bool) (
            preg_match('/^[a-zA-Z]{1,3}\d{6,}[a-zA-Z]*$/i', $coreName)
            || preg_match('/^[a-zA-Z0-9]{2,4}\d{8,}$/i', $coreName)
        );
    }

    /**
     * Detect long alphanumeric tokens with effectively no vowel structure.
     */
    protected function isZeroVowelLongToken(string $coreName, int $minLength = 20): bool
    {
        if (strlen($coreName) < $minLength || ! preg_match('/^[a-zA-Z0-9]+$/', $coreName)) {
            return false;
        }

        if (preg_match('/\b(19|20)\d{2}\b/', $coreName)) {
            return false;
        }

        $letterOnly = preg_replace('/\d+/', '', $coreName);

        if ($letterOnly === '' || strlen($letterOnly) < 10) {
            return false;
        }

        return preg_match('/[aeiou]/i', $letterOnly) !== 1;
    }

    /**
     * Check whether the letter-only portion of a token has a normal vowel ratio.
     */
    protected function hasNormalVowelRatio(string $str, float $min = 0.2): bool
    {
        $letters = preg_replace('/[^a-z]/i', '', $str);

        if ($letters === '' || strlen($letters) < 5) {
            return false;
        }

        preg_match_all('/[aeiou]/i', $letters, $matches);
        $vowelCount = count($matches[0]);

        return ($vowelCount / strlen($letters)) >= $min;
    }

    /**
     * Check if a string looks like a random/obfuscated string rather than a real title.
     *
     * Uses multiple heuristics: character-type transitions, consonant clusters,
     * mixed case with digits, and vowel-consonant patterns.
     */
    protected function looksLikeRandomString(string $str): bool
    {
        // All same-case letters → random if long enough
        if (preg_match('/^[A-Z]+$/', $str) || preg_match('/^[a-z]+$/', $str)) {
            return strlen($str) >= 12;
        }

        $len = strlen($str);
        $transitions = $this->countCharacterTransitions($str);
        $transitionRatio = $transitions / max(1, $len - 1);

        // Check for common English consonant-vowel patterns
        $hasWordPattern = preg_match(
            '/[bcdfghjklmnpqrstvwxyz]{1,2}[aeiou][bcdfghjklmnpqrstvwxyz]{1,2}[aeiou]/i',
            $str
        );

        // High transition ratio + no word patterns → random
        if ($transitionRatio > 0.3 && ! $hasWordPattern) {
            return true;
        }

        // Unlikely consonant clusters (≥5 consecutive consonants)
        if (preg_match('/[bcdfghjklmnpqrstvwxyz]{5,}/i', $str)) {
            return true;
        }

        // Mixed case AND digits with no clear structure
        if (preg_match('/[A-Z]/', $str) && preg_match('/[a-z]/', $str) && preg_match('/\d/', $str)) {
            return true;
        }

        return false;
    }

    // ---------------------------------------------------------------
    // Helper utilities
    // ---------------------------------------------------------------

    /**
     * Strip common file extensions for analysis.
     */
    protected function stripExtensionsForAnalysis(string $name): string
    {
        return preg_replace(
            '/\.(mkv|avi|mp4|m4v|mpg|mpeg|wmv|flv|mov|ts|vob|iso|divx|par2?|nfo|sfv|nzb|rar|r\d{2,3}|zip|7z|gz|tar|001|msix|msixbundle|appx|appxbundle|apk|xap|ipa|deb|rpm|pkg|dmg|exe|msi)$/i',
            '',
            trim($name)
        );
    }

    /**
     * Get core name by removing all separators.
     */
    protected function getCoreNameWithoutSeparators(string $cleaned): string
    {
        return preg_replace('/[.\-_\s]+/', '', $cleaned);
    }

    /**
     * Count character-type transitions (upper↔lower, letter↔digit).
     */
    protected function countCharacterTransitions(string $str): int
    {
        $transitions = 0;
        $len = strlen($str);

        for ($i = 1; $i < $len; $i++) {
            $prev = $str[$i - 1];
            $curr = $str[$i];

            $prevIsDigit = ctype_digit($prev);
            $currIsDigit = ctype_digit($curr);
            $prevIsUpper = ctype_upper($prev);
            $currIsUpper = ctype_upper($curr);

            if ($prevIsDigit !== $currIsDigit) {
                $transitions++;
            } elseif (! $prevIsDigit && ! $currIsDigit && $prevIsUpper !== $currIsUpper) {
                $transitions++;
            }
        }

        return $transitions;
    }

    /**
     * Get the maximum consecutive letter count in a string.
     */
    protected function getMaxConsecutiveLetters(string $str): int
    {
        $maxConsecutive = 0;
        $currentConsecutive = 0;
        $lastWasLetter = false;

        for ($i = 0, $len = strlen($str); $i < $len; $i++) {
            $isLetter = ctype_alpha($str[$i]);
            if ($isLetter) {
                $currentConsecutive = $lastWasLetter ? $currentConsecutive + 1 : 1;
                $maxConsecutive = max($maxConsecutive, $currentConsecutive);
            }
            $lastWasLetter = $isLetter;
        }

        return $maxConsecutive;
    }
}
