<?php

declare(strict_types=1);

require_once __DIR__ . '/../vendor/autoload.php';

use Minishlink\WebPush\Subscription;
use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\VAPID;

function push_normalize_base64url(string $value): string
{
    // Browsers provide base64url values for p256dh/auth, often without padding.
    // Normalize and pad to a length base64_decode() reliably accepts.
    $v = trim($value);
    // Some clients/storage layers may URL-encode these values.
    if ($v !== '' && str_contains($v, '%')) {
        $v = rawurldecode($v);
    }
    // Guard against accidental quoting.
    $v = trim($v, "\"' ");
    $v = preg_replace('/\s+/', '', $v) ?? $v;
    // If something provided standard base64, convert to base64url.
    $v = strtr($v, '+/', '-_');
    $pad = (4 - (strlen($v) % 4)) % 4;
    if ($pad > 0) {
        $v .= str_repeat('=', $pad);
    }
    return $v;
}

function push_is_valid_base64url(string $value): bool
{
    try {
        // The web-push stack uses Base64Url\Base64Url::decode() internally.
        // Validating here gives better errors and prevents runtime send failures.
        $norm = push_normalize_base64url($value);
        if ($norm === '') {
            return false;
        }
        \Base64Url\Base64Url::decode($norm);
        return true;
    } catch (Throwable $e) {
        return false;
    }
}

function push_ensure_openssl_conf(): void
{
    $cur = getenv('OPENSSL_CONF');
    if (is_string($cur) && $cur !== '' && file_exists($cur)) {
        return;
    }

    // XAMPP on Windows often ships these configs.
    $candidates = [
        'C:\\xampp\\apache\\conf\\openssl.cnf',
        'C:\\xampp\\php\\extras\\openssl\\openssl.cnf',
        'C:\\xampp\\php\\extras\\ssl\\openssl.cnf',
    ];

    foreach ($candidates as $path) {
        if (file_exists($path)) {
            putenv('OPENSSL_CONF=' . $path);
            return;
        }
    }
}

function push_subscriptions_headers(): array
{
    return ['id', 'user_id', 'endpoint', 'p256dh', 'auth', 'created_at', 'updated_at'];
}

function push_load_vapid_keys(): array
{
    push_ensure_openssl_conf();
    if (!defined('VAPID_KEYS_JSON')) {
        throw new RuntimeException('VAPID_KEYS_JSON not configured');
    }

    if (!file_exists(VAPID_KEYS_JSON)) {
        throw new RuntimeException('Missing VAPID keys. Run tools/generate_vapid.php first.');
    }

    $raw = file_get_contents(VAPID_KEYS_JSON);
    if ($raw === false) {
        throw new RuntimeException('Unable to read VAPID keys');
    }

    $json = json_decode($raw, true);
    if (!is_array($json)) {
        throw new RuntimeException('Invalid VAPID keys JSON');
    }

    $public = (string)($json['publicKey'] ?? '');
    $private = (string)($json['privateKey'] ?? '');
    $subject = (string)($json['subject'] ?? (defined('VAPID_SUBJECT') ? VAPID_SUBJECT : 'mailto:admin@timezup.local'));

    if ($public === '' || $private === '') {
        throw new RuntimeException('VAPID keys missing publicKey/privateKey');
    }

    // Normalize base64url for downstream libraries that decode strictly.
    $public = push_normalize_base64url($public);
    $private = push_normalize_base64url($private);

    if (!push_is_valid_base64url($public) || !push_is_valid_base64url($private)) {
        throw new RuntimeException('Invalid VAPID keys (base64url decode failed)');
    }

    return ['publicKey' => $public, 'privateKey' => $private, 'subject' => $subject];
}

function push_get_public_key(): string
{
    $keys = push_load_vapid_keys();
    return (string)$keys['publicKey'];
}

/**
 * @param array<string,mixed> $subscription
 */
function push_upsert_subscription(string $userId, array $subscription): void
{
    $endpoint = (string)($subscription['endpoint'] ?? '');
    $keys = $subscription['keys'] ?? null;
    $p256dh = is_array($keys) ? (string)($keys['p256dh'] ?? '') : '';
    $auth = is_array($keys) ? (string)($keys['auth'] ?? '') : '';

    $p256dh = push_normalize_base64url($p256dh);
    $auth = push_normalize_base64url($auth);

    if ($userId === '' || $endpoint === '' || $p256dh === '' || $auth === '') {
        throw new RuntimeException('Invalid push subscription payload');
    }

    if (!push_is_valid_base64url($p256dh) || !push_is_valid_base64url($auth)) {
        throw new RuntimeException('Invalid push subscription keys');
    }

    $rows = csv_read_assoc(PUSH_SUBSCRIPTIONS_CSV, push_subscriptions_headers());
    $ts = gmdate('c');

    $out = [];
    $found = false;
    foreach ($rows as $r) {
        if ((string)($r['endpoint'] ?? '') !== $endpoint) {
            $out[] = $r;
            continue;
        }

        $r['user_id'] = $userId;
        $r['p256dh'] = $p256dh;
        $r['auth'] = $auth;
        $r['updated_at'] = $ts;
        if ((string)($r['created_at'] ?? '') === '') {
            $r['created_at'] = $ts;
        }
        if ((string)($r['id'] ?? '') === '') {
            $r['id'] = generate_id();
        }

        $out[] = $r;
        $found = true;
    }

    if (!$found) {
        $out[] = [
            'id' => generate_id(),
            'user_id' => $userId,
            'endpoint' => $endpoint,
            'p256dh' => $p256dh,
            'auth' => $auth,
            'created_at' => $ts,
            'updated_at' => $ts,
        ];
    }

    csv_write_assoc(PUSH_SUBSCRIPTIONS_CSV, push_subscriptions_headers(), $out);
}

