<?php

declare(strict_types=1);

namespace CpCreation\VitiCore\Bridge\Symfony\Bundle\Tracking;

use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;

final class MarketingAttributionStorage
{
    public const string REQUEST_ATTRIBUTE = '_viti_core_marketing_attribution';
    public const string COOKIE_NAME = 'serenity_mk';

    private const int COOKIE_LIFETIME = 2592000;
    private const int COOKIE_URL_LENGTH = 500;

    private const array TRACKING_PARAMETERS = [
        'utm_source',
        'utm_medium',
        'utm_campaign',
        'utm_term',
        'utm_content',
        'utm_id',
        'mtm_source',
        'mtm_medium',
        'mtm_campaign',
        'mtm_keyword',
        'mtm_content',
        'mtm_cid',
        'gclid',
        'gbraid',
        'wbraid',
        'fbclid',
        'msclkid',
        'ttclid',
        'li_fat_id',
    ];

    /**
     * @return array<string, mixed>|null
     */
    public function capture(Request $request): ?array
    {
        $current = $this->extractCookieAttribution($request);
        $parameters = $this->extractTrackingParameters($request);
        $now = (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM);
        $meta = array_replace(
            \is_array($current['meta'] ?? null) ? $current['meta'] : [],
            $this->extractMetaBrowserIdentifiers($request),
        );

        if ([] === $parameters) {
            $current ??= [
                'visitor_id' => $this->generateVisitorId(),
                'landing_url' => mb_substr($request->getUri(), 0, 2000),
                'referrer' => $this->trimNullable($request->headers->get('Referer'), 2000),
                'captured_at' => $now,
                'parameters' => [],
            ];
            $current['updated_at'] = $now;

            if ([] !== $meta) {
                $current['meta'] = $meta;
            }

            return $current;
        }

        $visitorId = \is_string($current['visitor_id'] ?? null)
            ? $current['visitor_id']
            : $this->generateVisitorId();

        $attribution = [
            'visitor_id' => $visitorId,
            'landing_url' => mb_substr($request->getUri(), 0, 2000),
            'referrer' => $this->trimNullable($request->headers->get('Referer'), 2000),
            'captured_at' => $now,
            'updated_at' => $now,
            'parameters' => $parameters,
        ];

        if ([] !== $meta) {
            $attribution['meta'] = $meta;
        }

        return $attribution;
    }

    /**
     * @return array<string, mixed>|null
     */
    public function getCurrent(?Request $request): ?array
    {
        if (null === $request) {
            return null;
        }

        return $this->normalizeStoredAttribution($request->attributes->get(self::REQUEST_ATTRIBUTE))
            ?? $this->extractCookieAttribution($request);
    }

    /**
     * @param array<string, mixed> $attribution
     */
    public function createPersistentCookie(array $attribution): Cookie
    {
        return Cookie::create(
            self::COOKIE_NAME,
            $this->encodeCookieAttribution($attribution),
            time() + self::COOKIE_LIFETIME,
            '/',
            null,
            true,
            true,
            false,
            Cookie::SAMESITE_LAX,
        );
    }

    /**
     * @return array<string, string>
     */
    private function extractTrackingParameters(Request $request): array
    {
        $parameters = [];

        foreach (self::TRACKING_PARAMETERS as $name) {
            $value = $request->query->get($name);

            if (!\is_scalar($value)) {
                continue;
            }

            $value = mb_trim((string) $value);

            if ('' === $value) {
                continue;
            }

            $parameters[$name] = mb_substr($value, 0, 255);
        }

        return $parameters;
    }

