<?php

declare(strict_types=1);

namespace App\Services\Traffic;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;

class OnlineVisitorRepository
{
    public const INDEX_KEY = 'skinbase:presence:online:index';
    public const KEY_PREFIX = 'skinbase:presence:online';
    public const TTL_SECONDS = 300;

    public function __construct(private readonly BotClassifier $classifier)
    {
    }

    public function track(Request $request): void
    {
        try {
            $classification = $this->classifier->classify($request);
            $visitorKey = $this->resolveVisitorKey($request, $classification);
            $existing = $this->readRecord($visitorKey);
            $user = $request->user();
            $now = now()->toIso8601String();

            $record = [
                'visitor_key' => $visitorKey,
                'type' => $classification['is_bot']
                    ? (string) $classification['type']
                    : ($user ? 'human_logged' : 'human_guest'),
                'bot_family' => $classification['is_bot'] ? $classification['family'] : null,
                'user_id' => $this->resolveUserId($user),
                'user_name' => $this->resolveUserName($user),
                'ip_masked' => $this->maskIp($this->resolveIp($request)),
                'ip_hash' => hash('sha256', $this->resolveIp($request)),
                'user_agent' => $this->truncate((string) $request->userAgent(), 512),
                'browser' => $this->detectBrowser((string) $request->userAgent()),
                'platform' => $this->detectPlatform((string) $request->userAgent()),
                'current_url' => $this->currentUrl($request),
                'route_name' => $request->route()?->getName(),
                'referer' => $this->truncate((string) $request->headers->get('referer', ''), 512) ?: null,
                'first_seen_at' => is_string($existing['first_seen_at'] ?? null)
                    ? $existing['first_seen_at']
                    : $now,
                'last_seen_at' => $now,
                'hits' => (int) ($existing['hits'] ?? 0) + 1,
            ];

            $this->storeRecord($visitorKey, $record, self::TTL_SECONDS);
            $this->addIndexMember($visitorKey);
        } catch (\Throwable $e) {
            Log::warning('Online visitor tracking failed', [
                'error' => $e->getMessage(),
                'path' => $request->path(),
            ]);
        }
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function all(): array
    {
        try {
            $visitorKeys = array_values(array_unique(array_filter(array_map('strval', $this->readIndexMembers()))));
        } catch (\Throwable $e) {
            Log::warning('Online visitor index read failed', ['error' => $e->getMessage()]);

            return [];
        }

        $records = [];
        $expired = [];

        foreach ($visitorKeys as $visitorKey) {
            try {
                $record = $this->readRecord($visitorKey);
            } catch (\Throwable $e) {
                Log::warning('Online visitor record read failed', [
                    'error' => $e->getMessage(),
                    'visitor_key' => $visitorKey,
                ]);
                $record = null;
            }

            if ($record === null) {
                $expired[] = $visitorKey;
                continue;
            }

            $records[] = $record;
        }

        if ($expired !== []) {
            try {
                $this->removeIndexMembers($expired);
            } catch (\Throwable $e) {
                Log::warning('Online visitor index cleanup failed', ['error' => $e->getMessage()]);
            }
        }

        usort($records, static function (array $left, array $right): int {
            return strtotime((string) ($right['last_seen_at'] ?? '')) <=> strtotime((string) ($left['last_seen_at'] ?? ''));
        });

        return $records;
    }

    /**
     * @return array{total:int,logged:int,guests:int,bots:int,search_bots:int,ai_bots:int,social_bots:int,seo_bots:int,suspicious_bots:int}
     */
    public function summary(): array
    {
        $records = $this->all();

        return [
            'total' => count($records),
            'logged' => count(array_filter($records, static fn (array $record): bool => ($record['type'] ?? null) === 'human_logged')),
            'guests' => count(array_filter($records, static fn (array $record): bool => ($record['type'] ?? null) === 'human_guest')),
            'bots' => count(array_filter($records, static fn (array $record): bool => str_ends_with((string) ($record['type'] ?? ''), '_bot'))),
            'search_bots' => count(array_filter($records, static fn (array $record): bool => ($record['type'] ?? null) === 'search_bot')),
            'ai_bots' => count(array_filter($records, static fn (array $record): bool => ($record['type'] ?? null) === 'ai_bot')),
            'social_bots' => count(array_filter($records, static fn (array $record): bool => ($record['type'] ?? null) === 'social_bot')),
            'seo_bots' => count(array_filter($records, static fn (array $record): bool => ($record['type'] ?? null) === 'seo_bot')),
            'suspicious_bots' => count(array_filter($records, static fn (array $record): bool => ($record['type'] ?? null) === 'suspicious_bot')),
        ];
    }

    /**
     * @return array<int, array{url:string, visitors:int}>
     */
    public function activePages(): array
    {
        $counts = [];

        foreach ($this->all() as $record) {
            $url = trim((string) ($record['current_url'] ?? ''));

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

            $counts[$url] = ($counts[$url] ?? 0) + 1;
        }

        arsort($counts);

        $pages = [];

        foreach ($counts as $url => $visitors) {
            $pages[] = [
                'url' => $url,
                'visitors' => $visitors,
            ];
        }

        return $pages;
    }

    public function forget(string $visitorKey): void
    {
        try {
            $this->deleteRecord($visitorKey);
            $this->removeIndexMembers([$visitorKey]);
        } catch (\Throwable $e) {
            Log::warning('Online visitor forget failed', [
                'error' => $e->getMessage(),
                'visitor_key' => $visitorKey,
            ]);
        }
    }

    /**
     * @return array<int, string>
     */
    protected function readIndexMembers(): array
    {
        return array_map('strval', Redis::smembers(self::INDEX_KEY));
    }

    /**
     * @return array<string, mixed>|null
     */
    protected function readRecord(string $visitorKey): ?array
    {
        $raw = Redis::get($this->recordKey($visitorKey));

        if (! is_string($raw) || $raw === '') {
            return null;
        }

        $decoded = json_decode($raw, true);

        return is_array($decoded) ? $decoded : null;
    }

    /**
     * @param array<string, mixed> $record
     */
    protected function storeRecord(string $visitorKey, array $record, int $ttlSeconds): void
    {
        Redis::setex(
            $this->recordKey($visitorKey),
            $ttlSeconds,
            (string) json_encode($record, JSON_UNESCAPED_SLASHES)
        );
    }

    protected function addIndexMember(string $visitorKey): void
    {
        Redis::sadd(self::INDEX_KEY, $visitorKey);
    }

    /**
     * @param array<int, string> $visitorKeys
     */
    protected function removeIndexMembers(array $visitorKeys): void
    {
        if ($visitorKeys === []) {
            return;
        }

        Redis::srem(self::INDEX_KEY, ...$visitorKeys);
    }

    protected function deleteRecord(string $visitorKey): void
    {
        Redis::del($this->recordKey($visitorKey));
    }

    /**
     * @param array{is_bot: bool, type: ?string, family: ?string} $classification
     */
    private function resolveVisitorKey(Request $request, array $classification): string
    {
        $user = $request->user();

        if ($user) {
            return 'user:' . $user->getAuthIdentifier();
        }

        $ip = $this->resolveIp($request);
        $userAgent = (string) $request->userAgent();

        if ($classification['is_bot']) {
            return 'bot:' . hash('sha256', $ip . '|' . $userAgent);
        }

        $sessionCookieName = (string) config('session.cookie', 'laravel_session');
        $sessionCookie = (string) $request->cookies->get($sessionCookieName, '');
        $guestSeed = $sessionCookie !== ''
            ? 'session:' . $sessionCookie
            : 'fingerprint:' . $ip . '|' . $userAgent . '|' . (string) $request->header('Accept-Language', '');

        return 'guest:' . hash('sha256', $guestSeed);
    }

    private function resolveIp(Request $request): string
    {
        $cloudflareIp = trim((string) $request->headers->get('CF-Connecting-IP', ''));

        if ($cloudflareIp !== '' && filter_var($cloudflareIp, FILTER_VALIDATE_IP)) {
            return $cloudflareIp;
        }

        return (string) ($request->ip() ?: '0.0.0.0');
    }

    private function maskIp(string $ip): string
    {
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            $parts = explode('.', $ip);

            return sprintf('%s.%s.xxx.xxx', $parts[0] ?? '0', $parts[1] ?? '0');
        }

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            $parts = explode(':', $ip);
            $parts = array_pad($parts, 4, '');

            return sprintf('%s:%s:xxxx:xxxx', $parts[0] ?: '::', $parts[1] ?: '::');
        }

