<?php

declare(strict_types=1);

namespace MauticPlugin\DealFlowBundle\Controller;

use Doctrine\Persistence\ManagerRegistry;
use Mautic\CoreBundle\Controller\CommonController;
use Mautic\UserBundle\Entity\User;
use MauticPlugin\DealFlowBundle\Controller\Trait\QueryParamTrait;
use MauticPlugin\DealFlowBundle\Entity\Deal;
use MauticPlugin\DealFlowBundle\Entity\DealAuditEventRepository;
use MauticPlugin\DealFlowBundle\Entity\DealRepository;
use MauticPlugin\DealFlowBundle\Entity\DealStage;
use MauticPlugin\DealFlowBundle\Entity\DealTaskRepository;
use MauticPlugin\DealFlowBundle\Entity\Pipeline;
use MauticPlugin\DealFlowBundle\Service\DealAuthorization;
use MauticPlugin\DealFlowBundle\Service\DealContextBuilder;
use MauticPlugin\DealFlowBundle\Service\DefaultPipelineInitializer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

final class ReportController extends CommonController
{
    use QueryParamTrait;

    public function indexAction(
        Request $request,
        DealAuthorization $authorization,
        DefaultPipelineInitializer $pipelineInitializer,
        DealRepository $dealRepository,
        DealTaskRepository $taskRepository,
        DealAuditEventRepository $auditRepository,
        DealContextBuilder $contextBuilder,
        ManagerRegistry $managerRegistry,
    ): Response {
        if (!$authorization->canViewReports()) {
            return $this->accessDenied();
        }

        $pipeline = $pipelineInitializer->resolvePipeline($this->queryNullableInt($request, 'pipelineId'));
        $currentUserId = $authorization->getCurrentUserId();
        $includeOtherDeals = $authorization->canViewOtherReports();
        $wonLostCounts = $dealRepository->getWonLostCounts($pipeline, $currentUserId, $includeOtherDeals);
        $ownerLeaderboard = $dealRepository->getOwnerLeaderboard($pipeline, $currentUserId, $includeOtherDeals);
        $deals = $dealRepository->findForPipeline($pipeline, [], $currentUserId, $includeOtherDeals);
        $stageBreakdown = $this->buildStageBreakdown($pipelineInitializer->getOrderedStages($pipeline), $deals);
        $pipelineMetrics = $this->buildPipelineMetrics($deals);
        $atRiskDeals = $this->buildAtRiskDeals($deals, $taskRepository);
        $atRiskContexts = $contextBuilder->buildForDeals(array_map(static fn (array $entry): Deal => $entry['deal'], $atRiskDeals));
        $recentActivity = $auditRepository->findRecentForPipeline($pipeline, $currentUserId, $includeOtherDeals);
        $owners = [];

        $ownerIds = array_values(array_filter(array_map(static fn (array $row): ?int => null !== $row['ownerId'] ? (int) $row['ownerId'] : null, $ownerLeaderboard)));
        if ([] !== $ownerIds) {
            foreach ($managerRegistry->getRepository(User::class)->findBy(['id' => $ownerIds]) as $owner) {
                $owners[$owner->getId()] = method_exists($owner, 'getName') ? $owner->getName() : $owner->getUserIdentifier();
            }
        }

        $closedTotal = $wonLostCounts['won'] + $wonLostCounts['lost'];
        $winRate = $closedTotal > 0 ? round(($wonLostCounts['won'] / $closedTotal) * 100, 2) : 0.0;
        $velocityMetrics = $this->buildVelocityMetrics($dealRepository, $pipeline, $currentUserId, $includeOtherDeals);

        return $this->delegateView([
            'viewParameters' => [
                'pipelines' => $pipelineInitializer->getActivePipelines(),
                'selectedPipeline' => $pipeline,
                'pipelineValue' => $pipelineMetrics['pipelineValue'],
                'weightedForecast' => $pipelineMetrics['weightedForecast'],
                'openDealCount' => $pipelineMetrics['openDealCount'],
                'averageDealSize' => $pipelineMetrics['averageDealSize'],
                'atRiskCount' => count($atRiskDeals),
                'stageBreakdown' => $stageBreakdown,
                'wonLostCounts' => $wonLostCounts,
                'winRate' => $winRate,
                'ownerLeaderboard' => $ownerLeaderboard,
                'owners' => $owners,
                'atRiskDeals' => $atRiskDeals,
                'atRiskContexts' => $atRiskContexts,
                'recentActivity' => $recentActivity,
                'velocityMetrics' => $velocityMetrics,
            ],
            'contentTemplate' => '@DealFlow/Report/index.html.twig',
            'passthroughVars' => [
                'activeLink' => '#mautic_dealflow_reports',
                'route' => $this->generateUrl('mautic_dealflow_reports', ['pipelineId' => $pipeline->getId()]),
                'mauticContent' => 'dealflow',
            ],
        ]);
    }

