<?php

namespace App\Services\Whmcs;

use App\Models\WhmcsPackage;
use App\Services\AuditLogger;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

/**
 * Fetches the public-facing feature list from hostseba.com for each
 * WhmcsPackage's `product_url`. The WHMCS description has the bare
 * resource limits, but the website usually has a richer marketing-
 * curated feature list (e.g. "Free SSL", "cpGuard Malware Protection",
 * "Daily Backup", "99.9% Uptime", etc.) that the AI can quote to
 * customers asking "what does this plan include?".
 *
 * Strategy:
 *   - HTTP GET the product_url with a 10 s timeout.
 *   - Parse the HTML with PHP's DOMDocument (no extra dep).
 *   - Pick up <li> items inside feature-list containers (looks for
 *     common classes / parent text markers).
 *   - Store in whmcs_packages.scraped_features (array of strings) +
 *     scraped_at timestamp.
 *
 * Safe to re-run: idempotent upsert. Manual button + scheduled monthly
 * refresh (pages rarely change).
 */
class HostSebaWebScraper
{
    private const TIMEOUT_SECONDS = 10;
    private const MAX_FEATURES = 30;
    private const USER_AGENT = 'HostSeba-AI-Portal/1.0 (+self-fetch for AI ticket context)';

    /**
     * Scrape every WhmcsPackage that has a non-empty product_url.
     *
     * @return array{candidates:int, scraped:int, empty:int, errors:int}
     */
    public function syncAll(): array
    {
        $candidates = $scraped = $empty = $errors = 0;
        WhmcsPackage::query()
            ->whereNotNull('product_url')
            ->where('product_url', '!=', '')
            ->chunk(50, function ($chunk) use (&$candidates, &$scraped, &$empty, &$errors) {
                foreach ($chunk as $pkg) {
                    $candidates++;
                    try {
                        $features = $this->fetchFeaturesFor($pkg);
                        if (empty($features)) { $empty++; continue; }
                        $pkg->scraped_features = $features;
                        $pkg->scraped_at = now();
                        $pkg->scraped_url = $pkg->product_url;
                        $pkg->save();
                        $scraped++;
                    } catch (\Throwable $e) {
                        $errors++;
                        Log::info('hsap.scraper.fetch_failed',
                            ['pid' => $pkg->whmcs_pid, 'err' => $e->getMessage()]);
                    }
                }
            });

        AuditLogger::log('system', 'hostseba-scraper', 'sync',
            'hostseba.feature_scrape', 'whmcs_package', null,
            ['candidates' => $candidates, 'scraped' => $scraped,
             'empty' => $empty, 'errors' => $errors]);

        return compact('candidates', 'scraped', 'empty', 'errors');
    }

