<?php

declare(strict_types=1);

namespace MauticPlugin\DealFlowBundle\Controller\Api;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ManagerRegistry;
use Mautic\CoreBundle\Controller\CommonController;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use MauticPlugin\DealFlowBundle\Entity\Deal;
use MauticPlugin\DealFlowBundle\Entity\DealAttachment;
use MauticPlugin\DealFlowBundle\Entity\DealRepository;
use MauticPlugin\DealFlowBundle\Entity\DealTask;
use MauticPlugin\DealFlowBundle\Entity\DealTaskRepository;
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 Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

final class DealApiController extends CommonController
{
    public function listAction(
        Request $request,
        DealAuthorization $authorization,
        DefaultPipelineInitializer $pipelineInitializer,
        DealRepository $dealRepository,
        DealContextBuilder $contextBuilder,
        DealManager $dealManager,
        DealFieldManager $dealFieldManager,
        ManagerRegistry $managerRegistry,
    ): JsonResponse {
        if ('POST' === $request->getMethod()) {
            if (!$authorization->canCreateDeals()) {
                return $this->accessDeniedResponse();
            }

            $payload = $this->getJsonPayload($request);

            try {
                $deal = $dealManager->createDealFromArray($payload, $this->user?->getId(), 'api');
            } catch (\InvalidArgumentException $exception) {
                return new JsonResponse(['error' => $exception->getMessage()], 400);
            }

            $this->attachContactsFromPayload($deal, $payload, $dealManager, $managerRegistry);

            return new JsonResponse($this->serializeDeal($deal, $contextBuilder, $dealFieldManager), 201);
        }

        if (!$authorization->canViewDeals()) {
            return $this->accessDeniedResponse();
        }

        $pipeline = $pipelineInitializer->resolvePipeline(
            null !== $request->query->get('pipeline_id') ? (int) $request->query->get('pipeline_id') : null
        );
        $deals = $dealRepository->findForPipeline(
            $pipeline,
            [
                'pipelineId' => $request->query->get('pipeline_id'),
                'search' => $request->query->get('search'),
                'status' => $request->query->get('status'),
                'ownerId' => $request->query->get('owner_id'),
                'companyId' => $request->query->get('company_id'),
            ],
            $authorization->getCurrentUserId(),
            $authorization->canViewOtherDeals(),
        );

        $contexts = $contextBuilder->buildForDeals($deals);
        $items = [];
        foreach ($deals as $deal) {
            $items[] = $this->serializeDeal($deal, $contextBuilder, $dealFieldManager, $contexts[$deal->getId() ?? spl_object_id($deal)] ?? null);
        }

        return new JsonResponse([
            'total' => count($items),
            'items' => $items,
        ]);
    }

    public function detailAction(
        int $id,
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealContextBuilder $contextBuilder,
        DealManager $dealManager,
        DealFieldManager $dealFieldManager,
    ): JsonResponse {
        $deal = $dealRepository->find($id);
        if (!$deal instanceof Deal) {
            return new JsonResponse(['error' => 'Deal not found'], 404);
        }

        if ('PATCH' === $request->getMethod()) {
            if (!$authorization->canEditDeal($deal)) {
                return $this->accessDeniedResponse();
            }

            try {
                $deal = $dealManager->updateDealFromArray($deal, $this->getJsonPayload($request), $this->user?->getId(), 'api');
            } catch (\InvalidArgumentException $exception) {
                return new JsonResponse(['error' => $exception->getMessage()], 400);
            }
        } elseif (!$authorization->canViewDeal($deal)) {
            return $this->accessDeniedResponse();
        }

        return new JsonResponse($this->serializeDeal($deal, $contextBuilder, $dealFieldManager));
    }

