<?php

namespace KeyLair;

/**
 * Posts a bug report or feature request to the vendor's public Canny board
 * (admin/bug_reporting.php) via Canny's REST API. The connection is the vendor's,
 * not the customer's: same board for every installation, credentials loaded
 * from src/canny-credentials.php (built into the release zip by CI, but
 * gitignored — never committed), same pattern as Deployer.php's deploy
 * token. A customer never configures anything.
 *
 * Screenshots aren't uploaded to Canny directly -- its public API only
 * accepts already-hosted image URLs, not file uploads -- so they're saved
 * under public/uploads/feedback/ (same as product screenshots) and their
 * public HTTPS URL is passed along instead.
 */
class CannyReport
{
    private const API_BASE = 'https://canny.io/api/v1';

    /** @return array{api_key?: string, board_id?: string} */
    private static function credentials(): array
    {
        static $creds = null;
        if ($creds === null) {
            $path = dirname(__DIR__) . '/src/canny-credentials.php';
            $creds = file_exists($path) ? (require $path) : [];
        }
        return $creds;
    }

    public static function isConfigured(): bool
    {
        $creds = self::credentials();
        return !empty($creds['api_key']) && !empty($creds['board_id']);
    }

    /**
     * Prefers cURL over file_get_contents()/streams: on several real
     * installs, the stream wrapper's HTTPS request to canny.io failed
     * outright (indistinguishable from a network block, since the old
     * code here never logged *why*) while cURL succeeded against the same
     * host from the same server -- most likely PHP's openssl stream
     * wrapper picking up a missing/stale CA bundle that cURL's own
     * TLS backend doesn't rely on the same way. Falls back to streams only
     * if the curl extension genuinely isn't compiled in, and either path
     * now logs the actual failure reason instead of a silent false.
     *
     * $detail is a short, human-readable diagnostic (the curl/stream error,
     * or the HTTP status + response snippet) -- surfaced all the way up
     * through submit()'s error message, not just error_log()'d, since this
     * page is master/admin-only already and whoever's looking at a failed
     * submission has no other way to reach the server's error log.
     *
     * Body is JSON, not application/x-www-form-urlencoded: scalar fields
     * (title, details, apiKey, ...) parse fine either way, but Canny
     * silently drops the `imageURLs` array field -- not an error, just an
     * empty imageURLs on the created post -- unless the body is sent as
     * real JSON. Confirmed directly against the API: identical params sent
     * form-urlencoded (bracketed, unbracketed, or repeated-key array
     * syntax all tried) never attached an image, while a JSON body with
     * the exact same params worked first try and Canny re-hosted the URL
     * on canny-assets.io.
     *
     * @return array{ok: bool, status: int, data: ?array, detail: string}
     */
    private static function httpPost(string $endpoint, array $params): array
    {
        $url = self::API_BASE . $endpoint;
        $body = json_encode($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/json'],
                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);
                error_log("CannyReport: request to {$url} failed: {$detail}");
                curl_close($ch);
                return ['ok' => false, 'status' => 0, 'data' => null, 'detail' => $detail];
            }
            $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
        } else {
            $context = stream_context_create([
                'http' => [
                    'method' => 'POST',
                    'header' => "Content-Type: application/json\r\nUser-Agent: KeyLair/1.0\r\n",
                    'content' => $body,
                    'timeout' => 20,
                    'ignore_errors' => true,
                ],
            ]);

            $response = @file_get_contents($url, false, $context);
            $status = 0;
            if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
                $status = (int) $m[1];
            }
            if ($response === false) {
                $lastError = error_get_last();
                $detail = 'Stream error: ' . ($lastError['message'] ?? 'unknown error');
                error_log("CannyReport: request to {$url} failed: {$detail}");
                return ['ok' => false, 'status' => $status, 'data' => null, 'detail' => $detail];
            }
        }

        $data = $response !== false ? json_decode((string) $response, true) : null;
        $ok = $status >= 200 && $status < 300;
        $detail = $ok ? '' : "HTTP {$status}: " . substr((string) $response, 0, 300);
        if (!$ok) {
            error_log("CannyReport: {$url} returned {$detail}");
        }
        return ['ok' => $ok, 'status' => $status, 'data' => is_array($data) ? $data : null, 'detail' => $detail];
    }

    /**
     * @param string $type 'bug' or 'feature'
     * @param string[] $screenshotUrls Public HTTPS URLs of already-saved screenshots.
     * @return array{ok: bool, error?: string}
     */
    public static function submit(string $type, string $title, string $description, array $screenshotUrls, string $submittedBy): array
    {
        $creds = self::credentials();
        $apiKey = (string) ($creds['api_key'] ?? '');
        $boardId = (string) ($creds['board_id'] ?? '');

        if ($apiKey === '' || $boardId === '') {
            return ['ok' => false, 'error' => 'Bug reporting isn\'t set up on this install yet.'];
        }

        $author = self::httpPost('/users/find_or_create', [
            'apiKey' => $apiKey,
            'userID' => 'key-lair-admin-panel',
            'name' => 'Key Lair Admin Panel',
        ]);
        if (!$author['ok'] || empty($author['data']['id'])) {
            return ['ok' => false, 'error' => 'Could not reach the feedback service: ' . $author['detail']];
        }

        $label = $type === 'bug' ? 'Bug' : 'Feature Request';
        // The version this report was actually filed from -- so "is this
        // already fixed?" can be checked against the changelog instead of
        // guessing, since a customer install can easily be a few releases
        // behind whatever's current by the time a report comes in.
        $version = Deployer::currentVersion() ?? 'unknown';
        $channel = Settings::installedChannel();
        $versionLabel = ($channel !== null && $channel !== 'stable') ? "{$version}-{$channel}" : $version;
        $details = trim($description) . "\n\n---\nSubmitted via the Key Lair admin panel by {$submittedBy}, running version {$versionLabel}.";

        $params = [
            'apiKey' => $apiKey,
            'authorID' => $author['data']['id'],
            'boardID' => $boardId,
            'title' => "[{$label}] " . trim($title),
            'details' => $details,
        ];
        if (!empty($screenshotUrls)) {
            $params['imageURLs'] = $screenshotUrls;
        }

        $post = self::httpPost('/posts/create', $params);
        if (!$post['ok'] || empty($post['data']['id'])) {
            return ['ok' => false, 'error' => 'The feedback service rejected the submission: ' . $post['detail']];
        }

        return ['ok' => true];
    }
}
