<?php

declare(strict_types=1);

namespace App\Services\Findings;

use App\Models\AlertRule;
use App\Models\Finding;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Models\Workspace;
use App\Notifications\Findings\FindingEventNotification;
use App\Services\Alerts\AlertDispatchService;
use App\Services\Auth\CapabilityResolver;
use App\Support\Auth\Capabilities;
use Carbon\CarbonInterface;
use InvalidArgumentException;

final class FindingNotificationService
{
    public function __construct(
        private readonly AlertDispatchService $alertDispatchService,
        private readonly CapabilityResolver $capabilityResolver,
    ) {}

    /**
     * @param  array<string, mixed>  $context
     * @return array{
     *     event_type: string,
     *     fingerprint_key: string,
     *     direct_delivery_status: 'sent'|'suppressed'|'deduped'|'no_recipient',
     *     external_delivery_count: int
     * }
     */
    public function dispatch(Finding $finding, string $eventType, array $context = []): array
    {
        $finding = $this->reloadFinding($finding);
        $tenant = $finding->tenant;

        if (! $tenant instanceof ManagedEnvironment) {
            return $this->dispatchResult(
                eventType: $eventType,
                fingerprintKey: '',
                directDeliveryStatus: 'no_recipient',
                externalDeliveryCount: 0,
            );
        }

        if ($this->shouldSuppressEvent($finding, $eventType, $context)) {
            return $this->dispatchResult(
                eventType: $eventType,
                fingerprintKey: $this->fingerprintFor($finding, $eventType, $context),
                directDeliveryStatus: 'suppressed',
                externalDeliveryCount: 0,
            );
        }

        $resolution = $this->resolveRecipient($finding, $eventType, $context);
        $event = $this->buildEventEnvelope($finding, $tenant, $eventType, $resolution['reason'], $context);

        $directDeliveryStatus = $this->dispatchDirectNotification($finding, $tenant, $event, $resolution['user_id']);
        $externalDeliveryCount = $this->dispatchExternalCopies($finding, $event);

        return $this->dispatchResult(
            eventType: $eventType,
            fingerprintKey: (string) $event['fingerprint_key'],
            directDeliveryStatus: $directDeliveryStatus,
            externalDeliveryCount: $externalDeliveryCount,
        );
    }

    /**
     * @param  array<string, mixed>  $context
     * @return array{user_id: ?int, reason: ?string}
     */
    private function resolveRecipient(Finding $finding, string $eventType, array $context): array
    {
        return match ($eventType) {
            AlertRule::EVENT_FINDINGS_ASSIGNED => [
                'user_id' => $this->normalizeId($context['assignee_user_id'] ?? $finding->assignee_user_id),
                'reason' => 'new_assignee',
            ],
            AlertRule::EVENT_FINDINGS_REOPENED => $this->preferredRecipient(
                preferredUserId: $this->normalizeId($finding->assignee_user_id),
                preferredReason: 'current_assignee',
                fallbackUserId: $this->normalizeId($finding->owner_user_id),
                fallbackReason: 'current_owner',
            ),
            AlertRule::EVENT_FINDINGS_DUE_SOON => $this->preferredRecipient(
                preferredUserId: $this->normalizeId($finding->assignee_user_id),
                preferredReason: 'current_assignee',
                fallbackUserId: $this->normalizeId($finding->owner_user_id),
                fallbackReason: 'current_owner',
            ),
            AlertRule::EVENT_FINDINGS_OVERDUE => $this->preferredRecipient(
                preferredUserId: $this->normalizeId($finding->owner_user_id),
                preferredReason: 'current_owner',
                fallbackUserId: $this->normalizeId($finding->assignee_user_id),
                fallbackReason: 'current_assignee',
            ),
            default => throw new InvalidArgumentException(sprintf('Unsupported finding notification event [%s].', $eventType)),
        };
    }