    /**
     * @return array<string, string>
     */
    private function extractMetaBrowserIdentifiers(Request $request): array
    {
        $meta = [];
        $fbp = $this->trimScalarNullable($request->cookies->get('_fbp'), 255);
        $fbc = $this->trimScalarNullable($request->cookies->get('_fbc'), 255);
        $fbclid = $this->trimScalarNullable($request->query->get('fbclid'), 255);

        if (null !== $fbp) {
            $meta['fbp'] = $fbp;
        }

        if (null !== $fbc) {
            $meta['fbc'] = $fbc;
        } elseif (null !== $fbclid) {
            $meta['fbc'] = sprintf('fb.1.%d.%s', (int) floor(microtime(true) * 1000), $fbclid);
        }

        return $meta;
    }

    /**
     * @return array<string, mixed>|null
     */
    private function normalizeStoredAttribution(mixed $value): ?array
    {
        if (!\is_array($value)) {
            return null;
        }

        return $value;
    }

    /**
     * @return array<string, mixed>|null
     */
    private function extractCookieAttribution(Request $request): ?array
    {
        $encoded = $this->trimScalarNullable($request->cookies->get(self::COOKIE_NAME), 4096);

        if (null === $encoded) {
            return null;
        }

        $json = base64_decode(strtr($encoded, '-_', '+/'), true);

        if (!\is_string($json)) {
            return null;
        }

        try {
            $decoded = json_decode($json, true, 512, \JSON_THROW_ON_ERROR);
        } catch (\JsonException) {
            return null;
        }

        return $this->normalizeStoredAttribution($decoded);
    }

    /**
     * @param array<string, mixed> $attribution
     */
    private function encodeCookieAttribution(array $attribution): string
    {
        $payload = $this->prepareCookieAttribution($attribution);
        $json = json_encode($payload, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES);

        return rtrim(strtr(base64_encode($json), '+/', '-_'), '=');
    }

    /**
     * @param array<string, mixed> $attribution
     *
     * @return array<string, mixed>
     */
    private function prepareCookieAttribution(array $attribution): array
    {
        $payload = [];

        foreach (['visitor_id', 'captured_at', 'updated_at'] as $key) {
            if (\is_string($attribution[$key] ?? null) && '' !== $attribution[$key]) {
                $payload[$key] = mb_substr($attribution[$key], 0, 255);
            }
        }

        if (\is_string($attribution['landing_url'] ?? null) && '' !== $attribution['landing_url']) {
            $payload['landing_url'] = mb_substr($attribution['landing_url'], 0, self::COOKIE_URL_LENGTH);
        }

        if (\is_string($attribution['referrer'] ?? null) && '' !== $attribution['referrer']) {
            $payload['referrer'] = mb_substr($attribution['referrer'], 0, self::COOKIE_URL_LENGTH);
        }

        if (\is_array($attribution['parameters'] ?? null)) {
            $payload['parameters'] = $this->normalizeStringMap($attribution['parameters'], 255);
        }

        if (\is_array($attribution['meta'] ?? null)) {
            $payload['meta'] = $this->normalizeStringMap($attribution['meta'], 255);
        }

        return $payload;
    }

    /**
     * @return array<string, string>
     */
    private function normalizeStringMap(array $values, int $length): array
    {
        $normalized = [];

        foreach ($values as $key => $value) {
            if (!\is_string($key) || !\is_scalar($value)) {
                continue;
            }

            $value = $this->trimScalarNullable($value, $length);

            if (null !== $value) {
                $normalized[$key] = $value;
            }
        }

        return $normalized;
    }

    private function trimNullable(?string $value, int $length): ?string
    {
        $value = null !== $value ? mb_trim($value) : null;

        return '' !== (string) $value ? mb_substr((string) $value, 0, $length) : null;
    }

    private function trimScalarNullable(mixed $value, int $length): ?string
    {
        if (!\is_scalar($value)) {
            return null;
        }

        return $this->trimNullable((string) $value, $length);
    }

    private function generateVisitorId(): string
    {
        try {
            return bin2hex(random_bytes(8));
        } catch (\Random\RandomException) {
            return mb_substr(hash('sha256', uniqid('', true)), 0, 16);
        }
    }
}
