<?php

declare(strict_types=1);

namespace App\Services\Categorization\Categorizers;

use App\Models\Category;
use App\Services\Categorization\CategorizationResult;
use App\Services\Categorization\ReleaseContext;
use App\Traits\DetectsHashedNames;

/**
 * Categorizer for miscellaneous content and hash detection.
 * This runs FIRST with high priority to detect hashes early and prevent
 * them from being incorrectly categorized by group-based or content-based rules.
 */
class MiscCategorizer extends AbstractCategorizer
{
    use DetectsHashedNames;

    protected int $priority = 1; // Highest priority - run first to catch hashes

    public function getName(): string
    {
        return 'Misc';
    }

    public function categorize(ReleaseContext $context): CategorizationResult
    {
        $name = $context->releaseName;

        // Check for hash patterns first
        if ($result = $this->checkHash($name)) {
            return $result;
        }

        $analysis = $this->inspectSignals($name);
        if ($this->isZeroVowelLongToken($analysis['coreName'])) {
            return $this->matched(Category::OTHER_HASHED, 0.78, 'gibberish_zero_vowels');
        }

        // Check for obfuscated/encoded patterns
        if ($result = $this->checkObfuscated($name)) {
            return $result;
        }

        // Check low-signal names that only contain random-looking tokens
        if ($result = $this->checkLowSignal($name)) {
            return $result;
        }

        // Check for gibberish patterns (character-analysis heuristics)
        if ($result = $this->checkGibberish($name)) {
            return $result;
        }

        // Check for archive formats
        if ($result = $this->checkArchive($name)) {
            return $result;
        }

        // Check for dataset/dump patterns
        if ($result = $this->checkDataset($name)) {
            return $result;
        }

        return $this->noMatch();
    }

    /**
     * Inspect a release name for media signal markers used by the safety-net pipe.
     *
     * @return array{coreName: string, coreLength: int, signalScore: int, markers: list<string>, lowSignal: bool}
     */
    public function inspectSignals(ReleaseContext|string $context): array
    {
        $name = $context instanceof ReleaseContext ? $context->releaseName : $context;
        $cleaned = $this->stripExtensionsForAnalysis($name);
        $coreName = $this->getCoreNameWithoutSeparators($cleaned);

        $patterns = [
            'season_episode' => '/\bS\d{1,3}[._ -]?E\d{1,4}\b/i',
            'season_pack' => '/\bS\d{1,3}\b/i',
            'resolution' => '/\b(480p|576p|720p|1080[pi]?|2160p|4k|uhd)\b/i',
            'codec' => '/\b(x264|x265|h\.?264|h\.?265|hevc|xvid|av1)\b/i',
            'source' => '/\b(bluray|bdrip|brrip|hdtv|web[._ -]?dl|web[._ -]?rip|dvdrip|remux)\b/i',
            'audio' => '/\b(aac|ac3|ddp|dts|flac|mp3)\b/i',
            'scene_tag' => '/\b(proper|repack|internal|limited|complete|dubbed|subbed|readnfo)\b/i',
            'year' => '/\b(19|20)\d{2}\b/',
            'release_group' => '/-[A-Za-z0-9][A-Za-z0-9._-]{1,20}$/',
            'known_extension' => '/\.(mkv|avi|mp4|mp3|flac|iso|epub|pdf|exe|nzb|rar|7z)$/i',
        ];

        $markers = [];
        foreach ($patterns as $marker => $pattern) {
            if (preg_match($pattern, $name)) {
                $markers[] = $marker;
            }
        }

        $signalScore = count($markers);
        $isCoreToken = preg_match('/^[A-Za-z0-9+\/_=-]+$/', $coreName) === 1;
        $lowSignal = $signalScore === 0
            && $isCoreToken
            && strlen($coreName) >= 12
            && ! $this->hasStrongWordStructure($name, $coreName);

        return [
            'coreName' => $coreName,
            'coreLength' => strlen($coreName),
            'signalScore' => $signalScore,
            'markers' => $markers,
            'lowSignal' => $lowSignal,
        ];
    }

    protected function checkHash(string $name): ?CategorizationResult
    {
        // MD5 hash (32 hex characters)
        if ($this->isBoundedMd5Hash($name)) {
            return $this->matched(Category::OTHER_HASHED, 0.95, 'hash_md5');
        }

        // SHA-1 hash (40 hex characters)
        if ($this->isBoundedSha1Hash($name)) {
            return $this->matched(Category::OTHER_HASHED, 0.95, 'hash_sha1');
        }

        // SHA-256 hash (64 hex characters)
        if ($this->isBoundedSha256Hash($name)) {
            return $this->matched(Category::OTHER_HASHED, 0.95, 'hash_sha256');
        }

        // Generic long hex hash (32-128 chars)
        if ($this->isBoundedGenericHash($name)) {
            return $this->matched(Category::OTHER_HASHED, 0.95, 'hash_generic');
        }

        if ($this->isBase64LikeToken($name)) {
            return $this->matched(Category::OTHER_HASHED, 0.9, 'hash_base64_like');
        }

        // Strip extensions and separators for core-name checks
        $cleaned = $this->stripExtensionsForAnalysis($name);
        $coreName = $this->getCoreNameWithoutSeparators($cleaned);

        // UUID pattern
        if ($this->isUuidPattern($coreName)) {
            return $this->matched(Category::OTHER_HASHED, 0.95, 'hash_uuid');
        }

        // Pure hex string (≥16 chars)
        if ($this->isPureHexString($coreName)) {
            return $this->matched(Category::OTHER_HASHED, 0.95, 'hash_hex');
        }

        return null;
    }

