<?php

declare(strict_types=1);

namespace MauticPlugin\DealFlowBundle\Controller;

use Doctrine\Persistence\ManagerRegistry;
use Mautic\CoreBundle\Controller\CommonController;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\UserBundle\Entity\User;
use MauticPlugin\DealFlowBundle\Entity\Deal;
use MauticPlugin\DealFlowBundle\Entity\DealAttachmentRepository;
use MauticPlugin\DealFlowBundle\Entity\DealRepository;
use MauticPlugin\DealFlowBundle\Entity\DealStageRepository;
use MauticPlugin\DealFlowBundle\Entity\DealTaskRepository;
use MauticPlugin\DealFlowBundle\Form\Type\DealType;
use MauticPlugin\DealFlowBundle\Model\DealModel;
use MauticPlugin\DealFlowBundle\Service\AttachmentStorage;
use MauticPlugin\DealFlowBundle\Service\AutomationOptionsProvider;
use MauticPlugin\DealFlowBundle\Service\DealAuthorization;
use MauticPlugin\DealFlowBundle\Service\DealContextBuilder;
use MauticPlugin\DealFlowBundle\Service\DealFieldManager;
use MauticPlugin\DealFlowBundle\Service\DealManager;
use MauticPlugin\DealFlowBundle\Service\DealTimelineBuilder;
use MauticPlugin\DealFlowBundle\Service\DefaultPipelineInitializer;
use MauticPlugin\DealFlowBundle\Service\HealthIndicatorCalculator;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;

final class DealController extends CommonController
{
    public function pipelineAction(
        Request $request,
        DealAuthorization $authorization,
        DefaultPipelineInitializer $pipelineInitializer,
        DealRepository $dealRepository,
        DealContextBuilder $contextBuilder,
        HealthIndicatorCalculator $healthIndicator,
    ): Response {
        if (!$authorization->canViewDeals()) {
            return $this->accessDenied();
        }

        $search = (string) $this->getRequestValue($request, 'search', '');
        $pipelineId = $request->query->getInt('pipelineId', 0) ?: null;
        $pipeline = $pipelineInitializer->resolvePipeline($pipelineId);
        $stages = $pipelineInitializer->getOrderedStages($pipeline, true);
        $deals = $dealRepository->findForPipeline($pipeline, ['search' => $search], $authorization->getCurrentUserId(), $authorization->canViewOtherDeals());
        $contexts = $contextBuilder->buildForDeals($deals);
        $columns = [];
        $editableDealIds = [];
        $healthIndicators = [];

        foreach ($deals as $deal) {
            $editableDealIds[$deal->getId() ?? spl_object_id($deal)] = $authorization->canEditDeal($deal);
            $stageEnteredAt = $deal->getDateModified();
            if ($stageEnteredAt instanceof \DateTimeInterface) {
                $healthIndicators[$deal->getId()] = $healthIndicator->calculate($stageEnteredAt);
            }
        }

        foreach ($stages as $stage) {
            $columns[$stage->getId() ?? spl_object_id($stage)] = [
                'stage' => $stage,
                'deals' => [],
            ];
        }

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

            $columns[$stageId]['deals'][] = $deal;
        }

        $totalPipelineValue = $dealRepository->getPipelineValue($pipeline);