    public function stageAction(
        int $id,
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealManager $dealManager,
        \MauticPlugin\DealFlowBundle\Entity\DealStageRepository $stageRepository,
        DealContextBuilder $contextBuilder,
        DealFieldManager $dealFieldManager,
    ): JsonResponse {
        $deal = $dealRepository->find($id);
        if (!$deal instanceof Deal) {
            return new JsonResponse(['error' => 'Deal not found'], 404);
        }

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

        $payload = $this->getJsonPayload($request);
        $stageId = $payload['stage_id'] ?? null;
        if (null === $stageId) {
            return new JsonResponse(['error' => 'stage_id is required'], 400);
        }

        $stage = $stageRepository->find((int) $stageId);
        if (null === $stage || $stage->getPipeline()?->getId() !== $deal->getPipeline()?->getId()) {
            return new JsonResponse(['error' => 'Stage not found'], 404);
        }

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

        return new JsonResponse($this->serializeDeal($deal, $contextBuilder, $dealFieldManager));
    }

    public function contactsAction(
        int $id,
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealManager $dealManager,
        DealContextBuilder $contextBuilder,
        ManagerRegistry $managerRegistry,
        DealFieldManager $dealFieldManager,
    ): JsonResponse {
        $deal = $dealRepository->find($id);
        if (!$deal instanceof Deal) {
            return new JsonResponse(['error' => 'Deal not found'], 404);
        }

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

        $this->attachContactsFromPayload($deal, $this->getJsonPayload($request), $dealManager, $managerRegistry);

        return new JsonResponse($this->serializeDeal($deal, $contextBuilder, $dealFieldManager));
    }

    public function notesAction(
        int $id,
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealManager $dealManager,
        DealContextBuilder $contextBuilder,
        DealFieldManager $dealFieldManager,
    ): JsonResponse {
        $deal = $dealRepository->find($id);
        if (!$deal instanceof Deal) {
            return new JsonResponse(['error' => 'Deal not found'], 404);
        }

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

        $payload = $this->getJsonPayload($request);
        $body = trim((string) ($payload['body'] ?? ''));
        if ('' === $body) {
            return new JsonResponse(['error' => 'body is required'], 400);
        }

        $dealManager->addNote($deal, $body, $this->user?->getId(), 'api', (string) ($payload['source_type'] ?? 'manual'), $payload['source_ref'] ?? null);

        return new JsonResponse($this->serializeDeal($deal, $contextBuilder, $dealFieldManager), 201);
    }

    public function tasksAction(
        int $id,
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealManager $dealManager,
        DealContextBuilder $contextBuilder,
        DealFieldManager $dealFieldManager,
    ): JsonResponse {
        $deal = $dealRepository->find($id);
        if (!$deal instanceof Deal) {
            return new JsonResponse(['error' => 'Deal not found'], 404);
        }

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

        $payload = $this->getJsonPayload($request);
        if ('' === trim((string) ($payload['title'] ?? ''))) {
            return new JsonResponse(['error' => 'title is required'], 400);
        }

        $dealManager->addTask($deal, $payload, $this->user?->getId(), 'api');

        return new JsonResponse($this->serializeDeal($deal, $contextBuilder, $dealFieldManager), 201);
    }

