<?php

namespace App\Support;

/**
 * Transforms raw internal crawl / sync exception strings into customer-
 * safe messages for the Sources / Knowledge admin lists.
 *
 * The raw `sources.error` column stays untouched for operator log-spelunking
 * (rendered behind a "Show details" toggle, included in support reports).
 * This presenter only governs the headline string we surface in the UI:
 * no API keys, no upstream HTTP bodies, no provider names — just a short
 * actionable line the customer can read without filing a ticket.
 *
 * The matching is best-effort. Anything we don't have a rule for falls
 * back to a generic "Couldn't reach this page." line — better to under-
 * communicate than leak a stack trace.
 */
final class SourceErrorPresenter
{
    /**
     * Map an internal error string to the customer-facing message. Returns
     * null when there's no error.
     */
    public static function present(?string $raw): ?string
    {
        if ($raw === null || trim($raw) === '') {
            return null;
        }

        $needle = mb_strtolower($raw);

        // Cloudflare Browser Rendering — auth failures, rate limits,
        // service errors. The plaintext 401 body exposes the upstream
        // JSON envelope; we want zero of that on the customer side.
        if (str_contains($needle, 'cloudflare browser rendering')) {
            if (str_contains($needle, '401') || str_contains($needle, 'authentication')) {
                return 'The crawl service is temporarily unavailable on your workspace. Pitchbar support has been notified.';
            }
            if (str_contains($needle, '429') || str_contains($needle, 'rate-limited') || str_contains($needle, 'rate limited')) {
                return 'The crawl service is busy right now. We will retry this page automatically in a few minutes.';
            }
            if (str_contains($needle, '5')) {
                return 'The crawl service hit a transient error. We will retry this page automatically.';
            }

            return 'The crawl service could not render this page.';
        }

        // Browserless fallback — same shape, different vendor.
        if (str_contains($needle, 'browserless')) {
            if (str_contains($needle, '401')) {
                return 'The crawl service is temporarily unavailable on your workspace. Pitchbar support has been notified.';
            }
            if (str_contains($needle, '429') || str_contains($needle, 'rate')) {
                return 'The crawl service is busy right now. We will retry this page automatically in a few minutes.';
            }

            return 'The crawl service could not render this page.';
        }

        // Generic HTTP failures from cURL / wp_remote_post / Guzzle.
        if (preg_match('/\bhttp\s*([45]\d\d)\b/i', $needle, $m) === 1) {
            $status = (int) $m[1];
            if ($status === 401 || $status === 403) {
                return 'The page returned an authentication error and could not be crawled.';
            }
            if ($status === 404) {
                return "We couldn't find this page (404). It may have moved or been removed.";
            }
            if ($status === 429) {
                return 'The target site is rate-limiting our crawler. We will retry automatically.';
            }
            if ($status >= 500) {
                return 'The target site returned a server error while we were crawling it.';
            }

            return "We couldn't reach this page.";
        }

        // cURL transport-level errors (timeouts, DNS, SSL handshake).
        if (str_contains($needle, 'curl error')
            || str_contains($needle, 'operation timed out')
            || str_contains($needle, 'could not resolve')
            || str_contains($needle, 'ssl handshake')) {
            return "We couldn't reach this page. It may be temporarily offline or blocking automated requests.";
        }

        // SSRF / safety guards (private IPs, localhost, cloud metadata).
        if (str_contains($needle, 'forbidden host')
            || str_contains($needle, 'private ip')
            || str_contains($needle, 'loopback')) {
            return "We can't crawl this URL for safety reasons (private or internal address).";
        }

        // Empty / unparseable content surfaced by the parsers.
        if (str_contains($needle, 'no extractable text')
            || str_contains($needle, 'empty body')
            || str_contains($needle, 'parser failed')) {
            return "The page loaded but we couldn't extract any readable text from it.";
        }

        // Unsupported file type from the upload parser.
        if (str_contains($needle, 'unsupported file type')) {
            return 'This file type is not supported. Try uploading a PDF, DOCX, TXT, CSV, or Markdown file.';
        }

        // Quota / billing.
        if (str_contains($needle, 'quota') || str_contains($needle, 'usage cap')) {
            return 'This workspace has reached its crawl quota for the month.';
        }

        // Catch-all. Strip the "Crawl failed: " prefix CrawlPageJob stamps so
        // we don't show it next to the friendly message; never leak more than
        // a generic line.
        return "Couldn't index this page.";
    }
}
