<?php

namespace App\Services\AI;

use App\Contracts\CreativeBriefResolver;
use App\DTOs\CreativeBrief;
use DOMDocument;
use DOMXPath;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Laravel\Ai\Files\LocalImage;

use function Laravel\Ai\agent;

class LaravelAiCreativeBriefResolver implements CreativeBriefResolver
{
    public function fromProductUrl(string $url): CreativeBrief
    {
        $response = Http::timeout(15)
            ->withHeaders([
                'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
                'Accept-Language' => 'en-US,en;q=0.9',
            ])
            ->get($url);

        $html = (string) $response->body();

        $productMetadata = $this->extractProductMetadata($url, $html);
        $cleanText = $this->extractCleanPageText($html);
        $reference = $this->captureReferenceImages($url, $html);
        $prompt = $this->buildProductBriefPrompt($url, $productMetadata, $cleanText);

        $brief = $this->resolve(
            instructions: 'You are an expert marketing analyst. Extract a structured ad creative brief from a product or landing page. '
                .'CRITICAL PRODUCT FOCUS: When the page represents an e-commerce store, marketplace (such as Amazon, Flipkart, Walmart, Shopify, eBay, etc.), '
                .'or product catalog with a specific item or SKU: '
                .'1. The ad creative brief MUST be 100% focused on that specific product being sold (its unique features, materials, customer benefits, and use cases). '
                .'2. NEVER create an ad for the marketplace or retailer itself (e.g., do NOT make an ad about Amazon, Prime Video, grocery delivery, fast shipping, or general marketplace features). '
                .'3. The "product_name" MUST be the specific product item name (e.g., "Van Heusen Women\'s Anti-Bacterial T-Shirt"), NOT the retailer or marketplace name. '
                .'4. If reference images are attached (product photos or page screenshot), ground the product\'s visual appearance and packaging in them.',
            prompt: $prompt,
            attachments: $reference['attachments'],
        );

        if ($reference['urls'] === []) {
            return $brief;
        }

        return new CreativeBrief(
            productName: $brief->productName,
            benefits: $brief->benefits,
            features: $brief->features,
            images: array_values(array_unique([...$reference['urls'], ...$brief->images])),
            messaging: $brief->messaging,
            cta: $brief->cta,
            projectName: $brief->projectName,
            visualDescription: $brief->visualDescription,
        );
    }

    public function fromPrompt(string $prompt, array $imagePaths = []): CreativeBrief
    {
        $instructions = $imagePaths === []
            ? 'You are an expert marketing analyst. Turn the user\'s ad request into a structured ad creative brief, inferring reasonable product details from what they describe.'
            : 'You are an expert marketing and visual analyst. Turn the user\'s ad request into a structured ad creative brief. An attached image is a still of the featured product from a competitor ad. Ground visual_description in that image so video and image generators can replicate the product\'s silhouette, colors, materials, and form. Describe the product, not the competitor\'s presenter, layout, or logos.';

        $brief = $this->resolve(
            instructions: $instructions,
            prompt: $prompt,
            attachments: $this->localImagesFromPublicPaths($imagePaths),
        );

        return $this->withReferenceImageUrls($brief, $imagePaths);
    }

    public function fromImages(array $imagePaths): CreativeBrief
    {
        $brief = $this->resolve(
            instructions: 'You are an expert marketing and visual analyst. Extract a structured ad creative brief from the attached product images. Crucially, capture an exhaustive physical visual description of the product (silhouette, exact colors, materials, textures, buttons, straps, and details) so downstream AI video generators and image models can replicate the exact physical product faithfully.',
            prompt: 'Analyze these product images and produce a structured ad creative brief with an accurate physical visual description.',
            attachments: $this->localImagesFromPublicPaths($imagePaths),
        );

        return $this->withReferenceImageUrls($brief, $imagePaths);
    }

    /**
     * @param  list<string>  $imagePaths
     * @return list<LocalImage>
     */
    private function localImagesFromPublicPaths(array $imagePaths): array
    {
        return collect($imagePaths)
            ->map(function (string $path) {
                $fullPath = Storage::disk('public')->path($path);

                // OpenAI Vision and other LLM gateways only accept JPEG, PNG, WEBP, and GIF.
                // If the file is AVIF, convert it to a JPEG for the vision API.
                $ext = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
                if ($ext === 'avif' || @mime_content_type($fullPath) === 'image/avif') {
                    $jpgPath = preg_replace('/\.avif$/i', '.jpg', $fullPath);
                    if ($jpgPath === $fullPath) {
                        $jpgPath .= '.jpg';
                    }
                    if (! file_exists($jpgPath)) {
                        $img = @imagecreatefromavif($fullPath);
                        if ($img !== false) {
                            imagejpeg($img, $jpgPath, 95);
                            imagedestroy($img);
                        }
                    }
                    if (file_exists($jpgPath)) {
                        return new LocalImage($jpgPath, 'image/jpeg');
                    }
                }

                return new LocalImage($fullPath);
            })
            ->all();
    }

