<?php

namespace App\Services\AI;

use App\DTOs\WebsiteBrandSignals;
use App\Support\CssFontFamily;
use DOMDocument;
use DOMXPath;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use Throwable;

class WebsiteBrandSignalExtractor
{
    private const MAX_STYLESHEETS = 3;

    private const HTTP_TIMEOUT = 8;

    private const PUPPETEER_TIMEOUT = 500;

    /**
     * @return list<string> deduped http(s) URLs found in free text
     */
    public function extractUrls(string $text): array
    {
        if (! preg_match_all('/\bhttps?:\/\/[^\s<>"\']+/i', $text, $matches)) {
            return [];
        }

        return collect($matches[0])
            ->map(fn (string $url) => rtrim($url, '.,);'))
            ->unique()
            ->values()
            ->all();
    }

    public function scan(string $url): ?WebsiteBrandSignals
    {
        $header = $this->fetchHeaderWithPuppeteer($url);
        $html = $this->fetchHtml($url);

        if ($header === null && $html === null) {
            return null;
        }

        $header ??= [];

        $headerColor = $header['headerColor'] ?? null;
        $colorSource = $header['colorSource'] ?? null;
        $headerBackgroundColor = $header['headerBackgroundColor'] ?? null;
        $exactLogoSvg = $header['exactLogoSvg'] ?? null;
        $logoPublicUrl = $header['logoPublicUrl'] ?? null;
        $logoImagePath = null;
        $logoImageMime = null;
        $fontFamily = CssFontFamily::primary((string) ($header['fontFamily'] ?? ''));
        $fontStylesheetUrl = $this->httpUrlOrNull($header['fontStylesheetUrl'] ?? null);
        $faviconUrl = $html !== null ? $this->extractFaviconUrl($html, $url) : null;

        if ($html !== null) {
            if ($exactLogoSvg === null) {
                $exactLogoSvg = $this->extractLogoSvg($html);
            }

            if ($logoPublicUrl === null) {
                $fallback = $this->extractLogoImage($html, $url);

                if ($fallback !== null) {
                    [$logoImagePath, $logoImageMime] = $fallback;
                    $logoPublicUrl = $this->logoSrcFromHtml($html, $url);
                }
            }
        }

        if ($logoPublicUrl !== null && $logoImagePath === null) {
            $downloaded = $this->downloadImage($logoPublicUrl, $url);

            if ($downloaded !== null) {
                [$logoImagePath, $logoImageMime] = $downloaded;
            }
        }

        if ($exactLogoSvg !== null) {
            $logoNote = 'Exact logo SVG extracted from the website header — embed this verbatim, do not redraw or approximate it.';
        } elseif ($logoPublicUrl !== null) {
            $logoNote = "Public header logo URL (use this as the header <img> src): {$logoPublicUrl}";
        } elseif ($faviconUrl !== null) {
            $logoPublicUrl = $faviconUrl;
            $logoNote = "No distinct header logo; using the site favicon as the header <img> src: {$faviconUrl}";

            if ($logoImagePath === null) {
                $downloaded = $this->downloadImage($faviconUrl, $url);

                if ($downloaded !== null) {
                    [$logoImagePath, $logoImageMime] = $downloaded;
                }
            }
        } else {
            $logoNote = 'No logo image or SVG could be found on the page.';
        }

        $themeColor = $html !== null ? $this->extractThemeColorMeta($html) : null;
        $hexColors = [];
        $css = '';

        if ($html !== null && ($headerColor === null || $fontFamily === null)) {
            $css = $this->extractInlineStyleBlocks($html).$this->fetchLinkedStylesheets($html, $url);
        }

        if ($html !== null && $headerColor === null) {
            $hexColors = $this->extractHexColors($css);
        }

        if ($html !== null && ($fontFamily === null || $fontStylesheetUrl === null)) {
            $fromMarkup = $this->extractFontFromMarkup($html, $css);

            $fontFamily ??= $fromMarkup['family'];
            $fontStylesheetUrl ??= $fromMarkup['stylesheet'];
        }

        if ($fontFamily !== null && $fontStylesheetUrl === null) {
            $fontStylesheetUrl = CssFontFamily::stylesheetUrl($fontFamily);
        }

        $themeLine = $headerColor
            ? ($colorSource === 'header'
                ? "Header background color from the live page: {$headerColor}"
                : "Header CTA color from the live page (use as accent_color / theme): {$headerColor}")
            : ($themeColor ? "Declared theme-color meta tag: {$themeColor}" : 'No header theme color found.');

        $backgroundLine = $headerBackgroundColor
            ? "Header/page background from the live page: {$headerBackgroundColor}"
            : null;

        $hexLine = $headerColor
            ? 'Stylesheet hex colors were ignored because the header color was measured from the rendered page.'
            : ($hexColors !== []
                ? 'Hex colors found in page stylesheets (most frequent first): '.implode(', ', array_slice($hexColors, 0, 8))
                : 'No hex colors could be extracted from the page stylesheets.');

        $faviconLine = $faviconUrl
            ? "Site favicon URL (use as <link rel=\"icon\">): {$faviconUrl}"
            : null;

        $fontLine = $fontFamily
            ? "Body font-family from the live page (use unless the user named a different font): {$fontFamily}"
            : null;

        $summary = implode("\n", array_filter([
            "Website analyzed: {$url}",
            $themeLine,
            $backgroundLine,
            $hexLine,
            $logoNote,
            $faviconLine,
            $fontLine,
        ]));

        return new WebsiteBrandSignals(
            url: $url,
            summaryText: $summary,
            headerColor: $headerColor ?? $themeColor,
            headerBackgroundColor: $headerBackgroundColor,
            exactLogoSvg: $exactLogoSvg,
            logoPublicUrl: $logoPublicUrl,
            faviconUrl: $faviconUrl,
            logoImagePath: $logoImagePath,
            logoImageMime: $logoImageMime,
            fontFamily: $fontFamily,
            fontStylesheetUrl: $fontStylesheetUrl,
        );
    }

