<?php

declare(strict_types=1);

namespace MauticPlugin\DealFlowBundle\Service;

use Doctrine\ORM\EntityManagerInterface;
use MauticPlugin\DealFlowBundle\DealFlowEvents;
use MauticPlugin\DealFlowBundle\Entity\Deal;
use MauticPlugin\DealFlowBundle\Entity\DealAuditEventRepository;
use MauticPlugin\DealFlowBundle\Entity\DealNoteRepository;
use MauticPlugin\DealFlowBundle\Entity\DealTaskRepository;

class DealTimelineBuilder
{
    public function __construct(
        private readonly DealNoteRepository $noteRepository,
        private readonly DealTaskRepository $taskRepository,
        private readonly DealAuditEventRepository $auditRepository,
        private readonly EntityManagerInterface $entityManager,
    ) {
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function build(Deal $deal): array
    {
        $items = [];

        foreach ($this->noteRepository->findByDeal($deal) as $note) {
            $items[] = [
                'type' => 'note',
                'timestamp' => $note->getCreatedAt(),
                'label' => 'Note added',
                'body' => $note->getBody(),
                'meta' => [
                    'sourceType' => $note->getSourceType(),
                    'sourceRef' => $note->getSourceRef(),
                ],
            ];
        }

        foreach ($this->taskRepository->findByDeal($deal) as $task) {
            $items[] = [
                'type' => 'task',
                'timestamp' => $task->getUpdatedAt(),
                'label' => 'Task '.$task->getStatus(),
                'body' => $task->getTitle(),
                'meta' => [
                    'status' => $task->getStatus(),
                    'dueAt' => $task->getDueAt()?->format(DATE_ATOM),
                ],
            ];
        }

        $auditEvents = $this->auditRepository->findByDeal($deal);

        // Collect all actor IDs to resolve user names in a single query
        $actorIds = [];
        foreach ($auditEvents as $audit) {
            $actorId = $audit->getActorId();
            if (null !== $actorId && $actorId > 0) {
                $actorIds[$actorId] = true;
            }
        }
        $userNames = $this->resolveUserNames(array_keys($actorIds));

        foreach ($auditEvents as $audit) {
            $body = $this->buildAuditBody($audit->getEventType(), $audit->getPayload(), $audit->getActorId(), $userNames);

            $items[] = [
                'type' => 'audit',
                'timestamp' => $audit->getCreatedAt(),
                'label' => $audit->getEventType(),
                'body' => $body,
                'meta' => $audit->getPayload(),
            ];
        }

        usort(
            $items,
            static fn (array $left, array $right): int => $right['timestamp'] <=> $left['timestamp']
        );

        return $items;
    }

    /**
     * @param array<string, mixed> $payload
     * @param array<int, string>   $userNames
     */
    private function buildAuditBody(string $eventType, array $payload, ?int $actorId, array $userNames): string
    {
        $description = match ($eventType) {
            DealFlowEvents::DEAL_CREATED => 'Deal created',
            DealFlowEvents::DEAL_CONTACT_ATTACHED => $this->describeContactAttached($payload),
            DealFlowEvents::DEAL_CONTACT_DETACHED => $this->describeContactDetached($payload),
            DealFlowEvents::DEAL_STAGE_CHANGED => $this->describeStageChanged($payload),
            DealFlowEvents::DEAL_FIELD_UPDATED => $this->describeFieldUpdated($payload),
            DealFlowEvents::DEAL_CLOSED_WON => 'Deal closed won',
            DealFlowEvents::DEAL_CLOSED_LOST => 'Deal closed lost',
            DealFlowEvents::DEAL_NOTE_ADDED => 'Note added',
            DealFlowEvents::DEAL_TASK_ADDED => sprintf('Task added: %s', $payload['title'] ?? ''),
            DealFlowEvents::DEAL_TASK_COMPLETED => sprintf('Task completed: %s', $payload['title'] ?? ''),
            DealFlowEvents::DEAL_TASK_UNCOMPLETED => sprintf('Task reopened: %s', $payload['title'] ?? ''),
            DealFlowEvents::DEAL_TASK_UPDATED => sprintf('Task updated: %s', $payload['title'] ?? ''),
            DealFlowEvents::DEAL_ATTACHMENT_ADDED => sprintf('Attachment added: %s', $payload['filename'] ?? ''),
            DealFlowEvents::DEAL_ATTACHMENT_DELETED => sprintf('Attachment deleted: %s', $payload['filename'] ?? ''),
            DealFlowEvents::DEAL_OWNER_ASSIGNED => 'Owner assigned',
            DealFlowEvents::DEAL_OWNER_REASSIGNED => 'Owner reassigned',
            DealFlowEvents::DEAL_COMPANY_CHANGED => 'Company changed',
            DealFlowEvents::DEAL_UPDATED => $this->describeUpdated($payload),
            default => $eventType,
        };

        // Append actor name if available
        if (null !== $actorId && isset($userNames[$actorId])) {
            $description .= ' by '.$userNames[$actorId];
        }

        return $description;
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function describeContactAttached(array $payload): string
    {
        $label = $payload['label'] ?? null;

        return null !== $label && '' !== (string) $label
            ? sprintf('Added contact as %s', $label)
            : 'Contact added to buying committee';
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function describeContactDetached(array $payload): string
    {
        $label = $payload['label'] ?? null;

        return null !== $label && '' !== (string) $label
            ? sprintf('Removed contact (%s) from buying committee', $label)
            : 'Contact removed from buying committee';
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function describeStageChanged(array $payload): string
    {
        $prev = $payload['previousStage'] ?? $payload['changes']['stage'][0] ?? null;
        $curr = $payload['currentStage'] ?? $payload['changes']['stage'][1] ?? null;

        if (null !== $prev && null !== $curr) {
            return sprintf('Moved from %s to %s', $prev, $curr);
        }

        if (null !== $curr) {
            return sprintf('Moved to %s', $curr);
        }

        return 'Stage changed';
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function describeFieldUpdated(array $payload): string
    {
        $field = $payload['field'] ?? 'unknown';
        $old = $payload['old_value'] ?? null;
        $new = $payload['new_value'] ?? null;

        $fieldLabel = str_replace('_', ' ', (string) $field);

        if (null !== $old && '' !== (string) $old && null !== $new && '' !== (string) $new) {
            return sprintf('Changed %s from %s to %s', $fieldLabel, $old, $new);
        }

        if (null !== $new && '' !== (string) $new) {
            return sprintf('Set %s to %s', $fieldLabel, $new);
        }

        return sprintf('Cleared %s', $fieldLabel);
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function describeUpdated(array $payload): string
    {
        if (isset($payload['action'])) {
            return match ($payload['action']) {
                'archived' => 'Deal archived',
                'unarchived' => 'Deal restored',
                default => 'Deal updated',
            };
        }

        return 'Deal updated';
    }

    /**
     * @param list<int> $userIds
     *
     * @return array<int, string>
     */
    private function resolveUserNames(array $userIds): array
    {
        if ([] === $userIds) {
            return [];
        }

        // Build a safe IN clause without ArrayParameterType (requires DBAL 3.6+,
        // not available in Mautic 5/6). Cast to int to prevent SQL injection.
        $conn = $this->entityManager->getConnection();
        $inList = implode(',', array_map('intval', $userIds));
        $rows = $conn->fetchAllAssociative(
            sprintf("SELECT id, CONCAT(first_name, ' ', last_name) AS name FROM %susers WHERE id IN (%s)", MAUTIC_TABLE_PREFIX, $inList)
        );

        $map = [];
        foreach ($rows as $row) {
            $name = trim((string) $row['name']);
            if ('' !== $name) {
                $map[(int) $row['id']] = $name;
            }
        }

        return $map;
    }
}