    /**
     * @param list<DealStage> $stages
     * @param list<Deal>      $deals
     *
     * @return list<array{stageId: int, name: string, stageType: string, probability: int, count: int, totalAmount: string, weightedAmount: string}>
     */
    private function buildStageBreakdown(array $stages, array $deals): array
    {
        $rows = [];

        foreach ($stages as $stage) {
            $stageId = $stage->getId();
            if (null === $stageId) {
                continue;
            }

            $rows[$stageId] = [
                'stageId' => $stageId,
                'name' => $stage->getName(),
                'stageType' => $stage->getStageType(),
                'probability' => $stage->getProbability(),
                'count' => 0,
                'totalAmount' => '0',
                'weightedAmount' => '0',
            ];
        }

        foreach ($deals as $deal) {
            $stage = $deal->getStage();
            $stageId = $stage?->getId();
            if (null === $stageId || !isset($rows[$stageId])) {
                continue;
            }

            $amount = (float) ($deal->getAmount() ?? 0);
            ++$rows[$stageId]['count'];
            $rows[$stageId]['totalAmount'] = number_format(((float) $rows[$stageId]['totalAmount']) + $amount, 4, '.', '');

            if ('open' === $deal->getStatus()) {
                $rows[$stageId]['weightedAmount'] = number_format(
                    ((float) $rows[$stageId]['weightedAmount']) + ($amount * $stage->getProbability() / 100),
                    4,
                    '.',
                    ''
                );
            }
        }

        return array_values($rows);
    }

    /**
     * @param list<Deal> $deals
     *
     * @return array{pipelineValue: string, weightedForecast: string, openDealCount: int, averageDealSize: string}
     */
    private function buildPipelineMetrics(array $deals): array
    {
        $pipelineValue = 0.0;
        $weightedForecast = 0.0;
        $openDealCount = 0;

        foreach ($deals as $deal) {
            if ('open' !== $deal->getStatus()) {
                continue;
            }

            $amount = (float) ($deal->getAmount() ?? 0);
            $pipelineValue += $amount;
            ++$openDealCount;

            $stage = $deal->getStage();
            if ($stage instanceof DealStage) {
                $weightedForecast += $amount * ($stage->getProbability() / 100);
            }
        }

        $averageDealSize = $openDealCount > 0 ? number_format($pipelineValue / $openDealCount, 4, '.', '') : '0';

        return [
            'pipelineValue' => number_format($pipelineValue, 4, '.', ''),
            'weightedForecast' => number_format($weightedForecast, 4, '.', ''),
            'openDealCount' => $openDealCount,
            'averageDealSize' => $averageDealSize,
        ];
    }