        return 'unknown';
    }

    private function detectBrowser(string $userAgent): string
    {
        $normalized = strtolower($userAgent);

        return match (true) {
            str_contains($normalized, 'edg/') => 'Edge',
            str_contains($normalized, 'opr/') || str_contains($normalized, 'opera') => 'Opera',
            str_contains($normalized, 'chrome') && ! str_contains($normalized, 'edg/') => 'Chrome',
            str_contains($normalized, 'firefox') => 'Firefox',
            str_contains($normalized, 'safari') && ! str_contains($normalized, 'chrome') => 'Safari',
            default => 'Unknown',
        };
    }

    private function detectPlatform(string $userAgent): string
    {
        $normalized = strtolower($userAgent);

        return match (true) {
            str_contains($normalized, 'windows') => 'Windows',
            str_contains($normalized, 'iphone') || str_contains($normalized, 'ipad') || str_contains($normalized, 'ios') => 'iOS',
            str_contains($normalized, 'android') => 'Android',
            str_contains($normalized, 'mac os') || str_contains($normalized, 'macintosh') => 'macOS',
            str_contains($normalized, 'linux') => 'Linux',
            default => 'Unknown',
        };
    }

    private function currentUrl(Request $request): string
    {
        $path = '/' . ltrim($request->path(), '/');

        return $path === '//' ? '/' : $path;
    }

    private function recordKey(string $visitorKey): string
    {
        return self::KEY_PREFIX . ':' . $visitorKey;
    }

    private function truncate(string $value, int $limit): string
    {
        return Str::limit($value, $limit, '');
    }

    private function resolveUserId(?Authenticatable $user): ?int
    {
        if ($user === null) {
            return null;
        }

        $identifier = $user->getAuthIdentifier();

        return is_numeric($identifier) ? (int) $identifier : null;
    }

    private function resolveUserName(?Authenticatable $user): ?string
    {
        if ($user === null) {
            return null;
        }

        $name = data_get($user, 'name')
            ?? data_get($user, 'username')
            ?? data_get($user, 'email');

        return is_string($name) && $name !== '' ? $name : 'User';
    }
}