<?php

declare(strict_types=1);

/*
|--------------------------------------------------------------------------
| Member session registry
|--------------------------------------------------------------------------
| Raw PHP session ID tidak pernah disimpan. Database hanya menyimpan
| SHA-256 hash dari session ID.
|--------------------------------------------------------------------------
*/

function memberSessionRegistryHash(
    string $sessionId
): string {
    return hash(
        'sha256',
        $sessionId
    );
}

function memberSessionRegistryIp(): string
{
    return substr(
        (string) (
            $_SERVER['REMOTE_ADDR'] ?? ''
        ),
        0,
        45
    );
}

function memberSessionRegistryUserAgent(): string
{
    return substr(
        trim(
            (string) (
                $_SERVER[
                    'HTTP_USER_AGENT'
                ] ?? ''
            )
        ),
        0,
        500
    );
}

function memberSessionRegistryDevice(
    string $userAgent
): string {
    $userAgent = strtolower(
        $userAgent
    );

    $device =
        str_contains(
            $userAgent,
            'mobile'
        ) ||
        str_contains(
            $userAgent,
            'android'
        ) ||
        str_contains(
            $userAgent,
            'iphone'
        )
            ? 'Mobile'
            : 'Desktop';

    $browser = 'Browser';

    if (
        str_contains(
            $userAgent,
            'edg/'
        )
    ) {
        $browser = 'Edge';
    } elseif (
        str_contains(
            $userAgent,
            'opr/'
        )
    ) {
        $browser = 'Opera';
    } elseif (
        str_contains(
            $userAgent,
            'chrome/'
        )
    ) {
        $browser = 'Chrome';
    } elseif (
        str_contains(
            $userAgent,
            'firefox/'
        )
    ) {
        $browser = 'Firefox';
    } elseif (
        str_contains(
            $userAgent,
            'safari/'
        ) &&
        !str_contains(
            $userAgent,
            'chrome/'
        )
    ) {
        $browser = 'Safari';
    }

    return $device .
        ' · ' .
        $browser;
}

function memberSessionRegister(
    PDO $pdo,
    int $userId,
    ?string $sessionId = null,
    int $absoluteHours = 8
): void {
    $sessionId =
        $sessionId ?? session_id();

    if (
        $userId < 1 ||
        $sessionId === ''
    ) {
        return;
    }

    $absoluteHours = max(
        1,
        min(168, $absoluteHours)
    );

    $expiresAt = date(
        'Y-m-d H:i:s',
        time() +
        ($absoluteHours * 3600)
    );

    $userAgent =
        memberSessionRegistryUserAgent();

    $statement = $pdo->prepare(
        "INSERT INTO member_sessions (
            user_id,
            session_hash,
            ip_address,
            user_agent,
            device_name,
            login_at,
            last_seen_at,
            expires_at,
            revoked_at,
            revoked_by_admin_id,
            revoke_reason,
            logout_at,
            created_at,
            updated_at
        ) VALUES (
            :user_id,
            :session_hash,
            :ip_address,
            :user_agent,
            :device_name,
            CURRENT_TIMESTAMP,
            CURRENT_TIMESTAMP,
            :expires_at,
            NULL,
            NULL,
            NULL,
            NULL,
            CURRENT_TIMESTAMP,
            CURRENT_TIMESTAMP
        )
        ON DUPLICATE KEY UPDATE
            user_id =
                VALUES(user_id),
            ip_address =
                VALUES(ip_address),
            user_agent =
                VALUES(user_agent),
            device_name =
                VALUES(device_name),
            login_at =
                CURRENT_TIMESTAMP,
            last_seen_at =
                CURRENT_TIMESTAMP,
            expires_at =
                VALUES(expires_at),
            revoked_at = NULL,
            revoked_by_admin_id =
                NULL,
            revoke_reason = NULL,
            logout_at = NULL,
            updated_at =
                CURRENT_TIMESTAMP"
    );

    $statement->execute([
        ':user_id' => $userId,
        ':session_hash' =>
            memberSessionRegistryHash(
                $sessionId
            ),
        ':ip_address' =>
            memberSessionRegistryIp(),
        ':user_agent' =>
            $userAgent,
        ':device_name' =>
            memberSessionRegistryDevice(
                $userAgent
            ),
        ':expires_at' =>
            $expiresAt,
    ]);
}

