<?php

declare(strict_types=1);

namespace App\Framework\Database\Performance;

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use Psr\Log\LoggerInterface;

/**
 * Monitors database queries for N+1 and other performance issues
 */
final class QueryMonitor
{
    private array $queryLog = [];

    private array $queryPatterns = [];

    private int $queryCount = 0;

    private float $totalTime = 0.0;

    private bool $enabled = true;

    /**
     * Threshold for detecting N+1 queries
     */
    private const N1_THRESHOLD = 3;

    /**
     * Threshold for slow queries
     */
    private readonly Duration $slowQueryThreshold;

    public function __construct(
        private readonly ?LoggerInterface $logger = null,
        ?Duration $slowQueryThreshold = null
    ) {
        $this->slowQueryThreshold = $slowQueryThreshold ?? Duration::fromMilliseconds(100);
    }

    /**
     * Logs a query execution
     */
    public function logQuery(string $sql, array $params = [], ?Duration $executionTime = null): void
    {
        if (! $this->enabled) {
            return;
        }

        $executionTime ??= Duration::zero();

        $this->queryCount++;
        $this->totalTime += $executionTime->toSeconds();

        // Normalize query for pattern detection
        $pattern = $this->normalizeQuery($sql);

        // Track query patterns for N+1 detection
        if (! isset($this->queryPatterns[$pattern])) {
            $this->queryPatterns[$pattern] = [
                'count' => 0,
                'total_time' => 0.0,
                'first_sql' => $sql,
                'params_samples' => [],
            ];
        }

        $this->queryPatterns[$pattern]['count']++;
        $this->queryPatterns[$pattern]['total_time'] += $executionTime->toSeconds();

        // Store sample params for debugging
        if (count($this->queryPatterns[$pattern]['params_samples']) < 5) {
            $this->queryPatterns[$pattern]['params_samples'][] = $params;
        }

        // Add to query log
        $this->queryLog[] = [
            'sql' => $sql,
            'params' => $params,
            'time' => $executionTime,
            'pattern' => $pattern,
            'timestamp' => microtime(true),
        ];

        // Check for slow queries
        if ($executionTime->greaterThan($this->slowQueryThreshold)) {
            $this->handleSlowQuery($sql, $params, $executionTime);
        }

        // Check for N+1 patterns
        if ($this->queryPatterns[$pattern]['count'] >= self::N1_THRESHOLD) {
            $this->handlePotentialN1($pattern, $this->queryPatterns[$pattern]);
        }
    }

    /**
     * Normalizes a query to detect patterns
     */
    private function normalizeQuery(string $sql): string
    {
        // Remove whitespace variations
        $sql = preg_replace('/\s+/', ' ', trim($sql));

        // Replace values with placeholders
        // Numbers
        $sql = preg_replace('/\b\d+\b/', '?', $sql);

        // Quoted strings
        $sql = preg_replace("/'[^']*'/", '?', $sql);
        $sql = preg_replace('/"[^"]*"/', '?', $sql);

        // IN clauses with multiple values
        $sql = preg_replace('/IN\s*\([^)]+\)/i', 'IN (?)', $sql);

        // Remove comments
        $sql = preg_replace('/--.*$/', '', $sql);
        $sql = preg_replace('/\/\*.*?\*\//', '', $sql);

        return strtolower($sql);
    }

    /**
     * Handles detection of slow queries
     */
    private function handleSlowQuery(string $sql, array $params, Duration $executionTime): void
    {
        $message = sprintf(
            'Slow query detected (%.2fms): %s',
            $executionTime->toMilliseconds(),
            $this->truncateSQL($sql)
        );

        if ($this->logger) {
            $this->logger->warning($message, [
                'sql' => $sql,
                'params' => $params,
                'execution_time_ms' => $executionTime->toMilliseconds(),
            ]);
        }
    }

    /**
     * Handles detection of potential N+1 queries
     */
    private function handlePotentialN1(string $pattern, array $patternData): void
    {
        // Only warn once per pattern per request
        static $warnedPatterns = [];

        if (isset($warnedPatterns[$pattern])) {
            return;
        }

        $warnedPatterns[$pattern] = true;

        $message = sprintf(
            'Potential N+1 query detected: Pattern executed %d times (%.2fms total)',
            $patternData['count'],
            $patternData['total_time'] * 1000
        );

        if ($this->logger) {
            $this->logger->warning($message, [
                'pattern' => $pattern,
                'example_sql' => $patternData['first_sql'],
                'count' => $patternData['count'],
                'total_time_ms' => $patternData['total_time'] * 1000,
                'param_samples' => $patternData['params_samples'],
            ]);
        }
    }