    /**
     * @param list<Deal> $deals
     *
     * @return list<array{deal: Deal, riskSignals: list<string>, overdueTaskCount: int, staleDays: int, daysPastClose: int}>
     */
    private function buildAtRiskDeals(array $deals, DealTaskRepository $taskRepository): array
    {
        $openDeals = array_values(array_filter(
            $deals,
            static fn (Deal $deal): bool => 'open' === $deal->getStatus()
        ));
        $now = new \DateTimeImmutable();
        $today = new \DateTimeImmutable('today');
        $staleCutoff = $now->modify('-14 days');
        $overdueTaskCounts = $taskRepository->getOpenOverdueCountsByDealIds(
            array_values(array_filter(array_map(static fn (Deal $deal): ?int => $deal->getId(), $openDeals))),
            $now
        );
        $entries = [];

        foreach ($openDeals as $deal) {
            $dealId = $deal->getId();
            if (null === $dealId) {
                continue;
            }

            $riskSignals = [];
            $daysPastClose = 0;
            $staleDays = 0;
            $overdueTaskCount = $overdueTaskCounts[$dealId] ?? 0;
            $expectedCloseDate = $deal->getExpectedCloseDate();

            if ($expectedCloseDate instanceof \DateTimeInterface && $expectedCloseDate < $today) {
                $daysPastClose = (int) $expectedCloseDate->diff($today)->format('%a');
                $riskSignals[] = 0 === $daysPastClose ? 'Close date is due today' : sprintf('Close date overdue by %d day%s', $daysPastClose, 1 === $daysPastClose ? '' : 's');
            }

            if ($overdueTaskCount > 0) {
                $riskSignals[] = sprintf('%d overdue open task%s', $overdueTaskCount, 1 === $overdueTaskCount ? '' : 's');
            }

            if (null !== $deal->getDateModified() && $deal->getDateModified() < $staleCutoff) {
                $staleDays = (int) $deal->getDateModified()->diff($now)->format('%a');
                $riskSignals[] = sprintf('No deal activity for %d day%s', $staleDays, 1 === $staleDays ? '' : 's');
            }

            if ([] === $riskSignals) {
                continue;
            }

            $entries[] = [
                'deal' => $deal,
                'riskSignals' => $riskSignals,
                'overdueTaskCount' => $overdueTaskCount,
                'staleDays' => $staleDays,
                'daysPastClose' => $daysPastClose,
            ];
        }

        usort(
            $entries,
            static function (array $left, array $right): int {
                foreach (['daysPastClose', 'overdueTaskCount', 'staleDays'] as $metric) {
                    $comparison = $right[$metric] <=> $left[$metric];
                    if (0 !== $comparison) {
                        return $comparison;
                    }
                }

                return $left['deal']->getDateModified()->getTimestamp() <=> $right['deal']->getDateModified()->getTimestamp();
            }
        );

        return array_slice($entries, 0, 8);
    }

    /**
     * @return array{avgDaysToClose: ?float, avgCycleTime: ?float, monthlyCloseRate: list<array{month: string, wonCount: int, lostCount: int, totalClosed: int}>}
     */
    private function buildVelocityMetrics(DealRepository $dealRepository, Pipeline $pipeline, ?int $currentUserId, bool $includeOtherDeals): array
    {
        $wonDeals = $dealRepository->findWonDeals($pipeline, $currentUserId, $includeOtherDeals);
        $closedDeals = $dealRepository->findClosedDeals($pipeline, $currentUserId, $includeOtherDeals);

        $avgDaysToClose = $this->calculateAverageDays($wonDeals);
        $avgCycleTime = $this->calculateAverageDays($closedDeals);
        $monthlyCloseRate = $dealRepository->getMonthlyCloseCounts($pipeline, 6, $currentUserId, $includeOtherDeals);

        return [
            'avgDaysToClose' => $avgDaysToClose,
            'avgCycleTime' => $avgCycleTime,
            'monthlyCloseRate' => $monthlyCloseRate,
        ];
    }

    /**
     * @param list<Deal> $deals
     */
    private function calculateAverageDays(array $deals): ?float
    {
        if ([] === $deals) {
            return null;
        }

        $totalDays = 0;
        $count = 0;

        foreach ($deals as $deal) {
            $created = $deal->getDateAdded();
            $modified = $deal->getDateModified();

            if (null === $created || null === $modified) {
                continue;
            }

            $diff = $created->diff($modified);
            $totalDays += (int) $diff->format('%a');
            ++$count;
        }

        if (0 === $count) {
            return null;
        }

        return round($totalDays / $count, 1);
    }
}
