<?php

declare(strict_types=1);

namespace MauticPlugin\DealFlowBundle\Controller;

use Doctrine\Persistence\ManagerRegistry;
use Mautic\CoreBundle\Controller\CommonController;
use Mautic\LeadBundle\Entity\Lead;
use MauticPlugin\DealFlowBundle\Entity\Deal;
use MauticPlugin\DealFlowBundle\Entity\DealNote;
use MauticPlugin\DealFlowBundle\Entity\DealNoteRepository;
use MauticPlugin\DealFlowBundle\Entity\DealRepository;
use MauticPlugin\DealFlowBundle\Entity\DealStageRepository;
use MauticPlugin\DealFlowBundle\Entity\DealTaskRepository;
use MauticPlugin\DealFlowBundle\Service\DealAuthorization;
use MauticPlugin\DealFlowBundle\Service\DealManager;
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;

/**
 * Handles AJAX actions on deal view: notes, tasks, contacts, inline edit, stage moves.
 * Consumed by DealController (facade) — do not register routes here directly.
 */
final class DealViewAjaxController extends CommonController
{
    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 is handled by Mautic's global RequestSubscriber via X-CSRF-Token header for AJAX.
        // Only validate our per-deal 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();
        }

        if ($request->isXmlHttpRequest()) {
            $token = $request->headers->get('X-CSRF-Token', '');
            if (!$csrfTokenManager->isTokenValid(new CsrfToken('mautic_ajax_post', $token))) {
                return new JsonResponse(['success' => false, 'error' => 'Invalid CSRF token.'], 403);
            }
        } else {
            $this->validateCsrfToken($csrfTokenManager, sprintf('dealflow_add_note_%d', $deal->getId()), (string) $request->request->get('_token'));
        }

        $body = trim((string) $request->request->get('body'));
        if ('' === $body) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => 'Note body cannot be empty.'], 422);
            }

            $this->addFlash('error', 'Note body cannot be empty.');

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

        $dealManager->addNote($deal, $body, $this->user?->getId(), 'ui');

        if ($request->isXmlHttpRequest()) {
            return new JsonResponse(['success' => true]);
        }

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

    public function editNoteAction(
        int $objectId,
        int $noteId,
        Request $request,
        DealAuthorization $authorization,
        DealNoteRepository $noteRepository,
        DealManager $dealManager,
        CsrfTokenManagerInterface $csrfTokenManager,
    ): Response {
        $note = $noteRepository->find($noteId);
        if (!$note instanceof DealNote || $note->getDeal()?->getId() !== $objectId) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => sprintf('Note %d not found for deal %d.', $noteId, $objectId)], 404);
            }

            throw $this->createNotFoundException(sprintf('Note %d not found for deal %d.', $noteId, $objectId));
        }

        $deal = $note->getDeal();
        if (!$deal instanceof Deal) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => sprintf('Deal %d not found.', $objectId)], 404);
            }

            throw $this->createNotFoundException(sprintf('Deal %d not found.', $objectId));
        }

        if (!$authorization->canEditDeal($deal)) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => 'Access denied.'], 403);
            }

            return $this->accessDenied();
        }

        if ($request->isXmlHttpRequest()) {
            $token = $request->headers->get('X-CSRF-Token', '');
            if (!$csrfTokenManager->isTokenValid(new CsrfToken('mautic_ajax_post', $token))) {
                return new JsonResponse(['success' => false, 'error' => 'Invalid CSRF token.'], 403);
            }
        } else {
            $this->validateCsrfToken($csrfTokenManager, sprintf('dealflow_edit_note_%d', $note->getId()), (string) $request->request->get('_token'));
        }

        $body = trim((string) $request->request->get('body'));
        if ('' === $body) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => 'Note body cannot be empty.'], 422);
            }

            $this->addFlash('error', 'Note body cannot be empty.');

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

        $dealManager->updateNote($note, $body, $this->user?->getId(), 'ui');

        if ($request->isXmlHttpRequest()) {
            return new JsonResponse(['success' => true, 'noteId' => $note->getId()]);
        }

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

    public function deleteNoteAction(
        int $objectId,
        int $noteId,
        Request $request,
        DealAuthorization $authorization,
        DealNoteRepository $noteRepository,
        DealManager $dealManager,
        CsrfTokenManagerInterface $csrfTokenManager,
    ): Response {
        $note = $noteRepository->find($noteId);
        if (!$note instanceof DealNote || $note->getDeal()?->getId() !== $objectId) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => 'Note not found.'], 404);
            }

            throw $this->createNotFoundException('Note not found.');
        }

        $deal = $note->getDeal();
        if (!$deal instanceof Deal) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => 'Deal not found.'], 404);
            }

            throw $this->createNotFoundException('Deal not found.');
        }

        if (!$authorization->canEditDeal($deal)) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => 'Access denied.'], 403);
            }

            return $this->accessDenied();
        }

        if ($request->isXmlHttpRequest()) {
            $token = $request->headers->get('X-CSRF-Token', '');
            if (!$csrfTokenManager->isTokenValid(new CsrfToken('mautic_ajax_post', $token))) {
                return new JsonResponse(['success' => false, 'error' => 'Invalid CSRF token.'], 403);
            }
        } else {
            $this->validateCsrfToken($csrfTokenManager, sprintf('dealflow_delete_note_%d', $note->getId()), (string) $request->request->get('_token'));
        }

        $dealManager->deleteNote($note, $this->user?->getId(), 'ui');

        if ($request->isXmlHttpRequest()) {
            return new JsonResponse(['success' => true]);
        }

        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 uncompleteTaskAction(
        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();
        }

        // AJAX requests use the global mautic_ajax_post token via header; form POSTs use per-task token
        if ($request->isXmlHttpRequest()) {
            $token = $request->headers->get('X-CSRF-Token', '');
            if (!$csrfTokenManager->isTokenValid(new CsrfToken('mautic_ajax_post', $token))) {
                return new JsonResponse(['success' => false, 'error' => 'Invalid CSRF token.'], 403);
            }
        } else {
            $this->validateCsrfToken($csrfTokenManager, sprintf('dealflow_uncomplete_task_%d', $task->getId()), (string) $request->request->get('_token'));
        }

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

        if ($request->isXmlHttpRequest()) {
            return new JsonResponse(['success' => true]);
        }

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

    public function editTaskAction(
        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) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => sprintf('Task %d not found for deal %d.', $taskId, $objectId)], 404);
            }

            throw $this->createNotFoundException(sprintf('Task %d not found for deal %d.', $taskId, $objectId));
        }

        $deal = $task->getDeal();
        if (!$deal instanceof Deal) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => sprintf('Deal %d not found.', $objectId)], 404);
            }

            throw $this->createNotFoundException(sprintf('Deal %d not found.', $objectId));
        }

        if (!$authorization->canEditDeal($deal)) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => 'Access denied.'], 403);
            }

            return $this->accessDenied();
        }

        // AJAX requests use the global mautic_ajax_post token via header; form POSTs use per-task token
        if ($request->isXmlHttpRequest()) {
            $token = $request->headers->get('X-CSRF-Token', '');
            if (!$csrfTokenManager->isTokenValid(new CsrfToken('mautic_ajax_post', $token))) {
                return new JsonResponse(['success' => false, 'error' => 'Invalid CSRF token.'], 403);
            }
        } else {
            $this->validateCsrfToken($csrfTokenManager, sprintf('dealflow_edit_task_%d', $task->getId()), (string) $request->request->get('_token'));
        }

        $data = [];
        foreach (['title', 'description', 'priority', 'due_at', 'owner_id', 'status'] as $field) {
            if ($request->request->has($field)) {
                $data[$field] = $request->request->get($field);
            }
        }

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

        if ($request->isXmlHttpRequest()) {
            return new JsonResponse([
                'success' => true,
                'taskId' => $task->getId(),
                'title' => $task->getTitle(),
                'status' => $task->getStatus(),
                'priority' => $task->getPriority(),
            ]);
        }

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

    public function deleteTaskAction(
        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) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => 'Task not found.'], 404);
            }

            throw $this->createNotFoundException('Task not found.');
        }

        $deal = $task->getDeal();
        if (!$deal instanceof Deal) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => 'Deal not found.'], 404);
            }

            throw $this->createNotFoundException('Deal not found.');
        }

        if (!$authorization->canEditDeal($deal)) {
            if ($request->isXmlHttpRequest()) {
                return new JsonResponse(['success' => false, 'error' => 'Access denied.'], 403);
            }

            return $this->accessDenied();
        }

        // AJAX requests use the global mautic_ajax_post token via header; form POSTs use per-task token
        if ($request->isXmlHttpRequest()) {
            $token = $request->headers->get('X-CSRF-Token', '');
            if (!$csrfTokenManager->isTokenValid(new CsrfToken('mautic_ajax_post', $token))) {
                return new JsonResponse(['success' => false, 'error' => 'Invalid CSRF token.'], 403);
            }
        } else {
            $this->validateCsrfToken($csrfTokenManager, sprintf('dealflow_delete_task_%d', $task->getId()), (string) $request->request->get('_token'));
        }

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

        if ($request->isXmlHttpRequest()) {
            return new JsonResponse(['success' => true]);
        }

        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 detachContactAction(
        int $objectId,
        int $contactId,
        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)) {
            throw $this->createAccessDeniedException();
        }

        // CSRF validation — accept both header and POST body token
        $token = $request->headers->get('X-CSRF-Token') ?: $request->request->get('_token');
        if (!$csrfTokenManager->isTokenValid(new CsrfToken('mautic_ajax_post', (string) $token))) {
            return new JsonResponse(['success' => false, 'error' => 'Invalid CSRF token.'], 403);
        }

        $actorId = $this->user?->getId();
        $result = $dealManager->detachContact($deal, $contactId, $actorId);

        if ($request->isXmlHttpRequest()) {
            return new JsonResponse(['success' => $result]);
        }

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

    public function inlineEditAction(
        int $objectId,
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealManager $dealManager,
        CsrfTokenManagerInterface $csrfTokenManager,
    ): Response {
        $deal = $dealRepository->find($objectId);
        if (!$deal instanceof Deal) {
            return new JsonResponse(['success' => false, 'error' => 'Deal not found.'], 404);
        }

        if (!$authorization->canEditDeal($deal)) {
            return new JsonResponse(['success' => false, 'error' => 'Access denied.'], 403);
        }

        // CSRF validation — accept both header and POST body token
        $token = $request->headers->get('X-CSRF-Token') ?: $request->request->get('_token');
        if (!$csrfTokenManager->isTokenValid(new CsrfToken('mautic_ajax_post', (string) $token))) {
            return new JsonResponse(['success' => false, 'error' => 'Invalid CSRF token.'], 403);
        }

        $field = $request->request->get('field');
        $value = $request->request->get('value');

        $allowedFields = ['confidence', 'source', 'expected_close', 'owner_id', 'company_id', 'amount', 'currency'];
        if (!in_array($field, $allowedFields, true)) {
            return new JsonResponse(['success' => false, 'error' => 'Field not editable.'], 400);
        }

        $actorId = $this->user?->getId();

        try {
            $dealManager->updateField($deal, $field, $value, $actorId);
        } catch (\Exception $e) {
            return new JsonResponse(['success' => false, 'error' => $e->getMessage()], 400);
        }

        return new JsonResponse(['success' => true, 'field' => $field, 'value' => $value]);
    }

    public function companySearchAction(
        Request $request,
        ManagerRegistry $managerRegistry,
    ): JsonResponse {
        $query = trim((string) $request->query->get('q', ''));

        $conn = $managerRegistry->getConnection();

        if ('' === $query) {
            // Empty query: return first 10 companies alphabetically
            $results = $conn->fetchAllAssociative(
                sprintf(
                    'SELECT id, companyname AS name FROM %scompanies ORDER BY companyname LIMIT 10',
                    MAUTIC_TABLE_PREFIX
                )
            );
        } else {
            $results = $conn->fetchAllAssociative(
                sprintf(
                    'SELECT id, companyname AS name FROM %scompanies WHERE companyname LIKE ? ORDER BY companyname LIMIT 10',
                    MAUTIC_TABLE_PREFIX
                ),
                ['%'.$query.'%']
            );
        }

        return new JsonResponse($results);
    }

    /**
     * Returns a fresh mautic_ajax_post CSRF token for authenticated users.
     * Called via GET /s/dealflow/csrf-token by the dealflowFetch() JS wrapper
     * when a CSRF failure is detected, allowing the request to retry with a valid token.
     */
    public function csrfTokenAction(
        CsrfTokenManagerInterface $csrfTokenManager,
    ): JsonResponse {
        $token = $csrfTokenManager->getToken('mautic_ajax_post')->getValue();

        return new JsonResponse(['token' => $token]);
    }

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