<?php

namespace Doctrine\Bundle\DoctrineBundle\DataCollector;

use Doctrine\ORM\Cache\Logging\CacheLoggerChain;
use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Tools\SchemaValidator;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector as BaseCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Throwable;

class DoctrineDataCollector extends BaseCollector
{
    /** @var ManagerRegistry */
    private $registry;

    /** @var int|null */
    private $invalidEntityCount;

    /** @var string[] */
    private $groupedQueries;

    public function __construct(ManagerRegistry $registry)
    {
        $this->registry = $registry;

        parent::__construct($registry);
    }

    /**
     * {@inheritdoc}
     */
    public function collect(Request $request, Response $response, Throwable $exception = null)
    {
        parent::collect($request, $response, $exception);

        $errors   = [];
        $entities = [];
        $caches   = [
            'enabled' => false,
            'log_enabled' => false,
            'counts' => [
                'puts' => 0,
                'hits' => 0,
                'misses' => 0,
            ],
            'regions' => [
                'puts' => [],
                'hits' => [],
                'misses' => [],
            ],
        ];

        /** @var EntityManager $em */
        foreach ($this->registry->getManagers() as $name => $em) {
            $entities[$name] = [];
            /** @var ClassMetadataFactory $factory */
            $factory   = $em->getMetadataFactory();
            $validator = new SchemaValidator($em);

            /** @var ClassMetadataInfo $class */
            foreach ($factory->getLoadedMetadata() as $class) {
                if (isset($entities[$name][$class->getName()])) {
                    continue;
                }

                $classErrors                        = $validator->validateClass($class);
                $entities[$name][$class->getName()] = $class->getName();

                if (empty($classErrors)) {
                    continue;
                }

                $errors[$name][$class->getName()] = $classErrors;
            }

            /** @var Configuration $emConfig */
            $emConfig   = $em->getConfiguration();
            $slcEnabled = $emConfig->isSecondLevelCacheEnabled();

            if (! $slcEnabled) {
                continue;
            }

            $caches['enabled'] = true;

            /** @var $cacheConfiguration \Doctrine\ORM\Cache\CacheConfiguration */
            /** @var CacheLoggerChain $cacheLoggerChain */
            $cacheConfiguration = $emConfig->getSecondLevelCacheConfiguration();
            $cacheLoggerChain   = $cacheConfiguration->getCacheLogger();

            if (! $cacheLoggerChain || ! $cacheLoggerChain->getLogger('statistics')) {
                continue;
            }

            /** @var StatisticsCacheLogger $cacheLoggerStats */
            $cacheLoggerStats      = $cacheLoggerChain->getLogger('statistics');
            $caches['log_enabled'] = true;

            $caches['counts']['puts']   += $cacheLoggerStats->getPutCount();
            $caches['counts']['hits']   += $cacheLoggerStats->getHitCount();
            $caches['counts']['misses'] += $cacheLoggerStats->getMissCount();

            foreach ($cacheLoggerStats->getRegionsPut() as $key => $value) {
                if (! isset($caches['regions']['puts'][$key])) {
                    $caches['regions']['puts'][$key] = 0;
                }

                $caches['regions']['puts'][$key] += $value;
            }

            foreach ($cacheLoggerStats->getRegionsHit() as $key => $value) {
                if (! isset($caches['regions']['hits'][$key])) {
                    $caches['regions']['hits'][$key] = 0;
                }

                $caches['regions']['hits'][$key] += $value;
            }

            foreach ($cacheLoggerStats->getRegionsMiss() as $key => $value) {
                if (! isset($caches['regions']['misses'][$key])) {
                    $caches['regions']['misses'][$key] = 0;
                }

                $caches['regions']['misses'][$key] += $value;
            }
        }

        // Might be good idea to replicate this block in doctrine bridge so we can drop this from here after some time.
        // This code is compatible with such change, because cloneVar is supposed to check if input is already cloned.
        foreach ($this->data['queries'] as &$queries) {
            foreach ($queries as &$query) {
                $query['params'] = $this->cloneVar($query['params']);
                // To be removed when the required minimum version of symfony/doctrine-bridge is >= 4.4
                $query['runnable'] = $query['runnable'] ?? true;
            }
        }

        $this->data['entities'] = $entities;
        $this->data['errors']   = $errors;
        $this->data['caches']   = $caches;
        $this->groupedQueries   = null;
    }

    public function getEntities()
    {
        return $this->data['entities'];
    }

    public function getMappingErrors()
    {
        return $this->data['errors'];
    }

    public function getCacheHitsCount()
    {
        return $this->data['caches']['counts']['hits'];
    }

    public function getCachePutsCount()
    {
        return $this->data['caches']['counts']['puts'];
    }

    public function getCacheMissesCount()
    {
        return $this->data['caches']['counts']['misses'];
    }

    public function getCacheEnabled()
    {
        return $this->data['caches']['enabled'];
    }

    public function getCacheRegions()
    {
        return $this->data['caches']['regions'];
    }

    public function getCacheCounts()
    {
        return $this->data['caches']['counts'];
    }

    public function getInvalidEntityCount()
    {
        if ($this->invalidEntityCount === null) {
            $this->invalidEntityCount = array_sum(array_map('count', $this->data['errors']));
        }

        return $this->invalidEntityCount;
    }

    public function getGroupedQueries()
    {
        if ($this->groupedQueries !== null) {
            return $this->groupedQueries;
        }

        $this->groupedQueries = [];
        $totalExecutionMS     = 0;
        foreach ($this->data['queries'] as $connection => $queries) {
            $connectionGroupedQueries = [];
            foreach ($queries as $i => $query) {
                $key = $query['sql'];
                if (! isset($connectionGroupedQueries[$key])) {
                    $connectionGroupedQueries[$key]                = $query;
                    $connectionGroupedQueries[$key]['executionMS'] = 0;
                    $connectionGroupedQueries[$key]['count']       = 0;
                    $connectionGroupedQueries[$key]['index']       = $i; // "Explain query" relies on query index in 'queries'.
                }
                $connectionGroupedQueries[$key]['executionMS'] += $query['executionMS'];
                $connectionGroupedQueries[$key]['count']++;
                $totalExecutionMS += $query['executionMS'];
            }
            usort($connectionGroupedQueries, static function ($a, $b) {
                if ($a['executionMS'] === $b['executionMS']) {
                    return 0;
                }

                return $a['executionMS'] < $b['executionMS'] ? 1 : -1;
            });
            $this->groupedQueries[$connection] = $connectionGroupedQueries;
        }

        foreach ($this->groupedQueries as $connection => $queries) {
            foreach ($queries as $i => $query) {
                $this->groupedQueries[$connection][$i]['executionPercent'] =
                    $this->executionTimePercentage($query['executionMS'], $totalExecutionMS);
            }
        }

        return $this->groupedQueries;
    }

    private function executionTimePercentage($executionTimeMS, $totalExecutionTimeMS)
    {
        if ($totalExecutionTimeMS === 0.0 || $totalExecutionTimeMS === 0) {
            return 0;
        }

        return $executionTimeMS / $totalExecutionTimeMS * 100;
    }

    public function getGroupedQueryCount()
    {
        $count = 0;
        foreach ($this->getGroupedQueries() as $connectionGroupedQueries) {
            $count += count($connectionGroupedQueries);
        }

        return $count;
    }
}