    /**
     * @return array{headerColor: ?string, colorSource: ?string, headerBackgroundColor: ?string, exactLogoSvg: ?string, logoPublicUrl: ?string, fontFamily: ?string, fontStylesheetUrl: ?string}|null
     */
    private function fetchHeaderWithPuppeteer(string $url): ?array
    {
        try {
            $result = Process::path(base_path())
                ->timeout(self::PUPPETEER_TIMEOUT)
                ->run([
                    'node',
                    base_path('scripts/extract-header-brand.js'),
                    $url,
                ]);

            if (! $result->successful()) {
                Log::info('Website brand scan: puppeteer failed, falling back to HTTP', [
                    'url' => $url,
                    'error' => $result->errorOutput(),
                ]);

                return null;
            }

            $decoded = json_decode($result->output(), true);

            if (! is_array($decoded)) {
                return null;
            }

            $headerColor = $this->normalizeHex($decoded['headerColor'] ?? null);
            $headerBackgroundColor = $this->normalizeHex($decoded['headerBackground'] ?? null);
            $colorSource = in_array($decoded['colorSource'] ?? null, ['cta', 'header'], true)
                ? $decoded['colorSource']
                : null;
            $logo = is_array($decoded['logo'] ?? null) ? $decoded['logo'] : [];
            $kind = (string) ($logo['kind'] ?? '');
            $exactLogoSvg = null;
            $logoPublicUrl = null;

            if ($kind === 'svg' && is_string($logo['markup'] ?? null) && $logo['markup'] !== '') {
                $exactLogoSvg = $this->sanitizeSvgMarkup($logo['markup']);
            }

            if ($kind === 'img' && is_string($logo['src'] ?? null) && str_starts_with($logo['src'], 'http')) {
                $logoPublicUrl = $logo['src'];
            }

            $fontFamily = CssFontFamily::primary((string) ($decoded['fontFamily'] ?? ''));
            $fontStylesheetUrl = $this->httpUrlOrNull($decoded['fontStylesheet'] ?? null);

            if ($fontFamily === null && $fontStylesheetUrl !== null) {
                $fontFamily = CssFontFamily::fromGoogleFontsUrl($fontStylesheetUrl);
            }

            if ($headerColor === null && $exactLogoSvg === null && $logoPublicUrl === null && $fontFamily === null) {
                return null;
            }

            return [
                'headerColor' => $headerColor,
                'colorSource' => $colorSource,
                'headerBackgroundColor' => $headerBackgroundColor,
                'exactLogoSvg' => $exactLogoSvg !== '' ? $exactLogoSvg : null,
                'logoPublicUrl' => $logoPublicUrl,
                'fontFamily' => $fontFamily,
                'fontStylesheetUrl' => $fontStylesheetUrl,
            ];
        } catch (Throwable $e) {
            Log::info('Website brand scan: puppeteer threw, falling back to HTTP', [
                'url' => $url,
                'error' => $e->getMessage(),
            ]);

            return null;
        }
    }