    /**
     * @param  list<string>  $imagePaths
     */
    private function withReferenceImageUrls(CreativeBrief $brief, array $imagePaths): CreativeBrief
    {
        if ($imagePaths === [] || $brief->images !== []) {
            return $brief;
        }

        return new CreativeBrief(
            productName: $brief->productName,
            benefits: $brief->benefits,
            features: $brief->features,
            images: collect($imagePaths)
                ->map(fn (string $path) => Storage::disk('public')->url($path))
                ->all(),
            messaging: $brief->messaging,
            cta: $brief->cta,
            projectName: $brief->projectName,
            visualDescription: $brief->visualDescription,
        );
    }

    private function resolve(string $instructions, string $prompt, array $attachments = []): CreativeBrief
    {
        $response = agent(
            instructions: $instructions,
            schema: fn (JsonSchema $schema) => [
                'product_name' => $schema->string()->description('The specific product or service name (e.g., "Van Heusen Women\'s Anti-Bacterial T-Shirt", NOT the marketplace name like "Amazon").')->required(),
                'project_name' => $schema->string()->description('The brand, website, or app name this creative is for. Infer it from the page or user request; use the product name only when no brand, website, or app name is available.')->required(),
                'benefits' => $schema->array()->items($schema->string())->description('Key customer benefits of the specific product.')->required(),
                'features' => $schema->array()->items($schema->string())->description('Key product features, materials, and specs.')->required(),
                'visual_description' => $schema->string()->description('Photographic, highly specific physical description of the product appearance: form factor, silhouette, exact colors, primary materials, surface textures, visible buttons/controls, attachments/straps, and branding elements so AI video and image models can replicate this exact physical item without hallucination.')->required(),
                'images' => $schema->array()->items($schema->string())->description('Any image URLs found on the page.')->required(),
                'messaging' => $schema->string()->description('The overall product brand messaging tone, hook, and angle.')->required(),
                'cta' => $schema->string()->description('The most relevant call to action to purchase or check out this product.')->required(),
            ],
        )->prompt($prompt, attachments: $attachments, provider: TextProviderFailover::providers());

        return CreativeBrief::fromArray($response->toArray());
    }

    /**
     * Extracts targeted product metadata (title, brand, bullets, price, description)
     * especially useful for e-commerce marketplaces (Amazon, Flipkart, Shopify, etc.).
     *
     * @return array{title: ?string, brand: ?string, price: ?string, bullets: list<string>, description: ?string}
     */
    private function extractProductMetadata(string $url, string $html): array
    {
        if (trim($html) === '') {
            return ['title' => null, 'brand' => null, 'price' => null, 'bullets' => [], 'description' => null];
        }

        $doc = new DOMDocument;
        libxml_use_internal_errors(true);
        $doc->loadHTML('<?xml encoding="utf-8" ?>'.$html);
        libxml_clear_errors();

        $xpath = new DOMXPath($doc);

        // 1. Product Title
        $title = null;
        $titleNodes = $xpath->query('//span[@id="productTitle"] | //*[@itemprop="name"] | //h1[contains(@class, "product")] | //h1');
        foreach ($titleNodes as $node) {
            $val = trim($node->textContent);
            if (strlen($val) > 4) {
                $title = preg_replace('/\s+/', ' ', $val);
                break;
            }
        }

        if ($title === null) {
            $ogTitle = $xpath->query('//meta[@property="og:title"]/@content | //meta[@name="twitter:title"]/@content');
            if ($ogTitle->length > 0) {
                $title = trim($ogTitle->item(0)->nodeValue);
            } else {
                $titleTag = $xpath->query('//title');
                if ($titleTag->length > 0) {
                    $title = trim($titleTag->item(0)->textContent);
                    $title = preg_replace('/\s*(?:at Amazon\.[a-z.]+|:\s*Amazon\.[a-z.]+.*|\|\s*Flipkart\.com.*)$/i', '', $title);
                }
            }
        }

        // 2. Brand
        $brand = null;
        $brandNodes = $xpath->query('//*[@id="bylineInfo"] | //*[@id="brand"] | //*[@itemprop="brand"] | //meta[@property="product:brand"]/@content');
        foreach ($brandNodes as $node) {
            $val = trim($node->textContent ?? $node->nodeValue ?? '');
            $val = preg_replace('/^(?:Brand:\s*|Visit the\s*|Store\s*)/i', '', $val);
            $val = preg_replace('/\s*(?:Store|Brand)$/i', '', $val);
            if (strlen($val) > 1) {
                $brand = trim($val);
                break;
            }
        }

        // 3. Bullets / Features
        $bullets = [];
        $bulletNodes = $xpath->query('//div[@id="feature-bullets"]//li | //*[@id="featurebullets_feature_div"]//li | //div[@id="productDescription"]//p | //div[contains(@class, "product-description")] | //ul[contains(@class, "product-features")]//li');
        foreach ($bulletNodes as $node) {
            $val = preg_replace('/\s+/', ' ', trim($node->textContent));
            if (strlen($val) > 15 && ! preg_match('/(?:see more|show more|keyboard shortcut|shift\s*\+\s*alt)/i', $val)) {
                $bullets[] = $val;
            }
        }

        // 4. Price
        $price = null;
        $priceNodes = $xpath->query('//*[@id="corePrice_feature_div"]//span[contains(@class, "a-offscreen")] | //span[contains(@class, "priceToPay")]//span[contains(@class, "a-offscreen")] | //*[@itemprop="price"] | //meta[@property="product:price:amount"]/@content');
        foreach ($priceNodes as $node) {
            $val = trim($node->textContent ?? $node->nodeValue ?? '');
            if (strlen($val) > 0) {
                $price = $val;
                break;
            }
        }

        // 5. Description
        $description = null;
        $descNodes = $xpath->query('//meta[@name="description"]/@content | //meta[@property="og:description"]/@content');
        if ($descNodes->length > 0) {
            $description = trim($descNodes->item(0)->nodeValue);
        }

        return [
            'title' => $title,
            'brand' => $brand,
            'price' => $price,
            'bullets' => array_slice(array_unique($bullets), 0, 8),
            'description' => $description,
        ];
    }