/**
 * @param array<int,string> $userIds
 * @param array<string,mixed> $payload
 * @return array{attempted:int,sent:int,failed:int,removed:int}
 */
function push_send_to_users(array $userIds, array $payload): array
{
    push_ensure_openssl_conf();
    $userIdSet = [];
    foreach ($userIds as $uid) {
        $uid = (string)$uid;
        if ($uid !== '') {
            $userIdSet[$uid] = true;
        }
    }

    if (empty($userIdSet)) {
        return ['attempted' => 0, 'sent' => 0, 'failed' => 0, 'removed' => 0];
    }

    $rows = csv_read_assoc(PUSH_SUBSCRIPTIONS_CSV, push_subscriptions_headers());
    $targets = [];
    foreach ($rows as $r) {
        $uid = (string)($r['user_id'] ?? '');
        if ($uid === '' || empty($userIdSet[$uid])) {
            continue;
        }
        $targets[] = $r;
    }

    if (empty($targets)) {
        return ['attempted' => 0, 'sent' => 0, 'failed' => 0, 'removed' => 0];
    }

    $keys = push_load_vapid_keys();
    $auth = [
        'VAPID' => [
            'subject' => (string)$keys['subject'],
            'publicKey' => (string)$keys['publicKey'],
            'privateKey' => (string)$keys['privateKey'],
        ],
    ];

    // Validate config early
    // (This is a common failure point if base64url values are malformed/unpadded.)
    $auth['VAPID'] = VAPID::validate($auth['VAPID']);

    $webPush = new WebPush($auth);

    $payloadJson = json_encode($payload, JSON_UNESCAPED_SLASHES);
    if (!is_string($payloadJson)) {
        throw new RuntimeException('Unable to encode push payload: ' . json_last_error_msg());
    }

    $attempted = 0;
    $sent = 0;
    $failed = 0;
    $removeEndpoints = [];

    // Use per-subscription sends so one bad row cannot abort the whole batch.
    // (The underlying libraries may still throw during flush/send, even if queueing succeeded.)
    foreach ($targets as $t) {
        $endpoint = (string)($t['endpoint'] ?? '');
        $p256dh = push_normalize_base64url((string)($t['p256dh'] ?? ''));
        $authToken = push_normalize_base64url((string)($t['auth'] ?? ''));
        if ($endpoint === '' || $p256dh === '' || $authToken === '') {
            continue;
        }

        if (!push_is_valid_base64url($p256dh) || !push_is_valid_base64url($authToken)) {
            $removeEndpoints[] = $endpoint;
            $failed++;
            continue;
        }

        $attempted++;
        try {
            $sub = Subscription::create([
                'endpoint' => $endpoint,
                'publicKey' => $p256dh,
                'authToken' => $authToken,
            ]);

            $report = $webPush->sendOneNotification($sub, $payloadJson);
            if ($report && method_exists($report, 'isSuccess') && $report->isSuccess()) {
                $sent++;
                continue;
            }

            $failed++;
            try {
                if ($report && method_exists($report, 'getResponse')) {
                    $response = $report->getResponse();
                    if ($response) {
                        $code = $response->getStatusCode();
                        if ($code === 404 || $code === 410) {
                            $removeEndpoints[] = $endpoint;
                        }
                    }
                }
            } catch (Throwable $e) {
                // Ignore
            }
        } catch (Throwable $e) {
            // Treat as a bad subscription and prune it.
            $failed++;
            $removeEndpoints[] = $endpoint;
            continue;
        }
    }

    $removed = 0;
    if (!empty($removeEndpoints)) {
        $removeSet = [];
        foreach ($removeEndpoints as $ep) {
            $removeSet[(string)$ep] = true;
        }

        $newRows = [];
        foreach ($rows as $r) {
            $ep = (string)($r['endpoint'] ?? '');
            if ($ep !== '' && isset($removeSet[$ep])) {
                $removed++;
                continue;
            }
            $newRows[] = $r;
        }
        csv_write_assoc(PUSH_SUBSCRIPTIONS_CSV, push_subscriptions_headers(), $newRows);
    }

    return ['attempted' => $attempted, 'sent' => $sent, 'failed' => $failed, 'removed' => $removed];
}