        return $this->delegateView([
            'viewParameters' => [
                'pipelines' => $pipelineInitializer->getActivePipelines(),
                'selectedPipeline' => $pipeline,
                'columns' => $columns,
                'dealContexts' => $contexts,
                'stages' => $stages,
                'searchValue' => $search,
                'editableDealIds' => $editableDealIds,
                'dealHealthIndicators' => $healthIndicators,
                'totalPipelineValue' => $totalPipelineValue,
            ],
            'contentTemplate' => '@DealFlow/Deal/pipeline.html.twig',
            'passthroughVars' => [
                'activeLink' => '#mautic_dealflow_pipeline',
                'route' => $this->generateUrl('mautic_dealflow_pipeline', ['pipelineId' => $pipeline->getId()]),
                'mauticContent' => 'dealflow',
            ],
        ]);
    }

    public function indexAction(
        Request $request,
        DealAuthorization $authorization,
        DefaultPipelineInitializer $pipelineInitializer,
        DealRepository $dealRepository,
        DealContextBuilder $contextBuilder,
        DealStageRepository $stageRepository,
        DealFieldManager $dealFieldManager,
        CsrfTokenManagerInterface $csrfTokenManager,
        int $page = 1,
    ): Response {
        if (!$authorization->canViewDeals()) {
            return $this->accessDenied();
        }

        $pipelineId = $request->query->getInt('pipelineId', 0) ?: null;
        $pipeline = $pipelineInitializer->resolvePipeline($pipelineId);
        $activeStage = null;
        $stageId = $this->getRequestValue($request, 'stageId');
        $search = $this->getRequestValue($request, 'search');
        $status = $this->getRequestValue($request, 'status');
        $ownerId = $this->getRequestValue($request, 'ownerId');
        $companyId = $this->getRequestValue($request, 'companyId');

        if (null !== $stageId && '' !== (string) $stageId) {
            $activeStage = $stageRepository->find((int) $stageId);
            if ($activeStage instanceof \MauticPlugin\DealFlowBundle\Entity\DealStage && $activeStage->getPipeline()?->getId() !== $pipeline->getId()) {
                $activeStage = null;
                $stageId = null;
            }
        }

        $pageSize = 30;
        $filters = [
            'search' => $search,
            'status' => $status,
            'ownerId' => $ownerId,
            'companyId' => $companyId,
            'stageId' => $stageId,
        ];

        $totalCount = $dealRepository->countForPipeline(
            $pipeline,
            $filters,
            $authorization->getCurrentUserId(),
            $authorization->canViewOtherDeals(),
        );
        $totalPages = max(1, (int) ceil($totalCount / $pageSize));
        $page = max(1, min($page, $totalPages));

        $deals = $dealRepository->findForPipelinePaginated(
            $pipeline,
            $filters,
            $authorization->getCurrentUserId(),
            $authorization->canViewOtherDeals(),
            $page,
            $pageSize,
        );

        $session = $request->getSession();
        /** @var list<int> $undoIds */
        $undoIds = (array) $session->get('dealflow_undo_archived_ids', []);
        $undoTimestamp = (int) $session->get('dealflow_undo_timestamp', 0);
        $undoCount = ([] !== $undoIds && (time() - $undoTimestamp) <= 60) ? count($undoIds) : 0;

        return $this->delegateView([
            'viewParameters' => [
                'pipelines' => $pipelineInitializer->getActivePipelines(),
                'selectedPipeline' => $pipeline,
                'items' => $deals,
                'dealContexts' => $contextBuilder->buildForDeals($deals),
                'listCustomFields' => $dealFieldManager->getListFields(),
                'customFieldListValues' => $dealFieldManager->buildListValueMap($deals),
                'page' => $page,
                'totalPages' => $totalPages,
                'totalCount' => $totalCount,
                'searchValue' => (string) ($search ?? ''),
                'activeStage' => $activeStage,
                'clearFiltersUrl' => $this->generateUrl('mautic_dealflow_index', ['page' => $page, 'pipelineId' => $pipeline->getId()]),
                'pipelineStages' => $pipelineInitializer->getOrderedStages($pipeline, true),
                'batchCsrfToken' => $csrfTokenManager->getToken('dealflow_batch')->getValue(),
                'undoCsrfToken' => $csrfTokenManager->getToken('dealflow_undo')->getValue(),
                'undoCount' => $undoCount,
            ],
            'contentTemplate' => '@DealFlow/Deal/list.html.twig',
            'passthroughVars' => [
                'activeLink' => '#mautic_dealflow_index',
                'route' => $this->generateUrl('mautic_dealflow_index', ['page' => $page, 'pipelineId' => $pipeline->getId()]),
                'mauticContent' => 'dealflow',
            ],
        ]);
    }

    public function newAction(
        Request $request,
        DealAuthorization $authorization,
        FormFactoryInterface $formFactory,
        ManagerRegistry $managerRegistry,
        DefaultPipelineInitializer $pipelineInitializer,
        DealModel $dealModel,
        DealFieldManager $dealFieldManager,
        AutomationOptionsProvider $automationOptionsProvider,
    ): Response {
        if (!$authorization->canCreateDeals()) {
            return $this->accessDenied();
        }

        $isModal = $this->isModalRequest($request);
        $pipeline = $pipelineInitializer->resolvePipeline($request->query->getInt('pipelineId', 0) ?: null);
        $activePipelines = $pipelineInitializer->getActivePipelines();
        $stageChoices = $this->buildStageChoices($pipelineInitializer);
        $returnContext = $this->resolveReturnContext($request, 'mautic_dealflow_pipeline', ['pipelineId' => $pipeline->getId()]);
        $deal = new Deal();
        $deal->setPipeline($pipeline);
        $customFieldDefinitions = $dealFieldManager->getActiveFields();
        $customFieldValues = $dealFieldManager->getFormValues($deal, $customFieldDefinitions);
        $customFieldErrors = [];
        $sourceChoices = $this->buildSourceChoices($automationOptionsProvider, $deal->getSource());
        $contactRows = $this->ensureContactRows([]);
        $contactRowErrors = [];
        $roleSuggestions = array_values($automationOptionsProvider->getStakeholderRoleChoices());

        $stages = $pipelineInitializer->getOrderedStages($pipeline, true);
        if ([] !== $stages) {
            $deal->setStage($stages[0]);
        }

        $form = $formFactory->create(DealType::class, $deal, [
            'pipeline_choices' => $activePipelines,
            'stage_choices' => $stageChoices,
            'owner_choices' => $this->buildOwnerChoices($managerRegistry, $deal->getOwnerId()),
            'source_choices' => $sourceChoices,
            'currency_choices' => $this->buildCurrencyChoices($deal->getCurrency()),
            'current_source_selection' => $this->resolveSourceSelection($deal->getSource(), $sourceChoices),
            'current_source_new' => null,
        ]);
        $formAction = $this->generateUrl('mautic_dealflow_new', array_merge(
            ['pipelineId' => $pipeline->getId()],
            $this->buildFormActionParameters($request, $returnContext['url'])
        ));
        $form->handleRequest($request);

        if ($this->isCancelRequest($request)) {
            return $this->finishDealForm($request, $returnContext);
        }

        if ($form->isSubmitted()) {
            $customFieldValues = $this->resolveCustomFieldInputs($request);
            $normalizedCustomFields = $dealFieldManager->normalizeSubmittedValues($customFieldValues, $customFieldDefinitions);
            $customFieldErrors = $normalizedCustomFields['errors'];
            $contactRows = $this->resolveSubmittedContactRows($request);
            $contactRowErrors = $this->validateContactRows($contactRows);
            $this->applySourceSelection($form, $deal);

            if ([] === $customFieldErrors) {
                $deal->setCustomFields($dealFieldManager->mergeStoredValues($deal->getCustomFields(), $normalizedCustomFields['values'], $customFieldDefinitions));
            }
        }

        if ($form->isSubmitted() && $form->isValid() && [] === $customFieldErrors && [] === $contactRowErrors && $this->validatePipelineStageSelection($form, $deal)) {
            $dealModel->saveEntity($deal);
            $dealModel->getDealManager()->syncContactAssociations($deal, $this->buildContactSyncPayload($contactRows), $this->user?->getId(), 'ui');

            if ($this->shouldCloseAfterSubmit($request)) {
                return $this->finishDealForm($request, $returnContext);
            }

            if (!$authorization->canEditDeal($deal) && !$authorization->canViewDeal($deal)) {
                return $this->finishDealForm($request, $returnContext);
            }

            $nextRoute = $authorization->canEditDeal($deal) ? 'mautic_dealflow_edit' : 'mautic_dealflow_view';
            $nextUrl = $this->generateUrl($nextRoute, array_merge(
                ['objectId' => $deal->getId(), 'pipelineId' => $deal->getPipeline()?->getId()],
                'mautic_dealflow_edit' === $nextRoute ? $this->buildFormActionParameters($request, $returnContext['url']) : []
            ));

            if ($isModal) {
                return $this->postActionRedirect([
                    'returnUrl' => $nextUrl,
                    'contentTemplate' => 'mautic_dealflow_edit' === $nextRoute ? self::class.'::editAction' : self::class.'::viewAction',
                    'viewParameters' => [
                        'objectId' => $deal->getId(),
                        'modal' => 1,
                        'contentOnly' => 1,
                        'returnUrl' => $returnContext['url'],
                    ],
                    'passthroughVars' => [
                        'route' => $nextUrl,
                        'mauticContent' => 'dealflow',
                    ],
                ]);
            }

            return $this->delegateRedirect($nextUrl);
        }

        return $this->delegateView([
            'viewParameters' => [
                'form' => $form->createView(),
                'pageTitle' => 'New Deal',
                'cancelUrl' => $returnContext['url'],
                'formAction' => $formAction,
                'isModal' => $isModal,
                'addUserUrl' => $this->generateUrl('mautic_user_action', ['objectAction' => 'new']),
                'customFieldDefinitions' => $customFieldDefinitions,
                'customFieldValues' => $customFieldValues,
                'customFieldErrors' => $customFieldErrors,
                'contactRows' => $contactRows,
                'contactRowErrors' => $contactRowErrors,
                'roleSuggestions' => $roleSuggestions,
            ],
            'contentTemplate' => '@DealFlow/Deal/form.html.twig',
            'passthroughVars' => [
                'activeLink' => '#mautic_dealflow_new',
                'route' => $formAction,
                'mauticContent' => 'dealflow',
            ],
        ]);
    }

    public function editAction(
        int $objectId,
        Request $request,
        DealAuthorization $authorization,
        FormFactoryInterface $formFactory,
        ManagerRegistry $managerRegistry,
        DealRepository $dealRepository,
        DefaultPipelineInitializer $pipelineInitializer,
        DealModel $dealModel,
        DealFieldManager $dealFieldManager,
        DealContextBuilder $contextBuilder,
        AutomationOptionsProvider $automationOptionsProvider,
    ): Response {
        $isModal = $this->isModalRequest($request);
        $deal = $dealRepository->find($objectId);
        if (!$deal instanceof Deal) {
            throw $this->createNotFoundException(sprintf('Deal %d not found.', $objectId));
        }

        if (!$authorization->canEditDeal($deal)) {
            return $this->accessDenied();
        }

        $activePipelines = $pipelineInitializer->getActivePipelines();
        $stageChoices = $this->buildStageChoices($pipelineInitializer);
        $returnContext = $this->resolveReturnContext($request, 'mautic_dealflow_view', ['objectId' => $deal->getId()]);
        $previousStage = $deal->getStage();
        $previousOwnerId = $deal->getOwnerId();
        $previousCompanyId = $deal->getCompanyId();
        $previousStatus = $deal->getStatus();
        $dealModel->setPreviousState($previousStage, $previousOwnerId, $previousCompanyId, $previousStatus);
        $customFieldDefinitions = $dealFieldManager->getActiveFields();
        $customFieldValues = $dealFieldManager->getFormValues($deal, $customFieldDefinitions);
        $customFieldErrors = [];
        $sourceChoices = $this->buildSourceChoices($automationOptionsProvider, $deal->getSource());
        $contactRows = $this->buildContactRowsFromContext($contextBuilder->buildForDeal($deal)['contacts']);
        $contactRowErrors = [];
        $roleSuggestions = array_values($automationOptionsProvider->getStakeholderRoleChoices());

        $form = $formFactory->create(DealType::class, $deal, [
            'pipeline_choices' => $activePipelines,
            'stage_choices' => $stageChoices,
            'owner_choices' => $this->buildOwnerChoices($managerRegistry, $deal->getOwnerId()),
            'source_choices' => $sourceChoices,
            'currency_choices' => $this->buildCurrencyChoices($deal->getCurrency()),
            'current_source_selection' => $this->resolveSourceSelection($deal->getSource(), $sourceChoices),
            'current_source_new' => null,
        ]);
        $formAction = $this->generateUrl('mautic_dealflow_edit', array_merge(
            ['objectId' => $deal->getId()],
            $this->buildFormActionParameters($request, $returnContext['url'])
        ));
        $form->handleRequest($request);

        if ($this->isCancelRequest($request)) {
            return $this->finishDealForm($request, $returnContext);
        }

        if ($form->isSubmitted()) {
            $customFieldValues = $this->resolveCustomFieldInputs($request);
            $normalizedCustomFields = $dealFieldManager->normalizeSubmittedValues($customFieldValues, $customFieldDefinitions);
            $customFieldErrors = $normalizedCustomFields['errors'];
            $contactRows = $this->resolveSubmittedContactRows($request);
            $contactRowErrors = $this->validateContactRows($contactRows);
            $this->applySourceSelection($form, $deal);

            if ([] === $customFieldErrors) {
                $deal->setCustomFields($dealFieldManager->mergeStoredValues($deal->getCustomFields(), $normalizedCustomFields['values'], $customFieldDefinitions));
            }
        }

        if ($form->isSubmitted() && $form->isValid() && [] === $customFieldErrors && [] === $contactRowErrors && $this->validatePipelineStageSelection($form, $deal)) {
            $dealModel->saveEntity($deal);
            $dealModel->getDealManager()->syncContactAssociations($deal, $this->buildContactSyncPayload($contactRows), $this->user?->getId(), 'ui');

            if ($this->shouldCloseAfterSubmit($request)) {
                return $this->finishDealForm($request, $returnContext);
            }

            if ($isModal) {
                return $this->postActionRedirect([
                    'returnUrl' => $formAction,
                    'contentTemplate' => self::class.'::editAction',
                    'viewParameters' => [
                        'objectId' => $deal->getId(),
                        'modal' => 1,
                        'contentOnly' => 1,
                        'returnUrl' => $returnContext['url'],
                    ],
                    'passthroughVars' => [
                        'route' => $formAction,
                        'mauticContent' => 'dealflow',
                    ],
                ]);
            }

            return $this->delegateRedirect($formAction);
        }

        return $this->delegateView([
            'viewParameters' => [
                'form' => $form->createView(),
                'pageTitle' => 'Edit Deal',
                'cancelUrl' => $returnContext['url'],
                'formAction' => $formAction,
                'isModal' => $isModal,
                'addUserUrl' => $this->generateUrl('mautic_user_action', ['objectAction' => 'new']),
                'customFieldDefinitions' => $customFieldDefinitions,
                'customFieldValues' => $customFieldValues,
                'customFieldErrors' => $customFieldErrors,
                'contactRows' => $contactRows,
                'contactRowErrors' => $contactRowErrors,
                'roleSuggestions' => $roleSuggestions,
            ],
            'contentTemplate' => '@DealFlow/Deal/form.html.twig',
            'passthroughVars' => [
                'activeLink' => '#mautic_dealflow_pipeline',
                'route' => $formAction,
                'mauticContent' => 'dealflow',
            ],
        ]);
    }

    public function viewAction(
        int $objectId,
        DealAuthorization $authorization,
        ManagerRegistry $managerRegistry,
        DealRepository $dealRepository,
        DealTimelineBuilder $timelineBuilder,
        DealContextBuilder $contextBuilder,
        DealFieldManager $dealFieldManager,
        DefaultPipelineInitializer $pipelineInitializer,
    ): Response {
        if (!$authorization->canViewDeals()) {
            return $this->accessDenied();
        }

        $deal = $dealRepository->find($objectId);
        if (!$deal instanceof Deal) {
            throw $this->createNotFoundException(sprintf('Deal %d not found.', $objectId));
        }

        if (!$authorization->canViewDeal($deal)) {
            return $this->accessDenied();
        }

        $pipeline = $deal->getPipeline();
        $stages = $pipeline ? $pipelineInitializer->getOrderedStages($pipeline, true) : [];

        return $this->delegateView([
            'viewParameters' => [
                'deal' => $deal,
                'dealContext' => $contextBuilder->buildForDeal($deal),
                'timeline' => $timelineBuilder->build($deal),
                'backUrl' => $this->generateUrl('mautic_dealflow_pipeline', ['pipelineId' => $deal->getPipeline()?->getId()]),
                'taskOwnerChoices' => $this->buildOwnerChoices($managerRegistry),
                'taskOwnerLabels' => array_flip($this->buildOwnerChoices($managerRegistry)),
                'canEditDeal' => $authorization->canEditDeal($deal),
                'customFieldDisplayRows' => $dealFieldManager->buildDisplayRows($deal),
                'stages' => $stages,
            ],
            'contentTemplate' => '@DealFlow/Deal/view.html.twig',
            'passthroughVars' => [
                'activeLink' => '#mautic_dealflow_pipeline',
                'route' => $this->generateUrl('mautic_dealflow_view', ['objectId' => $deal->getId()]),
                'mauticContent' => 'dealflow',
            ],
        ]);
    }

    public function moveStageAction(
        int $objectId,
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealStageRepository $stageRepository,
        DealManager $dealManager,
        CsrfTokenManagerInterface $csrfTokenManager,
    ): Response {
        $deal = $dealRepository->find($objectId);
        if (!$deal instanceof Deal) {
            throw $this->createNotFoundException(sprintf('Deal %d not found.', $objectId));
        }

        if (!$authorization->canEditDeal($deal)) {
            return $this->accessDenied();
        }

        // CSRF check: XHR requests with X-Requested-With header are inherently
        // CSRF-safe (browsers block cross-origin custom headers). Only validate
        // the token for non-AJAX form submissions.
        if (!$request->isXmlHttpRequest()) {
            $this->validateCsrfToken($csrfTokenManager, sprintf('dealflow_move_stage_%d', $deal->getId()), (string) $request->request->get('_token'));
        }

        $stage = $stageRepository->find((int) $request->request->get('stage_id'));
        if (null === $stage) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => 'Stage not found'], 400);
            }

            return $this->delegateRedirect($request->request->get('return_url', $this->generateUrl('mautic_dealflow_pipeline')));
        }

        if ($stage->getPipeline()?->getId() !== $deal->getPipeline()?->getId()) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => 'Stage does not belong to this pipeline'], 400);
            }

            return $this->delegateRedirect($request->request->get('return_url', $this->generateUrl('mautic_dealflow_pipeline')));
        }

        $dealManager->moveToStage($deal, $stage, $this->user?->getId(), 'ui');

        if ($request->isXmlHttpRequest()) {
            return new JsonResponse(['success' => true, 'dealId' => $deal->getId(), 'stageId' => $stage->getId()]);
        }

        $returnUrl = (string) $request->request->get('return_url', $this->generateUrl('mautic_dealflow_pipeline'));

        return $this->delegateRedirect($returnUrl);
    }

    public function addNoteAction(
        int $objectId,
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealManager $dealManager,
        CsrfTokenManagerInterface $csrfTokenManager,
    ): Response {
        $deal = $dealRepository->find($objectId);
        if (!$deal instanceof Deal) {
            throw $this->createNotFoundException(sprintf('Deal %d not found.', $objectId));
        }

        if (!$authorization->canEditDeal($deal)) {
            return $this->accessDenied();
        }

        $this->validateCsrfToken($csrfTokenManager, sprintf('dealflow_add_note_%d', $deal->getId()), (string) $request->request->get('_token'));

        if ('' !== trim((string) $request->request->get('body'))) {
            $dealManager->addNote($deal, trim((string) $request->request->get('body')), $this->user?->getId(), 'ui');
        }

        return $this->delegateRedirect($this->generateUrl('mautic_dealflow_view', ['objectId' => $objectId]));
    }

    public function addTaskAction(
        int $objectId,
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealManager $dealManager,
        CsrfTokenManagerInterface $csrfTokenManager,
    ): Response {
        $deal = $dealRepository->find($objectId);
        if (!$deal instanceof Deal) {
            throw $this->createNotFoundException(sprintf('Deal %d not found.', $objectId));
        }

        if (!$authorization->canEditDeal($deal)) {
            return $this->accessDenied();
        }

        $this->validateCsrfToken($csrfTokenManager, sprintf('dealflow_add_task_%d', $deal->getId()), (string) $request->request->get('_token'));

        if ('' !== trim((string) $request->request->get('title'))) {
            $dealManager->addTask(
                $deal,
                [
                    'title' => $request->request->get('title'),
                    'description' => $request->request->get('description'),
                    'priority' => $request->request->get('priority'),
                    'due_at' => $request->request->get('due_at'),
                    'reminder_at' => $request->request->get('reminder_at'),
                    'owner_id' => $request->request->get('owner_id'),
                ],
                $this->user?->getId(),
                'ui'
            );
        }

        return $this->delegateRedirect($this->generateUrl('mautic_dealflow_view', ['objectId' => $objectId]));
    }

    public function completeTaskAction(
        int $objectId,
        int $taskId,
        Request $request,
        DealAuthorization $authorization,
        DealTaskRepository $taskRepository,
        DealManager $dealManager,
        CsrfTokenManagerInterface $csrfTokenManager,
    ): Response {
        $task = $taskRepository->find($taskId);
        if (null === $task || $task->getDeal()?->getId() !== $objectId) {
            throw $this->createNotFoundException(sprintf('Task %d not found for deal %d.', $taskId, $objectId));
        }

        $deal = $task->getDeal();
        if (!$deal instanceof Deal) {
            throw $this->createNotFoundException(sprintf('Deal %d not found.', $objectId));
        }

        if (!$authorization->canEditDeal($deal)) {
            return $this->accessDenied();
        }

        $this->validateCsrfToken($csrfTokenManager, sprintf('dealflow_complete_task_%d', $task->getId()), (string) $request->request->get('_token'));

        $dealManager->completeTask($task, $this->user?->getId(), 'ui');

        return $this->delegateRedirect($this->generateUrl('mautic_dealflow_view', ['objectId' => $objectId]));
    }

    public function associateContactAction(
        int $objectId,
        Request $request,
        DealAuthorization $authorization,
        ManagerRegistry $managerRegistry,
        DealRepository $dealRepository,
        DealManager $dealManager,
        CsrfTokenManagerInterface $csrfTokenManager,
    ): Response {
        $deal = $dealRepository->find($objectId);
        if (!$deal instanceof Deal) {
            throw $this->createNotFoundException(sprintf('Deal %d not found.', $objectId));
        }

        if (!$authorization->canEditDeal($deal)) {
            return $this->accessDenied();
        }

        $this->validateCsrfToken($csrfTokenManager, sprintf('dealflow_attach_contact_%d', $deal->getId()), (string) $request->request->get('_token'));

        $contactId = (int) $request->request->get('contact_id');
        if ($contactId <= 0) {
            $this->addFlash('error', 'Select a contact to associate.');

            return $this->delegateRedirect($this->generateUrl('mautic_dealflow_view', ['objectId' => $objectId]));
        }

        $contact = $managerRegistry->getRepository(Lead::class)->find($contactId);
        if (!$contact instanceof Lead) {
            $this->addFlash('error', 'Selected contact could not be found.');

            return $this->delegateRedirect($this->generateUrl('mautic_dealflow_view', ['objectId' => $objectId]));
        }

        $label = trim((string) $request->request->get('label', ''));
        $dealManager->attachContact($deal, $contactId, '' !== $label ? $label : null, $this->user?->getId(), 'ui');
        $this->addFlash('notice', 'Contact associated.');

        return $this->delegateRedirect($this->generateUrl('mautic_dealflow_view', ['objectId' => $objectId]));
    }

    public function addAttachmentAction(
        int $objectId,
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealManager $dealManager,
        CsrfTokenManagerInterface $csrfTokenManager,
    ): Response {
        $deal = $dealRepository->find($objectId);
        if (!$deal instanceof Deal) {
            throw $this->createNotFoundException(sprintf('Deal %d not found.', $objectId));
        }

        if (!$authorization->canEditDeal($deal)) {
            return $this->accessDenied();
        }

        $this->validateCsrfToken($csrfTokenManager, sprintf('dealflow_add_attachment_%d', $deal->getId()), (string) $request->request->get('_token'));

        $file = $request->files->get('attachment');
        if (null !== $file) {
            try {
                $dealManager->addAttachment($deal, $file, $this->user?->getId(), 'ui');
                $this->addFlash('notice', 'Attachment uploaded.');
            } catch (\Throwable $exception) {
                $this->addFlash('error', $exception->getMessage());
            }
        }

        return $this->delegateRedirect($this->generateUrl('mautic_dealflow_view', ['objectId' => $objectId]));
    }

    public function downloadAttachmentAction(
        int $attachmentId,
        DealAuthorization $authorization,
        DealAttachmentRepository $attachmentRepository,
        AttachmentStorage $attachmentStorage,
    ): BinaryFileResponse {
        $attachment = $attachmentRepository->find($attachmentId);
        if (null === $attachment) {
            throw $this->createNotFoundException(sprintf('Attachment %d not found.', $attachmentId));
        }

        $deal = $attachment->getDeal();
        if (!$deal instanceof Deal || !$authorization->canViewDeal($deal)) {
            throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
        }

        $path = $attachmentStorage->getAbsolutePath($attachment->getStoragePath());
        if (!is_file($path)) {
            throw new FileNotFoundException($path);
        }

        $response = new BinaryFileResponse($path);
        $disposition = HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, $attachment->getOriginalFilename());
        $response->headers->set('Content-Disposition', $disposition);

        return $response;
    }

    public function deleteAttachmentAction(
        int $objectId,
        int $attachmentId,
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealAttachmentRepository $attachmentRepository,
        DealManager $dealManager,
        CsrfTokenManagerInterface $csrfTokenManager,
    ): Response {
        $deal = $dealRepository->find($objectId);
        if (!$deal instanceof Deal) {
            throw $this->createNotFoundException(sprintf('Deal %d not found.', $objectId));
        }

        if (!$authorization->canEditDeal($deal)) {
            return $this->accessDenied();
        }

        $attachment = $attachmentRepository->find($attachmentId);
        if (null === $attachment || $attachment->getDeal()?->getId() !== $deal->getId()) {
            throw $this->createNotFoundException(sprintf('Attachment %d not found for deal %d.', $attachmentId, $objectId));
        }

        $this->validateCsrfToken($csrfTokenManager, sprintf('dealflow_delete_attachment_%d', $attachment->getId()), (string) $request->request->get('_token'));

        $dealManager->deleteAttachment($attachment, $this->user?->getId(), 'ui');
        $this->addFlash('notice', 'Attachment deleted.');

        return $this->delegateRedirect($this->generateUrl('mautic_dealflow_view', ['objectId' => $deal->getId()]));
    }

    public function batchAction(
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealStageRepository $stageRepository,
        DealManager $dealManager,
        CsrfTokenManagerInterface $csrfTokenManager,
    ): Response {
        if (!$authorization->canViewDeals()) {
            return $this->accessDenied();
        }

        $this->validateCsrfToken($csrfTokenManager, 'dealflow_batch', (string) $request->request->get('_token'));

        $action = (string) $request->request->get('batch_action');
        /** @var list<string> $rawIds */
        $rawIds = (array) $request->request->all('deal_ids');
        $ids = array_filter(array_map('intval', $rawIds), static fn (int $id): bool => $id > 0);

        $returnUrl = (string) $request->request->get('return_url', $this->generateUrl('mautic_dealflow_index'));

        if ([] === $ids || '' === $action) {
            $this->addFlash('error', 'No deals selected.');

            return $this->delegateRedirect($returnUrl);
        }

        $deals = $dealRepository->findBy(['id' => $ids]);

        switch ($action) {
            case 'delete':
                $permittedDeals = array_values(array_filter($deals, static fn (Deal $deal): bool => $authorization->canDeleteDeal($deal)));

                if ([] === $permittedDeals) {
                    $this->addFlash('error', 'You do not have permission to archive these deals.');

                    return $this->delegateRedirect($returnUrl);
                }

                $count = $dealManager->archiveDeals($permittedDeals, $this->user?->getId(), 'ui');

                $archivedIds = array_map(static fn (Deal $d): int => (int) $d->getId(), $permittedDeals);
                $request->getSession()->set('dealflow_undo_archived_ids', $archivedIds);
                $request->getSession()->set('dealflow_undo_timestamp', time());

                $this->addFlash('notice', sprintf('%d deal(s) archived.', $count));

                break;

            case 'changeStage':
                $stageId = (int) $request->request->get('batch_stage_id');
                $stage = $stageRepository->find($stageId);

                if (null === $stage) {
                    $this->addFlash('error', 'Invalid stage selected.');

                    return $this->delegateRedirect($returnUrl);
                }

                $permittedDeals = array_values(array_filter($deals, static fn (Deal $deal): bool => $authorization->canEditDeal($deal)));

                if ([] === $permittedDeals) {
                    $this->addFlash('error', 'You do not have permission to edit these deals.');

                    return $this->delegateRedirect($returnUrl);
                }

                $count = $dealManager->batchChangeStage($permittedDeals, $stage, $this->user?->getId(), 'ui');
                $this->addFlash('notice', sprintf('%d deal(s) moved to %s.', $count, $stage->getName()));

                break;

            default:
                $this->addFlash('error', 'Unknown batch action.');
        }

        return $this->delegateRedirect($returnUrl);
    }

    public function undoBatchAction(
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealManager $dealManager,
        CsrfTokenManagerInterface $csrfTokenManager,
    ): Response {
        if (!$authorization->canViewDeals()) {
            return $this->accessDenied();
        }

        $this->validateCsrfToken($csrfTokenManager, 'dealflow_undo', (string) $request->request->get('_token'));

        $session = $request->getSession();
        /** @var list<int> $archivedIds */
        $archivedIds = (array) $session->get('dealflow_undo_archived_ids', []);
        $timestamp = (int) $session->get('dealflow_undo_timestamp', 0);

        $session->remove('dealflow_undo_archived_ids');
        $session->remove('dealflow_undo_timestamp');

        $returnUrl = (string) $request->request->get('return_url', $this->generateUrl('mautic_dealflow_index'));

        if ([] === $archivedIds || (time() - $timestamp) > 60) {
            $this->addFlash('error', 'Undo window has expired.');

            return $this->delegateRedirect($returnUrl);
        }

        $deals = $dealRepository->findBy(['id' => $archivedIds]);
        $permittedDeals = array_values(array_filter($deals, static fn (Deal $d): bool => $authorization->canDeleteDeal($d)));
        $count = $dealManager->unarchiveDeals($permittedDeals, $this->user?->getId(), 'ui');

        $this->addFlash('notice', sprintf('%d deal(s) restored.', $count));

        return $this->delegateRedirect($returnUrl);
    }

    /**
     * @return array{url: string, contentTemplate: string, viewParameters: array<string, mixed>}
     */
    private function resolveReturnContext(Request $request, string $defaultRoute, array $defaultRouteParameters = []): array
    {
        $defaultUrl = $this->generateUrl($defaultRoute, $defaultRouteParameters);
        $returnUrl = (string) $this->getRequestValue($request, 'returnUrl', $defaultUrl);
        $parsedUrl = parse_url($returnUrl);
        $path = $parsedUrl['path'] ?? $defaultUrl;
        $query = [];

        if (isset($parsedUrl['query'])) {
            parse_str($parsedUrl['query'], $query);
        }

        unset(
            $query['_'],
            $query['ignoreAjax'],
            $query['mauticUserLastActive'],
            $query['mauticLastNotificationId'],
            $query['modal'],
            $query['contentOnly']
        );

        $returnUrl = $path.([] !== $query ? '?'.http_build_query($query) : '');

        $routeParameters = $defaultRouteParameters;
        $routeName = $defaultRoute;

        try {
            $matchedRoute = $this->container->get('router')->match($path);
            $routeName = $matchedRoute['_route'] ?? $defaultRoute;
            unset($matchedRoute['_route'], $matchedRoute['_controller']);
            $routeParameters = array_merge($routeParameters, $matchedRoute);
        } catch (\Throwable) {
        }

        $contentTemplate = match ($routeName) {
            'mautic_dealflow_index' => self::class.'::indexAction',
            'mautic_dealflow_view' => self::class.'::viewAction',
            'mautic_dealflow_edit',
            'mautic_dealflow_edit_legacy' => self::class.'::editAction',
            'mautic_dealflow_reports' => ReportController::class.'::indexAction',
            'mautic_dealflow_stage_index' => StageController::class.'::indexAction',
            default => self::class.'::pipelineAction',
        };

        return [
            'url' => $returnUrl,
            'contentTemplate' => $contentTemplate,
            'viewParameters' => array_merge($routeParameters, $query),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function buildFormActionParameters(Request $request, string $returnUrl): array
    {
        $parameters = ['returnUrl' => $returnUrl];

        if ($this->isModalRequest($request)) {
            $parameters['modal'] = 1;
            $parameters['contentOnly'] = 1;
        }

        return $parameters;
    }

    /**
     * @param array{url: string, contentTemplate: string, viewParameters: array<string, mixed>} $returnContext
     */
    private function finishDealForm(Request $request, array $returnContext): Response
    {
        if ($this->isModalRequest($request)) {
            return $this->postActionRedirect([
                'returnUrl' => $returnContext['url'],
                'contentTemplate' => $returnContext['contentTemplate'],
                'viewParameters' => $returnContext['viewParameters'],
                'passthroughVars' => [
                    'closeModal' => true,
                    'route' => $returnContext['url'],
                    'mauticContent' => 'dealflow',
                ],
            ]);
        }

        return $this->delegateRedirect($returnContext['url']);
    }

    private function isModalRequest(Request $request): bool
    {
        return (bool) $this->getRequestValue($request, 'modal', false);
    }

    private function shouldCloseAfterSubmit(Request $request): bool
    {
        return !$this->isApplyRequest($request);
    }

    private function isApplyRequest(Request $request): bool
    {
        return array_key_exists('apply', $this->getSubmittedButtons($request));
    }

    private function isCancelRequest(Request $request): bool
    {
        return array_key_exists('cancel', $this->getSubmittedButtons($request));
    }

    /**
     * @return array<string, mixed>
     */
    private function getSubmittedButtons(Request $request): array
    {
        $formData = $request->request->all('deal');
        $buttons = $formData['buttons'] ?? [];

        return is_array($buttons) ? $buttons : [];
    }

    /**
     * @return array<string, mixed>
     */
    private function resolveCustomFieldInputs(Request $request): array
    {
        $values = $request->request->all('dealflow_custom_fields');

        return is_array($values) ? $values : [];
    }

    private function getRequestValue(Request $request, string $key, mixed $default = null): mixed
    {
        if ($request->query->has($key)) {
            return $request->query->get($key);
        }

        if ($request->attributes->has($key)) {
            return $request->attributes->get($key);
        }

        return $default;
    }

    /**
     * @return list<\MauticPlugin\DealFlowBundle\Entity\DealStage>
     */
    private function buildStageChoices(DefaultPipelineInitializer $pipelineInitializer): array
    {
        $stages = [];

        foreach ($pipelineInitializer->getActivePipelines() as $pipeline) {
            foreach ($pipelineInitializer->getOrderedStages($pipeline, true) as $stage) {
                $stages[] = $stage;
            }
        }

        return $stages;
    }

    /**
     * @return array<string, string>
     */
    private function buildSourceChoices(AutomationOptionsProvider $automationOptionsProvider, ?string $currentSource): array
    {
        $choices = $automationOptionsProvider->getSourceChoices($currentSource);
        $choices['Add new source...'] = '__new__';

        return $choices;
    }

    /**
     * @return array<string, string>
     */
    private function buildCurrencyChoices(?string $currentCurrency): array
    {
        $choices = [
            'USD' => 'USD',
            'EUR' => 'EUR',
            'GBP' => 'GBP',
            'CAD' => 'CAD',
            'AUD' => 'AUD',
            'JPY' => 'JPY',
            'CHF' => 'CHF',
        ];

        $currentCurrency = strtoupper(trim((string) $currentCurrency));
        if ('' !== $currentCurrency && !array_key_exists($currentCurrency, $choices)) {
            $choices = [$currentCurrency => $currentCurrency] + $choices;
        }

        return $choices;
    }

    private function resolveSourceSelection(?string $currentSource, array $sourceChoices): ?string
    {
        $currentSource = trim((string) $currentSource);
        if ('' === $currentSource) {
            return null;
        }

        return array_key_exists($currentSource, $sourceChoices) ? $currentSource : '__new__';
    }

    private function applySourceSelection(\Symfony\Component\Form\FormInterface $form, Deal $deal): void
    {
        $sourceSelection = trim((string) $form->get('sourceSelection')->getData());
        $newSource = trim((string) $form->get('sourceNew')->getData());

        if ('__new__' === $sourceSelection) {
            if ('' === $newSource) {
                $form->get('sourceNew')->addError(new FormError('Enter a source name or choose an existing source.'));

                return;
            }

            $deal->setSource($newSource);

            return;
        }

        $deal->setSource('' !== $sourceSelection ? $sourceSelection : null);
    }

    /**
     * @param array<int, array{id:int, label:?string, name:string, email:?string}> $contacts
     *
     * @return list<array{contact_id:?int, contact_name:string, label:string}>
     */
    private function buildContactRowsFromContext(array $contacts): array
    {
        $rows = [];

        foreach ($contacts as $contact) {
            $rows[] = [
                'contact_id' => (int) $contact['id'],
                'contact_name' => trim(sprintf('%s%s', $contact['name'], $contact['email'] ? ' ('.$contact['email'].')' : '')),
                'label' => trim((string) ($contact['label'] ?? '')),
            ];
        }

        return $this->ensureContactRows($rows);
    }

    /**
     * @return list<array{contact_id:?int, contact_name:string, label:string}>
     */
    private function resolveSubmittedContactRows(Request $request): array
    {
        $submittedRows = $request->request->all('deal_contacts');
        if (!is_array($submittedRows)) {
            return $this->ensureContactRows([]);
        }

        $rows = [];

        foreach ($submittedRows as $row) {
            if (!is_array($row)) {
                continue;
            }

            $contactId = isset($row['contact_id']) ? (int) $row['contact_id'] : 0;
            $contactName = trim((string) ($row['contact_name'] ?? ''));
            $label = $this->resolveSubmittedContactLabel($row);

            if ($contactId <= 0 && '' === $contactName && '' === $label) {
                continue;
            }

            $rows[] = [
                'contact_id' => $contactId > 0 ? $contactId : null,
                'contact_name' => $contactName,
                'label' => $label,
            ];
        }

        return $this->ensureContactRows($rows);
    }

    /**
     * @param array<string, mixed> $row
     */
    private function resolveSubmittedContactLabel(array $row): string
    {
        $selectedLabel = trim((string) ($row['label_selection'] ?? ''));
        $customLabel = trim((string) ($row['label_custom'] ?? ''));
        $legacyLabel = trim((string) ($row['label'] ?? ''));

        if ('__custom__' === $selectedLabel) {
            return '' !== $customLabel ? $customLabel : $legacyLabel;
        }

        if ('' !== $selectedLabel) {
            return $selectedLabel;
        }

        if ('' !== $customLabel) {
            return $customLabel;
        }

        return $legacyLabel;
    }

    /**
     * @param list<array{contact_id:?int, contact_name:string, label:string}> $rows
     *
     * @return list<array{contact_id:?int, contact_name:string, label:string}>
     */
    private function ensureContactRows(array $rows): array
    {
        return [] !== $rows ? $rows : [[
            'contact_id' => null,
            'contact_name' => '',
            'label' => '',
        ]];
    }

    /**
     * @param list<array{contact_id:?int, contact_name:string, label:string}> $rows
     *
     * @return list<string>
     */
    private function validateContactRows(array $rows): array
    {
        $errors = [];
        $seenContactIds = [];

        foreach ($rows as $row) {
            $contactId = $row['contact_id'];
            $contactName = trim($row['contact_name']);
            $label = trim($row['label']);

            if (null === $contactId && ('' !== $contactName || '' !== $label)) {
                $errors[] = 'Select each stakeholder from the contact lookup list.';
                continue;
            }

            if (null !== $contactId) {
                if (isset($seenContactIds[$contactId])) {
                    $errors[] = 'Each contact can only be associated with the deal once.';
                    continue;
                }

                $seenContactIds[$contactId] = true;
            }
        }

        return array_values(array_unique($errors));
    }

    /**
     * @param list<array{contact_id:?int, contact_name:string, label:string}> $rows
     *
     * @return list<array{contact_id:int, label:?string}>
     */
    private function buildContactSyncPayload(array $rows): array
    {
        $payload = [];

        foreach ($rows as $row) {
            if (null === $row['contact_id'] || $row['contact_id'] <= 0) {
                continue;
            }

            $payload[] = [
                'contact_id' => (int) $row['contact_id'],
                'label' => '' !== trim($row['label']) ? trim($row['label']) : null,
            ];
        }

        return $payload;
    }

    private function validatePipelineStageSelection(\Symfony\Component\Form\FormInterface $form, Deal $deal): bool
    {
        $pipeline = $deal->getPipeline();
        $stage = $deal->getStage();

        if (null !== $pipeline && null !== $stage && $stage->getPipeline()?->getId() !== $pipeline->getId()) {
            $form->get('stage')->addError(new FormError('Select a stage from the selected pipeline.'));

            return false;
        }

        return true;
    }

    /**
     * @return array<string, int>
     */
    private function buildOwnerChoices(ManagerRegistry $managerRegistry, ?int $selectedOwnerId = null): array
    {
        $entries = [];
        $seenIds = [];

        foreach ($managerRegistry->getRepository(User::class)->findAll() as $owner) {
            $ownerId = $owner->getId();
            if (null === $ownerId) {
                continue;
            }

            $entries[] = [
                'id' => $ownerId,
                'label' => sprintf('%s (#%d)', $this->formatOwnerName($owner), $ownerId),
            ];
            $seenIds[$ownerId] = true;
        }

        if (null !== $selectedOwnerId && !isset($seenIds[$selectedOwnerId])) {
            $entries[] = [
                'id' => $selectedOwnerId,
                'label' => sprintf('User #%d (missing)', $selectedOwnerId),
            ];
        }

        usort($entries, static fn (array $left, array $right): int => strcasecmp($left['label'], $right['label']));

        $choices = [];
        foreach ($entries as $entry) {
            $choices[$entry['label']] = $entry['id'];
        }

        return $choices;
    }

    private function formatOwnerName(User $owner): string
    {
        $name = method_exists($owner, 'getName') ? (string) $owner->getName() : '';
        if ('' !== trim($name)) {
            return trim($name);
        }

        if (method_exists($owner, 'getUserIdentifier')) {
            $identifier = (string) $owner->getUserIdentifier();
            if ('' !== trim($identifier)) {
                return trim($identifier);
            }
        }

        return sprintf('User #%d', $owner->getId());
    }

    private function validateCsrfToken(CsrfTokenManagerInterface $csrfTokenManager, string $tokenId, string $submittedToken): void
    {
        if (!$csrfTokenManager->isTokenValid(new CsrfToken($tokenId, $submittedToken))) {
            throw $this->createAccessDeniedException('Invalid CSRF token.');
        }
    }
}
