<?php
declare(strict_types=1);

namespace Help4BuilderSuite\Modules\SEO;

use Help4BuilderSuite\Modules\Commerce\ProductRepository;
use Help4BuilderSuite\Support\Settings;

if (! defined('ABSPATH')) {
    exit;
}

final class CommerceDiscovery
{
    private ProductRepository $products;

    /** @var array<int, array<string, mixed>> */
    private array $auditCache = [];

    public function __construct(?ProductRepository $products = null)
    {
        $this->products = $products ?? new ProductRepository();
    }

    public function boot(): void
    {
        add_action('template_redirect', [$this, 'maybeRenderMerchantFeed'], 0);
    }

    /**
     * Samples active products without loading an entire large catalog into memory.
     *
     * @return array<string, mixed>
     */
    public function catalogAudit(int $limit = 500): array
    {
        $limit = max(1, min(2000, $limit));
        if (isset($this->auditCache[$limit])) {
            return $this->auditCache[$limit];
        }
        $settings = Settings::all();
        $counts = [
            'sampled' => 0,
            'feed_eligible' => 0,
            'description' => 0,
            'image' => 0,
            'landing_url' => 0,
            'identifier' => 0,
            'brand' => 0,
            'price_currency' => 0,
            'availability' => 0,
            'variants' => 0,
        ];
        $missingExamples = [];
        $offset = 0;

        while ($counts['sampled'] < $limit) {
            $batchSize = min(100, $limit - $counts['sampled']);
            $rows = $this->products->list([
                'status' => 'active',
                'sort' => 'id_asc',
                'limit' => $batchSize,
                'offset' => $offset,
            ]);
            if ($rows === []) {
                break;
            }
            foreach ($rows as $row) {
                $product = $this->productData($row, $settings);
                $counts['sampled']++;
                foreach (['description', 'image', 'landing_url', 'identifier', 'brand', 'price_currency', 'availability', 'variants'] as $field) {
                    if (! empty($product['coverage'][$field])) {
                        $counts[$field]++;
                    }
                }
                if (! empty($product['feed_eligible'])) {
                    $counts['feed_eligible']++;
                } elseif (count($missingExamples) < 8) {
                    $missingExamples[] = [
                        'id' => absint($row['id'] ?? 0),
                        'name' => sanitize_text_field((string) ($row['name'] ?? '')),
                        'missing' => array_keys(array_filter(
                            (array) ($product['coverage'] ?? []),
                            static fn (mixed $ready): bool => ! $ready
                        )),
                    ];
                }
            }
            $offset += count($rows);
            if (count($rows) < $batchSize) {
                break;
            }
        }

        $sampled = max(1, (int) $counts['sampled']);
        $coverage = [];
        foreach (['feed_eligible', 'description', 'image', 'landing_url', 'identifier', 'brand', 'price_currency', 'availability', 'variants'] as $field) {
            $coverage[$field] = (int) round(((int) $counts[$field] / $sampled) * 100);
        }

        $score = (int) round(
            ($coverage['description'] * 0.16)
            + ($coverage['image'] * 0.16)
            + ($coverage['landing_url'] * 0.18)
            + ($coverage['identifier'] * 0.12)
            + ($coverage['brand'] * 0.08)
            + ($coverage['price_currency'] * 0.14)
            + ($coverage['availability'] * 0.10)
            + ($coverage['variants'] * 0.06)
        );

        $report = [
            'score' => $counts['sampled'] > 0 ? max(0, min(100, $score)) : 0,
            'counts' => $counts,
            'coverage' => $coverage,
            'missing_examples' => $missingExamples,
            'feed_enabled' => rest_sanitize_boolean($settings['commerce_seo_feed_enabled'] ?? '0'),
            'feed_url' => home_url('/h4-commerce-feed.xml'),
            'sample_limit' => $limit,
        ];

        $filtered = apply_filters('h4bs_commerce_seo_catalog_audit', $report, $settings);
        $this->auditCache[$limit] = is_array($filtered) ? $filtered : $report;
        return $this->auditCache[$limit];
    }