    public function taskUpdateAction(
        int $id,
        int $taskId,
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealTaskRepository $taskRepository,
        EntityManagerInterface $entityManager,
        DealManager $dealManager,
    ): JsonResponse {
        $deal = $dealRepository->find($id);
        if (!$deal instanceof Deal) {
            return new JsonResponse(['error' => 'Deal not found'], 404);
        }

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

        $task = $taskRepository->find($taskId);
        if (!$task instanceof DealTask || $task->getDeal()?->getId() !== $deal->getId()) {
            return new JsonResponse(['error' => 'Task not found'], 404);
        }

        $payload = $this->getJsonPayload($request);
        if (isset($payload['status']) && 'completed' === $payload['status']) {
            $dealManager->completeTask($task, $this->user?->getId(), 'api');
        } else {
            if (isset($payload['title'])) {
                $task->setTitle((string) $payload['title']);
            }

            if (array_key_exists('description', $payload)) {
                $task->setDescription(null !== $payload['description'] ? (string) $payload['description'] : null);
            }

            if (array_key_exists('priority', $payload)) {
                $task->setPriority((string) $payload['priority']);
            }

            if (array_key_exists('owner_id', $payload)) {
                $task->setOwnerId(null !== $payload['owner_id'] ? (int) $payload['owner_id'] : null);
            }

            if (array_key_exists('due_at', $payload)) {
                $task->setDueAt(null !== $payload['due_at'] && '' !== $payload['due_at'] ? new \DateTimeImmutable((string) $payload['due_at']) : null);
            }

            if (array_key_exists('reminder_at', $payload)) {
                $task->setReminderAt(null !== $payload['reminder_at'] && '' !== $payload['reminder_at'] ? new \DateTimeImmutable((string) $payload['reminder_at']) : null);
            }

            $task->setUpdatedAt(new \DateTimeImmutable());
            $entityManager->flush();
        }

        return new JsonResponse([
            'task' => [
                'id' => $task->getId(),
                'status' => $task->getStatus(),
                'title' => $task->getTitle(),
                'priority' => $task->getPriority(),
                'due_at' => $task->getDueAt()?->format(DATE_ATOM),
                'reminder_at' => $task->getReminderAt()?->format(DATE_ATOM),
                'owner_id' => $task->getOwnerId(),
            ],
        ]);
    }

    public function attachmentsAction(
        int $id,
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealManager $dealManager,
    ): JsonResponse {
        $deal = $dealRepository->find($id);
        if (!$deal instanceof Deal) {
            return new JsonResponse(['error' => 'Deal not found'], 404);
        }

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

        $file = $request->files->get('attachment');
        if (!$file instanceof UploadedFile) {
            return new JsonResponse(['error' => 'attachment file is required'], 400);
        }

        try {
            $attachment = $dealManager->addAttachment($deal, $file, $this->user?->getId(), 'api');
        } catch (\Throwable $exception) {
            return new JsonResponse(['error' => $exception->getMessage()], 400);
        }

        return new JsonResponse([
            'attachment' => $this->serializeAttachment($attachment),
        ], 201);
    }