    private function normalizeHex(mixed $value): ?string
    {
        $hex = is_string($value) ? Str::lower(trim($value)) : '';

        if (preg_match('/^#[0-9a-f]{3}([0-9a-f]{3})?$/D', $hex) !== 1) {
            return null;
        }

        return $hex;
    }

    private function logoSrcFromHtml(string $html, string $baseUrl): ?string
    {
        if (! preg_match('/<img\b[^>]*(?:class|id|alt|src)=["\'][^"\']*logo[^"\']*["\'][^>]*>/i', $html, $m)) {
            return null;
        }

        if (! preg_match('/src=["\']([^"\']+)["\']/i', $m[0], $srcMatch)) {
            return null;
        }

        return $this->toAbsoluteUrl($srcMatch[1], $baseUrl);
    }

    private function fetchHtml(string $url): ?string
    {
        try {
            $response = Http::timeout(self::HTTP_TIMEOUT)
                ->withHeaders(['User-Agent' => 'Mozilla/5.0 (compatible; LandingPageBot/1.0)'])
                ->get($url);

            return $response->successful() ? $response->body() : null;
        } catch (Throwable $e) {
            Log::info('Website brand scan: fetch failed', ['url' => $url, 'error' => $e->getMessage()]);

            return null;
        }
    }

    private function extractThemeColorMeta(string $html): ?string
    {
        if (preg_match('/<meta[^>]+name=["\']theme-color["\'][^>]+content=["\'](#[0-9a-fA-F]{3,8})["\']/i', $html, $m)) {
            return $m[1];
        }

        return null;
    }

    private function extractInlineStyleBlocks(string $html): string
    {
        if (preg_match_all('/<style\b[^>]*>(.*?)<\/style>/is', $html, $m)) {
            return implode("\n", $m[1]);
        }

        return '';
    }

    private function fetchLinkedStylesheets(string $html, string $baseUrl): string
    {
        if (! preg_match_all('/<link[^>]+rel=["\']stylesheet["\'][^>]+href=["\']([^"\']+)["\']/i', $html, $m)) {
            return '';
        }

        $css = '';
        $count = 0;

        foreach ($m[1] as $href) {
            if ($count >= self::MAX_STYLESHEETS) {
                break;
            }

            $absolute = $this->toAbsoluteUrl($href, $baseUrl);

            if ($absolute === null) {
                continue;
            }

            try {
                $response = Http::timeout(self::HTTP_TIMEOUT)->get($absolute);

                if ($response->successful()) {
                    $css .= "\n".$response->body();
                    $count++;
                }
            } catch (Throwable) {
                continue;
            }
        }

        return $css;
    }

    /**
     * @return array{family: ?string, stylesheet: ?string}
     */
    private function extractFontFromMarkup(string $html, string $css): array
    {
        $source = $html."\n".$css;
        $stylesheet = $this->extractFontStylesheetUrl($source);
        $family = CssFontFamily::primary($this->firstCssFontFamily($css) ?? '');

        if ($family === null && $stylesheet !== null) {
            $family = CssFontFamily::fromGoogleFontsUrl($stylesheet);
        }

        return [
            'family' => $family,
            'stylesheet' => $stylesheet,
        ];
    }

    private function firstCssFontFamily(string $css): ?string
    {
        if (preg_match('/--(?:font(?:-family)?|ff)\s*:\s*([^;}{]+)/i', $css, $match) === 1) {
            return trim($match[1]);
        }

        if (preg_match('/(?:html|body)[^{}]*\{[^}]*font-family\s*:\s*([^;}{]+)/i', $css, $match) === 1) {
            return trim($match[1]);
        }

        if (preg_match('/font-family\s*:\s*([^;}{]+)/i', $css, $match) === 1) {
            return trim($match[1]);
        }

        return null;
    }

    private function extractFontStylesheetUrl(string $markup): ?string
    {
        if (preg_match_all('/<link\b[^>]*>/i', $markup, $tags) !== false) {
            foreach ($tags[0] as $tag) {
                if (preg_match('/\bhref=["\']([^"\']+)["\']/i', $tag, $href) !== 1) {
                    continue;
                }

                $url = html_entity_decode($href[1], ENT_QUOTES | ENT_HTML5, 'UTF-8');

                if (preg_match('/fonts\.googleapis\.com|use\.typekit\.net|fonts\.adobe\.com/i', $url) === 1) {
                    return $this->httpUrlOrNull($url);
                }
            }
        }

        if (preg_match('/@import\s+(?:url\()?\s*["\']([^"\']+)["\']/i', $markup, $match) === 1
            && preg_match('/fonts\.googleapis\.com|use\.typekit\.net|fonts\.adobe\.com/i', $match[1]) === 1) {
            return $this->httpUrlOrNull($match[1]);
        }

        return null;
    }