    public function maybeRenderMerchantFeed(): void
    {
        if (is_admin() || wp_doing_ajax()) {
            return;
        }
        $requestUri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_URI'])) : '';
        $path = trim((string) wp_parse_url($requestUri, PHP_URL_PATH), '/');
        $homePath = trim((string) wp_parse_url(home_url('/'), PHP_URL_PATH), '/');
        if ($homePath !== '' && str_starts_with($path, $homePath . '/')) {
            $path = substr($path, strlen($homePath) + 1);
        }
        if ($path !== 'h4-commerce-feed.xml') {
            return;
        }

        $settings = Settings::all();
        if (
            ! rest_sanitize_boolean($settings['commerce_seo_feed_enabled'] ?? '0')
            || (string) get_option('blog_public', '1') !== '1'
        ) {
            status_header(404);
            exit;
        }

        status_header(200);
        nocache_headers();
        header('Content-Type: application/rss+xml; charset=UTF-8');
        header('X-Content-Type-Options: nosniff');
        echo $this->feedHeaderXml($settings); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- XML is assembled exclusively from escaped values.
        foreach ($this->merchantFeedItems($settings) as $itemXml) {
            echo $itemXml; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Each item is assembled exclusively from escaped values.
        }
        echo '</channel></rss>' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static XML closing tags.
        exit;
    }

    /**
     * @param array<string, mixed> $settings
     */
    public function merchantFeedXml(array $settings): string
    {
        $xml = $this->feedHeaderXml($settings);
        foreach ($this->merchantFeedItems($settings) as $itemXml) {
            $xml .= $itemXml;
        }
        return $xml . '</channel></rss>' . "\n";
    }

    /**
     * Returns one normalized product for on-page schema and extension integrations.
     *
     * @param array<string, mixed> $settings
     * @return array<string, mixed>|null
     */
    public function productById(int $productId, array $settings): ?array
    {
        if ($productId <= 0) {
            return null;
        }
        $row = $this->products->get($productId);
        if (! is_array($row) || sanitize_key((string) ($row['status'] ?? '')) !== 'active') {
            return null;
        }
        return $this->productData($row, $settings);
    }

    /**
     * Streams product XML in fixed-size cursor batches for large catalogs.
     *
     * @param array<string, mixed> $settings
     * @return \Generator<int, string>
     */
    private function merchantFeedItems(array $settings): \Generator
    {
        $cursorUpdatedAt = '';
        $cursorId = 0;
        $emitted = 0;
        $maxProducts = max(0, (int) apply_filters('h4bs_commerce_feed_max_products', 0));

        do {
            $args = [
                'status' => 'active',
                'sort' => 'updated_asc',
                'limit' => 100,
            ];
            if ($cursorUpdatedAt !== '' && $cursorId > 0) {
                $args['cursor_updated_at'] = $cursorUpdatedAt;
                $args['cursor_id'] = $cursorId;
            }
            $rows = $this->products->list($args);
            foreach ($rows as $row) {
                $product = $this->productData($row, $settings);
                if (empty($product['feed_eligible'])) {
                    continue;
                }
                $item = apply_filters('h4bs_commerce_feed_item', $product, $row, $settings);
                if (! is_array($item) || empty($item['feed_eligible'])) {
                    continue;
                }
                yield $this->feedItemXml($item);
                $emitted++;
                if ($maxProducts > 0 && $emitted >= $maxProducts) {
                    return;
                }
            }
            $last = end($rows);
            if (! is_array($last)) {
                break;
            }
            $nextUpdatedAt = sanitize_text_field((string) ($last['updated_at'] ?? ''));
            $nextId = absint($last['id'] ?? 0);
            if ($nextUpdatedAt === '' || $nextId <= 0 || ($nextUpdatedAt === $cursorUpdatedAt && $nextId === $cursorId)) {
                break;
            }
            $cursorUpdatedAt = $nextUpdatedAt;
            $cursorId = $nextId;
        } while (count($rows) === 100);
    }