    /**
     * Scrape ONE package — used by the manual "Re-scrape this row" button
     * and by the bulk syncAll.
     *
     * @return string[]  list of feature strings (empty if nothing usable found)
     */
    public function fetchFeaturesFor(WhmcsPackage $pkg): array
    {
        $url = (string) $pkg->product_url;
        if (! $url) return [];

        // Guard against fetching anything other than hostseba.com — we
        // don't want to accidentally scrape third-party content if WHMCS
        // had a bad product_url.
        $host = parse_url($url, PHP_URL_HOST) ?: '';
        if (! str_ends_with(strtolower($host), 'hostseba.com')) {
            Log::info('hsap.scraper.skipped_external_host',
                ['pid' => $pkg->whmcs_pid, 'host' => $host]);
            return [];
        }

        // SSL verification — mirrors the WhmcsClient pattern. Defaults
        // to ON; can be disabled per env (.env: HOSTSEBA_SCRAPE_VERIFY_SSL=false)
        // for Windows dev boxes that don't ship a CA bundle. Production
        // MUST leave it on.
        $verify = filter_var(env('HOSTSEBA_SCRAPE_VERIFY_SSL', true),
            FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
        if ($verify === null) $verify = true;

        $resp = Http::withHeaders(['User-Agent' => self::USER_AGENT])
            ->timeout(self::TIMEOUT_SECONDS)
            ->withOptions(['verify' => $verify])
            ->get($url);

        if (! $resp->successful()) {
            Log::info('hsap.scraper.bad_status',
                ['pid' => $pkg->whmcs_pid, 'status' => $resp->status()]);
            return [];
        }

        return $this->parseFeatureList($resp->body());
    }

    /**
     * Pull <li> contents from feature-list containers. We try several
     * heuristics so the parser tolerates HostSeba's marketing markup
     * (which has changed shape between page redesigns):
     *
     *   1. <ul class="...features|plan-features|pricing-features|*list*">
     *   2. <div class="...features|specs"> containing <li>s
     *   3. Any <ul> that has 4+ <li>s where the average <li> text length
     *      is 8-150 chars (rules out nav menus + footer link lists)
     *
     * Limit to the first MAX_FEATURES distinct entries.
     */
    private function parseFeatureList(string $html): array
    {
        if (! $html) return [];

        $dom = new \DOMDocument();
        // suppress libxml warnings on marketing HTML (always has minor issues)
        libxml_use_internal_errors(true);
        $dom->loadHTML('<?xml encoding="UTF-8">' . $html);
        libxml_clear_errors();

        $xpath = new \DOMXPath($dom);

        // Strategy: among every <ul> on the page, pick the one whose <li>s
        // look MOST like hosting features. "Look like" = ≥70% of items
        // contain a feature keyword (GB / SSL / cPanel / Bandwidth / …)
        // OR contain a number followed by a unit. This filters out
        // navigation menus that have 1-2 hosting words mixed in with
        // dozens of nav labels (which was the original bug — "Home /
        // Login / Pricing / BDIX Premium / Mern Stack" looked vaguely
        // feature-y to the old scorer).
        $best = ['ratio' => 0.0, 'items' => []];

        foreach ($xpath->query('//ul') as $ul) {
            $items = [];
            foreach ($ul->getElementsByTagName('li') as $li) {
                $txt = $this->cleanText($li->textContent);
                if ($this->isPlausibleFeature($txt)) $items[] = $txt;
            }
            if (count($items) < 4) continue;

            $matched = 0;
            foreach ($items as $it) {
                if ($this->hasFeatureKeyword($it)) $matched++;
            }
            $ratio = $matched / max(1, count($items));

            // Class-hint bonus — when the UL has feature/plan/spec/pricing
            // in its class, accept it at a lower ratio (60%).
            $cls = strtolower($ul->getAttribute('class') ?? '');
            $hasHint = (bool) preg_match('/feature|plan|spec|pricing|product/u', $cls);
            $threshold = $hasHint ? 0.6 : 0.7;

            if ($ratio >= $threshold && $ratio > $best['ratio']) {
                $best = ['ratio' => $ratio, 'items' => $items];
            }
        }

        // No high-quality UL found — return empty. The WHMCS description
        // already has the resource limits; better to surface NOTHING
        // scraped than dump nav-menu noise into the prompt. The empty
        // count is logged via syncAll's "empty" stat so admins can see
        // which pages need manual feature curation in admin_notes.
        return array_slice(array_unique($best['items']), 0, self::MAX_FEATURES);
    }

    /**
     * Does this string look like a hosting-feature bullet? True if it
     * mentions storage / bandwidth / RAM / SSL / domain / panel / etc —
     * the exact words customers shop for. Used to distinguish a real
     * feature list from a navigation menu in `parseFeatureList`.
     */
    private function hasFeatureKeyword(string $s): bool
    {
        $lc = strtolower($s);

        // Tier 1: number-followed-by-unit. Nav menus essentially never
        // contain "5 GB" / "260 GB Bandwidth" / "1 CPU Core" patterns,
        // but feature lists always do.
        if (preg_match('/\b\d+\s*(gb|mb|tb|kb|gib|mib|mbps|gbps|cores?|cpus?|inodes?|processes?|emails?|databases?|subdomains?|domains?|websites?|ftp|accounts?|connections?|users?|addon|park)\b/iu', $s)) {
            return true;
        }

        // Tier 2: specific product / tool names that appear in feature
        // bullets but rarely in navigation. Generic words like "domain"
        // and "hosting" are deliberately NOT here — they're too common
        // in nav menus.
        $specific = [
            'cpanel', 'plesk', 'webuzo', 'directadmin',
            'cloudlinux', 'litespeed', 'softaculous', 'cpguard',
            'imunify', "let's encrypt", 'lets encrypt',
            'free ssl', 'wildcard ssl', 'positive ssl', 'rapidssl',
            'jetbackup', 'daily backup', 'weekly backup', 'off-site backup',
            'lscache', 'lscwp', 'cloudflare cdn',
            'nvme ssd', 'pure ssd', 'raid',
            'mern stack', 'node.js', 'nodejs',
            'ms sql', 'asp.net', 'wordpress',
            'unlimited bandwidth', 'unlimited email', 'unmetered bandwidth',
            'live chat', '24/7 support', 'priority support',
            'free migration', 'free domain', '99.9% uptime', '99.99% uptime',
        ];
        foreach ($specific as $k) {
            if (str_contains($lc, $k)) return true;
        }
        return false;
    }

    private function cleanText(string $s): string
    {
        $s = preg_replace("/\s+/u", ' ', $s) ?? $s;
        return trim($s);
    }

    /**
     * Filter out nav-bar items, prices, and other false positives.
     * Real feature bullets are 8-200 chars and contain at least one
     * letter; they don't start with currency markers or look like menu
     * labels ("Home", "About", etc.).
     */
    private function isPlausibleFeature(string $s): bool
    {
        $len = mb_strlen($s, 'UTF-8');
        if ($len < 6 || $len > 220) return false;
        // No letters → probably a price-only string or graphic.
        if (! preg_match('/\p{L}/u', $s)) return false;
        // Skip nav-like single-word entries.
        if (preg_match('/^(home|about|contact|login|signup|sign\s*up|register|menu|toggle)$/iu', $s)) {
            return false;
        }
        // Skip prices-as-features (we already have these in WhmcsPackage.pricing).
        if (preg_match('/^[৳$€£]?\s*\d+([.,]\d+)?\s*(usd|bdt|tk|\/m|\/y|month|year)?$/iu', $s)) {
            return false;
        }
        return true;
    }
}