    /**
     * @param  array<string, mixed>  $context
     * @return array<string, mixed>
     */
    private function buildEventEnvelope(
        Finding $finding,
        ManagedEnvironment $tenant,
        string $eventType,
        ?string $recipientReason,
        array $context,
    ): array {
        $severity = strtolower(trim((string) $finding->severity));
        $summary = $finding->resolvedSubjectDisplayName() ?? 'Finding #'.(int) $finding->getKey();
        $title = $this->eventLabel($eventType);
        $fingerprintKey = $this->fingerprintFor($finding, $eventType, $context);
        $dueCycleKey = $this->dueCycleKey($finding, $eventType);

        return [
            'event_type' => $eventType,
            'workspace_id' => (int) $finding->workspace_id,
            'managed_environment_id' => (int) $tenant->getKey(),
            'finding_id' => (int) $finding->getKey(),
            'severity' => $severity,
            'title' => $title,
            'body' => $this->eventBody($eventType, $tenant, $summary, ucfirst($severity)),
            'fingerprint_key' => $fingerprintKey,
            'due_cycle_key' => $dueCycleKey,
            'metadata' => [
                'tenant_name' => $tenant->getFilamentName(),
                'summary' => $summary,
                'recipient_reason' => $recipientReason,
                'owner_user_id' => $this->normalizeId($finding->owner_user_id),
                'assignee_user_id' => $this->normalizeId($finding->assignee_user_id),
                'due_at' => $this->optionalIso8601($finding->due_at),
                'reopened_at' => $this->optionalIso8601($finding->reopened_at),
                'severity_label' => ucfirst($severity),
            ],
        ];
    }

    /**
     * @param  array<string, mixed>  $event
     */
    private function dispatchDirectNotification(Finding $finding, ManagedEnvironment $tenant, array $event, ?int $userId): string
    {
        if (! is_int($userId) || $userId <= 0) {
            return 'no_recipient';
        }

        $user = User::query()->find($userId);

        if (! $user instanceof User) {
            return 'no_recipient';
        }

        if (! $user->canAccessTenant($tenant)) {
            return 'suppressed';
        }

        if (! $this->capabilityResolver->can($user, $tenant, Capabilities::TENANT_FINDINGS_VIEW)) {
            return 'suppressed';
        }

        if ($this->alreadySentDirectNotification($user, (string) $event['fingerprint_key'])) {
            return 'deduped';
        }

        $user->notify(new FindingEventNotification($finding, $tenant, $event));

        return 'sent';
    }

    /**
     * @param  array<string, mixed>  $event
     */
    private function dispatchExternalCopies(Finding $finding, array $event): int
    {
        $workspace = Workspace::query()->whereKey((int) $finding->workspace_id)->first();

        if (! $workspace instanceof Workspace) {
            return 0;
        }

        return $this->alertDispatchService->dispatchEvent($workspace, $event);
    }

    private function alreadySentDirectNotification(User $user, string $fingerprintKey): bool
    {
        if ($fingerprintKey === '') {
            return false;
        }

        return $user->notifications()
            ->where('type', FindingEventNotification::class)
            ->where('data->finding_event->fingerprint_key', $fingerprintKey)
            ->exists();
    }

    private function reloadFinding(Finding $finding): Finding
    {
        $fresh = Finding::query()
            ->with('tenant')
            ->withSubjectDisplayName()
            ->find($finding->getKey());

        if ($fresh instanceof Finding) {
            return $fresh;
        }

        $finding->loadMissing('tenant');

        return $finding;
    }

    /**
     * @param  array<string, mixed>  $context
     */
    private function shouldSuppressEvent(Finding $finding, string $eventType, array $context): bool
    {
        return match ($eventType) {
            AlertRule::EVENT_FINDINGS_ASSIGNED => ! $finding->hasOpenStatus()
                || $this->normalizeId($context['assignee_user_id'] ?? $finding->assignee_user_id) === null,
            AlertRule::EVENT_FINDINGS_DUE_SOON, AlertRule::EVENT_FINDINGS_OVERDUE => ! $finding->hasOpenStatus()
                || ! $finding->due_at instanceof CarbonInterface,
            default => false,
        };
    }

    /**
     * @param  array<string, mixed>  $context
     */
    private function fingerprintFor(Finding $finding, string $eventType, array $context): string
    {
        $findingId = (int) $finding->getKey();

        return match ($eventType) {
            AlertRule::EVENT_FINDINGS_ASSIGNED => sprintf(
                'finding:%d:%s:assignee:%d:updated:%s',
                $findingId,
                $eventType,
                $this->normalizeId($context['assignee_user_id'] ?? $finding->assignee_user_id) ?? 0,
                $this->optionalIso8601($finding->updated_at) ?? 'none',
            ),
            AlertRule::EVENT_FINDINGS_REOPENED => sprintf(
                'finding:%d:%s:reopened:%s',
                $findingId,
                $eventType,
                $this->optionalIso8601($finding->reopened_at) ?? 'none',
            ),
            AlertRule::EVENT_FINDINGS_DUE_SOON,
            AlertRule::EVENT_FINDINGS_OVERDUE => sprintf(
                'finding:%d:%s:due:%s',
                $findingId,
                $eventType,
                $this->dueCycleKey($finding, $eventType) ?? 'none',
            ),
            default => throw new InvalidArgumentException(sprintf('Unsupported finding notification event [%s].', $eventType)),
        };
    }

