<?php

namespace KeyLair;

/**
 * Pushes Key Lair users' claim links into a customer's own self-hosted
 * Sendy install as subscriber custom-field values (admin/api_access.php),
 * so a Sendy campaign can drop the personalized link into an email via a
 * merge tag. Sendy has no live per-recipient API lookup at send time --
 * only pre-stored custom field values -- so this sync has to run *before*
 * a campaign goes out, not during it.
 */
class SendySync
{
    /**
     * Same cURL-preferred, stream-fallback approach as CannyReport::httpPost()
     * -- cURL has proven more reliable against real hosts from real installs
     * than PHP's stream wrapper. Sendy's API is form-urlencoded, not JSON.
     *
     * @return array{ok: bool, body: string, detail: string}
     */
    private static function httpPost(string $url, array $params): array
    {
        $body = http_build_query($params);

        if (function_exists('curl_init')) {
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => $body,
                CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => 20,
                CURLOPT_USERAGENT => 'KeyLair/1.0',
            ]);
            $response = curl_exec($ch);
            if ($response === false) {
                $detail = 'cURL error ' . curl_errno($ch) . ': ' . curl_error($ch);
                curl_close($ch);
                return ['ok' => false, 'body' => '', 'detail' => $detail];
            }
            curl_close($ch);
        } else {
            $context = stream_context_create([
                'http' => [
                    'method' => 'POST',
                    'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
                    'content' => $body,
                    'timeout' => 20,
                    'ignore_errors' => true,
                ],
            ]);
            $response = @file_get_contents($url, false, $context);
            if ($response === false) {
                $lastError = error_get_last();
                return ['ok' => false, 'body' => '', 'detail' => 'Stream error: ' . ($lastError['message'] ?? 'unknown error')];
            }
        }

        $body = trim((string) $response);
        // Sendy's documented "boolean=true" response is plain text: "1" on
        // success, or a short human-readable error/status string otherwise
        // (e.g. "Already subscribed.", "Invalid API key.") -- there's no
        // JSON envelope to parse, so the raw string doubles as the detail.
        return ['ok' => $body === '1', 'body' => $body, 'detail' => $body];
    }

    /**
     * @return array{synced: int, skipped: int, failed: int, errors: string[]}
     */
    public static function syncAll(): array
    {
        $result = ['synced' => 0, 'skipped' => 0, 'failed' => 0, 'errors' => []];

        if (!Settings::sendyConfigured()) {
            $result['errors'][] = "Sendy isn't configured yet -- fill in the URL, API key, and list id below first.";
            return $result;
        }

        $url = rtrim((string) Settings::sendyUrl(), '/') . '/subscribe';
        $apiKey = Settings::sendyApiKey();
        $listId = Settings::sendyListId();
        $fieldName = Settings::sendyFieldName();
        $target = Settings::sendyLinkTarget();
        $baseUrl = Helpers::baseUrl();

        $pdo = Database::connection();
        $users = $pdo->query("SELECT id, name, email FROM users WHERE email IS NOT NULL AND email != ''")->fetchAll();

        $overviewStmt = $pdo->prepare(
            "SELECT token FROM claim_links WHERE user_id = :uid AND status = 'active' ORDER BY created_at DESC LIMIT 1"
        );
        $productStmt = $pdo->prepare(
            "SELECT l.token FROM claim_links l
             JOIN claim_link_products clp ON clp.claim_link_id = l.id
             JOIN products p ON p.id = clp.product_id
             WHERE l.user_id = :uid AND l.status = 'active' AND p.slug = :slug
             ORDER BY l.created_at DESC LIMIT 1"
        );

        // set_time_limit rather than relying on PHP's default -- a list in
        // the thousands, each a separate outbound HTTP call to Sendy, can
        // easily outrun a stricter shared-host default before this loop
        // finishes.
        set_time_limit(300);

        foreach ($users as $user) {
            $claimUrl = null;
            if ($target === 'overview') {
                $overviewStmt->execute(['uid' => $user['id']]);
                $link = $overviewStmt->fetch();
                if ($link) {
                    $claimUrl = $baseUrl . '/claim/' . $link['token'];
                }
            } else {
                $productStmt->execute(['uid' => $user['id'], 'slug' => $target]);
                $link = $productStmt->fetch();
                if ($link) {
                    $claimUrl = $baseUrl . '/claim/' . $link['token'] . '/' . $target;
                }
            }

            if ($claimUrl === null) {
                $result['skipped']++;
                continue;
            }

            $params = [
                'api_key' => $apiKey,
                'email' => $user['email'],
                'list' => $listId,
                'boolean' => 'true',
                $fieldName => $claimUrl,
            ];
            if ($user['name']) {
                $params['name'] = $user['name'];
            }

            $response = self::httpPost($url, $params);
            if ($response['ok']) {
                $result['synced']++;
            } else {
                $result['failed']++;
                $result['errors'][] = $user['email'] . ': ' . $response['detail'];
            }
        }

        return $result;
    }
}