    private function httpUrlOrNull(mixed $value): ?string
    {
        if (! is_string($value) || $value === '') {
            return null;
        }

        $url = trim($value);

        if (str_starts_with($url, '//')) {
            $url = 'https:'.$url;
        }

        return str_starts_with($url, 'http://') || str_starts_with($url, 'https://')
            ? $url
            : null;
    }

    /**
     * @return list<string> hex colors, most frequent first, whites/blacks excluded
     */
    private function extractHexColors(string $css): array
    {
        if (! preg_match_all('/#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b/', $css, $m)) {
            return [];
        }

        $ignored = ['#ffffff', '#fff', '#000000', '#000'];

        return collect($m[0])
            ->map(fn (string $hex) => Str::lower($hex))
            ->reject(fn (string $hex) => in_array($hex, $ignored, true))
            ->countBy()
            ->sortDesc()
            ->keys()
            ->values()
            ->all();
    }

    /**
     * DOM-based lookup for the site's logo <svg>, tolerant of attribute
     * order and nested markup that regex handles poorly.
     */
    private function extractLogoSvg(string $html): ?string
    {
        $doc = new DOMDocument;
        libxml_use_internal_errors(true);
        $loaded = $doc->loadHTML('<?xml encoding="utf-8" ?>'.$html);
        libxml_clear_errors();

        if (! $loaded) {
            return null;
        }

        $xpath = new DOMXPath($doc);

        $svgNodes = $xpath->query(
            "//svg[contains(@class, 'logo') or contains(@id, 'logo') or contains(@aria-label, 'ogo')]"
        );

        if (($svgNodes === false || $svgNodes->length === 0)) {
            $svgNodes = $xpath->query(
                "//*[contains(@class, 'logo') or contains(@aria-label, 'ogo')]//svg"
            );
        }

        if ($svgNodes === false || $svgNodes->length === 0) {
            return null;
        }

        $node = $svgNodes->item(0);
        $markup = $doc->saveHTML($node);

        if ($markup === false || $markup === '' || strlen($markup) > 30000) {
            return null;
        }

        return $this->sanitizeSvgMarkup($markup);
    }

    /**
     * Strip anything unsafe before this markup can be embedded verbatim in
     * generated output: scripts, event handlers, and external references.
     */
    private function sanitizeSvgMarkup(string $svg): string
    {
        $svg = (string) preg_replace('/<script\b[^>]*>.*?<\/script>/is', '', $svg);
        $svg = (string) preg_replace('/\son\w+\s*=\s*"[^"]*"/i', '', $svg);
        $svg = (string) preg_replace("/\son\w+\s*=\s*'[^']*'/i", '', $svg);
        $svg = (string) preg_replace('/(xlink:href|href)\s*=\s*"(?!#)[^"]*"/i', '', $svg);
        $svg = (string) preg_replace("/(xlink:href|href)\s*=\s*'(?!#)[^']*'/i", '', $svg);

        return trim($svg);
    }

    /**
     * @return array{0: string, 1: string}|null [path, mime]
     */
    private function extractLogoImage(string $html, string $baseUrl): ?array
    {
        if (! preg_match('/<img\b[^>]*(?:class|id|alt|src)=["\'][^"\']*logo[^"\']*["\'][^>]*>/i', $html, $m)) {
            Log::info('Website brand scan: no logo <img> matched in HTML', ['url' => $baseUrl]);

            return null;
        }

        if (! preg_match('/src=["\']([^"\']+)["\']/i', $m[0], $srcMatch)) {
            Log::info('Website brand scan: logo <img> had no src', ['url' => $baseUrl]);

            return null;
        }

        $absolute = $this->toAbsoluteUrl($srcMatch[1], $baseUrl);

        if ($absolute === null) {
            Log::info('Website brand scan: could not resolve logo image URL', ['url' => $baseUrl, 'src' => $srcMatch[1]]);

            return null;
        }

        return $this->downloadImage($absolute, $baseUrl);
    }

    private function extractFaviconUrl(string $html, string $baseUrl): ?string
    {
        $fromLinks = $this->faviconUrlFromLinkTags($html, $baseUrl);

        if ($fromLinks !== null) {
            return $fromLinks;
        }

        $default = $this->toAbsoluteUrl('/favicon.ico', $baseUrl);

        if ($default === null || ! $this->publicAssetExists($default, $baseUrl)) {
            return null;
        }

        return $default;
    }

