<?php

namespace classes;

use DateTimeImmutable;
use DateTimeZone;
use Exception;
use objects\subusers_o;
use objects\users_o;

class system_session_activity_tracker
{
    public const ACTIVE_WINDOW_MINUTES = 15;
    public const PRUNE_AFTER_DAYS = 30;
    private const DATABASE_TIMEZONE = 'UTC';

    /**
     * Deduplicate touches within a single request lifecycle.
     *
     * @var array<string, bool>
     */
    private static array $touchedSessions = [];

    public function __construct()
    {
        system_session_activity_schema_bootstrap::ensureTables();
    }

    public function touchUser(users_o $user, string $token): void
    {
        $customerNumber = null;
        if (isset($user->customer_number)) {
            try {
                $customerNumber = (int)$user->customer_number->value();
            } catch (Exception) {
                $customerNumber = null;
            }
        }

        $this->touch('user', (int)$user->id, $token, $customerNumber);
    }

    public function touchSubuser(subusers_o $subuser, string $token, ?int $customerNumberContext = null): void
    {
        $this->touch('subuser', (int)$subuser->id, $token, $customerNumberContext);
    }

    public function touch(string $sessionKind, int $principalId, string $token, ?int $customerNumberContext = null): void
    {
        global $db;

        $token = trim($token);
        if ($token === '' || $principalId <= 0) {
            return;
        }

        $sessionHash = hash('sha256', $token);
        if (isset(self::$touchedSessions[$sessionHash])) {
            return;
        }
        self::$touchedSessions[$sessionHash] = true;

        $headers = function_exists('getallheaders') ? (getallheaders() ?: []) : [];
        $userAgent = trim((string)($headers['User-Agent'] ?? $headers['user-agent'] ?? ''));
        $lastRoute = trim((string)($_SERVER['REQUEST_URI'] ?? ''));
        if ($lastRoute !== '') {
            $lastRoute = explode('?', $lastRoute)[0] ?? $lastRoute;
        }

        $sessionHashEscaped = $db->escape_string($sessionHash);
        $sessionKindEscaped = $db->escape_string($sessionKind);
        $deviceTypeEscaped = $db->escape_string(self::detectDeviceType($userAgent));
        $userAgentEscaped = $db->escape_string(substr($userAgent, 0, 1024));
        $lastRouteEscaped = $db->escape_string(substr($lastRoute, 0, 255));
        $customerNumberSql = $customerNumberContext === null ? 'NULL' : (string)(int)$customerNumberContext;
        $currentUtcDateTime = self::utcSqlDateTime();

        $sql = "INSERT INTO system_session_activity (
                    session_hash,
                    session_kind,
                    principal_id,
                    customer_number_context,
                    device_type,
                    user_agent,
                    last_route,
                    first_seen_at,
                    last_seen_at
                ) VALUES (
                    '$sessionHashEscaped',
                    '$sessionKindEscaped',
                    " . (int)$principalId . ",
                    $customerNumberSql,
                    '$deviceTypeEscaped',
                    '$userAgentEscaped',
                    '$lastRouteEscaped',
                    '$currentUtcDateTime',
                    '$currentUtcDateTime'
                )
                ON DUPLICATE KEY UPDATE
                    customer_number_context = VALUES(customer_number_context),
                    device_type = VALUES(device_type),
                    user_agent = VALUES(user_agent),
                    last_route = VALUES(last_route),
                    last_seen_at = '$currentUtcDateTime'";