    /**
     * Safely strips script, style, nav, and chrome elements to extract relevant page body text.
     */
    private function extractCleanPageText(string $html): string
    {
        if (trim($html) === '') {
            return '';
        }

        $doc = new DOMDocument;
        libxml_use_internal_errors(true);
        $doc->loadHTML('<?xml encoding="utf-8" ?>'.$html);
        libxml_clear_errors();

        $xpath = new DOMXPath($doc);
        foreach ($xpath->query('//script | //style | //noscript | //svg | //nav | //footer | //header') as $node) {
            $node->parentNode?->removeChild($node);
        }

        return Str::of($doc->textContent ?? '')
            ->squish()
            ->limit(12000)
            ->toString();
    }

    /**
     * Builds a structured prompt prioritizing detected product features over generic platform chrome.
     */
    private function buildProductBriefPrompt(string $url, array $product, string $cleanText): string
    {
        $parts = ["Landing page URL: {$url}"];

        $hasProductDetails = filled($product['title']) || filled($product['brand']) || ! empty($product['bullets']);

        if ($hasProductDetails) {
            $productLines = [];
            if (filled($product['title'])) {
                $productLines[] = "Specific Product Name: {$product['title']}";
            }
            if (filled($product['brand'])) {
                $productLines[] = "Brand/Manufacturer: {$product['brand']}";
            }
            if (filled($product['price'])) {
                $productLines[] = "Price: {$product['price']}";
            }
            if (filled($product['description'])) {
                $productLines[] = "Product Summary: {$product['description']}";
            }
            if (! empty($product['bullets'])) {
                $productLines[] = "Key Features & Highlights:\n- ".implode("\n- ", $product['bullets']);
            }

            $parts[] = "PRIMARY PRODUCT DETAILS (The ad MUST be 100% focused on this product):\n".implode("\n", $productLines);
        }

        if (filled($cleanText)) {
            $parts[] = "Additional Page Context:\n{$cleanText}";
        }

        return implode("\n\n", $parts);
    }

    /**
     * Pulls real visual context for a landing page: og:image/twitter:image + inline <img> tags
     * parsed straight from the raw HTML, before it's stripped to plain text above (stripped text
     * never contains image URLs, so without this the "images" field was the AI guessing/hallucinating
     * from prose alone). If fewer than 2 real product photos are found, the page is treated as a
     * general site/portal rather than a single product listing, and a full-page screenshot is
     * captured instead so there's still visual grounding to work from.
     *
     * @return array{attachments: list<LocalImage>, urls: list<string>}
     */
    private function captureReferenceImages(string $url, string $html): array
    {
        if (! config('creative.url_brief.capture_reference_images', true)) {
            return ['attachments' => [], 'urls' => []];
        }

        $candidates = $this->extractProductImageUrls($url, $html);

        if (count($candidates) < 2) {
            $screenshot = $this->captureScreenshotUrl($url);

            if ($screenshot !== null) {
                array_unshift($candidates, $screenshot);
            }
        }

        $attachments = [];
        $urls = [];

        foreach (array_slice(array_unique($candidates), 0, 3) as $imageUrl) {
            $stored = $this->downloadAndStoreImage($imageUrl);

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

            [$path, $publicUrl] = $stored;
            $attachments[] = new LocalImage(Storage::disk('public')->path($path));
            $urls[] = $publicUrl;
        }

        return ['attachments' => $attachments, 'urls' => $urls];
    }