    protected function checkObfuscated(string $name): ?CategorizationResult
    {
        // Release names consisting only of uppercase letters and numbers
        if ($this->isObfuscatedUppercaseString($name)) {
            return $this->matched(Category::OTHER_HASHED, 0.7, 'obfuscated_uppercase');
        }

        // Mixed-case alphanumeric strings without separators
        if ($this->isObfuscatedMixedAlphanumeric($name)) {
            return $this->matched(Category::OTHER_HASHED, 0.7, 'obfuscated_mixed_alphanumeric');
        }

        // Obfuscated filename embedded in usenet subject line format
        if ($this->isObfuscatedUsenetFilename($name)) {
            return $this->matched(Category::OTHER_HASHED, 0.85, 'obfuscated_usenet_filename');
        }

        // Only punctuation and numbers with no clear structure
        if ($this->isObfuscatedPunctuation($name)) {
            $analysis = $this->inspectSignals($name);
            $hashLike = $this->isBase64LikeToken($name)
                || $this->isBoundedGenericHash($name)
                || $this->isZeroVowelLongToken($analysis['coreName'], 12)
                || $analysis['lowSignal'];

            return $this->matched(
                $hashLike ? Category::OTHER_HASHED : Category::OTHER_MISC,
                $hashLike ? 0.75 : 0.5,
                'obfuscated_pattern',
                ['signal_score' => $analysis['signalScore'], 'markers' => $analysis['markers']]
            );
        }

        return null;
    }

    /**
     * Check for gibberish patterns using character-analysis heuristics.
     * These are patterns ported from FileNameCleaner that were previously
     * missing from the categorization pipeline.
     */
    protected function checkGibberish(string $name): ?CategorizationResult
    {
        $cleaned = $this->stripExtensionsForAnalysis($name);
        $coreName = $this->getCoreNameWithoutSeparators($cleaned);

        // High character-transition rate suggests randomness
        if ($this->isRandomByCharacterAnalysis($coreName, $name)) {
            return $this->matched(Category::OTHER_HASHED, 0.75, 'gibberish_random_transitions');
        }

        // Long alphanumeric but lacks word-like letter sequences
        if ($this->hasInsufficientWordStructure($coreName)) {
            return $this->matched(Category::OTHER_HASHED, 0.75, 'gibberish_no_word_structure');
        }

        // Random-looking patterns dominated by digits
        if ($this->isRandomDigitPattern($coreName)) {
            return $this->matched(Category::OTHER_HASHED, 0.7, 'gibberish_random_digits');
        }

        if ($this->isZeroVowelLongToken($coreName)) {
            return $this->matched(Category::OTHER_HASHED, 0.78, 'gibberish_zero_vowels');
        }

        return null;
    }

    protected function checkLowSignal(string $name): ?CategorizationResult
    {
        $analysis = $this->inspectSignals($name);

        if ($this->hasStrongWordStructure($name, $analysis['coreName'])) {
            return null;
        }

        if ($this->isZeroVowelLongToken($analysis['coreName'])) {
            return null;
        }

        if ($analysis['lowSignal'] && $analysis['coreLength'] >= 20) {
            return $this->matched(
                Category::OTHER_HASHED,
                0.8,
                'gibberish_no_signal',
                [
                    'signal_score' => $analysis['signalScore'],
                    'markers' => $analysis['markers'],
                    'core_length' => $analysis['coreLength'],
                ]
            );
        }

        return null;
    }

    protected function hasStrongWordStructure(string $name, string $coreName): bool
    {
        return $this->getMaxConsecutiveLetters($coreName) >= 5
            && $this->hasNormalVowelRatio($coreName)
            && $this->countAlphabeticWordTokens($name) >= 2;
    }

    protected function countAlphabeticWordTokens(string $name): int
    {
        $tokens = preg_split('/[.\s_-]+/', $this->stripExtensionsForAnalysis($name)) ?: [];

        return count(array_filter(
            $tokens,
            static fn (string $token): bool => preg_match('/[a-z]{3,}/i', $token) === 1
        ));
    }

    protected function checkArchive(string $name): ?CategorizationResult
    {
        if (preg_match('/\.(zip|rar|7z|tar|gz|bz2|xz|tgz|tbz2|cab|iso|img|dmg|pkg|archive)$/i', $name)) {
            return $this->matched(Category::OTHER_MISC, 0.5, 'archive');
        }

        return null;
    }

    protected function checkDataset(string $name): ?CategorizationResult
    {
        // Dataset/dump patterns that aren't media
        if (preg_match('/\b(sql|csv|dump|backup|dataset|collection)\b/i', $name) &&
            ! preg_match('/\b(movie|tv|show|audio|video|book|game)\b/i', $name)) {
            return $this->matched(Category::OTHER_MISC, 0.6, 'dataset');
        }

        // Data leaks/dumps (be careful with these)
        if (preg_match('/\b(leak|breach|data|database)\b/i', $name) &&
            preg_match('/\b(dump|export|backup)\b/i', $name) &&
            ! preg_match('/\b(movie|tv|show|audio|video|book|game)\b/i', $name)) {
            return $this->matched(Category::OTHER_MISC, 0.6, 'data_dump');
        }

        return null;
    }
}