    /** @param array<string, mixed> $settings */
    private function feedHeaderXml(array $settings): string
    {
        $siteName = sanitize_text_field((string) ($settings['brand_name'] ?? '')) ?: get_bloginfo('name');
        $xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        $xml .= '<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0"><channel>' . "\n";
        $xml .= '<title>' . esc_xml($siteName . ' product feed') . '</title>' . "\n";
        $xml .= '<link>' . esc_xml(home_url('/')) . '</link>' . "\n";
        /* translators: %s: Public store or brand name. */
        return $xml . '<description>' . esc_xml(sprintf(__('Current purchasable products from %s.', 'help4-builder-suite'), $siteName)) . '</description>' . "\n";
    }

    /**
     * @param array<string, mixed> $row
     * @param array<string, mixed> $settings
     * @return array<string, mixed>
     */
    public function productData(array $row, array $settings): array
    {
        $payload = json_decode((string) ($row['payload'] ?? ''), true);
        $payload = is_array($payload) ? $payload : [];
        $name = sanitize_text_field((string) ($row['name'] ?? ''));
        $description = trim(wp_strip_all_tags((string) (
            $payload['description']
            ?? ($payload['short_description']
            ?? ($payload['summary'] ?? ($payload['excerpt'] ?? '')))
        )));
        $landingUrl = esc_url_raw((string) (
            $payload['external_url']
            ?? ($payload['product_url']
            ?? ($payload['permalink'] ?? ($payload['url'] ?? '')))
        ), ['http', 'https']);
        $image = $this->productImageUrl($payload);
        $brand = sanitize_text_field((string) ($payload['brand'] ?? ($settings['commerce_seo_default_brand'] ?? ($settings['brand_name'] ?? ''))));
        $gtin = (string) preg_replace('/[^0-9]/', '', sanitize_text_field((string) ($payload['gtin'] ?? '')));
        if (! in_array(strlen($gtin), [8, 12, 13, 14], true)) {
            $gtin = '';
        }
        $mpn = sanitize_text_field((string) ($payload['mpn'] ?? ''));
        $sku = sanitize_text_field((string) ($row['sku'] ?? ''));
        $currency = strtoupper(substr(sanitize_text_field((string) ($row['currency'] ?? '')), 0, 3));
        $price = max(0.0, (float) ($row['price'] ?? 0));
        $condition = sanitize_key((string) ($payload['condition'] ?? ($settings['commerce_seo_default_condition'] ?? 'new')));
        if (! in_array($condition, ['new', 'used', 'refurbished'], true)) {
            $condition = 'new';
        }
        $availability = $this->availability($row, $payload);
        $productCategory = sanitize_text_field((string) ($payload['google_product_category'] ?? ''));
        $productType = sanitize_key((string) ($payload['type'] ?? 'simple'));
        $variants = $productType === 'variable' && absint($row['id'] ?? 0) > 0
            ? $this->products->listVariants(absint($row['id']))
            : [];
        $identifier = $gtin !== '' || $mpn !== '' || $sku !== '';
        $coverage = [
            'description' => $description !== '',
            'image' => $image !== '',
            'landing_url' => $landingUrl !== '',
            'identifier' => $identifier,
            'brand' => $brand !== '',
            'price_currency' => $price > 0 && preg_match('/^[A-Z]{3}$/', $currency) === 1,
            'availability' => $availability !== '',
            'variants' => $variants === [] || $this->variantsHaveIdentity($variants),
        ];

        return [
            'id' => (string) (absint($row['id'] ?? 0) ?: ($sku !== '' ? $sku : sanitize_title($name))),
            'name' => $name,
            'description' => $description,
            'landing_url' => $landingUrl,
            'image' => $image,
            'brand' => $brand,
            'gtin' => $gtin,
            'mpn' => $mpn,
            'sku' => $sku,
            'price' => $price,
            'currency' => $currency,
            'condition' => $condition,
            'availability' => $availability,
            'product_category' => $productCategory,
            'coverage' => $coverage,
            'variant_count' => count($variants),
            'feed_eligible' => $name !== '' && ! in_array(false, $coverage, true),
        ];
    }