    /**
     * Gets query statistics
     */
    public function getStatistics(): QueryStatistics
    {
        $n1Queries = array_filter($this->queryPatterns, fn ($p) => $p['count'] >= self::N1_THRESHOLD);
        $slowQueries = array_filter($this->queryLog, fn ($q) => $q['time']->greaterThan($this->slowQueryThreshold));

        return QueryStatistics::fromRawData(
            totalQueries: $this->queryCount,
            totalTimeSeconds: $this->totalTime,
            uniquePatterns: count($this->queryPatterns),
            n1QueryPatterns: count($n1Queries),
            slowQueries: count($slowQueries),
            queryPatterns: $this->queryPatterns,
            queryLog: $this->queryLog
        );
    }

    /**
     * Resets the monitor
     */
    public function reset(): void
    {
        $this->queryLog = [];
        $this->queryPatterns = [];
        $this->queryCount = 0;
        $this->totalTime = 0.0;
    }

    /**
     * Enables or disables monitoring
     */
    public function setEnabled(bool $enabled): void
    {
        $this->enabled = $enabled;
    }

    /**
     * Checks if monitoring is enabled
     */
    public function isEnabled(): bool
    {
        return $this->enabled;
    }

    /**
     * Truncates SQL for display
     */
    private function truncateSQL(string $sql, int $maxLength = 100): string
    {
        if (strlen($sql) <= $maxLength) {
            return $sql;
        }

        return substr($sql, 0, $maxLength) . '...';
    }

    /**
     * Analyzes queries and returns recommendations
     */
    public function getRecommendations(): array
    {
        $recommendations = [];
        $stats = $this->getStatistics();

        // Check for N+1 queries
        if ($stats->n1QueryPatterns > 0) {
            $n1Patterns = array_filter($this->queryPatterns, fn ($p) => $p['count'] >= self::N1_THRESHOLD);

            foreach ($n1Patterns as $pattern => $data) {
                $recommendations[] = [
                    'type' => 'n1_query',
                    'severity' => 'high',
                    'message' => sprintf(
                        'N+1 Query Pattern: "%s" executed %d times. Consider using batch loading with findWithRelations().',
                        $this->truncateSQL($data['first_sql']),
                        $data['count']
                    ),
                    'impact' => sprintf('%.2fms total time', $data['total_time'] * 1000),
                    'solution' => 'Use EntityManager::findWithRelations() or BatchLoader to preload relations',
                ];
            }
        }

        // Check for slow queries
        $slowQueries = array_filter($this->queryLog, fn ($q) => $q['time']->greaterThan($this->slowQueryThreshold));
        foreach ($slowQueries as $query) {
            $recommendations[] = [
                'type' => 'slow_query',
                'severity' => 'medium',
                'message' => sprintf(
                    'Slow Query: "%s" took %.2fms',
                    $this->truncateSQL($query['sql']),
                    $query['time']->toMilliseconds()
                ),
                'impact' => 'Performance degradation',
                'solution' => 'Consider adding indexes or optimizing the query',
            ];
        }

        // Check for too many queries
        if ($stats->totalQueries > 50) {
            $recommendations[] = [
                'type' => 'too_many_queries',
                'severity' => 'medium',
                'message' => sprintf('%d queries executed in this request', $stats->totalQueries),
                'impact' => sprintf('%.2fms total database time', $stats->totalTime->toMilliseconds()),
                'solution' => 'Consider caching or batch loading to reduce query count',
            ];
        }

        return $recommendations;
    }

    /**
     * Throws exception if critical issues detected (for dev mode)
     */
    public function assertNoPerformanceIssues(): void
    {
        $stats = $this->getStatistics();

        if ($stats->n1QueryPatterns > 0) {
            $n1Patterns = array_filter($this->queryPatterns, fn ($p) => $p['count'] >= self::N1_THRESHOLD);
            $examples = array_map(fn ($p) => $p['first_sql'], array_slice($n1Patterns, 0, 3));

            throw FrameworkException::create(
                ErrorCode::DB_PERFORMANCE_ISSUE,
                sprintf(
                    'N+1 Query detected: %d patterns found. Examples: %s',
                    $stats->n1QueryPatterns,
                    implode('; ', $examples)
                )
            );
        }
    }
}
