<?php

declare(strict_types=1);

namespace App\Support\ProductTelemetry;

use App\Models\ProductUsageEvent;
use BackedEnum;
use DateTimeInterface;
use Illuminate\Support\Carbon;
use InvalidArgumentException;

final class ProductTelemetryRecorder
{
    public function __construct(private readonly ProductUsageEventCatalog $catalog) {}

    /**
     * @param  array<string, mixed>  $metadata
     */
    public function record(
        string $eventName,
        int $workspaceId,
        int $tenantId,
        int $userId,
        string $subjectType,
        string|int $subjectId,
        array $metadata = [],
        ?DateTimeInterface $occurredAt = null,
    ): ProductUsageEvent {
        $occurredAt ??= now();

        return ProductUsageEvent::query()->create([
            'workspace_id' => $workspaceId,
            'managed_environment_id' => $tenantId,
            'user_id' => $userId,
            'event_name' => $eventName,
            'feature_area' => $this->catalog->featureArea($eventName),
            'subject_type' => $this->normalizeToken($subjectType, 'subject type'),
            'subject_id' => $this->normalizeSubjectId($subjectId),
            'metadata' => $this->normalizeMetadata($eventName, $metadata),
            'occurred_at' => Carbon::instance($occurredAt),
        ]);
    }

    /**
     * @param  array<string, mixed>  $metadata
     * @return array<string, bool|int|string>
     */
    private function normalizeMetadata(string $eventName, array $metadata): array
    {
        $allowedKeys = $this->catalog->allowedMetadataKeys($eventName);
        $unknownKeys = array_values(array_diff(array_keys($metadata), $allowedKeys));

        if ($unknownKeys !== []) {
            $keys = implode(', ', $unknownKeys);

            throw new InvalidArgumentException("Unsupported telemetry metadata keys [{$keys}] for [{$eventName}].");
        }

        $normalized = [];

        foreach ($metadata as $key => $value) {
            if ($value === null) {
                continue;
            }

            $normalized[$key] = $this->normalizeMetadataValue($value, $key);
        }

        return $normalized;
    }

    /**
     * @return bool|int|string
     */
    private function normalizeMetadataValue(mixed $value, string $key): bool|int|string
    {
        if ($value instanceof BackedEnum) {
            return $this->normalizeToken((string) $value->value, $key);
        }

        if ($value instanceof DateTimeInterface) {
            return Carbon::instance($value)->toIso8601String();
        }

        if (is_bool($value) || is_int($value)) {
            return $value;
        }

        if (is_string($value)) {
            return $this->normalizeToken($value, $key);
        }

        throw new InvalidArgumentException("Telemetry metadata [{$key}] must be a bounded scalar, enum, or timestamp.");
    }

    private function normalizeSubjectId(string|int $subjectId): string
    {
        if (is_int($subjectId)) {
            return (string) $subjectId;
        }

        return $this->normalizeToken($subjectId, 'subject id');
    }

    private function normalizeToken(string $value, string $field): string
    {
        $normalized = trim($value);

        if ($normalized === '') {
            throw new InvalidArgumentException("Telemetry {$field} cannot be empty.");
        }

        if (strlen($normalized) > 120) {
            throw new InvalidArgumentException("Telemetry {$field} is too long.");
        }

        if (! preg_match('/\A[a-zA-Z0-9._:+\/-]+\z/', $normalized)) {
            throw new InvalidArgumentException("Telemetry {$field} must be a machine-safe token.");
        }

        if (str_contains($normalized, '@')) {
            throw new InvalidArgumentException("Telemetry {$field} cannot contain email-like data.");
        }

        return $normalized;
    }
}