    /**
     * @param array<string, mixed> $product
     */
    private function feedItemXml(array $product): string
    {
        $xml = '<item>' . "\n";
        $xml .= '<g:id>' . esc_xml((string) $product['id']) . '</g:id>' . "\n";
        $xml .= '<title>' . esc_xml((string) $product['name']) . '</title>' . "\n";
        $xml .= '<description>' . esc_xml((string) $product['description']) . '</description>' . "\n";
        $xml .= '<link>' . esc_xml((string) $product['landing_url']) . '</link>' . "\n";
        $xml .= '<g:image_link>' . esc_xml((string) $product['image']) . '</g:image_link>' . "\n";
        $xml .= '<g:price>' . esc_xml(number_format((float) $product['price'], 2, '.', '') . ' ' . (string) $product['currency']) . '</g:price>' . "\n";
        $xml .= '<g:availability>' . esc_xml((string) $product['availability']) . '</g:availability>' . "\n";
        $xml .= '<g:condition>' . esc_xml((string) $product['condition']) . '</g:condition>' . "\n";
        if ((string) $product['brand'] !== '') {
            $xml .= '<g:brand>' . esc_xml((string) $product['brand']) . '</g:brand>' . "\n";
        }
        if ((string) $product['gtin'] !== '') {
            $xml .= '<g:gtin>' . esc_xml((string) $product['gtin']) . '</g:gtin>' . "\n";
        } elseif ((string) $product['mpn'] !== '') {
            $xml .= '<g:mpn>' . esc_xml((string) $product['mpn']) . '</g:mpn>' . "\n";
        }
        if ((string) ($product['product_category'] ?? '') !== '') {
            $xml .= '<g:google_product_category>' . esc_xml((string) $product['product_category']) . '</g:google_product_category>' . "\n";
        }
        return $xml . '</item>' . "\n";
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function productImageUrl(array $payload): string
    {
        $imageId = absint($payload['featured_image_id'] ?? ($payload['image_id'] ?? ($payload['thumbnail_id'] ?? 0)));
        if ($imageId > 0) {
            $image = wp_get_attachment_image_url($imageId, 'full');
            if (is_string($image) && $image !== '') {
                return esc_url_raw($image, ['http', 'https']);
            }
        }
        $url = (string) ($payload['featured_image_url'] ?? ($payload['image_url'] ?? ($payload['thumbnail_url'] ?? '')));
        if ($url === '' && is_array($payload['gallery_urls'] ?? null)) {
            $url = (string) ($payload['gallery_urls'][0] ?? '');
        }
        return esc_url_raw($url, ['http', 'https']);
    }

    /**
     * @param array<string, mixed> $row
     * @param array<string, mixed> $payload
     */
    private function availability(array $row, array $payload): string
    {
        $inventory = is_array($payload['inventory'] ?? null) ? $payload['inventory'] : [];
        $policy = sanitize_key((string) ($inventory['policy'] ?? 'deny'));
        $stock = (int) ($row['stock_qty'] ?? 0);
        if ($stock > 0 || empty($inventory['track_stock'])) {
            return 'in_stock';
        }
        if ($policy === 'preorder') {
            return 'preorder';
        }
        if (in_array($policy, ['backorder', 'backorder_notify'], true)) {
            return 'backorder';
        }
        return 'out_of_stock';
    }

    /**
     * @param array<int, array<string, mixed>> $variants
     */
    private function variantsHaveIdentity(array $variants): bool
    {
        foreach ($variants as $variant) {
            if (sanitize_text_field((string) ($variant['sku'] ?? '')) === '') {
                return false;
            }
            $attributes = json_decode((string) ($variant['attributes_json'] ?? ''), true);
            if (! is_array($attributes) || $attributes === []) {
                return false;
            }
        }
        return true;
    }
}