    private function dueCycleKey(Finding $finding, string $eventType): ?string
    {
        if (! in_array($eventType, [
            AlertRule::EVENT_FINDINGS_DUE_SOON,
            AlertRule::EVENT_FINDINGS_OVERDUE,
        ], true)) {
            return null;
        }

        return $this->optionalIso8601($finding->due_at);
    }

    private function eventLabel(string $eventType): string
    {
        return match ($eventType) {
            AlertRule::EVENT_FINDINGS_ASSIGNED => 'Finding assigned',
            AlertRule::EVENT_FINDINGS_REOPENED => 'Finding reopened',
            AlertRule::EVENT_FINDINGS_DUE_SOON => 'Finding due soon',
            AlertRule::EVENT_FINDINGS_OVERDUE => 'Finding overdue',
            default => throw new InvalidArgumentException(sprintf('Unsupported finding notification event [%s].', $eventType)),
        };
    }

    private function eventBody(string $eventType, ManagedEnvironment $tenant, string $summary, string $severityLabel): string
    {
        return match ($eventType) {
            AlertRule::EVENT_FINDINGS_ASSIGNED => sprintf(
                '%s in %s was assigned. %s severity.',
                $summary,
                $tenant->getFilamentName(),
                $severityLabel,
            ),
            AlertRule::EVENT_FINDINGS_REOPENED => sprintf(
                '%s in %s reopened and needs follow-up. %s severity.',
                $summary,
                $tenant->getFilamentName(),
                $severityLabel,
            ),
            AlertRule::EVENT_FINDINGS_DUE_SOON => sprintf(
                '%s in %s is due within 24 hours. %s severity.',
                $summary,
                $tenant->getFilamentName(),
                $severityLabel,
            ),
            AlertRule::EVENT_FINDINGS_OVERDUE => sprintf(
                '%s in %s is overdue. %s severity.',
                $summary,
                $tenant->getFilamentName(),
                $severityLabel,
            ),
            default => throw new InvalidArgumentException(sprintf('Unsupported finding notification event [%s].', $eventType)),
        };
    }

    /**
     * @return array{user_id: ?int, reason: ?string}
     */
    private function preferredRecipient(
        ?int $preferredUserId,
        string $preferredReason,
        ?int $fallbackUserId,
        string $fallbackReason,
    ): array {
        if (is_int($preferredUserId) && $preferredUserId > 0) {
            return [
                'user_id' => $preferredUserId,
                'reason' => $preferredReason,
            ];
        }

        if (is_int($fallbackUserId) && $fallbackUserId > 0) {
            return [
                'user_id' => $fallbackUserId,
                'reason' => $fallbackReason,
            ];
        }

        return [
            'user_id' => null,
            'reason' => null,
        ];
    }

    private function normalizeId(mixed $value): ?int
    {
        if (! is_numeric($value)) {
            return null;
        }

        $normalized = (int) $value;

        return $normalized > 0 ? $normalized : null;
    }

    private function optionalIso8601(mixed $value): ?string
    {
        if (! $value instanceof CarbonInterface) {
            return null;
        }

        return $value->toIso8601String();
    }

    /**
     * @return array{
     *     event_type: string,
     *     fingerprint_key: string,
     *     direct_delivery_status: 'sent'|'suppressed'|'deduped'|'no_recipient',
     *     external_delivery_count: int
     * }
     */
    private function dispatchResult(
        string $eventType,
        string $fingerprintKey,
        string $directDeliveryStatus,
        int $externalDeliveryCount,
    ): array {
        return [
            'event_type' => $eventType,
            'fingerprint_key' => $fingerprintKey,
            'direct_delivery_status' => $directDeliveryStatus,
            'external_delivery_count' => $externalDeliveryCount,
        ];
    }
}
