<?php

declare(strict_types=1);

namespace App\Framework\Tokenizer;

use App\Framework\Tokenizer\Contracts\TokenizerInterface;
use App\Framework\Tokenizer\ValueObjects\DiscoveryState;
use App\Framework\Tokenizer\ValueObjects\Token;
use App\Framework\Tokenizer\ValueObjects\TokenCollection;
use App\Framework\Tokenizer\ValueObjects\TokenContext;
use PhpToken;

/**
 * Modern PHP tokenizer using PhpToken API
 * Can be used for syntax highlighting, code analysis, and discovery
 */
final readonly class PhpTokenizer implements TokenizerInterface
{
    public function __construct(
        private TokenClassifier $classifier = new TokenClassifier(),
        private TokenContextAnalyzer $contextAnalyzer = new TokenContextAnalyzer()
    ) {
    }

    /**
     * Tokenize PHP code into a collection of tokens
     */
    public function tokenize(string $code): TokenCollection
    {
        // Ensure code has PHP tags for proper tokenization
        $processedCode = $this->ensurePhpTags($code);
        $needsTagRemoval = $processedCode !== $code;

        // Use PhpToken for robust parsing
        $phpTokens = PhpToken::tokenize($processedCode);

        // Build token collection with context
        $tokens = [];
        $context = new TokenContext();

        foreach ($phpTokens as $index => $phpToken) {
            // Skip artificial PHP tag if we added it
            if ($needsTagRemoval && $index === 0 && $phpToken->is(T_OPEN_TAG)) {
                continue;
            }

            // Update context
            $context = $this->contextAnalyzer->analyzeContext($phpToken, $phpTokens, $index, $context);

            // Create token with full context
            $token = $this->createToken($phpToken, $context, $phpTokens, $index);
            $tokens[] = $token;
        }

        return new TokenCollection($tokens);
    }

    /**
     * Tokenize for discovery purposes (lightweight, metadata focused)
     * Uses smart line-by-line streaming to minimize processing
     */
    public function tokenizeForDiscovery(string $code): TokenCollection
    {
        return $this->streamTokenizeForDiscovery($code);
    }

    /**
     * Smart streaming tokenization that stops early when possible
     * Processes line-by-line and identifies content type early
     */
    private function streamTokenizeForDiscovery(string $code): TokenCollection
    {
        $lines = explode("\n", $code);
        $discoveryState = new DiscoveryState();
        $processedCode = '';
        $lineNumber = 0;

        foreach ($lines as $line) {
            $lineNumber++;
            $processedCode .= $line . "\n";

            // Quick line-based detection for early stopping
            $lineContent = trim($line);

            // Update discovery state based on line content
            $this->updateDiscoveryState($discoveryState, $lineContent, $lineNumber);

            // Check if we can stop early (found all relevant metadata)
            if ($this->canStopDiscovery($discoveryState, $lineNumber)) {
                break;
            }

            // Safety limit: don't process more than 100 lines for discovery
            if ($lineNumber >= 100) {
                break;
            }
        }

        // Now tokenize only the relevant portion
        return $this->tokenizeRelevantPortion($processedCode, $discoveryState);
    }

    /**
     * Update discovery state based on line content
     */
    private function updateDiscoveryState(DiscoveryState $state, string $line, int $lineNumber): void
    {
        // Namespace detection
        if (! $state->namespaceFound && str_starts_with($line, 'namespace ')) {
            $state->namespaceFound = true;
            $state->namespaceLineFound = $lineNumber;
        }

        // Use statement detection
        if (str_starts_with($line, 'use ') && ! str_contains($line, ' as ') && str_contains($line, '\\')) {
            $state->useStatementsCount++;
        }

        // Class/Interface/Trait/Enum detection
        if (preg_match('/^(final\s+)?(abstract\s+)?(readonly\s+)?(class|interface|trait|enum)\s+(\w+)/', $line)) {
            $state->classFound = true;
            $state->classLineFound = $lineNumber;
        }

        // Function detection (public functions are usually the first ones we care about)
        if (preg_match('/^\s*(public|protected|private)?\s*(static\s+)?function\s+(\w+)/', $line)) {
            $state->functionsCount++;
        }

        // Attribute detection
        if (str_starts_with($line, '#[')) {
            $state->attributesCount++;
        }

        // Doc comment detection
        if (str_starts_with($line, '/**')) {
            $state->docCommentsCount++;
        }

        // Class body start
        if ($state->classFound && str_contains($line, '{')) {
            $state->classBodyStarted = true;
        }
    }

    /**
     * Check if we can stop discovery early
     */
    private function canStopDiscovery(DiscoveryState $state, int $currentLine): bool
    {
        // If we found a class and we're well into the class body, we can stop
        if ($state->classFound && $state->classBodyStarted && $currentLine > ($state->classLineFound + 20)) {
            return true;
        }

        // If we haven't found any structural elements after 50 lines, likely not much to discover
        if ($currentLine > 50 && ! $state->classFound && ! $state->namespaceFound) {
            return true;
        }

        return false;
    }

    /**
     * Tokenize the relevant portion we've identified
     */
    private function tokenizeRelevantPortion(string $code, DiscoveryState $state): TokenCollection
    {
        // Use the optimized tokenization on the reduced code
        $tokens = $this->tokenize($code);

        // Filter to only relevant tokens for discovery
        $filteredTokens = [];
        $includeNextString = false;

        foreach ($tokens as $token) {
            if ($token->isStructural() || $token->isAttribute() || $token->isDocComment()) {
                $filteredTokens[] = $token;

                // Mark to include the next identifier token (class/function/namespace names)
                if ($token->is([T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM, T_FUNCTION, T_USE])) {
                    $includeNextString = true;
                }
            } elseif ($includeNextString && ($token->is(T_STRING) || $token->is(T_NAME_QUALIFIED))) {
                // Include both T_STRING and T_NAME_QUALIFIED (for namespaced identifiers)
                $filteredTokens[] = $token;
                $includeNextString = false;
            } elseif ($token->is(T_NS_SEPARATOR)) {
                // Always include namespace separators for proper namespace parsing
                $filteredTokens[] = $token;
            }
        }

        return new TokenCollection($filteredTokens);
    }

    /**
     * Stream tokenization for large files
     */
    public function tokenizeStream(string $filePath): \Generator
    {
        $handle = fopen($filePath, 'r');
        if (! $handle) {
            throw new \RuntimeException("Cannot open file: $filePath");
        }

        try {
            $buffer = '';
            $inPhpBlock = false;

            while (! feof($handle)) {
                $chunk = fread($handle, 8192); // 8KB chunks
                $buffer .= $chunk;

                // Find complete PHP blocks in buffer
                if ($completeBlock = $this->extractCompleteBlock($buffer, $inPhpBlock)) {
                    $tokens = $this->tokenize($completeBlock);
                    yield from $tokens;

                    // Keep remainder in buffer
                    $buffer = $this->getBlockRemainder($buffer, $completeBlock);
                }
            }

            // Process remaining buffer
            if ($buffer) {
                $tokens = $this->tokenize($buffer);
                yield from $tokens;
            }

        } finally {
            fclose($handle);
        }
    }

    /**
     * Create a token with full context information
     */
    private function createToken(
        PhpToken $phpToken,
        TokenContext $context,
        array $allTokens,
        int $index
    ): Token {
        // Determine the token type with contextual classification
        $type = $this->classifier->classify($phpToken, $allTokens, $index, $context);

        return new Token(
            type: $type,
            value: $phpToken->text,
            line: $phpToken->line,
            position: $phpToken->pos,
            id: $phpToken->id,
            context: $context->clone()
        );
    }

    /**
     * Ensure code has PHP tags for tokenization
     */
    private function ensurePhpTags(string $code): string
    {
        $trimmed = trim($code);
        if (! str_starts_with($trimmed, '<?php') && ! str_starts_with($trimmed, '<?=')) {
            return "<?php\n" . $code;
        }

        return $code;
    }

    /**
     * Extract a complete PHP block from buffer
     */
    private function extractCompleteBlock(string &$buffer, bool &$inPhpBlock): ?string
    {
        // Simple implementation - can be enhanced for better block detection
        if (str_contains($buffer, '<?php')) {
            $inPhpBlock = true;
        }

        if ($inPhpBlock && str_contains($buffer, '?>')) {
            $endPos = strpos($buffer, '?>') + 2;
            $block = substr($buffer, 0, $endPos);
            $inPhpBlock = false;

            return $block;
        }

        // If we have enough buffer, process it
        if (strlen($buffer) > 16384) { // 16KB threshold
            $block = $buffer;
            $buffer = '';

            return $block;
        }

        return null;
    }

    /**
     * Get remainder after extracting block
     */
    private function getBlockRemainder(string $buffer, string $extractedBlock): string
    {
        $pos = strlen($extractedBlock);

        return substr($buffer, $pos);
    }
}