        $db->query($sql);
    }

    public function getSnapshot(int $limit = 50): array
    {
        global $db;

        system_session_activity_schema_bootstrap::ensureTables();

        $limit = max(1, min(200, $limit));
        $cutoff = self::utcSqlDateTime(time() - (self::ACTIVE_WINDOW_MINUTES * 60));
        $cutoffEscaped = $db->escape_string($cutoff);

        $activeUsersResult = $db->query(
            "SELECT COUNT(DISTINCT CONCAT(session_kind, ':', principal_id)) AS c
             FROM system_session_activity
             WHERE last_seen_at >= '$cutoffEscaped'"
        );
        $activeSessionsResult = $db->query(
            "SELECT COUNT(*) AS c
             FROM system_session_activity
             WHERE last_seen_at >= '$cutoffEscaped'"
        );

        $recentRows = $db->query(
            "SELECT
                s.session_kind,
                s.principal_id,
                s.customer_number_context,
                s.device_type,
                s.user_agent,
                s.last_route,
                s.first_seen_at,
                s.last_seen_at,
                u.display_name AS user_display_name,
                u.customer_number AS user_customer_number,
                su.name AS subuser_name,
                su.username AS subuser_username
            FROM system_session_activity s
            LEFT JOIN users u
                ON s.session_kind = 'user'
               AND u.id = s.principal_id
            LEFT JOIN subusers su
                ON s.session_kind = 'subuser'
               AND su.id = s.principal_id
            ORDER BY s.last_seen_at DESC
            LIMIT $limit"
        );

        $recentSessions = [];
        while ($row = $recentRows->fetch_assoc()) {
            $isUser = ($row['session_kind'] ?? '') === 'user';
            $displayName = $isUser
                ? trim((string)($row['user_display_name'] ?? ''))
                : trim((string)($row['subuser_name'] ?? ''));
            $displayNameKey = null;
            $displayNameParams = [];

            if ($displayName === '') {
                if ($isUser && !empty($row['user_customer_number'])) {
                    $displayName = 'Customer ' . $row['user_customer_number'];
                    $displayNameKey = 'customer_number';
                    $displayNameParams = ['number' => (int)$row['user_customer_number']];
                } elseif (!$isUser && !empty($row['subuser_username'])) {
                    $displayName = $row['subuser_username'];
                } elseif ($isUser) {
                    $displayName = 'User #' . (int)($row['principal_id'] ?? 0);
                    $displayNameKey = 'user_with_id';
                    $displayNameParams = ['id' => (int)($row['principal_id'] ?? 0)];
                } elseif (($row['session_kind'] ?? '') === 'subuser') {
                    $displayName = 'Subuser #' . (int)($row['principal_id'] ?? 0);
                    $displayNameKey = 'subuser_with_id';
                    $displayNameParams = ['id' => (int)($row['principal_id'] ?? 0)];
                } else {
                    $displayName = 'Session #' . (int)($row['principal_id'] ?? 0);
                    $displayNameKey = 'session_with_id';
                    $displayNameParams = ['id' => (int)($row['principal_id'] ?? 0)];
                }
            }

            $contextLabel = null;
            $contextLabelKey = null;
            $contextLabelParams = [];
            if ($isUser && !empty($row['user_customer_number'])) {
                $contextLabel = 'Customer ' . $row['user_customer_number'];
                $contextLabelKey = 'customer_number';
                $contextLabelParams = ['number' => (int)$row['user_customer_number']];
            } elseif (!empty($row['customer_number_context'])) {
                $contextLabel = 'Customer ' . $row['customer_number_context'];
                $contextLabelKey = 'customer_number';
                $contextLabelParams = ['number' => (int)$row['customer_number_context']];
            }

            $firstSeenAt = self::databaseDateTimeToIso8601($row['first_seen_at'] ?? null);
            $lastSeenAt = self::databaseDateTimeToIso8601($row['last_seen_at'] ?? null);

            $recentSessions[] = [
                'session_kind' => (string)($row['session_kind'] ?? 'unknown'),
                'principal_id' => (int)($row['principal_id'] ?? 0),
                'display_name' => $displayName,
                'display_name_key' => $displayNameKey,
                'display_name_params' => $displayNameParams,
                'context_label' => $contextLabel,
                'context_label_key' => $contextLabelKey,
                'context_label_params' => $contextLabelParams,
                'customer_number_context' => isset($row['customer_number_context']) ? (int)$row['customer_number_context'] : null,
                'device_type' => (string)($row['device_type'] ?? 'unknown'),
                'user_agent' => (string)($row['user_agent'] ?? ''),
                'last_route' => (string)($row['last_route'] ?? ''),
                'first_seen_at' => $firstSeenAt,
                'last_seen_at' => $lastSeenAt,
                'active' => self::isActive($lastSeenAt),
            ];
        }

        $activeUsers = (int)(($activeUsersResult?->fetch_assoc()['c']) ?? 0);
        $activeSessions = (int)(($activeSessionsResult?->fetch_assoc()['c']) ?? 0);

        return [
            'active_window_minutes' => self::ACTIVE_WINDOW_MINUTES,
            'active_users' => $activeUsers,
            'active_sessions' => $activeSessions,
            'recent_sessions' => $recentSessions,
        ];
    }

    public function pruneOlderThanDays(int $days = self::PRUNE_AFTER_DAYS): int
    {
        global $db;

        system_session_activity_schema_bootstrap::ensureTables();

        $days = max(1, $days);
        $cutoff = self::utcSqlDateTime(time() - ($days * 86400));
        $cutoffEscaped = $db->escape_string($cutoff);

        $db->query("DELETE FROM system_session_activity WHERE last_seen_at < '$cutoffEscaped'");
        return (int)$db->conn()->affected_rows;
    }

    public static function detectDeviceType(string $userAgent): string
    {
        $userAgent = strtolower(trim($userAgent));
        if ($userAgent === '') {
            return 'unknown';
        }

        if (preg_match('/bot|crawler|spider|slurp|curl|wget|postman|insomnia/', $userAgent) === 1) {
            return 'bot';
        }
        if (preg_match('/ipad|tablet|kindle|playbook|silk/', $userAgent) === 1) {
            return 'tablet';
        }
        if (preg_match('/iphone|ipod|android.+mobile|windows phone|mobile/', $userAgent) === 1) {
            return 'mobile';
        }
        if (preg_match('/macintosh|windows nt|linux|x11|cros/', $userAgent) === 1) {
            return 'desktop';
        }

        return 'unknown';
    }

    public static function isActive(?string $lastSeenAt, int $windowMinutes = self::ACTIVE_WINDOW_MINUTES, ?int $referenceTimestamp = null): bool
    {
        if ($lastSeenAt === null || trim($lastSeenAt) === '') {
            return false;
        }

        $lastSeenTimestamp = strtotime($lastSeenAt);
        if ($lastSeenTimestamp === false) {
            return false;
        }

        $referenceTimestamp = $referenceTimestamp ?? time();
        return $lastSeenTimestamp >= ($referenceTimestamp - (max(1, $windowMinutes) * 60));
    }

    public static function databaseDateTimeToIso8601(?string $value): ?string
    {
        if ($value === null || trim($value) === '') {
            return null;
        }

        $dateTime = DateTimeImmutable::createFromFormat(
            'Y-m-d H:i:s',
            trim($value),
            new DateTimeZone(self::DATABASE_TIMEZONE)
        );
        if (!$dateTime instanceof DateTimeImmutable) {
            return null;
        }

        return $dateTime->format(DATE_ATOM);
    }

    private static function utcSqlDateTime(?int $timestamp = null): string
    {
        return gmdate('Y-m-d H:i:s', $timestamp ?? time());
    }
}
