<?php

declare(strict_types=1);

namespace App\Framework\Console\ErrorRecovery;

use App\Framework\Console\CommandList;
use App\Framework\Console\ConsoleCommand;

final readonly class CommandSuggestionEngine
{
    public function __construct(
        private CommandList $commandList,
        private int $maxSuggestions = 5,
        private float $minSimilarity = 0.3
    ) {
    }

    public function suggestCommand(string $inputCommand): CommandSuggestions
    {
        $suggestions = [];
        $commands = $this->commandList->getAllCommands();

        foreach ($commands as $command) {
            $similarity = $this->calculateSimilarity($inputCommand, $command->name);

            if ($similarity >= $this->minSimilarity) {
                $suggestions[] = new CommandSuggestion(
                    command: $command,
                    similarity: $similarity,
                    reason: $this->determineSuggestionReason($inputCommand, $command->name, $similarity)
                );
            }
        }

        // Sort by similarity (highest first)
        usort($suggestions, fn ($a, $b) => $b->similarity <=> $a->similarity);

        // Limit to max suggestions
        $suggestions = array_slice($suggestions, 0, $this->maxSuggestions);

        return new CommandSuggestions($inputCommand, $suggestions);
    }

    public function suggestCommandsByCategory(string $category): array
    {
        $commands = $this->commandList->getAllCommands();
        $categoryCommands = [];

        foreach ($commands as $command) {
            if ($this->matchesCategory($command, $category)) {
                $categoryCommands[] = $command;
            }
        }

        return $categoryCommands;
    }

    public function suggestSimilarCommands(ConsoleCommand $baseCommand, int $limit = 3): array
    {
        $commands = $this->commandList->getAllCommands();
        $suggestions = [];

        foreach ($commands as $command) {
            if ($command->name === $baseCommand->name) {
                continue;
            }

            $similarity = $this->calculateCommandSimilarity($baseCommand, $command);

            if ($similarity > 0.4) {
                $suggestions[] = [
                    'command' => $command,
                    'similarity' => $similarity,
                    'relation' => $this->determineCommandRelation($baseCommand, $command),
                ];
            }
        }

        usort($suggestions, fn ($a, $b) => $b['similarity'] <=> $a['similarity']);

        return array_slice($suggestions, 0, $limit);
    }

    private function calculateSimilarity(string $input, string $command): float
    {
        // Exact match
        if (strtolower($input) === strtolower($command)) {
            return 1.0;
        }

        // Prefix match
        if (str_starts_with(strtolower($command), strtolower($input))) {
            return 0.9;
        }

        // Contains match
        if (str_contains(strtolower($command), strtolower($input))) {
            return 0.7;
        }

        // Levenshtein distance similarity
        $levenshtein = levenshtein(strtolower($input), strtolower($command));
        $maxLength = max(strlen($input), strlen($command));

        if ($maxLength === 0) {
            return 0.0;
        }

        $levenshteinSimilarity = 1 - ($levenshtein / $maxLength);

        // Word-based similarity for commands with colons
        $wordSimilarity = $this->calculateWordSimilarity($input, $command);

        return max($levenshteinSimilarity, $wordSimilarity);
    }

    private function calculateWordSimilarity(string $input, string $command): float
    {
        $inputWords = preg_split('/[:\-_\s]+/', strtolower($input));
        $commandWords = preg_split('/[:\-_\s]+/', strtolower($command));

        if (empty($inputWords) || empty($commandWords)) {
            return 0.0;
        }

        $matches = 0;
        $total = max(count($inputWords), count($commandWords));

        foreach ($inputWords as $inputWord) {
            foreach ($commandWords as $commandWord) {
                if ($inputWord === $commandWord) {
                    $matches++;

                    break;
                } elseif (str_starts_with($commandWord, $inputWord) || str_starts_with($inputWord, $commandWord)) {
                    $matches += 0.7;

                    break;
                }
            }
        }

        return $matches / $total;
    }

    private function calculateCommandSimilarity(ConsoleCommand $base, ConsoleCommand $other): float
    {
        $namesimilarity = $this->calculateSimilarity($base->name, $other->name);
        $descriptionSimilarity = $this->calculateDescriptionSimilarity($base->description, $other->description);

        return ($namesimilarity * 0.7) + ($descriptionSimilarity * 0.3);
    }

    private function calculateDescriptionSimilarity(string $desc1, string $desc2): float
    {
        $words1 = array_filter(preg_split('/\s+/', strtolower($desc1)));
        $words2 = array_filter(preg_split('/\s+/', strtolower($desc2)));

        if (empty($words1) || empty($words2)) {
            return 0.0;
        }

        $commonWords = array_intersect($words1, $words2);
        $totalWords = array_unique(array_merge($words1, $words2));

        return count($commonWords) / count($totalWords);
    }

    private function determineSuggestionReason(string $input, string $command, float $similarity): SuggestionReason
    {
        if ($similarity >= 0.9) {
            return SuggestionReason::EXACT_MATCH;
        }

        if (str_starts_with(strtolower($command), strtolower($input))) {
            return SuggestionReason::PREFIX_MATCH;
        }

        if (str_contains(strtolower($command), strtolower($input))) {
            return SuggestionReason::PARTIAL_MATCH;
        }

        if ($this->calculateWordSimilarity($input, $command) > 0.5) {
            return SuggestionReason::WORD_SIMILARITY;
        }

        return SuggestionReason::FUZZY_MATCH;
    }

    private function determineCommandRelation(ConsoleCommand $base, ConsoleCommand $other): CommandRelation
    {
        $baseNamespace = $this->extractNamespace($base->name);
        $otherNamespace = $this->extractNamespace($other->name);

        if ($baseNamespace === $otherNamespace && ! empty($baseNamespace)) {
            return CommandRelation::SAME_CATEGORY;
        }

        if ($this->calculateDescriptionSimilarity($base->description, $other->description) > 0.3) {
            return CommandRelation::SIMILAR_FUNCTION;
        }

        return CommandRelation::RELATED;
    }

    private function matchesCategory(ConsoleCommand $command, string $category): bool
    {
        $namespace = $this->extractNamespace($command->name);

        return strtolower($namespace) === strtolower($category) ||
               str_contains(strtolower($command->description), strtolower($category));
    }

    private function extractNamespace(string $commandName): string
    {
        $parts = explode(':', $commandName);

        return count($parts) > 1 ? $parts[0] : '';
    }
}