    private function faviconUrlFromLinkTags(string $html, string $baseUrl): ?string
    {
        if (! preg_match_all('/<link\b[^>]*>/i', $html, $tags)) {
            return null;
        }

        $candidates = [];

        foreach ($tags[0] as $tag) {
            if (! preg_match('/\brel=["\']([^"\']+)["\']/i', $tag, $relMatch)) {
                continue;
            }

            $rel = Str::lower($relMatch[1]);

            if (! str_contains($rel, 'icon')) {
                continue;
            }

            if (! preg_match('/\bhref=["\']([^"\']+)["\']/i', $tag, $hrefMatch)) {
                continue;
            }

            $absolute = $this->toAbsoluteUrl($hrefMatch[1], $baseUrl);

            if ($absolute === null) {
                continue;
            }

            $path = Str::lower((string) (parse_url($absolute, PHP_URL_PATH) ?? ''));
            $priority = match (true) {
                str_contains($rel, 'apple-touch-icon') => 0,
                str_ends_with($path, '.svg') => 1,
                str_contains($rel, 'shortcut') => 3,
                default => 2,
            };

            $candidates[] = [$priority, $absolute];
        }

        if ($candidates === []) {
            return null;
        }

        usort($candidates, fn (array $left, array $right): int => $left[0] <=> $right[0]);

        return $candidates[0][1];
    }

    private function publicAssetExists(string $url, string $referer): bool
    {
        try {
            $response = Http::timeout(self::HTTP_TIMEOUT)
                ->withHeaders([
                    'User-Agent' => 'Mozilla/5.0 (compatible; LandingPageBot/1.0)',
                    'Referer' => $referer,
                ])
                ->get($url);

            if (! $response->successful()) {
                return false;
            }

            $contentType = Str::lower((string) ($response->header('Content-Type') ?: ''));

            if ($contentType !== '' && (str_starts_with($contentType, 'text/html') || str_starts_with($contentType, 'application/json'))) {
                return false;
            }

            return true;
        } catch (Throwable) {
            return false;
        }
    }

    /**
     * @return array{0: string, 1: string}|null [path, mime]
     */
    private function downloadImage(string $url, string $referer): ?array
    {
        try {
            $response = Http::timeout(self::HTTP_TIMEOUT)
                ->withHeaders([
                    'User-Agent' => 'Mozilla/5.0 (compatible; LandingPageBot/1.0)',
                    'Referer' => $referer,
                    'Accept' => 'image/avif,image/webp,image/png,image/jpeg,image/*,*/*;q=0.8',
                ])
                ->get($url);

            if (! $response->successful()) {
                Log::info('Website brand scan: image download non-2xx', ['url' => $url, 'status' => $response->status()]);

                return null;
            }

            $contentType = $response->header('Content-Type') ?: 'image/png';

            if (! str_starts_with($contentType, 'image/') || str_contains($contentType, 'svg')) {
                Log::info('Website brand scan: image download wrong content-type', ['url' => $url, 'content_type' => $contentType]);

                return null;
            }

            $extension = match (true) {
                str_contains($contentType, 'png') => 'png',
                str_contains($contentType, 'jpeg'), str_contains($contentType, 'jpg') => 'jpg',
                str_contains($contentType, 'webp') => 'webp',
                str_contains($contentType, 'gif') => 'gif',
                default => 'png',
            };

            $dir = storage_path('app/tmp');

            if (! is_dir($dir)) {
                mkdir($dir, 0755, true);
            }

            $path = $dir.'/brand-scan-'.Str::random(20).'.'.$extension;
            file_put_contents($path, $response->body());

            return [$path, $contentType];
        } catch (Throwable $e) {
            Log::info('Website brand scan: image download threw', ['url' => $url, 'error' => $e->getMessage()]);

            return null;
        }
    }

    private function toAbsoluteUrl(string $href, string $baseUrl): ?string
    {
        if (Str::startsWith($href, ['http://', 'https://'])) {
            return $href;
        }

        if (Str::startsWith($href, '//')) {
            return 'https:'.$href;
        }

        $parts = parse_url($baseUrl);

        if ($parts === false || ! isset($parts['host'])) {
            return null;
        }

        $scheme = $parts['scheme'] ?? 'https';
        $host = $parts['host'];
        $port = isset($parts['port']) ? ':'.$parts['port'] : '';

        if (Str::startsWith($href, '/')) {
            return "{$scheme}://{$host}{$port}{$href}";
        }

        $basePath = isset($parts['path']) ? rtrim(dirname($parts['path']), '/') : '';

        return "{$scheme}://{$host}{$port}{$basePath}/{$href}";
    }
}