    public function attachmentFromFormAction(
        int $id,
        Request $request,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealManager $dealManager,
        EntityManagerInterface $entityManager,
        CoreParametersHelper $coreParametersHelper,
    ): JsonResponse {
        $deal = $dealRepository->find($id);
        if (!$deal instanceof Deal) {
            return new JsonResponse(['error' => 'Deal not found'], 404);
        }

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

        $payload = $this->getJsonPayload($request);
        $submissionId = $payload['form_submission_id'] ?? null;
        $fieldAlias = $payload['field_alias'] ?? 'deal_file';

        if (null === $submissionId) {
            return new JsonResponse(['error' => 'form_submission_id is required'], 400);
        }

        $conn = $entityManager->getConnection();

        // Look up the form submission
        $submission = $conn->fetchAssociative(
            'SELECT s.id, s.form_id, f.alias AS form_alias FROM '.MAUTIC_TABLE_PREFIX.'form_submissions s '
            .'JOIN '.MAUTIC_TABLE_PREFIX.'forms f ON f.id = s.form_id WHERE s.id = ?',
            [(int) $submissionId]
        );
        if (!$submission) {
            return new JsonResponse(['error' => 'Form submission not found'], 404);
        }

        // Mautic stores form results in per-form tables: form_results_{formId}_{alias}
        $resultsTable = MAUTIC_TABLE_PREFIX.'form_results_'.$submission['form_id'].'_'.$submission['form_alias'];

        // Look up the field ID for this alias (needed to locate the upload subdirectory)
        $fieldId = $conn->fetchOne(
            'SELECT id FROM '.MAUTIC_TABLE_PREFIX.'form_fields WHERE form_id = ? AND alias = ?',
            [(int) $submission['form_id'], (string) $fieldAlias]
        );
        if (!$fieldId) {
            return new JsonResponse(['error' => sprintf('Field alias "%s" does not exist on form %d', $fieldAlias, $submission['form_id'])], 400);
        }

        // Get the uploaded filename from the per-form results table
        $filename = $conn->fetchOne(
            'SELECT '.$conn->quoteIdentifier($fieldAlias).' FROM '.$conn->quoteIdentifier($resultsTable).' WHERE submission_id = ?',
            [(int) $submissionId]
        );
        if (!$filename || '' === trim((string) $filename)) {
            return new JsonResponse(['error' => sprintf('No file upload found for field alias "%s" in submission %d', $fieldAlias, $submissionId)], 400);
        }

        $filename = (string) $filename;

        // Check for duplicate: same original filename already attached to this deal
        $existing = $conn->fetchOne(
            'SELECT id FROM '.MAUTIC_TABLE_PREFIX.DealAttachment::TABLE_NAME.' WHERE deal_id = ? AND original_filename = ?',
            [$deal->getId(), $filename]
        );
        if ($existing) {
            return new JsonResponse(['error' => 'This form submission file is already attached to this deal'], 409);
        }

        // Mautic stores form uploads at: {form_upload_dir}/{formId}/{fieldId}/{filename}
        $formUploadDir = (string) $coreParametersHelper->get('form_upload_dir');
        $sourcePath = $formUploadDir.'/'.$submission['form_id'].'/'.$fieldId.'/'.$filename;

        if (!is_file($sourcePath)) {
            return new JsonResponse(['error' => sprintf('Source file no longer exists on disk: %s', $filename)], 400);
        }

        try {
            $attachment = $dealManager->addAttachmentFromPath($deal, $sourcePath, $filename, $this->user?->getId(), 'api');
        } catch (\Throwable $exception) {
            return new JsonResponse(['error' => $exception->getMessage()], 400);
        }

        return new JsonResponse([
            'attachment' => $this->serializeAttachment($attachment),
        ], 201);
    }

    public function timelineAction(
        int $id,
        DealAuthorization $authorization,
        DealRepository $dealRepository,
        DealTimelineBuilder $timelineBuilder,
    ): JsonResponse {
        $deal = $dealRepository->find($id);
        if (!$deal instanceof Deal) {
            return new JsonResponse(['error' => 'Deal not found'], 404);
        }

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

        return new JsonResponse([
            'timeline' => array_map(
                static fn (array $item): array => [
                    'type' => $item['type'],
                    'timestamp' => $item['timestamp']->format(DATE_ATOM),
                    'label' => $item['label'],
                    'body' => $item['body'],
                    'meta' => $item['meta'],
                ],
                $timelineBuilder->build($deal)
            ),
        ]);
    }