    /** @return list<string> absolute image URLs, best photos first (og:image/twitter:image, then inline <img> tags). */
    private function extractProductImageUrls(string $url, string $html): array
    {
        $metaImages = [];

        if (preg_match_all('/<meta[^>]+(?:property|name)=["\'](?:og:image(?::secure_url)?|twitter:image)["\'][^>]+content=["\']([^"\']+)["\']/i', $html, $matches)) {
            $metaImages = $matches[1];
        }

        $ecommerceImages = [];

        // E-commerce high-res data attributes
        if (preg_match_all('/data-old-hires=["\'](https:\/\/[^"\']+)["\']/i', $html, $matches)) {
            $ecommerceImages = array_merge($ecommerceImages, $matches[1]);
        }
        if (preg_match_all('/data-a-dynamic-image=["\']({[^"\']+)["\']/i', $html, $matches)) {
            foreach ($matches[1] as $jsonStr) {
                $decoded = json_decode(htmlspecialchars_decode($jsonStr), true);
                if (is_array($decoded)) {
                    $ecommerceImages = array_merge($ecommerceImages, array_keys($decoded));
                }
            }
        }

        // Amazon clean CDN image pattern
        if (preg_match_all('/https:\/\/m\.media-amazon\.com\/images\/I\/[a-zA-Z0-9_\-\.\+]+\.jpg/i', $html, $matches)) {
            foreach ($matches[0] as $imgUrl) {
                // Strip out thumbnail resize modifiers e.g. ._SX38_SY50_CR,0,0,38,50_ or ._AC_SR38,50_
                $ecommerceImages[] = preg_replace('/\._[A-Z0-9_\,]+_\./i', '.', $imgUrl);
            }
        }

        $inlineImages = [];

        if (preg_match_all('/<img[^>]+src=["\']([^"\']+)["\']/i', $html, $matches)) {
            $inlineImages = collect($matches[1])
                ->reject(fn (string $src) => str_starts_with($src, 'data:') || Str::contains(strtolower($src), ['sprite', 'icon', 'logo', 'pixel', '.svg']))
                ->take(10)
                ->all();
        }

        return collect([...$metaImages, ...$ecommerceImages, ...$inlineImages])
            ->map(fn (string $src) => $this->resolveAbsoluteUrl($url, $src))
            ->filter()
            ->unique()
            ->values()
            ->all();
    }

    private function resolveAbsoluteUrl(string $baseUrl, string $src): ?string
    {
        $src = trim($src);

        if ($src === '') {
            return null;
        }

        if (Str::startsWith($src, ['http://', 'https://'])) {
            return $src;
        }

        $base = parse_url($baseUrl);

        if (! isset($base['scheme'], $base['host'])) {
            return null;
        }

        if (Str::startsWith($src, '//')) {
            return "{$base['scheme']}:{$src}";
        }

        $origin = "{$base['scheme']}://{$base['host']}".(isset($base['port']) ? ":{$base['port']}" : '');

        return $origin.'/'.ltrim($src, '/');
    }

    /**
     * Full-page screenshot via Microlink's public screenshot endpoint (config('creative.url_brief
     * .screenshot_endpoint')) — a plain HTTP call, no headless browser install required.
     */
    private function captureScreenshotUrl(string $url): ?string
    {
        if (! config('creative.url_brief.screenshot_enabled', true)) {
            return null;
        }

        try {
            $response = Http::timeout(20)->get(config('creative.url_brief.screenshot_endpoint'), [
                'url' => $url,
                'screenshot' => true,
                'meta' => false,
            ]);

            return $response->successful() ? $response->json('data.screenshot.url') : null;
        } catch (\Throwable $e) {
            logger()->warning('Website screenshot capture failed', ['url' => $url, 'exception' => $e]);

            return null;
        }
    }

    /** @return array{0: string, 1: string}|null [stored path on the "public" disk, public URL] */
    private function downloadAndStoreImage(string $imageUrl): ?array
    {
        try {
            $response = Http::timeout(20)->get($imageUrl);

            if ($response->failed() || $response->body() === '') {
                return null;
            }

            $extension = Str::of($response->header('Content-Type', 'image/jpeg'))->after('image/')->before(';')->toString();
            $extension = $extension !== '' ? $extension : 'jpg';
            $path = 'creatives/brief-images/'.Str::uuid().'.'.$extension;

            Storage::disk('public')->put($path, $response->body());

            return [$path, Storage::disk('public')->url($path)];
        } catch (\Throwable $e) {
            logger()->warning('Reference image download failed', ['image_url' => $imageUrl, 'exception' => $e]);

            return null;
        }
    }
}