function memberSessionGuard(
    PDO $pdo,
    int $userId,
    ?string $sessionId = null,
    int $touchIntervalSeconds = 60
): bool {
    $sessionId =
        $sessionId ?? session_id();

    if (
        $userId < 1 ||
        $sessionId === ''
    ) {
        return false;
    }

    $sessionHash =
        memberSessionRegistryHash(
            $sessionId
        );

    $statement = $pdo->prepare(
        "SELECT
            id,
            revoked_at,
            logout_at,
            expires_at
         FROM member_sessions
         WHERE user_id = :user_id
           AND session_hash =
               :session_hash
         LIMIT 1"
    );

    $statement->execute([
        ':user_id' => $userId,
        ':session_hash' =>
            $sessionHash,
    ]);

    $row = $statement->fetch(
        PDO::FETCH_ASSOC
    );

    /*
     * Login lama yang belum tercatat otomatis didaftarkan pada request
     * pertama setelah bootstrap baru dipasang.
     */
    if (!$row) {
        memberSessionRegister(
            $pdo,
            $userId,
            $sessionId,
            8
        );

        return true;
    }

    $revoked =
        trim(
            (string) (
                $row['revoked_at'] ?? ''
            )
        ) !== '';

    $loggedOut =
        trim(
            (string) (
                $row['logout_at'] ?? ''
            )
        ) !== '';

    $expiresAt = strtotime(
        (string) (
            $row['expires_at'] ?? ''
        )
    );

    $expired =
        $expiresAt === false ||
        $expiresAt <= time();

    if (
        $revoked ||
        $loggedOut ||
        $expired
    ) {
        return false;
    }

    $touchIntervalSeconds = max(
        30,
        min(
            600,
            $touchIntervalSeconds
        )
    );

    $lastTouch = (int) (
        $_SESSION[
            '__member_session_touch'
        ] ?? 0
    );

    if (
        time() - $lastTouch >=
        $touchIntervalSeconds
    ) {
        $userAgent =
            memberSessionRegistryUserAgent();

        $update = $pdo->prepare(
            "UPDATE member_sessions
             SET
                ip_address =
                    :ip_address,
                user_agent =
                    :user_agent,
                device_name =
                    :device_name,
                last_seen_at =
                    CURRENT_TIMESTAMP,
                updated_at =
                    CURRENT_TIMESTAMP
             WHERE id = :registry_id
               AND revoked_at IS NULL
               AND logout_at IS NULL
               AND expires_at >
                    CURRENT_TIMESTAMP"
        );

        $update->execute([
            ':ip_address' =>
                memberSessionRegistryIp(),
            ':user_agent' =>
                $userAgent,
            ':device_name' =>
                memberSessionRegistryDevice(
                    $userAgent
                ),
            ':registry_id' =>
                (int) $row['id'],
        ]);

        $_SESSION[
            '__member_session_touch'
        ] = time();
    }

    return true;
}

function memberSessionLogout(
    PDO $pdo,
    int $userId,
    ?string $sessionId = null
): void {
    $sessionId =
        $sessionId ?? session_id();

    if (
        $userId < 1 ||
        $sessionId === ''
    ) {
        return;
    }

    $statement = $pdo->prepare(
        "UPDATE member_sessions
         SET
            logout_at =
                CURRENT_TIMESTAMP,
            last_seen_at =
                CURRENT_TIMESTAMP,
            updated_at =
                CURRENT_TIMESTAMP
         WHERE user_id = :user_id
           AND session_hash =
               :session_hash
           AND logout_at IS NULL"
    );

    $statement->execute([
        ':user_id' => $userId,
        ':session_hash' =>
            memberSessionRegistryHash(
                $sessionId
            ),
    ]);
}