    public function pipelinesAction(
        DealAuthorization $authorization,
        \MauticPlugin\DealFlowBundle\Entity\PipelineRepository $pipelineRepository,
    ): JsonResponse {
        if (!$authorization->canViewDeals()) {
            return $this->accessDeniedResponse();
        }

        $pipelines = $pipelineRepository->findBy([], ['name' => 'ASC']);
        $items = [];
        foreach ($pipelines as $pipeline) {
            $items[] = [
                'id' => $pipeline->getId(),
                'name' => $pipeline->getName(),
                'is_default' => $pipeline->isDefault(),
            ];
        }

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

    public function pipelineStagesAction(
        int $pipelineId,
        DealAuthorization $authorization,
        \MauticPlugin\DealFlowBundle\Entity\PipelineRepository $pipelineRepository,
        \MauticPlugin\DealFlowBundle\Entity\DealStageRepository $stageRepository,
    ): JsonResponse {
        if (!$authorization->canViewDeals()) {
            return $this->accessDeniedResponse();
        }

        $pipeline = $pipelineRepository->find($pipelineId);
        if (null === $pipeline) {
            return new JsonResponse(['error' => 'Pipeline not found'], 404);
        }

        $stages = $stageRepository->findBy(['pipeline' => $pipeline], ['sortOrder' => 'ASC']);
        $items = [];
        foreach ($stages as $stage) {
            $items[] = [
                'id' => $stage->getId(),
                'name' => $stage->getName(),
                'stage_type' => $stage->getStageType(),
                'probability' => $stage->getProbability(),
                'sort_order' => $stage->getSortOrder(),
            ];
        }

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

    /**
     * @return array<string, mixed>
     */
    private function getJsonPayload(Request $request): array
    {
        $payload = json_decode($request->getContent(), true);

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

    /**
     * @param array<string, mixed>|null $context
     *
     * @return array<string, mixed>
     */
    private function serializeDeal(Deal $deal, DealContextBuilder $contextBuilder, DealFieldManager $dealFieldManager, ?array $context = null): array
    {
        $context ??= $contextBuilder->buildForDeal($deal);

        return [
            'id' => $deal->getId(),
            'title' => $deal->getTitle(),
            'status' => $deal->getStatus(),
            'pipeline' => [
                'id' => $deal->getPipeline()?->getId(),
                'name' => $deal->getPipeline()?->getName(),
            ],
            'stage' => [
                'id' => $deal->getStage()?->getId(),
                'name' => $deal->getStage()?->getName(),
                'type' => $deal->getStage()?->getStageType(),
            ],
            'amount' => $deal->getAmount(),
            'currency' => $deal->getCurrency(),
            'confidence' => $deal->getConfidence(),
            'company_id' => $deal->getCompanyId(),
            'company_name' => $context['companyName'],
            'owner_id' => $deal->getOwnerId(),
            'owner_name' => $context['ownerName'],
            'external_ref' => $deal->getExternalRef(),
            'expected_close_date' => $deal->getExpectedCloseDate()?->format('Y-m-d'),
            'description' => $deal->getDescription(),
            'custom_fields' => $dealFieldManager->getApiValues($deal),
            'contacts' => $context['contacts'],
            'updated_at' => $deal->getDateModified()?->format(DATE_ATOM),
            'created_at' => $deal->getDateAdded()?->format(DATE_ATOM),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function serializeAttachment(DealAttachment $attachment): array
    {
        return [
            'id' => $attachment->getId(),
            'deal_id' => $attachment->getDeal()?->getId(),
            'original_filename' => $attachment->getOriginalFilename(),
            'mime_type' => $attachment->getMimeType(),
            'size_bytes' => $attachment->getSizeBytes(),
            'uploaded_by' => $attachment->getUploadedBy(),
            'created_at' => $attachment->getCreatedAt()->format(DATE_ATOM),
            'download_path' => $this->generateUrl('mautic_dealflow_attachment_download', ['attachmentId' => $attachment->getId()]),
        ];
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function attachContactsFromPayload(Deal $deal, array $payload, DealManager $dealManager, ManagerRegistry $managerRegistry): void
    {
        $contacts = $payload['contacts'] ?? [];
        if (!is_array($contacts)) {
            return;
        }

        foreach ($contacts as $contact) {
            if (!is_array($contact)) {
                continue;
            }

            $contactId = $contact['id'] ?? $contact['contact_id'] ?? null;
            if (null === $contactId && !empty($contact['email'])) {
                $matched = $managerRegistry->getRepository(\Mautic\LeadBundle\Entity\Lead::class)->findOneBy(['email' => (string) $contact['email']]);
                $contactId = $matched?->getId();
            }

            if (null === $contactId) {
                continue;
            }

            $lead = $managerRegistry->getRepository(\Mautic\LeadBundle\Entity\Lead::class)->find((int) $contactId);
            if (!$lead) {
                continue;
            }

            $dealManager->attachContact($deal, (int) $contactId, $contact['label'] ?? null, $this->user?->getId(), 'api');
        }
    }

    private function accessDeniedResponse(): JsonResponse
    {
        return new JsonResponse(['error' => 'Forbidden'], 403);
    }
}
